本文目录导读:

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">图片按尺寸分类 · 批量移动工具</title>
<style>
* {
box-sizing: border-box;
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif;
}
body {
background: #f4f5f9;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
padding: 20px;
}
.card {
max-width: 1000px;
width: 100%;
background: white;
border-radius: 32px;
padding: 30px 28px;
box-shadow: 0 15px 35px rgba(0,0,0,0.08);
backdrop-filter: blur(2px);
transition: 0.2s;
}
h1 {
font-size: 1.9rem;
font-weight: 500;
margin: 0 0 8px 0;
letter-spacing: -0.01em;
display: flex;
align-items: center;
gap: 12px;
color: #1e293b;
}
.subhead {
color: #64748b;
margin-top: -4px;
margin-bottom: 28px;
font-weight: 400;
font-size: 0.95rem;
border-left: 4px solid #a5b4fc;
padding-left: 16px;
background: #f8fafd;
border-radius: 0 12px 12px 0;
}
.upload-area {
background: #f1f5f9;
border: 2px dashed #cbd5e1;
border-radius: 24px;
padding: 40px 20px;
text-align: center;
cursor: pointer;
transition: all 0.2s ease;
margin-bottom: 28px;
}
.upload-area:hover {
border-color: #818cf8;
background: #eef2ff;
}
.upload-area.dragover {
border-color: #6366f1;
background: #e0e7ff;
box-shadow: 0 0 0 5px rgba(99,102,241,0.15);
}
.upload-icon {
font-size: 3rem;
margin-bottom: 8px;
opacity: 0.6;
}
.upload-area p {
font-size: 1.1rem;
font-weight: 500;
color: #1e293b;
margin: 8px 0 4px;
}
.upload-area small {
color: #64748b;
font-size: 0.9rem;
}
.config-panel {
background: #f8fafc;
border-radius: 20px;
padding: 20px 24px;
margin-bottom: 28px;
border: 1px solid #e9edf2;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 20px 32px;
}
.size-group {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.size-group label {
font-weight: 500;
color: #0f172a;
display: flex;
align-items: center;
gap: 6px;
}
.size-group input {
width: 70px;
padding: 8px 8px;
border-radius: 30px;
border: 1px solid #cbd5e1;
background: white;
font-weight: 500;
text-align: center;
font-size: 0.9rem;
}
.size-group input:focus {
outline: 2px solid #818cf8;
border-color: transparent;
}
.size-group span {
color: #475569;
font-size: 0.9rem;
}
.actions {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 16px;
}
.btn {
background: white;
border: 1px solid #d1d9e6;
padding: 10px 24px;
border-radius: 60px;
font-weight: 500;
font-size: 0.95rem;
display: inline-flex;
align-items: center;
gap: 8px;
cursor: pointer;
transition: 0.15s;
box-shadow: 0 1px 3px rgba(0,0,0,0.02);
color: #1e293b;
}
.btn-primary {
background: #4f46e5;
border: 1px solid #4f46e5;
color: white;
box-shadow: 0 4px 8px rgba(79,70,229,0.2);
}
.btn-primary:hover {
background: #4338ca;
transform: scale(0.97);
}
.btn-primary:disabled {
opacity: 0.5;
pointer-events: none;
filter: grayscale(0.3);
}
.btn-outline {
border: 1px solid #e2e8f0;
background: #ffffff;
}
.btn-outline:hover {
background: #f1f5f9;
}
.stats {
font-size: 0.9rem;
color: #475569;
padding: 6px 16px 6px 0;
background: #f1f5f9;
border-radius: 30px;
display: inline-flex;
align-items: center;
gap: 6px;
}
.file-list {
margin-top: 30px;
border-radius: 18px;
background: #f8fafc;
border: 1px solid #e9edf2;
overflow: hidden;
}
.file-header {
padding: 14px 20px;
background: #f1f5f9;
font-weight: 600;
color: #0f172a;
border-bottom: 1px solid #e2e8f0;
display: flex;
justify-content: space-between;
}
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 8px;
padding: 12px 16px;
max-height: 280px;
overflow-y: auto;
}
.file-item {
background: white;
border-radius: 14px;
padding: 6px 10px;
font-size: 0.8rem;
box-shadow: 0 1px 4px rgba(0,0,0,0.02);
border: 1px solid #eef2f6;
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-item .dim {
background: #dbeafe;
padding: 0 10px;
border-radius: 40px;
font-size: 0.7rem;
font-weight: 500;
color: #1e40af;
flex-shrink: 0;
}
.dimension-badge {
background: #e6e9f0;
padding: 2px 10px;
border-radius: 40px;
font-size: 0.75rem;
font-weight: 500;
}
.log-area {
margin-top: 20px;
background: #0f172a;
color: #b9c7e0;
border-radius: 20px;
padding: 14px 18px;
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 0.8rem;
white-space: pre-wrap;
max-height: 120px;
overflow-y: auto;
}
.log-area:empty {
display: none;
}
.warning-note {
background: #fff8e6;
border-left: 6px solid #fbbf24;
padding: 8px 18px;
border-radius: 30px;
font-size: 0.85rem;
color: #7c5e1a;
margin-top: 20px;
}
.flex-spacer {
flex: 1;
}
@media (max-width: 680px) {
.card { padding: 20px 16px; }
.config-panel { flex-direction: column; align-items: stretch; }
.size-group { justify-content: space-between; }
}
</style>
</head>
<body>
<div class="card">
<h1>
<span>📐 图片按尺寸分类</span>
<span class="dimension-badge">批量移动</span>
</h1>
<div class="subhead">根据图片宽度与高度,自动复制到对应子文件夹(原图保留)</div>
<!-- 上传区 -->
<div class="upload-area" id="dropZone">
<div class="upload-icon">🖼️</div>
<p>拖放图片到此处,或点击选择</p>
<small>支持 JPG / PNG / WEBP / BMP / GIF</small>
<input type="file" id="fileInput" accept="image/*" multiple hidden>
</div>
<!-- 配置分类尺寸 -->
<div class="config-panel">
<div class="size-group">
<label>📏 宽度 <input type="number" id="widthLimit" value="800" min="1" step="10"></label>
<span>px</span>
</div>
<div class="size-group">
<label>📐 高度 <input type="number" id="heightLimit" value="600" min="1" step="10"></label>
<span>px</span>
</div>
<div style="display: flex; gap: 8px; margin-left: auto;">
<span class="stats" id="fileCountDisplay">📁 0 张</span>
</div>
</div>
<!-- 操作按钮 -->
<div class="actions">
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
<button class="btn btn-primary" id="processBtn">⚡ 按尺寸分类移动</button>
<button class="btn btn-outline" id="clearBtn">🗑️ 清空列表</button>
</div>
<div>
<button class="btn btn-outline" id="selectFolderBtn">📂 选择目标文件夹</button>
</div>
</div>
<!-- 文件列表 -->
<div class="file-list" id="fileListContainer">
<div class="file-header">
<span>📎 已添加图片</span>
<span id="fileCountBadge">0</span>
</div>
<div class="file-grid" id="fileGrid"></div>
</div>
<!-- 日志 / 反馈 -->
<div class="log-area" id="logContainer"></div>
<div class="warning-note">
⚠️ 操作不会删除原图,只会将图片 <strong>复制</strong> 到对应子文件夹。<br>
子文件夹命名:<code>宽>W</code>、<code>高>H</code>、<code>宽高均大</code>、<code>其他</code>
</div>
</div>
<script>
(function() {
// ---------- DOM 引用 ----------
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
const fileGrid = document.getElementById('fileGrid');
const fileCountBadge = document.getElementById('fileCountBadge');
const fileCountDisplay = document.getElementById('fileCountDisplay');
const processBtn = document.getElementById('processBtn');
const clearBtn = document.getElementById('clearBtn');
const selectFolderBtn = document.getElementById('selectFolderBtn');
const logContainer = document.getElementById('logContainer');
const widthLimitInput = document.getElementById('widthLimit');
const heightLimitInput = document.getElementById('heightLimit');
// ---------- 状态 ----------
let files = []; // 存储 File 对象
let targetDirectoryHandle = null; // 文件夹句柄 (FileSystemDirectoryHandle)
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/bmp', 'image/gif'];
// ---------- 辅助函数 ----------
function addLog(message, isError = false) {
const time = new Date().toLocaleTimeString('zh-CN', { hour12: false });
const prefix = isError ? '❌' : '✅';
logContainer.innerHTML += `[${time}] ${prefix} ${message}\n`;
logContainer.scrollTop = logContainer.scrollHeight;
}
function clearLog() {
logContainer.innerHTML = '';
}
function updateFileListUI() {
// 更新计数
fileCountBadge.textContent = files.length;
fileCountDisplay.textContent = `📁 ${files.length} 张`;
// 更新网格
if (files.length === 0) {
fileGrid.innerHTML = `<div style="grid-column:1/-1; text-align:center; padding:22px 0; color:#94a3b8;">暂无图片,请添加</div>`;
return;
}
let html = '';
// 只显示最近20个 (性能)
const showFiles = files.slice(-40);
for (const file of showFiles) {
// 显示名称 (截断)
let displayName = file.name;
if (displayName.length > 18) {
displayName = displayName.slice(0, 8) + '…' + displayName.slice(-7);
}
// 尺寸占位(稍后异步更新)
html += `<div class="file-item" data-file-name="${file.name}">
<span>🖼️ ${displayName}</span>
<span class="dim" id="dim-${file.name.replace(/[^a-zA-Z0-9]/g,'-')}">⋯</span>
</div>`;
}
fileGrid.innerHTML = html;
// 异步获取尺寸并更新显示 (只对可见的item更新)
for (const file of showFiles) {
const safeId = `dim-${file.name.replace(/[^a-zA-Z0-9]/g,'-')}`;
const dimSpan = document.getElementById(safeId);
if (!dimSpan) continue;
// 获取图片尺寸
const img = new Image();
const objectUrl = URL.createObjectURL(file);
img.onload = function() {
dimSpan.textContent = `${this.width}×${this.height}`;
URL.revokeObjectURL(objectUrl);
};
img.onerror = function() {
dimSpan.textContent = '?×?';
URL.revokeObjectURL(objectUrl);
};
img.src = objectUrl;
}
}
// ---------- 添加文件 (过滤) ----------
function addFiles(fileList) {
let added = 0;
for (const file of fileList) {
if (!ALLOWED_TYPES.includes(file.type)) {
addLog(`⏭️ 跳过非图片: ${file.name}`, false);
continue;
}
// 避免重复(基于名字+大小+最后修改)
const duplicate = files.some(f => f.name === file.name && f.size === file.size && f.lastModified === file.lastModified);
if (!duplicate) {
files.push(file);
added++;
} else {
addLog(`⏭️ 重复跳过: ${file.name}`, false);
}
}
if (added > 0) {
addLog(`➕ 添加了 ${added} 张图片`);
updateFileListUI();
} else {
addLog(`没有新增图片`);
}
}
// ---------- 文件选择/拖拽 ----------
dropZone.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (e) => {
if (e.target.files.length) {
addFiles(e.target.files);
}
fileInput.value = ''; // 允许重复选择相同文件
});
// 拖拽
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('dragover');
if (e.dataTransfer.files.length) {
addFiles(e.dataTransfer.files);
}
});
// ---------- 清空 ----------
clearBtn.addEventListener('click', () => {
if (files.length === 0) return;
files = [];
updateFileListUI();
clearLog();
addLog('已清空文件列表');
});
// ---------- 选择目标文件夹 (现代API) ----------
selectFolderBtn.addEventListener('click', async () => {
try {
const handle = await window.showDirectoryPicker({ mode: 'readwrite' });
targetDirectoryHandle = handle;
addLog(`📂 目标文件夹: ${handle.name}`);
} catch (err) {
if (err.name !== 'AbortError') {
addLog(`⚠️ 选择文件夹失败: ${err.message}`, true);
}
}
});
// ---------- 核心分类移动 (复制) ----------
processBtn.addEventListener('click', async () => {
if (files.length === 0) {
addLog('⚠️ 没有图片可以分类', true);
return;
}
if (!targetDirectoryHandle) {
addLog('⚠️ 请先选择目标文件夹 (点击 "选择目标文件夹")', true);
return;
}
const wLimit = parseInt(widthLimitInput.value, 10);
const hLimit = parseInt(heightLimitInput.value, 10);
if (isNaN(wLimit) || isNaN(hLimit) || wLimit < 1 || hLimit < 1) {
addLog('⚠️ 宽度/高度限制必须为正整数', true);
return;
}
processBtn.disabled = true;
processBtn.textContent = '⏳ 处理中…';
// 文件夹名称映射
const folderNames = {
wide: `宽>${wLimit}`,
tall: `高>${hLimit}`,
both: `宽高均大`,
other: `其他`
};
// 确保子文件夹存在 (获取或创建)
async function ensureDir(dirHandle, subName) {
try {
return await dirHandle.getDirectoryHandle(subName, { create: true });
} catch (e) {
addLog(`❌ 无法创建文件夹 ${subName}: ${e.message}`, true);
throw e;
}
}
try {
// 建立所有子文件夹
const dirWide = await ensureDir(targetDirectoryHandle, folderNames.wide);
const dirTall = await ensureDir(targetDirectoryHandle, folderNames.tall);
const dirBoth = await ensureDir(targetDirectoryHandle, folderNames.both);
const dirOther = await ensureDir(targetDirectoryHandle, folderNames.other);
let successCount = 0;
let skipCount = 0;
for (const file of files) {
// 获取图片尺寸 (同步转)
const img = await new Promise((resolve, reject) => {
const image = new Image();
const url = URL.createObjectURL(file);
image.onload = () => {
resolve({ width: image.naturalWidth, height: image.naturalHeight });
URL.revokeObjectURL(url);
};
image.onerror = () => {
reject(new Error(`无法解码: ${file.name}`));
URL.revokeObjectURL(url);
};
image.src = url;
});
const w = img.width;
const h = img.height;
const isWide = w > wLimit;
const isTall = h > hLimit;
let targetDir;
if (isWide && isTall) {
targetDir = dirBoth;
} else if (isWide) {
targetDir = dirWide;
} else if (isTall) {
targetDir = dirTall;
} else {
targetDir = dirOther;
}
// 写文件 (复制)
try {
const writable = await targetDir.getFileHandle(file.name, { create: true });
const writer = await writable.createWritable({ keepExistingData: false });
const blob = file.slice(); // 原样
await writer.write(blob);
await writer.close();
successCount++;
} catch (writeErr) {
addLog(`❌ 写入失败 ${file.name}: ${writeErr.message}`, true);
skipCount++;
}
}
addLog(`✅ 分类完成!成功复制 ${successCount} 张,跳过 ${skipCount} 张`);
addLog(`📁 子文件夹: "${folderNames.wide}" / "${folderNames.tall}" / "${folderNames.both}" / "${folderNames.other}"`);
} catch (err) {
addLog(`❌ 处理异常: ${err.message}`, true);
} finally {
processBtn.disabled = false;
processBtn.textContent = '⚡ 按尺寸分类移动';
}
});
// ---------- 初始示例 ----------
// 预置几条提示
addLog('🟢 工具就绪,添加图片并选择目标文件夹');
updateFileListUI();
})();
</script>
</body>
</html>
按尺寸分类逻辑
您可以通过设定宽度和高度阈值,将图片自动归入以下四个类别:
- 宽 > 阈值:图片宽度大于您设定的值
- 高 > 阈值:图片高度大于您设定的值
- 宽高均大:宽度和高度都大于对应阈值
- 其他:宽度和高度均不大于阈值
如何使用
- 添加图片:点击上传区域或拖拽图片到其中,支持 JPG、PNG、WEBP 等常见格式。
- 设置尺寸阈值:在“宽度”和“高度”输入框中设定您的分类标准(单位:像素)。
- 选择目标文件夹:点击“选择目标文件夹”按钮,选定一个本地文件夹,工具会在其中自动创建四个子文件夹。
- 执行分类:点击“按尺寸分类移动”按钮,图片将根据尺寸被复制到对应的子文件夹中,原图不会丢失。
优化建议: 您可以根据需要调整 widthLimit 和 heightLimit 输入框的默认值(当前为800和600)。
标签: 批量移动
版权声明:除非特别标注,否则均为本站原创文章,转载时请以链接形式注明文章出处。