143 lines
4.6 KiB
JavaScript
143 lines
4.6 KiB
JavaScript
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,
|
||
};
|