import color from "picocolors"; import { createStepUI } from "../../utils/stepui.js"; import { title, getSteps } from "./config.js"; import { stopKeypress, waitForKey, initKeypress } from "../../keyboard.js"; import { record } from "../../stats.js"; import { processImage } from "./service.js"; import path from "path"; const OPERATIONS = [ { id: "compress", name: "压缩", stepIndex: 1, folder: "compressed" }, { id: "resize", name: "生成多尺寸", stepIndex: 2, folder: "resized" }, { id: "background", name: "统一背景", stepIndex: 3, folder: "background" }, { id: "crop", name: "裁剪补边", stepIndex: 4, folder: "cropped" } ]; const run = async () => { try { // 强制清理并重新初始化键盘 stopKeypress(); await new Promise(resolve => setTimeout(resolve, 100)); const steps = getSteps(); const ui = createStepUI({ title, getSteps: () => steps, validate: (results) => { // 验证是否有文件选择且至少有一个操作未跳过 const files = results[0] || []; const hasOperation = OPERATIONS.some(op => results[op.stepIndex] !== "skip"); return files.length > 0 && hasOperation; } }); const result = await ui.runInteractive(); if (!result) { stopKeypress(); return "back"; } const files = result.results[0] || []; const selectedOp = OPERATIONS.find(op => result.results[op.stepIndex] !== "skip"); stopKeypress(); const params = buildProcessParams(selectedOp.id, result.results[selectedOp.stepIndex]); ui.showSummary([ "源文件: " + files.join(", "), "操作: " + selectedOp.name, getParamLabel(selectedOp.id, params) ]); const total = files.length; let success = 0; const failed = []; console.log(color.cyan(`开始处理 ${total} 个文件...\\n`)); for (let i = 0; i < files.length; i++) { const file = files[i]; const progress = `[${i + 1}/${total}]`; console.log(color.dim(`${progress} 处理中: ${file}`)); try { const outputs = await processImage(file, selectedOp.id, params); success++; record("image_operation", selectedOp.id); const outputNames = Array.isArray(outputs) ? outputs.map(o => path.relative(process.cwd(), o)).join(", ") : path.relative(process.cwd(), outputs); console.log(color.green(`${progress} ✓ ${file} → ${outputNames}`)); } catch (err) { failed.push({ file, reason: err?.message || String(err) }); console.log(color.red(`${progress} ✖ 失败: ${file}`)); console.log(color.dim(" " + String(err?.message || err))); } } console.log("\\n" + color.bgGreen(color.black(" 处理完成 "))); console.log(color.green(`成功: ${success} 个`)); if (failed.length) console.log(color.yellow(`失败: ${failed.length} 个`)); console.log(color.cyan(`输出目录: ${selectedOp.folder}/`)); await waitForKey(color.dim("按 Esc 返回"), key => key?.name === "escape" || (key?.ctrl && key?.name === "c")); stopKeypress(); return "back"; } catch (err) { stopKeypress(); console.error(color.red("发生错误:"), err); return "back"; } }; function getParamLabel(operation, params) { switch (operation) { case "compress": return `压缩质量: ${params.quality}%`; case "resize": const presets = { responsive: "响应式 (800/400/200)", thumbnail: "缩略图 (300/150/75)", large: "大图 (1920/1280/640)" }; return `尺寸预设: ${presets[params.sizes] || "响应式"}`; case "background": return `背景颜色: ${params.bgColor === 0xFFFFFFFF ? "白色" : "其他"}`; case "crop": return `画布尺寸: ${params.width}×${params.height}`; default: return ""; } } function buildProcessParams(operation, paramValue) { switch (operation) { case "compress": return { quality: paramValue }; case "resize": const sizePresets = { responsive: [{ width: 800 }, { width: 400 }, { width: 200 }], thumbnail: [{ width: 300 }, { width: 150 }, { width: 75 }], large: [{ width: 1920 }, { width: 1280 }, { width: 640 }] }; return { sizes: sizePresets[paramValue] || sizePresets.responsive }; case "background": return { bgColor: paramValue }; case "crop": const [width, height] = paramValue.split("x").map(Number); return { width, height, bgColor: 0xFFFFFFFF }; default: return null; } } export default { id: "image", name: "图片批量处理", desc: "裁剪/缩放/转换", run, };