refactor(install): 统一安装脚本日志风格与UI样式

- 新增日志图标、分隔符和格式化工具函数
- 统一所有安装脚本的输出格式与视觉风格
- 使用统一的图标和配色方案优化交互体验
- 重构命令行输入提示样式,添加视觉标识
This commit is contained in:
zhang1106
2026-05-11 15:29:41 +08:00
parent 811c585845
commit 9a7a975b57
7 changed files with 310 additions and 227 deletions
+6 -8
View File
@@ -1,7 +1,7 @@
const fs = require('fs');
const { execSync } = require('child_process');
const { MIN_NODE_VERSION } = require('./constants');
const { colors, log } = require('./logger');
const { colors, ICONS, log } = require('./logger');
const { runCommand, commandExists } = require('./utils');
const { ask } = require('./ui');
@@ -75,7 +75,7 @@ function showNodeInstallGuide() {
const isMac = platform === 'darwin';
const isLinux = platform === 'linux';
console.log('\n' + colors.bright + 'Node.js 安装指引' + colors.reset);
log.section('Node.js 安装指引');
if (isLinux) {
const distro = detectLinuxDistro();
@@ -293,9 +293,8 @@ async function installNodeViaNvm() {
async function handleNodeNotInstalledLinux() {
log.error('未检测到 Node.js');
console.log('\n' + colors.yellow + '您可以选择:' + colors.reset);
console.log(` ${colors.cyan}1.${colors.reset} 自动安装 Node.js 20.x(推荐,需要 sudo 权限)`);
console.log(` ${colors.cyan}2.${colors.reset} 显示手动安装指引,退出脚本`);
console.log(` ${ICONS.pointer} ${colors.cyan}1.${colors.reset} 自动安装 Node.js 20.x(推荐,需要 sudo 权限)`);
console.log(` ${colors.dim}2.${colors.reset} 显示手动安装指引,退出脚本`);
const choice = await ask('请选择', '1');
@@ -362,9 +361,8 @@ async function checkEnvironment() {
log.error(`Node.js 版本过低 (${version}),需要 >= 14.0.0`);
if (isLinux) {
console.log('\n' + colors.yellow + '您可以选择:' + colors.reset);
console.log(` ${colors.cyan}1.${colors.reset} 自动升级到 Node.js 20.x`);
console.log(` ${colors.cyan}2.${colors.reset} 显示手动升级指引,退出脚本`);
console.log(` ${ICONS.pointer} ${colors.cyan}1.${colors.reset} 自动升级到 Node.js 20.x`);
console.log(` ${colors.dim}2.${colors.reset} 显示手动升级指引,退出脚本`);
const upgradeChoice = await ask('请选择', '1');
if (upgradeChoice === '1') {
+18 -14
View File
@@ -1,6 +1,6 @@
const fs = require('fs');
const path = require('path');
const { colors, log } = require('./logger');
const { colors, ICONS, log } = require('./logger');
const { generateSecretKey } = require('./utils');
const { config } = require('./config');
const { ask } = require('./ui');
@@ -213,8 +213,8 @@ server {
if (isWindows) {
log.info('Windows Nginx 配置路径示例:');
console.log(` 将配置文件复制到: ${colors.cyan}C:/nginx/conf/conf.d/idc.conf${colors.reset}`);
console.log(` 或修改主配置 include: ${colors.cyan}C:/nginx/conf/nginx.conf${colors.reset}`);
console.log(` ${ICONS.pipe} 将配置文件复制到: ${colors.cyan}C:/nginx/conf/conf.d/idc.conf${colors.reset}`);
console.log(` ${ICONS.pipe} 或修改主配置 include: ${colors.cyan}C:/nginx/conf/nginx.conf${colors.reset}`);
}
} catch (error) {
log.error(`Nginx 配置文件生成失败: ${error.message}`);
@@ -225,22 +225,26 @@ server {
async function confirmConfiguration() {
log.step('配置确认');
console.log('\n' + colors.bright + '部署配置摘要:' + colors.reset);
console.log(` 数据库类型: ${colors.cyan}${config.dbType}${colors.reset}`);
console.log(` ${colors.bright}部署配置摘要${colors.reset}`);
log.thickDivider();
log.keyValue('数据库类型', config.dbType);
if (config.dbType === 'mysql') {
console.log(` MySQL主机: ${colors.cyan}${config.dbConfig.host}:${config.dbConfig.port}${colors.reset}`);
console.log(` 数据库名: ${colors.cyan}${config.dbConfig.database}${colors.reset}`);
console.log(` 用户名: ${colors.cyan}${config.dbConfig.username}${colors.reset}`);
log.keyValue('MySQL 主机', `${config.dbConfig.host}:${config.dbConfig.port}`);
log.keyValue('数据库名', config.dbConfig.database);
log.keyValue('用户名', config.dbConfig.username);
}
console.log(` 后端端口: ${colors.cyan}${config.backendPort}${colors.reset}`);
console.log(` 运行环境: ${colors.cyan}${config.nodeEnv}${colors.reset}`);
console.log(` 前端部署: ${colors.cyan}${config.frontendDeploy}${colors.reset}`);
console.log(` 前端端口: ${colors.cyan}${config.frontendPort}${colors.reset}`);
log.keyValue('后端端口', String(config.backendPort));
log.keyValue('运行环境', config.nodeEnv);
log.keyValue('前端部署', config.frontendDeploy);
log.keyValue('前端端口', String(config.frontendPort));
if (config.frontendDeploy === 'nginx') {
console.log(` 域名: ${colors.cyan}${config.domain}${colors.reset}`);
log.keyValue('域名', config.domain);
}
const confirm = await ask('\n确认以上配置并开始部署? (Y/n)', 'Y');
log.thickDivider();
const confirm = await ask('确认以上配置并开始部署? (Y/n)', 'Y');
if (confirm.toLowerCase() !== 'y') {
log.info('已取消部署');
process.exit(0);
+52 -44
View File
@@ -1,5 +1,5 @@
const { SCRIPT_VERSION, LOG_DIR } = require('./constants');
const { colors, log, initLogFile, closeLogFile } = require('./logger');
const { colors, ICONS, log, padCenter } = require('./logger');
const { ask, closeReadline } = require('./ui');
const { config, saveConfig, loadSavedConfig, applySavedConfig, parseArgs, showHelp } = require('./config');
const { checkEnvironment } = require('./env-check');
@@ -14,35 +14,45 @@ let installStartTime = Date.now();
function printSummary() {
const duration = ((Date.now() - installStartTime) / 1000).toFixed(1);
console.log(`
${colors.bright}${colors.magenta}
╔══════════════════════════════════════════════════════════╗
║ 安装摘要 ║
╚══════════════════════════════════════════════════════════╝${colors.reset}`);
console.log(`\n ${colors.cyan}安装耗时:${colors.reset} ${duration}`);
console.log(` ${colors.cyan}数据库类型:${colors.reset} ${config.dbType}`);
console.log(` ${colors.cyan}后端端口:${colors.reset} ${config.backendPort}`);
console.log(` ${colors.cyan}前端部署:${colors.reset} ${config.frontendDeploy}`);
console.log(`\n${colors.bright}服务管理命令:${colors.reset}`);
console.log(` ${colors.cyan}pm2 status${colors.reset} 查看服务状态`);
console.log(` ${colors.cyan}pm2 logs idc-backend${colors.reset} 查看后端日志`);
console.log(` ${colors.cyan}pm2 restart idc-backend${colors.reset} 重启后端`);
console.log(` ${colors.cyan}pm2 stop idc-backend${colors.reset} 停止后端`);
console.log(`\n${colors.bright}访问地址:${colors.reset}`);
console.log(` 后端API: ${colors.cyan}http://localhost:${config.backendPort}/api${colors.reset}`);
log.banner(
'安装部署完成',
'Installation Complete',
null
);
console.log(` ${ICONS.pipe} ${colors.dim}安装耗时${colors.reset} ${colors.cyan}${duration}${colors.reset}`);
console.log(` ${ICONS.pipe} ${colors.dim}数据库类型${colors.reset} ${colors.cyan}${config.dbType}${colors.reset}`);
console.log(` ${ICONS.pipe} ${colors.dim}后端端口${colors.reset} ${colors.cyan}${config.backendPort}${colors.reset}`);
console.log(` ${ICONS.pipe} ${colors.dim}前端部署${colors.reset} ${colors.cyan}${config.frontendDeploy}${colors.reset}`);
if (config.frontendDeploy === 'nginx') {
console.log(` 前端页面: ${colors.cyan}http://${config.domain}:${config.frontendPort}${colors.reset}`);
} else {
console.log(` 前端页面: ${colors.cyan}http://localhost:${config.frontendPort}${colors.reset}`);
console.log(` ${ICONS.pipe} ${colors.dim}前端端口${colors.reset} ${colors.cyan}${config.frontendPort}${colors.reset}`);
console.log(` ${ICONS.pipe} ${colors.dim}域名${colors.reset} ${colors.cyan}${config.domain}${colors.reset}`);
}
console.log(`\n${colors.bright}更新升级:${colors.reset}`);
console.log(` ${colors.cyan}node update.js${colors.reset} 一键更新`);
log.divider();
log.section('服务管理命令');
console.log(` ${ICONS.pipe} ${colors.cyan}pm2 status${colors.reset} 查看服务状态`);
console.log(` ${ICONS.pipe} ${colors.cyan}pm2 logs idc-backend${colors.reset} 查看后端日志`);
console.log(` ${ICONS.pipe} ${colors.cyan}pm2 restart idc-backend${colors.reset} 重启后端`);
console.log(` ${ICONS.pipe} ${colors.cyan}pm2 stop idc-backend${colors.reset} 停止后端`);
log.divider();
log.section('访问地址');
console.log(` ${ICONS.pipe} ${colors.dim}后端 API${colors.reset} ${colors.cyan}http://localhost:${config.backendPort}/api${colors.reset}`);
if (config.frontendDeploy === 'nginx') {
console.log(` ${ICONS.pipe} ${colors.dim}前端页面${colors.reset} ${colors.cyan}http://${config.domain}:${config.frontendPort}${colors.reset}`);
} else {
console.log(` ${ICONS.pipe} ${colors.dim}前端页面${colors.reset} ${colors.cyan}http://localhost:${config.frontendPort}${colors.reset}`);
}
log.divider();
log.section('更新升级');
console.log(` ${ICONS.pipe} ${colors.cyan}node update.js${colors.reset} 一键更新`);
log.divider();
log.success('安装部署完成!');
}
@@ -58,13 +68,11 @@ async function main() {
initLogFile(LOG_DIR);
console.log(`
${colors.bright}${colors.cyan}
╔══════════════════════════════════════════════════════════╗
║ IDC设备管理系统 - 安装部署脚本 v${SCRIPT_VERSION}
║ Interactive Installation & Deployment Script ║
╚══════════════════════════════════════════════════════════╝
${colors.reset}`);
log.banner(
'IDC 设备管理系统',
'安装部署脚本',
SCRIPT_VERSION
);
if (cmdArgs.nonInteractive) {
log.info('运行模式: 非交互式(使用默认配置)');
@@ -94,26 +102,26 @@ ${colors.reset}`);
if (useSavedConfig) {
log.step('数据库配置');
log.info(`数据库类型: ${config.dbType}`);
log.keyValue('数据库类型', config.dbType);
if (config.dbType === 'mysql') {
log.info(`MySQL: ${config.dbConfig.host}:${config.dbConfig.port}/${config.dbConfig.database}`);
log.keyValue('MySQL 地址', `${config.dbConfig.host}:${config.dbConfig.port}/${config.dbConfig.database}`);
}
log.divider();
log.step('服务配置');
log.info(`后端端口: ${config.backendPort}`);
log.info(`运行环境: ${config.nodeEnv}`);
log.info(`前端部署: ${config.frontendDeploy}`);
log.info(`前端端口: ${config.frontendPort}`);
log.keyValue('后端端口', String(config.backendPort));
log.keyValue('运行环境', config.nodeEnv);
log.keyValue('前端部署', config.frontendDeploy);
log.keyValue('前端端口', String(config.frontendPort));
if (config.frontendDeploy === 'nginx') {
log.info(`域名: ${config.domain}`);
log.keyValue('域名', config.domain);
}
log.divider();
} else {
await configureDatabase(cmdArgs);
await configureServices(cmdArgs);
}
if (!cmdArgs.nonInteractive) {
await confirmConfiguration();
}
@@ -128,13 +136,13 @@ ${colors.reset}`);
await installDependencies();
await initDatabase();
if (!cmdArgs.skipBuild) {
await buildFrontend();
} else {
log.info('已跳过前端构建');
}
await startServices();
if (process.platform !== 'win32' && config.frontendDeploy === 'nginx') {
+101 -11
View File
@@ -6,31 +6,119 @@ let logFileStream = null;
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
dim: '\x1b[2m',
green: '\x1b[32m',
yellow: '\x1b[33m',
red: '\x1b[31m',
cyan: '\x1b[36m',
gray: '\x1b[90m',
magenta: '\x1b[35m'
magenta: '\x1b[35m',
blue: '\x1b[34m',
bgCyan: '\x1b[46m',
bgBlue: '\x1b[44m',
white: '\x1b[37m',
};
const ICONS = {
info: '',
success: '✔',
warning: '⚠',
error: '✖',
arrow: '▶',
bullet: '●',
pointer: '',
check: '✓',
cross: '✗',
dash: '─',
doubleDash: '═',
corner: '└',
pipe: '│',
diamond: '◆',
star: '★',
};
function padCenter(text, width) {
const stripped = text.replace(/\x1b\[[0-9;]*m/g, '');
const len = stripped.length;
const pad = Math.max(0, width - len);
const left = Math.floor(pad / 2);
const right = pad - left;
return ' '.repeat(left) + text + ' '.repeat(right);
}
function progressBar(current, total, width = 20) {
const filled = Math.round((current / total) * width);
const empty = width - filled;
const bar = `${colors.cyan}${'━'.repeat(filled)}${colors.dim}${'╌'.repeat(empty)}${colors.reset}`;
return bar;
}
const log = {
info: (msg) => console.log(`${colors.cyan}${colors.reset} ${msg}`),
success: (msg) => console.log(`${colors.green}${colors.reset} ${msg}`),
warning: (msg) => console.log(`${colors.yellow}${colors.reset} ${msg}`),
error: (msg) => console.log(`${colors.red}${colors.reset} ${msg}`),
info: (msg) => console.log(` ${colors.cyan}${ICONS.info}${colors.reset} ${msg}`),
success: (msg) => console.log(` ${colors.green}${ICONS.success}${colors.reset} ${msg}`),
warning: (msg) => console.log(` ${colors.yellow}${ICONS.warning}${colors.reset} ${msg}`),
error: (msg) => console.log(` ${colors.red}${ICONS.error}${colors.reset} ${msg}`),
step: (msg) => {
const idx = INSTALL_STEPS.indexOf(msg);
if (idx >= 0) {
currentStepIndex = idx;
}
const progress = currentStepIndex < INSTALL_STEPS.length
? `${colors.gray}[${currentStepIndex + 1}/${INSTALL_STEPS.length}]${colors.reset} `
: '';
console.log(`\n${progress}${colors.bright}${colors.cyan}${msg}${colors.reset}`);
const current = currentStepIndex + 1;
const total = INSTALL_STEPS.length;
const bar = progressBar(current, total, 15);
const progress = `${colors.dim}[${current}/${total}]${colors.reset} ${bar}`;
console.log('');
console.log(`${progress}`);
console.log(` ${colors.bright}${colors.cyan}${ICONS.arrow}${colors.reset} ${colors.bright}${msg}${colors.reset}`);
},
divider: () => console.log(` ${colors.dim}${ICONS.dash.repeat(56)}${colors.reset}`),
thickDivider: () => console.log(` ${colors.dim}${ICONS.doubleDash.repeat(56)}${colors.reset}`),
subStep: (msg) => console.log(` ${colors.dim}${ICONS.corner}${colors.reset} ${msg}`),
keyValue: (key, value) => {
const keyStr = `${colors.dim}${key}${colors.reset}`;
const valueStr = `${colors.cyan}${value}${colors.reset}`;
console.log(` ${ICONS.pipe} ${keyStr} ${valueStr}`);
},
banner: (title, subtitle, version) => {
const width = 58;
const top = `${colors.bright}${colors.cyan}${ICONS.doubleDash.repeat(width + 2)}${colors.reset}`;
const bottom = top;
const titleLine = padCenter(`${colors.bright}${colors.white}${title}${colors.reset}`, width);
const subtitleLine = padCenter(`${colors.dim}${subtitle}${colors.reset}`, width);
const versionLine = version ? padCenter(`${colors.cyan}v${version}${colors.reset}`, width) : '';
console.log('');
console.log(` ${colors.cyan}${top.substring(2)}${colors.reset}`);
console.log(` ${colors.cyan}${titleLine}${colors.reset}`);
if (subtitle) {
console.log(` ${colors.cyan}${subtitleLine}${colors.reset}`);
}
if (version) {
console.log(` ${colors.cyan}${versionLine}${colors.reset}`);
}
console.log(` ${colors.cyan}${bottom.substring(2)}${colors.reset}`);
console.log('');
},
section: (title) => {
console.log('');
console.log(` ${colors.bright}${colors.magenta}${ICONS.diamond}${colors.reset} ${colors.bright}${title}${colors.reset}`);
log.divider();
},
tableRow: (items, widths) => {
const parts = items.map((item, i) => {
const stripped = item.replace(/\x1b\[[0-9;]*m/g, '');
const pad = Math.max(0, (widths[i] || 20) - stripped.length);
return item + ' '.repeat(pad);
});
console.log(` ${ICONS.pipe} ${parts.join(' ')}`);
},
divider: () => console.log(`${colors.gray}${'─'.repeat(60)}${colors.reset}`),
subStep: (msg) => console.log(` ${colors.gray}${colors.reset} ${msg}`)
};
function initLogFile(LOG_DIR) {
@@ -62,7 +150,9 @@ function closeLogFile() {
module.exports = {
colors,
ICONS,
log,
padCenter,
initLogFile,
closeLogFile,
};
+3 -3
View File
@@ -1,7 +1,7 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const { colors, log } = require('./logger');
const { colors, ICONS, log } = require('./logger');
const { ask, select } = require('./ui');
const { config } = require('./config');
const { runCommand } = require('./utils');
@@ -12,9 +12,9 @@ function showNginxInstallGuideLinux() {
const root = isRootUser();
const sudoPrefix = root ? '' : 'sudo ';
console.log('\n' + colors.bright + 'Nginx 安装命令' + colors.reset);
log.section('Nginx 安装命令');
if (root) {
console.log(colors.gray + '(当前以 root 用户运行,无需 sudo)' + colors.reset);
console.log(` ${ICONS.pipe} ${colors.gray}(当前以 root 用户运行,无需 sudo)${colors.reset}`);
}
switch (distro) {
+23 -9
View File
@@ -1,5 +1,5 @@
const readline = require('readline');
const { log } = require('./logger');
const { colors, ICONS, log } = require('./logger');
const rl = readline.createInterface({
input: process.stdin,
@@ -17,7 +17,8 @@ rl.on('error', (err) => {
function ask(question, defaultValue = '') {
return new Promise((resolve) => {
const prompt = defaultValue ? `${question} (${defaultValue}): ` : `${question}: `;
const defaultHint = defaultValue ? `${colors.dim}(${defaultValue})${colors.reset}` : '';
const prompt = ` ${colors.cyan}${ICONS.pointer}${colors.reset} ${colors.bright}${question}${colors.reset} ${defaultHint}${colors.dim}:${colors.reset} `;
rl.question(prompt, (answer) => {
resolve(answer.trim() || defaultValue);
});
@@ -25,10 +26,10 @@ function ask(question, defaultValue = '') {
}
function askPassword(question) {
return new Promise((resolve) => {
const prompt = `${question}: `;
process.stdout.write(prompt);
const prompt = ` ${colors.cyan}${ICONS.pointer}${colors.reset} ${colors.bright}${question}${colors.reset} ${colors.dim}:${colors.reset} `;
process.stdout.write(prompt);
return new Promise((resolve) => {
let password = '';
const stdin = process.stdin;
const stdout = process.stdout;
@@ -52,7 +53,8 @@ function askPassword(question) {
resolve(password);
} else if (code === 3) {
cleanup();
stdout.write('\n已取消\n');
stdout.write('\n');
log.warning('已取消');
process.exit(0);
} else if (code === 127 || code === 8) {
if (password.length > 0) {
@@ -78,10 +80,22 @@ function askPassword(question) {
}
async function select(question, options, defaultIndex = 1) {
const { colors } = require('./logger');
console.log(`\n${question}`);
console.log('');
console.log(` ${colors.bright}${question}${colors.reset}`);
log.divider();
options.forEach((opt, idx) => {
console.log(` ${colors.cyan}${idx + 1}.${colors.reset} ${opt.label}`);
const isDefault = idx + 1 === defaultIndex;
const marker = isDefault
? `${colors.cyan}${ICONS.pointer}${colors.reset}`
: ` `;
const label = isDefault
? `${colors.bright}${opt.label}${colors.reset}`
: `${colors.dim}${opt.label}${colors.reset}`;
const num = isDefault
? `${colors.cyan}${idx + 1}${colors.reset}`
: `${colors.dim}${idx + 1}${colors.reset}`;
console.log(` ${marker} ${num}. ${label}`);
});
const answer = await ask('请选择', String(defaultIndex));