优化卡死的问题

This commit is contained in:
yinsx
2025-12-20 14:13:59 +08:00
parent 3098796a08
commit ad60f8c3ec
7 changed files with 495 additions and 108 deletions

View File

@ -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,15 +13,10 @@ 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,
@ -39,6 +27,7 @@ export async function gridSelect(options) {
} = options;
let current = 0;
let resolved = false;
const rows = Math.ceil(items.length / cols);
const termWidth = process.stdout.columns || 80;
const totalWidth = cols * colWidth;
@ -60,69 +49,76 @@ export async function gridSelect(options) {
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);
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");
}
}
return new Promise(resolve => {
initKeypress();
return new Promise((resolve) => {
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") {
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]));
} else if (key.name === "escape" || (key.ctrl && key.name === "c")) {
process.stdin.setRawMode(false);
break;
case "escape":
resolved = true;
stopKeypress();
clearScreen();
console.log(color.yellow("👋 再见!"));
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;
}
});
});

View File

@ -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);
});
});
}

View File

@ -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
View 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 RouterReact项目" },
{ value: "vue-router", label: "Vue RouterVue项目" },
],
default: "none"
},
{
name: "状态管理",
type: "select",
message: "选择状态管理方案",
options: [
{ value: "none", label: "不需要状态管理" },
{ value: "zustand", label: "Zustand轻量级React推荐" },
{ value: "pinia", label: "PiniaVue官方推荐" },
{ 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: "ofetchNuxt团队出品" },
],
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 UIVue3原生" },
{ 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 ComponentsCSS-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: "VueUseVue组合式工具" },
{ value: "ahooks", label: "ahooksReact Hooks库" },
],
default: []
},
{
name: "表单验证",
type: "select",
message: "选择表单验证方案",
options: [
{ value: "none", label: "不需要表单验证" },
{ value: "zod", label: "ZodTypeScript优先" },
{ value: "yup", label: "Yup声明式验证" },
{ value: "vee-validate", label: "VeeValidateVue专用" },
{ 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 I18nVue专用" },
],
default: "none"
},
{
name: "代码规范",
type: "multiselect",
message: "选择代码规范工具",
options: [
{ value: "eslint", label: "ESLint代码检查" },
{ value: "prettier", label: "Prettier代码格式化" },
{ value: "husky", label: "HuskyGit 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
View 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
View 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("功能开发中,敬请期待..."));
}

View File

@ -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;
function handleKey(key, resolve) {
if (!key || resolved) return;
const step = steps[currentStep];
const optCount = step.options?.length || 0;
if (key.name === "left") {
switch (key.name) {
case "left":
if (currentStep > 0) { currentStep--; currentOption = 0; }
render();
} else if (key.name === "right") {
break;
case "right":
if (currentStep < steps.length - 1) { currentStep++; currentOption = 0; }
render();
} else if (key.name === "up") {
if (!optCount) return;
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));