本文目录导读:

<!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)。
标签: 批量移动
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播