图片批量分类工具按尺寸移动

联启 电脑工具 15

本文目录导读:

图片批量分类工具按尺寸移动-第1张图片-电脑手机工具软件下载 - 免费实用工具合集 | 联启科技

  1. 按尺寸分类逻辑
  2. 如何使用
<!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>

按尺寸分类逻辑

您可以通过设定宽度和高度阈值,将图片自动归入以下四个类别:

  • 宽 > 阈值:图片宽度大于您设定的值
  • 高 > 阈值:图片高度大于您设定的值
  • 宽高均大:宽度和高度都大于对应阈值
  • 其他:宽度和高度均不大于阈值

如何使用

  1. 添加图片:点击上传区域或拖拽图片到其中,支持 JPG、PNG、WEBP 等常见格式。
  2. 设置尺寸阈值:在“宽度”和“高度”输入框中设定您的分类标准(单位:像素)。
  3. 选择目标文件夹:点击“选择目标文件夹”按钮,选定一个本地文件夹,工具会在其中自动创建四个子文件夹。
  4. 执行分类:点击“按尺寸分类移动”按钮,图片将根据尺寸被复制到对应的子文件夹中,原图不会丢失。

优化建议: 您可以根据需要调整 widthLimitheightLimit 输入框的默认值(当前为800和600)。

标签: 批量移动

抱歉,评论功能暂时关闭!