import color from "picocolors"; import { initKeypress, onKey, stopKeypress } from "../keyboard.js"; import { clearScreen } from "./terminal.js"; export function createStepUI(options) { const { title, getSteps, onStepChange, validate } = options; let steps = []; let results = []; let completed = new Set(); let currentStep = 0; let currentOption = 0; let resolved = false; function initResults() { steps = typeof getSteps === "function" ? getSteps() : getSteps; results = steps.map(s => s.type === "multiselect" ? [...(s.default || [])] : s.default); completed = new Set(); // 有默认值的步骤自动标记为已完成 steps.forEach((s, i) => { if (s.default && s.default !== "none" && (!Array.isArray(s.default) || s.default.length > 0)) { completed.add(i); } }); currentStep = 0; currentOption = 0; resolved = false; } function updateSteps(newSteps) { steps = newSteps; // 保留第一个步骤的结果,重置其他步骤为默认值 const firstResult = results[0]; results = steps.map((s, i) => { if (i === 0) return firstResult; return s.type === "multiselect" ? [...(s.default || [])] : s.default; }); if (currentStep >= steps.length) currentStep = steps.length - 1; } function renderNav() { const nav = steps.map((step, i) => { if (step.disabled) return color.dim("□ " + step.name); if (i === currentStep) return color.bgCyan(color.black(" " + step.name + " ")); return color.dim(step.name); }); return "← " + nav.join(" ") + " " + color.green("✓Submit") + " →"; } function renderOptions() { const step = steps[currentStep]; const lines = [color.cyan(step.message), ""]; if (!step.options || !step.options.length) { lines.push(color.dim(step.emptyMessage || "无可用选项")); return lines.join("\n"); } const termHeight = process.stdout.rows || 24; const maxVisible = Math.max(5, termHeight - 8); const total = step.options.length; let start = 0; if (total > maxVisible) { start = Math.max(0, Math.min(currentOption - Math.floor(maxVisible / 2), total - maxVisible)); } const end = Math.min(start + maxVisible, total); if (start > 0) lines.push(color.dim(" ↑ 更多选项...")); for (let i = start; i < end; i++) { const opt = step.options[i]; const isCurrent = i === currentOption; const isSelected = step.type === "multiselect" ? results[currentStep]?.includes(opt.value) : results[currentStep] === opt.value; const prefix = step.type === "multiselect" ? (isSelected ? color.green("◉ ") : "○ ") : (isSelected ? color.green("● ") : "○ "); const cursor = isCurrent ? color.cyan("❯ ") : " "; const label = isCurrent ? color.cyan(opt.label) : opt.label; const check = isSelected ? color.green(" ✓") : ""; lines.push(cursor + prefix + label + check); } if (end < total) lines.push(color.dim(" ↓ 更多选项...")); return lines.join("\n"); } function render() { clearScreen(); console.log(color.bgCyan(color.black(` ${title} `))); console.log("\n" + renderNav()); console.log(color.dim("\n← → 切换步骤 | ↑ ↓ 选择 | Space 选中 | Tab 提交 | Esc 返回\n")); console.log(renderOptions()); } function findPrevStep(from) { for (let i = from - 1; i >= 0; i--) { if (!steps[i].disabled) return i; } return from; } function findNextStep(from) { for (let i = from + 1; i < steps.length; i++) { if (!steps[i].disabled) return i; } return from; } function handleKey(key, resolve) { if (!key || resolved) return; const step = steps[currentStep]; const optCount = step.options?.length || 0; switch (key.name) { case "left": const prevStep = findPrevStep(currentStep); if (prevStep !== currentStep) { currentStep = prevStep; currentOption = 0; } render(); break; case "right": const nextStep = findNextStep(currentStep); if (nextStep !== currentStep) { currentStep = nextStep; currentOption = 0; } render(); break; case "up": if (optCount) { currentOption = (currentOption - 1 + optCount) % optCount; render(); } break; case "down": if (optCount) { currentOption = (currentOption + 1) % optCount; render(); } break; case "space": if (optCount) { const opt = step.options[currentOption]; if (step.type === "multiselect") { const list = results[currentStep]; const idx = list.indexOf(opt.value); if (idx >= 0) list.splice(idx, 1); else list.push(opt.value); } else { results[currentStep] = opt.value; } completed.add(currentStep); if (onStepChange && currentStep === 0) { const newSteps = onStepChange(results[0]); if (newSteps) updateSteps(newSteps); } render(); } break; case "return": if (optCount && step.type === "select") { results[currentStep] = step.options[currentOption].value; completed.add(currentStep); if (onStepChange && currentStep === 0) { const newSteps = onStepChange(results[0]); if (newSteps) updateSteps(newSteps); } const next = findNextStep(currentStep); if (next !== currentStep) { currentStep = next; currentOption = 0; } render(); } break; case "tab": // 如果提供了验证函数,先验证 if (validate && !validate(results)) { // 验证失败,忽略此次提交 break; } resolved = true; stopKeypress(); setImmediate(() => resolve({ steps, results })); break; case "escape": resolved = true; stopKeypress(); setImmediate(() => resolve(null)); break; case "c": if (key.ctrl) { resolved = true; stopKeypress(); setImmediate(() => resolve(null)); } break; } } function runInteractive() { initResults(); initKeypress(); return new Promise(resolve => { render(); onKey((str, key) => handleKey(key, resolve)); }); } function showSummary(lines) { clearScreen(); console.log(color.bgCyan(color.black(` ${title} `))); console.log("\n" + color.green("配置完成!当前设置:")); lines.forEach(line => console.log(" " + line)); console.log(); } return { runInteractive, showSummary, initResults }; }