优化卡死的问题
This commit is contained in:
116
lib/grid.js
116
lib/grid.js
@ -1,17 +1,10 @@
|
||||
import color from "picocolors";
|
||||
import { initKeypress, onKey } from "./keyboard.js";
|
||||
import { initKeypress, onKey, stopKeypress } from "./keyboard.js";
|
||||
|
||||
function clearScreen() {
|
||||
if (process.stdout.isTTY) {
|
||||
process.stdout.write("\x1b[2J"); // clear screen
|
||||
process.stdout.write("\x1b[3J"); // clear scrollback
|
||||
process.stdout.write("\x1b[H"); // move cursor home
|
||||
} else {
|
||||
console.clear();
|
||||
}
|
||||
process.stdout.write('\x1Bc');
|
||||
}
|
||||
|
||||
// 计算字符串显示宽度
|
||||
function strWidth(str) {
|
||||
let width = 0;
|
||||
for (const char of str) {
|
||||
@ -20,25 +13,21 @@ function strWidth(str) {
|
||||
return width;
|
||||
}
|
||||
|
||||
// 右填充到指定宽度
|
||||
function padEnd(str, width) {
|
||||
const diff = width - strWidth(str);
|
||||
return str + " ".repeat(Math.max(0, diff));
|
||||
return str + " ".repeat(Math.max(0, width - strWidth(str)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 网格选择菜单
|
||||
*/
|
||||
export async function gridSelect(options) {
|
||||
const {
|
||||
items,
|
||||
cols = 3,
|
||||
const {
|
||||
items,
|
||||
cols = 3,
|
||||
colWidth = 24,
|
||||
title = "",
|
||||
renderHeader = null
|
||||
renderHeader = null
|
||||
} = options;
|
||||
|
||||
let current = 0;
|
||||
let resolved = false;
|
||||
const rows = Math.ceil(items.length / cols);
|
||||
const termWidth = process.stdout.columns || 80;
|
||||
const totalWidth = cols * colWidth;
|
||||
@ -46,83 +35,90 @@ export async function gridSelect(options) {
|
||||
|
||||
function render() {
|
||||
clearScreen();
|
||||
|
||||
|
||||
if (renderHeader) {
|
||||
console.log(renderHeader());
|
||||
}
|
||||
|
||||
|
||||
console.log("");
|
||||
|
||||
|
||||
if (title) {
|
||||
const titlePad = " ".repeat(Math.max(0, Math.floor((termWidth - title.length - 4) / 2)));
|
||||
console.log(titlePad + color.bgMagenta(color.white(` ${title} `)));
|
||||
}
|
||||
|
||||
|
||||
console.log("");
|
||||
console.log(pad + color.dim("↑ ↓ ← → 选择 | Enter 确认 | Esc 退出"));
|
||||
console.log("");
|
||||
console.log("");
|
||||
console.log("\n");
|
||||
|
||||
for (let row = 0; row < rows; row++) {
|
||||
let line = "";
|
||||
let descLine = "";
|
||||
|
||||
for (let col = 0; col < cols; col++) {
|
||||
const idx = row * cols + col;
|
||||
if (idx < items.length) {
|
||||
const item = items[idx];
|
||||
if (idx === current) {
|
||||
line += color.cyan("[" + item.name + "]");
|
||||
line += " ".repeat(colWidth - strWidth(item.name) - 2);
|
||||
} else {
|
||||
line += " " + item.name + " ";
|
||||
line += " ".repeat(colWidth - strWidth(item.name) - 2);
|
||||
}
|
||||
line += " ".repeat(colWidth - strWidth(item.name) - 2);
|
||||
descLine += padEnd(item.desc || "", colWidth);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(pad + line);
|
||||
|
||||
let descLine = "";
|
||||
for (let col = 0; col < cols; col++) {
|
||||
const idx = row * cols + col;
|
||||
if (idx < items.length) {
|
||||
descLine += padEnd(items[idx].desc || "", colWidth);
|
||||
}
|
||||
}
|
||||
console.log(pad + color.dim(descLine));
|
||||
console.log("");
|
||||
console.log("");
|
||||
console.log("\n");
|
||||
}
|
||||
}
|
||||
|
||||
initKeypress();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
initKeypress();
|
||||
render();
|
||||
|
||||
onKey((str, key) => {
|
||||
if (!key) return;
|
||||
if (!key || resolved) return;
|
||||
|
||||
const row = Math.floor(current / cols);
|
||||
const col = current % cols;
|
||||
|
||||
if (key.name === "up" && row > 0) {
|
||||
current -= cols;
|
||||
render();
|
||||
} else if (key.name === "down" && row < rows - 1 && current + cols < items.length) {
|
||||
current += cols;
|
||||
render();
|
||||
} else if (key.name === "left" && col > 0) {
|
||||
current--;
|
||||
render();
|
||||
} else if (key.name === "right" && col < cols - 1 && current < items.length - 1) {
|
||||
current++;
|
||||
render();
|
||||
} else if (key.name === "return") {
|
||||
setImmediate(() => resolve(items[current]));
|
||||
} else if (key.name === "escape" || (key.ctrl && key.name === "c")) {
|
||||
process.stdin.setRawMode(false);
|
||||
clearScreen();
|
||||
console.log(color.yellow("👋 再见!"));
|
||||
process.exit(0);
|
||||
switch (key.name) {
|
||||
case "up":
|
||||
if (row > 0) { current -= cols; render(); }
|
||||
break;
|
||||
case "down":
|
||||
if (row < rows - 1 && current + cols < items.length) { current += cols; render(); }
|
||||
break;
|
||||
case "left":
|
||||
if (col > 0) { current--; render(); }
|
||||
break;
|
||||
case "right":
|
||||
if (col < cols - 1 && current < items.length - 1) { current++; render(); }
|
||||
break;
|
||||
case "return":
|
||||
resolved = true;
|
||||
stopKeypress();
|
||||
setImmediate(() => resolve(items[current]));
|
||||
break;
|
||||
case "escape":
|
||||
resolved = true;
|
||||
stopKeypress();
|
||||
clearScreen();
|
||||
console.log(color.yellow("再见!"));
|
||||
process.exit(0);
|
||||
break;
|
||||
case "c":
|
||||
if (key.ctrl) {
|
||||
resolved = true;
|
||||
stopKeypress();
|
||||
clearScreen();
|
||||
console.log(color.yellow("再见!"));
|
||||
process.exit(0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,37 +1,48 @@
|
||||
import readline from "readline";
|
||||
|
||||
let initialized = false;
|
||||
let currentHandler = null;
|
||||
|
||||
function ensureRawMode(enabled) {
|
||||
if (process.stdin.isTTY) {
|
||||
process.stdin.setRawMode(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
export function initKeypress() {
|
||||
// 清理所有keypress监听器
|
||||
process.stdin.removeAllListeners("keypress");
|
||||
currentHandler = null;
|
||||
|
||||
// 初始化readline(只需一次)
|
||||
if (!initialized) {
|
||||
readline.emitKeypressEvents(process.stdin);
|
||||
initialized = true;
|
||||
}
|
||||
process.stdin.removeAllListeners("keypress");
|
||||
if (process.stdin.isTTY) {
|
||||
process.stdin.setRawMode(true);
|
||||
}
|
||||
|
||||
ensureRawMode(true);
|
||||
}
|
||||
|
||||
export function onKey(handler) {
|
||||
// 清理旧的监听器
|
||||
process.stdin.removeAllListeners("keypress");
|
||||
currentHandler = handler;
|
||||
process.stdin.on("keypress", handler);
|
||||
}
|
||||
|
||||
export function stopKeypress() {
|
||||
process.stdin.removeAllListeners("keypress");
|
||||
if (process.stdin.isTTY) {
|
||||
process.stdin.setRawMode(false);
|
||||
}
|
||||
currentHandler = null;
|
||||
ensureRawMode(false);
|
||||
}
|
||||
|
||||
export function waitForKey(message = "按任意键返回...") {
|
||||
return new Promise(resolve => {
|
||||
console.log("\n" + message);
|
||||
initKeypress();
|
||||
const handler = () => {
|
||||
onKey(() => {
|
||||
stopKeypress();
|
||||
resolve();
|
||||
};
|
||||
process.stdin.once("keypress", handler);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -26,6 +26,7 @@ const tools = [
|
||||
{ name: "Sprite图集", desc: "合并精灵图集", module: "./lib/sprite/index.js" },
|
||||
{ name: "LOD生成器", desc: "生成多级细节", module: "./lib/lod/index.js" },
|
||||
{ name: "音频压缩", desc: "压缩音频文件", module: "./lib/audio/index.js" },
|
||||
{ name: "项目脚手架", desc: "快速创建项目模板", module: "./lib/scaffold/index.js" },
|
||||
];
|
||||
|
||||
export function setPoem(lines, perLine = 2) {
|
||||
|
||||
158
lib/scaffold/config.js
Normal file
158
lib/scaffold/config.js
Normal file
@ -0,0 +1,158 @@
|
||||
// 二级菜单 - 项目类型
|
||||
export const projectTypes = [
|
||||
{ name: "前端", desc: "React/Vue项目" },
|
||||
{ name: "后端", desc: "Node.js服务端" },
|
||||
{ name: "全栈", desc: "前后端一体化" },
|
||||
];
|
||||
|
||||
// 三级菜单 - 具体框架
|
||||
export const frameworks = {
|
||||
前端: [
|
||||
{ name: "React", desc: "Vite + React", value: "react-vite" },
|
||||
{ name: "Vue", desc: "Vite + Vue", value: "vue-vite" },
|
||||
],
|
||||
后端: [
|
||||
{ name: "Bun", desc: "Bun运行时", value: "bun" },
|
||||
{ name: "NestJS", desc: "企业级框架", value: "nestjs" },
|
||||
{ name: "Express", desc: "Express Generator", value: "express" },
|
||||
{ name: "Koa", desc: "Koa Generator", value: "koa" },
|
||||
{ name: "Egg", desc: "阿里Egg.js", value: "egg" },
|
||||
{ name: "Midway", desc: "阿里Midway", value: "midway" },
|
||||
{ name: "Fastify", desc: "高性能框架", value: "fastify" },
|
||||
{ name: "AdonisJS", desc: "全功能MVC", value: "adonisjs" },
|
||||
],
|
||||
全栈: [
|
||||
{ name: "Next.js", desc: "React全栈框架", value: "nextjs" },
|
||||
{ name: "Nuxt", desc: "Vue全栈框架", value: "nuxt" },
|
||||
],
|
||||
};
|
||||
|
||||
// 前端组件配置步骤
|
||||
export const frontendSteps = [
|
||||
{
|
||||
name: "路由",
|
||||
type: "select",
|
||||
message: "选择路由方案",
|
||||
options: [
|
||||
{ value: "none", label: "不需要路由" },
|
||||
{ value: "react-router", label: "React Router(React项目)" },
|
||||
{ value: "vue-router", label: "Vue Router(Vue项目)" },
|
||||
],
|
||||
default: "none"
|
||||
},
|
||||
{
|
||||
name: "状态管理",
|
||||
type: "select",
|
||||
message: "选择状态管理方案",
|
||||
options: [
|
||||
{ value: "none", label: "不需要状态管理" },
|
||||
{ value: "zustand", label: "Zustand(轻量级,React推荐)" },
|
||||
{ value: "pinia", label: "Pinia(Vue官方推荐)" },
|
||||
{ value: "redux", label: "Redux Toolkit(大型项目)" },
|
||||
{ value: "mobx", label: "MobX(响应式状态管理)" },
|
||||
],
|
||||
default: "none"
|
||||
},
|
||||
{
|
||||
name: "HTTP请求",
|
||||
type: "select",
|
||||
message: "选择HTTP请求库",
|
||||
options: [
|
||||
{ value: "none", label: "不需要HTTP库" },
|
||||
{ value: "axios", label: "Axios(功能全面,拦截器支持)" },
|
||||
{ value: "ky", label: "Ky(轻量级,基于Fetch)" },
|
||||
{ value: "ofetch", label: "ofetch(Nuxt团队出品)" },
|
||||
],
|
||||
default: "none"
|
||||
},
|
||||
{
|
||||
name: "UI组件库",
|
||||
type: "select",
|
||||
message: "选择UI组件库",
|
||||
options: [
|
||||
{ value: "none", label: "不需要UI库" },
|
||||
{ value: "antd", label: "Ant Design(企业级,React)" },
|
||||
{ value: "element-plus", label: "Element Plus(饿了么,Vue)" },
|
||||
{ value: "arco", label: "Arco Design(字节跳动)" },
|
||||
{ value: "naive-ui", label: "Naive UI(Vue3原生)" },
|
||||
{ value: "shadcn", label: "shadcn/ui(可定制,React)" },
|
||||
],
|
||||
default: "none"
|
||||
},
|
||||
{
|
||||
name: "CSS方案",
|
||||
type: "select",
|
||||
message: "选择CSS方案",
|
||||
options: [
|
||||
{ value: "none", label: "原生CSS" },
|
||||
{ value: "tailwind", label: "Tailwind CSS(原子化CSS)" },
|
||||
{ value: "unocss", label: "UnoCSS(即时原子化)" },
|
||||
{ value: "sass", label: "Sass/SCSS(预处理器)" },
|
||||
{ value: "less", label: "Less(预处理器)" },
|
||||
{ value: "styled", label: "Styled Components(CSS-in-JS)" },
|
||||
],
|
||||
default: "none"
|
||||
},
|
||||
{
|
||||
name: "工具库",
|
||||
type: "multiselect",
|
||||
message: "选择常用工具库",
|
||||
options: [
|
||||
{ value: "lodash", label: "Lodash(工具函数库)" },
|
||||
{ value: "dayjs", label: "Day.js(日期处理)" },
|
||||
{ value: "iconify", label: "Iconify(图标库)" },
|
||||
{ value: "vueuse", label: "VueUse(Vue组合式工具)" },
|
||||
{ value: "ahooks", label: "ahooks(React Hooks库)" },
|
||||
],
|
||||
default: []
|
||||
},
|
||||
{
|
||||
name: "表单验证",
|
||||
type: "select",
|
||||
message: "选择表单验证方案",
|
||||
options: [
|
||||
{ value: "none", label: "不需要表单验证" },
|
||||
{ value: "zod", label: "Zod(TypeScript优先)" },
|
||||
{ value: "yup", label: "Yup(声明式验证)" },
|
||||
{ value: "vee-validate", label: "VeeValidate(Vue专用)" },
|
||||
{ value: "react-hook-form", label: "React Hook Form" },
|
||||
],
|
||||
default: "none"
|
||||
},
|
||||
{
|
||||
name: "国际化",
|
||||
type: "select",
|
||||
message: "选择国际化方案",
|
||||
options: [
|
||||
{ value: "none", label: "不需要国际化" },
|
||||
{ value: "i18next", label: "i18next(通用方案)" },
|
||||
{ value: "vue-i18n", label: "Vue I18n(Vue专用)" },
|
||||
],
|
||||
default: "none"
|
||||
},
|
||||
{
|
||||
name: "代码规范",
|
||||
type: "multiselect",
|
||||
message: "选择代码规范工具",
|
||||
options: [
|
||||
{ value: "eslint", label: "ESLint(代码检查)" },
|
||||
{ value: "prettier", label: "Prettier(代码格式化)" },
|
||||
{ value: "husky", label: "Husky(Git Hooks)" },
|
||||
{ value: "lint-staged", label: "lint-staged(暂存区检查)" },
|
||||
{ value: "commitlint", label: "Commitlint(提交信息规范)" },
|
||||
],
|
||||
default: []
|
||||
},
|
||||
{
|
||||
name: "其他",
|
||||
type: "multiselect",
|
||||
message: "选择其他功能",
|
||||
options: [
|
||||
{ value: "mock", label: "Mock.js(模拟数据)" },
|
||||
{ value: "pwa", label: "PWA支持" },
|
||||
{ value: "storage", label: "持久化存储封装" },
|
||||
{ value: "env", label: "环境变量配置" },
|
||||
],
|
||||
default: []
|
||||
}
|
||||
];
|
||||
39
lib/scaffold/index.js
Normal file
39
lib/scaffold/index.js
Normal file
@ -0,0 +1,39 @@
|
||||
import color from "picocolors";
|
||||
import { projectTypes, frameworks } from "./config.js";
|
||||
import { gridSelect, createComponentUI, formatResults, waitKey, showPlaceholder } from "./ui.js";
|
||||
|
||||
export async function run() {
|
||||
while (true) {
|
||||
// 二级菜单 - 项目类型
|
||||
const typeResult = await gridSelect(projectTypes, "项目脚手架 - 选择类型");
|
||||
if (typeResult.action === "back") return "back";
|
||||
|
||||
const projectType = typeResult.item.name;
|
||||
const frameworkList = frameworks[projectType];
|
||||
|
||||
while (true) {
|
||||
// 三级菜单 - 框架选择
|
||||
const frameworkResult = await gridSelect(frameworkList, `${projectType}项目 - 选择框架`);
|
||||
if (frameworkResult.action === "back") break;
|
||||
|
||||
const framework = frameworkResult.item;
|
||||
|
||||
if (projectType === "前端" || projectType === "全栈") {
|
||||
// 组件配置
|
||||
const ui = createComponentUI(framework.name);
|
||||
const result = await ui.runInteractive();
|
||||
|
||||
if (result) {
|
||||
const summary = formatResults(result.results);
|
||||
ui.showSummary(summary);
|
||||
console.log(color.yellow("功能开发中,敬请期待..."));
|
||||
await waitKey();
|
||||
}
|
||||
} else {
|
||||
// 后端暂无组件配置
|
||||
showPlaceholder(framework);
|
||||
await waitKey();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
152
lib/scaffold/ui.js
Normal file
152
lib/scaffold/ui.js
Normal file
@ -0,0 +1,152 @@
|
||||
import color from "picocolors";
|
||||
import { initKeypress, onKey, stopKeypress } from "../keyboard.js";
|
||||
import { createStepUI } from "../utils/stepui.js";
|
||||
import { frontendSteps } from "./config.js";
|
||||
|
||||
function clearScreen() {
|
||||
process.stdout.write('\x1Bc');
|
||||
}
|
||||
|
||||
function strWidth(str) {
|
||||
let width = 0;
|
||||
for (const char of str) {
|
||||
width += char.charCodeAt(0) > 127 ? 2 : 1;
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
function padEnd(str, width) {
|
||||
return str + " ".repeat(Math.max(0, width - strWidth(str)));
|
||||
}
|
||||
|
||||
// 网格选择器
|
||||
export async function gridSelect(items, title) {
|
||||
let current = 0;
|
||||
let resolved = false;
|
||||
const termWidth = process.stdout.columns || 80;
|
||||
const cols = Math.min(3, items.length);
|
||||
const colWidth = 20;
|
||||
const rows = Math.ceil(items.length / cols);
|
||||
const totalWidth = cols * colWidth;
|
||||
const pad = " ".repeat(Math.max(0, Math.floor((termWidth - totalWidth) / 2)));
|
||||
|
||||
function render() {
|
||||
clearScreen();
|
||||
console.log("");
|
||||
const titlePad = " ".repeat(Math.max(0, Math.floor((termWidth - strWidth(title) - 4) / 2)));
|
||||
console.log(titlePad + color.bgMagenta(color.white(` ${title} `)));
|
||||
console.log("");
|
||||
console.log(pad + color.dim("↑ ↓ ← → 选择 | Enter 确认 | Esc 返回"));
|
||||
console.log("\n");
|
||||
|
||||
for (let row = 0; row < rows; row++) {
|
||||
let line = "";
|
||||
let descLine = "";
|
||||
|
||||
for (let col = 0; col < cols; col++) {
|
||||
const idx = row * cols + col;
|
||||
if (idx < items.length) {
|
||||
const item = items[idx];
|
||||
const isSelected = idx === current;
|
||||
|
||||
if (isSelected) {
|
||||
line += color.cyan("[" + item.name + "]");
|
||||
} else {
|
||||
line += " " + item.name + " ";
|
||||
}
|
||||
line += " ".repeat(colWidth - strWidth(item.name) - 2);
|
||||
descLine += padEnd(item.desc || "", colWidth);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(pad + line);
|
||||
console.log(pad + color.dim(descLine));
|
||||
console.log("\n");
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
initKeypress();
|
||||
render();
|
||||
|
||||
onKey((str, key) => {
|
||||
if (!key || resolved) return;
|
||||
|
||||
const row = Math.floor(current / cols);
|
||||
const col = current % cols;
|
||||
|
||||
switch (key.name) {
|
||||
case "up":
|
||||
if (row > 0) { current -= cols; render(); }
|
||||
break;
|
||||
case "down":
|
||||
if (row < rows - 1 && current + cols < items.length) { current += cols; render(); }
|
||||
break;
|
||||
case "left":
|
||||
if (col > 0) { current--; render(); }
|
||||
break;
|
||||
case "right":
|
||||
if (col < cols - 1 && current < items.length - 1) { current++; render(); }
|
||||
break;
|
||||
case "return":
|
||||
resolved = true;
|
||||
stopKeypress();
|
||||
setImmediate(() => resolve({ action: "select", item: items[current] }));
|
||||
break;
|
||||
case "escape":
|
||||
resolved = true;
|
||||
stopKeypress();
|
||||
setImmediate(() => resolve({ action: "back" }));
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 创建组件配置UI
|
||||
export function createComponentUI(frameworkName) {
|
||||
return createStepUI({
|
||||
title: `${frameworkName} - 组件配置`,
|
||||
getSteps: () => frontendSteps
|
||||
});
|
||||
}
|
||||
|
||||
// 解析配置结果
|
||||
export function formatResults(results) {
|
||||
const stepNames = frontendSteps.map(s => s.name);
|
||||
const summary = [];
|
||||
|
||||
results.forEach((val, i) => {
|
||||
if (Array.isArray(val) && val.length > 0) {
|
||||
summary.push(`${stepNames[i]}: ${val.join(", ")}`);
|
||||
} else if (val && val !== "none") {
|
||||
summary.push(`${stepNames[i]}: ${val}`);
|
||||
}
|
||||
});
|
||||
|
||||
return summary.length ? summary : ["未选择任何组件"];
|
||||
}
|
||||
|
||||
// 等待按键
|
||||
export async function waitKey(message = "按任意键返回") {
|
||||
console.log(color.dim(`\n${message}`));
|
||||
|
||||
return new Promise(resolve => {
|
||||
initKeypress();
|
||||
onKey(() => {
|
||||
stopKeypress();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 显示占位信息
|
||||
export function showPlaceholder(framework) {
|
||||
clearScreen();
|
||||
console.log("");
|
||||
console.log(color.bgGreen(color.black(" 配置完成 ")));
|
||||
console.log("");
|
||||
console.log(color.cyan("框架: ") + framework.name);
|
||||
console.log("");
|
||||
console.log(color.yellow("功能开发中,敬请期待..."));
|
||||
}
|
||||
@ -1,5 +1,9 @@
|
||||
import color from "picocolors";
|
||||
import { initKeypress, onKey } from "../keyboard.js";
|
||||
import { initKeypress, onKey, stopKeypress } from "../keyboard.js";
|
||||
|
||||
function clearScreen() {
|
||||
process.stdout.write('\x1Bc');
|
||||
}
|
||||
|
||||
export function createStepUI(options) {
|
||||
const { title, getSteps } = options;
|
||||
@ -9,6 +13,7 @@ export function createStepUI(options) {
|
||||
let completed = new Set();
|
||||
let currentStep = 0;
|
||||
let currentOption = 0;
|
||||
let resolved = false;
|
||||
|
||||
function initResults() {
|
||||
steps = typeof getSteps === "function" ? getSteps() : getSteps;
|
||||
@ -16,6 +21,7 @@ export function createStepUI(options) {
|
||||
completed = new Set();
|
||||
currentStep = 0;
|
||||
currentOption = 0;
|
||||
resolved = false;
|
||||
}
|
||||
|
||||
function renderNav() {
|
||||
@ -35,13 +41,10 @@ export function createStepUI(options) {
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
// 计算可显示的行数(终端高度 - 标题/导航/提示占用的行数)
|
||||
const termHeight = process.stdout.rows || 24;
|
||||
const reservedLines = 8;
|
||||
const maxVisible = Math.max(5, termHeight - reservedLines);
|
||||
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));
|
||||
@ -71,38 +74,42 @@ export function createStepUI(options) {
|
||||
}
|
||||
|
||||
function render() {
|
||||
process.stdout.write('\x1Bc');
|
||||
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 runInteractive() {
|
||||
initResults();
|
||||
initKeypress();
|
||||
return new Promise(resolve => {
|
||||
render();
|
||||
onKey((str, key) => {
|
||||
if (!key) return;
|
||||
const step = steps[currentStep];
|
||||
const optCount = step.options?.length || 0;
|
||||
if (key.name === "left") {
|
||||
if (currentStep > 0) { currentStep--; currentOption = 0; }
|
||||
render();
|
||||
} else if (key.name === "right") {
|
||||
if (currentStep < steps.length - 1) { currentStep++; currentOption = 0; }
|
||||
render();
|
||||
} else if (key.name === "up") {
|
||||
if (!optCount) return;
|
||||
function handleKey(key, resolve) {
|
||||
if (!key || resolved) return;
|
||||
|
||||
const step = steps[currentStep];
|
||||
const optCount = step.options?.length || 0;
|
||||
|
||||
switch (key.name) {
|
||||
case "left":
|
||||
if (currentStep > 0) { currentStep--; currentOption = 0; }
|
||||
render();
|
||||
break;
|
||||
case "right":
|
||||
if (currentStep < steps.length - 1) { currentStep++; currentOption = 0; }
|
||||
render();
|
||||
break;
|
||||
case "up":
|
||||
if (optCount) {
|
||||
currentOption = (currentOption - 1 + optCount) % optCount;
|
||||
render();
|
||||
} else if (key.name === "down") {
|
||||
if (!optCount) return;
|
||||
}
|
||||
break;
|
||||
case "down":
|
||||
if (optCount) {
|
||||
currentOption = (currentOption + 1) % optCount;
|
||||
render();
|
||||
} else if (key.name === "space") {
|
||||
if (!optCount) return;
|
||||
}
|
||||
break;
|
||||
case "space":
|
||||
if (optCount) {
|
||||
const opt = step.options[currentOption];
|
||||
if (step.type === "multiselect") {
|
||||
const list = results[currentStep];
|
||||
@ -114,25 +121,48 @@ export function createStepUI(options) {
|
||||
}
|
||||
completed.add(currentStep);
|
||||
render();
|
||||
} else if (key.name === "return") {
|
||||
if (!optCount) return;
|
||||
if (step.type === "select") {
|
||||
results[currentStep] = step.options[currentOption].value;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "return":
|
||||
if (optCount && step.type === "select") {
|
||||
results[currentStep] = step.options[currentOption].value;
|
||||
completed.add(currentStep);
|
||||
if (currentStep < steps.length - 1) { currentStep++; currentOption = 0; }
|
||||
render();
|
||||
} else if (key.name === "tab") {
|
||||
resolve({ steps, results });
|
||||
} else if (key.name === "escape" || (key.ctrl && key.name === "c")) {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "tab":
|
||||
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) {
|
||||
console.clear();
|
||||
clearScreen();
|
||||
console.log(color.bgCyan(color.black(` ${title} `)));
|
||||
console.log("\n" + color.green("配置完成!当前设置:"));
|
||||
lines.forEach(line => console.log(" " + line));
|
||||
|
||||
Reference in New Issue
Block a user