From 63de61bf7a70a344b262c4e32c0960bd8e1d556d Mon Sep 17 00:00:00 2001 From: zhang1106 <849185023@qq.com> Date: Sat, 9 May 2026 11:09:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(login):=20=E4=BC=98=E5=8C=96=E9=A6=96?= =?UTF-8?q?=E6=AC=A1=E7=94=A8=E6=88=B7=E6=B3=A8=E5=86=8C=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E5=92=8C=E8=A1=A8=E5=8D=95=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复首次用户注册成功后未正确设置 isFirstUser 状态的问题 - 调整表单 name 和 onFinish 逻辑以区分首次用户注册和普通注册 - 更新按钮文本以明确不同场景下的操作 refactor(uninstall): 增强卸载脚本功能性和健壮性 - 新增 --dry-run 预览模式和 --skip-uploads 选项 - 改进 PM2 服务检测和清理逻辑 - 优化数据库配置读取和文件路径处理 - 增加更多清理项(Vite 缓存、临时文件等) - 完善错误处理和用户提示信息 --- frontend/src/pages/Login.jsx | 9 +- install.js | 28 ++- uninstall.js | 444 ++++++++++++++++++++++++++++++----- 3 files changed, 417 insertions(+), 64 deletions(-) diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx index 6df37e0..3602c45 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -78,6 +78,7 @@ const Login = () => { if (result.success) { message.success(result.isFirstUser ? '管理员账号创建成功' : '注册成功,请等待管理员审核'); if (result.isFirstUser) { + setIsFirstUser(false); navigate('/dashboard'); } else { setRegisterMode(false); @@ -242,9 +243,9 @@ const Login = () => { )}
@@ -327,7 +328,7 @@ const Login = () => { /> - {registerMode && ( + {(registerMode || isFirstUser) && ( { loading={loading} className="submit-button" > - {registerMode ? '立即注册' : '登 录'} + {isFirstUser ? '创建管理员' : (registerMode ? '立即注册' : '登 录')}
diff --git a/install.js b/install.js index 2728fb2..6c63347 100644 --- a/install.js +++ b/install.js @@ -1618,7 +1618,13 @@ function generateNginxConfig() { } const isWindows = process.platform === 'win32'; - const frontendPath = path.join(__dirname, 'frontend', 'dist').replace(/\\/g, '/'); + + let frontendPath; + if (isWindows) { + frontendPath = path.join(__dirname, 'frontend', 'dist').replace(/\\/g, '/'); + } else { + frontendPath = '/var/www/idc'; + } const nginxConfig = `# IDC设备管理系统 - Nginx配置 # 生成时间: ${new Date().toISOString()} @@ -1720,6 +1726,26 @@ function autoConfigureNginx() { const root = isRootUser(); const sudoPrefix = root ? '' : 'sudo '; + + // 复制前端构建产物到 /var/www/idc(Nginx 可访问的目录) + const wwwDir = '/var/www/idc'; + const distDir = path.join(__dirname, 'frontend', 'dist'); + + if (fs.existsSync(distDir)) { + log.info(`复制前端文件到 ${wwwDir}...`); + runCommand(`${sudoPrefix}mkdir -p ${wwwDir}`, { silent: true }); + const copyResult = runCommand(`${sudoPrefix}cp -r ${distDir}/* ${wwwDir}/`, { silent: true }); + if (copyResult.success) { + runCommand(`${sudoPrefix}chmod -R 755 ${wwwDir}`, { silent: true }); + log.success('前端文件部署完成'); + } else { + log.warning('前端文件复制失败,Nginx 可能无法访问前端页面'); + } + } else { + log.warning(`前端构建产物不存在: ${distDir}`); + log.info('请先完成前端构建后再配置 Nginx'); + } + const configSource = path.join(__dirname, 'deploy', 'nginx-idc.conf'); if (!fs.existsSync(configSource)) { diff --git a/uninstall.js b/uninstall.js index 0d82161..3259015 100644 --- a/uninstall.js +++ b/uninstall.js @@ -23,6 +23,7 @@ const readline = require('readline'); const { execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); +const os = require('os'); const SCRIPT_VERSION = '2.0.0'; const BACKUP_DIR = path.join(__dirname, 'backup'); @@ -57,10 +58,12 @@ let backedUpItems = []; function parseArgs() { const args = process.argv.slice(2); return { - force: args.includes('--force') || args.includes('-f'), + force: args.includes('--force') || args.includes('-f') || args.includes('--yes') || args.includes('-y'), backup: args.includes('--backup') || args.includes('-b'), + dryRun: args.includes('--dry-run'), skipDb: args.includes('--skip-db'), skipDeps: args.includes('--skip-deps'), + skipUploads: args.includes('--skip-uploads'), help: args.includes('--help') || args.includes('-h') }; } @@ -73,15 +76,19 @@ ${colors.bright}IDC设备管理系统 - 卸载脚本 v${SCRIPT_VERSION}${colors. 选项: -f, --force 强制卸载(无需确认) + -y, --yes 强制卸载别名(同 --force) -b, --backup 卸载前自动备份数据库 + --dry-run 预览模式:仅显示将要删除的内容,不实际执行 --skip-db 跳过数据库删除 --skip-deps 跳过依赖删除 + --skip-uploads 跳过上传文件目录删除 -h, --help 显示帮助信息 示例: node uninstall.js # 交互式卸载 node uninstall.js --force # 强制卸载 node uninstall.js --backup # 卸载前备份 + node uninstall.js --dry-run # 预览将删除的内容 `); process.exit(0); } @@ -156,6 +163,84 @@ function commandExists(command) { } } +/** + * 从 ecosystem.config.js 读取 PM2 服务名 + * 如果配置文件不存在则返回默认值 + * + * @returns {{ backendName: string, frontendName: string }} PM2 服务名 + */ +function getPM2ServiceNames() { + const defaultNames = { backendName: 'idc-backend', frontendName: 'idc-frontend' }; + const configPath = path.join(__dirname, 'deploy', 'ecosystem.config.js'); + + if (!fs.existsSync(configPath)) { + return defaultNames; + } + + try { + const config = require(configPath); + if (!config || !Array.isArray(config.apps)) { + return defaultNames; + } + + const names = { ...defaultNames }; + config.apps.forEach(app => { + if (app.name && app.name.startsWith('idc-')) { + if (app.name.includes('backend') || (app.script && app.script.includes('server'))) { + names.backendName = app.name; + } else if (app.name.includes('frontend') || app.name === 'idc-frontend') { + names.frontendName = app.name; + } + } + }); + return names; + } catch { + return defaultNames; + } +} + +/** + * 从 .env 文件读取数据库配置 + * 返回数据库类型和 SQLite 数据库文件路径 + * + * @returns {{ dbType: string, sqliteDbPath: string }} + */ +function getDatabaseConfig() { + const defaultConfig = { + dbType: 'sqlite', + sqliteDbPath: path.join(__dirname, 'backend', 'idc_management.db') + }; + + const envPath = path.join(__dirname, 'backend', '.env'); + if (!fs.existsSync(envPath)) { + return defaultConfig; + } + + try { + const envContent = fs.readFileSync(envPath, 'utf8'); + const config = { ...defaultConfig }; + + const dbTypeMatch = envContent.match(/^DB_TYPE\s*=\s*(\w+)/m); + if (dbTypeMatch) { + config.dbType = dbTypeMatch[1].toLowerCase(); + } + + const dbPathMatch = envContent.match(/^DB_PATH\s*=\s*(.+)$/m); + if (dbPathMatch && config.dbType === 'sqlite') { + const rawPath = dbPathMatch[1].trim().replace(/['"]/g, ''); + if (path.isAbsolute(rawPath)) { + config.sqliteDbPath = rawPath; + } else { + config.sqliteDbPath = path.resolve(__dirname, 'backend', rawPath); + } + } + + return config; + } catch { + return defaultConfig; + } +} + // ============================================================================= // 检测是否以 root 用户运行 // ============================================================================= @@ -176,37 +261,79 @@ async function stopAndDeleteServices() { return; } + const { backendName, frontendName } = getPM2ServiceNames(); const isWindows = process.platform === 'win32'; const nullRedirect = isWindows ? '2>nul' : '2>/dev/null'; const orTrue = isWindows ? '|| exit 0' : '|| true'; - log.info('停止后端服务 (idc-backend)...'); - const backendStop = runCommand(`pm2 stop idc-backend ${nullRedirect} ${orTrue}`, { silent: true }); - if (backendStop.success) { - log.success('后端服务已停止'); + /** + * 检查 PM2 中是否存在指定名称的进程 + * + * @param {string} name - 进程名称 + * @returns {boolean} 进程是否存在 + */ + function pm2ProcessExists(name) { + const result = runCommand(`pm2 describe ${name} ${nullRedirect}`, { silent: true }); + return result.success; } - log.info('删除后端服务 (idc-backend)...'); - const backendDelete = runCommand(`pm2 delete idc-backend ${nullRedirect} ${orTrue}`, { silent: true }); - if (backendDelete.success) { - log.success('后端服务已删除'); - } + const services = [ + { name: backendName, label: '后端服务' }, + { name: frontendName, label: '前端服务' } + ]; - log.info('停止前端服务 (idc-frontend)...'); - const frontendStop = runCommand(`pm2 stop idc-frontend ${nullRedirect} ${orTrue}`, { silent: true }); - if (frontendStop.success) { - log.success('前端服务已停止'); - } + for (const service of services) { + if (!pm2ProcessExists(service.name)) { + log.info(`${service.label} (${service.name}) 不存在于 PM2 中,跳过`); + continue; + } - log.info('删除前端服务 (idc-frontend)...'); - const frontendDelete = runCommand(`pm2 delete idc-frontend ${nullRedirect} ${orTrue}`, { silent: true }); - if (frontendDelete.success) { - log.success('前端服务已删除'); + log.info(`停止 ${service.label} (${service.name})...`); + const stopResult = runCommand(`pm2 stop ${service.name} ${nullRedirect} ${orTrue}`, { silent: true }); + if (stopResult.success) { + log.success(`${service.label} 已停止`); + } else { + log.warning(`${service.label} 停止失败(可能已停止)`); + } + + log.info(`删除 ${service.label} (${service.name})...`); + const deleteResult = runCommand(`pm2 delete ${service.name} ${nullRedirect} ${orTrue}`, { silent: true }); + if (deleteResult.success) { + log.success(`${service.label} 已删除`); + } else { + log.warning(`${service.label} 删除失败`); + } } log.info('保存 PM2 配置...'); runCommand(`pm2 save ${nullRedirect} ${orTrue}`, { silent: true }); + log.info('清理 PM2 开机自启配置...'); + const startupResult = runCommand(`pm2 unstartup ${nullRedirect} ${orTrue}`, { silent: true }); + if (startupResult.success) { + log.success('PM2 开机自启配置已清理'); + } + + log.info('清理 PM2 日志文件...'); + const pm2LogDir = path.join(os.homedir(), '.pm2', 'logs'); + if (fs.existsSync(pm2LogDir)) { + const pm2LogFiles = fs.readdirSync(pm2LogDir).filter(f => + f.startsWith(backendName) || f.startsWith(frontendName) + ); + if (pm2LogFiles.length > 0) { + pm2LogFiles.forEach(f => { + try { + fs.unlinkSync(path.join(pm2LogDir, f)); + log.subStep(`PM2 日志已清理: ${f}`); + } catch (e) { + log.warning(`PM2 日志清理失败: ${f}`); + } + }); + } else { + log.subStep('未找到 PM2 相关日志文件'); + } + } + log.divider(); } @@ -214,7 +341,7 @@ async function stopAndDeleteServices() { // Nginx 配置清理 // ============================================================================= -async function cleanupNginxConfig() { +async function cleanupNginxConfig(cmdArgs) { log.step('清理 Nginx 配置'); const isWindows = process.platform === 'win32'; @@ -227,9 +354,13 @@ async function cleanupNginxConfig() { console.log(` 2. 编辑主配置: 从 ${colors.cyan}C:/nginx/conf/nginx.conf${colors.reset} 中移除 include conf.d/*.conf;`); console.log(` 3. 停止 Nginx: ${colors.cyan}nginx -s stop${colors.reset}`); - const confirm = await ask('是否已手动清理 Nginx 配置? (Y/n)', 'Y'); - if (confirm.toLowerCase() !== 'y') { - log.warning('请记得手动清理 Nginx 配置'); + if (!cmdArgs?.force) { + const confirm = await ask('是否已手动清理 Nginx 配置? (Y/n)', 'Y'); + if (confirm.toLowerCase() !== 'y') { + log.warning('请记得手动清理 Nginx 配置'); + } + } else { + log.info('强制模式:跳过 Nginx 清理确认'); } } else if (isLinux) { // Linux 下自动清理 @@ -240,6 +371,7 @@ async function cleanupNginxConfig() { const sitesAvailablePath = '/etc/nginx/sites-available/idc'; const sitesEnabledPath = '/etc/nginx/sites-enabled/idc'; const confDPath = '/etc/nginx/conf.d/idc'; + const confDWithExtPath = '/etc/nginx/conf.d/idc.conf'; let configExists = false; @@ -267,7 +399,7 @@ async function cleanupNginxConfig() { } } - // 删除 conf.d 中的配置 + // 删除 conf.d 中的配置(无扩展名) if (fs.existsSync(confDPath)) { configExists = true; log.info('删除 conf.d 配置...'); @@ -279,6 +411,18 @@ async function cleanupNginxConfig() { } } + // 删除 conf.d 中的配置(带 .conf 扩展名) + if (fs.existsSync(confDWithExtPath)) { + configExists = true; + log.info('删除 conf.d 配置(带扩展名)...'); + const result = runCommand(`${sudoPrefix}rm -f "${confDWithExtPath}"`, { silent: true }); + if (result.success) { + log.success('conf.d/idc.conf 已删除'); + } else { + log.error('删除失败'); + } + } + if (!configExists) { log.info('未找到系统 Nginx 配置文件'); } else { @@ -359,11 +503,11 @@ async function cleanupConfigFiles() { // 数据库清理 // ============================================================================= -async function cleanupDatabase() { +async function cleanupDatabase(cmdArgs) { log.step('数据库清理'); - // 检查 SQLite 数据库文件 - const sqliteDbPath = path.join(__dirname, 'backend', 'idc_management.db'); + const dbConfig = getDatabaseConfig(); + const sqliteDbPath = dbConfig.sqliteDbPath; const sqliteExists = fs.existsSync(sqliteDbPath); // 获取数据库文件信息 @@ -375,16 +519,8 @@ async function cleanupDatabase() { dbCreateTime = stats.birthtime.toLocaleString(); } - // 检测数据库类型(从 .env 文件读取) - const envPath = path.join(__dirname, 'backend', '.env'); - let dbType = 'sqlite'; - if (fs.existsSync(envPath)) { - const envContent = fs.readFileSync(envPath, 'utf8'); - const dbTypeMatch = envContent.match(/DB_TYPE\s*=\s*(\w+)/); - if (dbTypeMatch) { - dbType = dbTypeMatch[1].toLowerCase(); - } - } + // 检测数据库类型(从 getDatabaseConfig 获取) + const dbType = dbConfig.dbType; console.log(`\n${colors.bright}当前数据库配置:${colors.reset}`); console.log(` 数据库类型: ${colors.cyan}${dbType === 'sqlite' ? 'SQLite' : 'MySQL'}${colors.reset}`); @@ -400,7 +536,7 @@ async function cleanupDatabase() { console.log(`\n${colors.bright}删除方法:${colors.reset}`); console.log(` 方法1 - 脚本自动删除:输入 Y 确认删除`); - console.log(` 方法2 - 手动删除文件:直接删除 ${colors.cyan}backend/idc_management.db${colors.reset}`); + console.log(` 方法2 - 手动删除文件:直接删除 ${colors.cyan}${path.relative(__dirname, sqliteDbPath)}${colors.reset}`); console.log(` 方法3 - 命令行删除:`); if (process.platform === 'win32') { console.log(` ${colors.cyan}del "${sqliteDbPath}"${colors.reset}`); @@ -410,26 +546,56 @@ async function cleanupDatabase() { const confirm = await ask('\n是否删除 SQLite 数据库文件? (y/N)', 'N'); if (confirm.toLowerCase() === 'y') { + // 先尝试备份(不管是否指定 --backup) + if (!cmdArgs?.backup && !cmdArgs?.force) { + const backupConfirm = await ask('是否先备份数据库再删除? (Y/n)', 'Y'); + if (backupConfirm.toLowerCase() === 'y') { + await backupDatabase(); + } + } else if (cmdArgs?.backup) { + await backupDatabase(); + } try { log.info('正在删除数据库文件...'); fs.unlinkSync(sqliteDbPath); log.success('SQLite 数据库已删除'); deletedItems.push({ type: '数据库', name: `SQLite (${dbSize} MB)` }); } catch (error) { - log.error(`删除失败: ${error.message}`); - if (error.message.includes('EBUSY') || error.message.includes('resource busy')) { - log.warning('数据库文件被占用,请先停止服务后再手动删除'); - console.log(` 手动删除命令:`); - if (process.platform === 'win32') { - console.log(` ${colors.cyan}del /f "${sqliteDbPath}"${colors.reset}`); - } else { - console.log(` ${colors.cyan}rm -f "${sqliteDbPath}"${colors.reset}`); + log.error(`直接删除失败: ${error.message}`); + if (error.message.includes('EBUSY') || error.message.includes('resource busy') || error.message.includes('used by another process')) { + log.warning('数据库文件被占用,尝试强制删除...'); + try { + const isWindows = process.platform === 'win32'; + let forceResult; + if (isWindows) { + forceResult = runCommand(`del /f /q "${sqliteDbPath}" 2>nul`, { silent: true }); + if (!forceResult.success) { + forceResult = runCommand(`powershell -Command "Remove-Item -Path '${sqliteDbPath}' -Force -ErrorAction SilentlyContinue" 2>nul`, { silent: true }); + } + } else { + forceResult = runCommand(`rm -f "${sqliteDbPath}"`, { silent: true }); + } + if (forceResult.success && !fs.existsSync(sqliteDbPath)) { + log.success('数据库文件已强制删除'); + deletedItems.push({ type: '数据库', name: `SQLite (${dbSize} MB)` }); + } else { + log.error('强制删除失败,文件仍被占用'); + log.warning('请手动停止占用数据库文件的进程后再删除'); + console.log(` 手动删除命令:`); + if (process.platform === 'win32') { + console.log(` ${colors.cyan}del /f "${sqliteDbPath}"${colors.reset}`); + } else { + console.log(` ${colors.cyan}rm -f "${sqliteDbPath}"${colors.reset}`); + } + } + } catch (forceError) { + log.error(`强制删除失败: ${forceError.message}`); } } } } else { log.info('保留 SQLite 数据库文件'); - console.log(` 文件位置: ${colors.cyan}${sqliteDbPath}${colors.reset}`); + console.log(` 文件位置: ${colors.cyan}${path.relative(__dirname, sqliteDbPath)}${colors.reset}`); } } else { log.info('未检测到 SQLite 数据库文件'); @@ -449,7 +615,7 @@ async function cleanupDatabase() { // 备份提示 console.log(`\n${colors.bright}数据备份建议:${colors.reset}`); if (dbType === 'sqlite' && sqliteExists) { - console.log(` 备份 SQLite:直接复制 ${colors.cyan}backend/idc_management.db${colors.reset} 文件`); + console.log(` 备份 SQLite:直接复制 ${colors.cyan}${path.relative(__dirname, sqliteDbPath)}${colors.reset} 文件`); } console.log(` 如需保留数据,请在卸载前手动备份`); @@ -474,7 +640,9 @@ async function cleanupDependencies(cmdArgs) { const dirsToDelete = [ { path: path.join(__dirname, 'backend', 'node_modules'), name: '后端 node_modules', type: '依赖' }, { path: path.join(__dirname, 'frontend', 'node_modules'), name: '前端 node_modules', type: '依赖' }, - { path: path.join(__dirname, 'frontend', 'dist'), name: '前端构建产物 (dist)', type: '构建产物' } + { path: path.join(__dirname, 'frontend', 'node_modules', '.vite'), name: '前端 Vite 缓存 (.vite)', type: '构建产物' }, + { path: path.join(__dirname, 'frontend', 'dist'), name: '前端构建产物 (dist)', type: '构建产物' }, + { path: path.join(__dirname, 'backend', 'temp'), name: '后端临时文件 (temp)', type: '临时文件' } ]; for (const dir of dirsToDelete) { @@ -498,24 +666,75 @@ async function cleanupDependencies(cmdArgs) { // 日志清理 // ============================================================================= -async function cleanupLogs() { +async function cleanupLogs(cmdArgs) { log.step('日志文件清理'); const logsDir = path.join(__dirname, 'backend', 'logs'); if (fs.existsSync(logsDir)) { + if (cmdArgs?.force) { + log.info('强制模式:跳过日志文件删除'); + return; + } const confirm = await ask('是否删除后端日志文件? (y/N)', 'N'); if (confirm.toLowerCase() === 'y') { try { fs.rmSync(logsDir, { recursive: true, force: true }); - log.success('日志文件已删除'); + log.success('后端日志文件已删除'); } catch (error) { log.error(`删除失败: ${error.message}`); } } else { - log.info('保留日志文件'); + log.info('保留后端日志文件'); } } else { - log.info('未找到日志目录'); + log.info('未找到后端日志目录'); + } + + // 清理脚本自身生成的卸载日志(根目录 logs/) + const rootLogsDir = path.join(__dirname, 'logs'); + if (fs.existsSync(rootLogsDir)) { + if (cmdArgs?.force) { + log.info('强制模式:跳过脚本日志删除'); + } else { + const rootLogConfirm = await ask('是否删除安装/卸载脚本日志? (y/N)', 'N'); + if (rootLogConfirm.toLowerCase() === 'y') { + try { + fs.rmSync(rootLogsDir, { recursive: true, force: true }); + log.success('脚本日志文件已删除'); + } catch (error) { + log.error(`删除失败: ${error.message}`); + } + } else { + log.info('保留脚本日志文件'); + } + } + } + + log.divider(); +} + +async function cleanupUploads(cmdArgs) { + log.step('上传文件清理'); + + const uploadsDir = path.join(__dirname, 'backend', 'uploads'); + if (fs.existsSync(uploadsDir)) { + if (cmdArgs?.force) { + log.info('强制模式:跳过上传文件目录删除'); + return; + } + const confirm = await ask('是否删除上传文件目录? (y/N)', 'N'); + if (confirm.toLowerCase() === 'y') { + try { + fs.rmSync(uploadsDir, { recursive: true, force: true }); + log.success('上传文件目录已删除'); + } catch (error) { + log.error(`删除失败: ${error.message}`); + } + } else { + log.info('保留上传文件目录'); + } + } else { + log.info('未找到上传文件目录'); } log.divider(); @@ -528,7 +747,8 @@ async function cleanupLogs() { async function backupDatabase() { log.step('备份数据库'); - const sqliteDbPath = path.join(__dirname, 'backend', 'idc_management.db'); + const dbConfig = getDatabaseConfig(); + const sqliteDbPath = dbConfig.sqliteDbPath; if (!fs.existsSync(sqliteDbPath)) { log.info('未找到 SQLite 数据库文件,跳过备份'); return false; @@ -551,6 +771,95 @@ async function backupDatabase() { } } +function showDryRunPreview(cmdArgs) { + const { backendName, frontendName } = getPM2ServiceNames(); + const dbConfig = getDatabaseConfig(); + + console.log(` + ${colors.bright}${colors.cyan}以下是将要执行的操作(预览模式):${colors.reset}\n`); + + // PM2 服务 + console.log(` ${colors.bright}1. 停止并删除 PM2 服务${colors.reset}`); + console.log(` - ${backendName}`); + console.log(` - ${frontendName}`); + console.log(` - 清理 PM2 开机自启配置`); + console.log(` - 清理 PM2 相关日志文件`); + + // Nginx 配置 + console.log(`\n ${colors.bright}2. 清理 Nginx 配置${colors.reset}`); + if (process.platform === 'linux') { + const paths = ['/etc/nginx/sites-available/idc', '/etc/nginx/sites-enabled/idc', + '/etc/nginx/conf.d/idc', '/etc/nginx/conf.d/idc.conf']; + paths.forEach(p => { + if (fs.existsSync(p)) { + console.log(` - 删除: ${p}`); + } + }); + } else { + console.log(` - 提示手动清理(${process.platform === 'win32' ? 'Windows' : 'macOS'})`); + } + + // 配置文件 + console.log(`\n ${colors.bright}3. 清理生成的配置文件${colors.reset}`); + const configFiles = [ + { path: path.join(__dirname, 'backend', '.env'), name: '后端环境变量 (.env)' }, + { path: path.join(__dirname, 'deploy', 'ecosystem.config.js'), name: 'PM2 配置' }, + { path: path.join(__dirname, 'deploy', 'nginx-idc.conf'), name: 'Nginx 配置' } + ]; + configFiles.forEach(f => { + if (fs.existsSync(f.path)) { + console.log(` - 删除: ${f.name}`); + } + }); + + // 数据库 + console.log(`\n ${colors.bright}4. 数据库${colors.reset}`); + if (dbConfig.dbType === 'sqlite' && fs.existsSync(dbConfig.sqliteDbPath)) { + console.log(` - 删除 SQLite 数据库: ${path.relative(__dirname, dbConfig.sqliteDbPath)}`); + if (cmdArgs?.backup) { + console.log(` - 备份数据库到: backup/`); + } + } else if (dbConfig.dbType === 'mysql') { + console.log(` - 提示手动删除 MySQL 数据库`); + } else { + console.log(` - 未检测到数据库文件`); + } + + // 日志 + console.log(`\n ${colors.bright}5. 日志文件${colors.reset}`); + if (fs.existsSync(path.join(__dirname, 'backend', 'logs'))) { + console.log(` - 后端日志目录 (backend/logs/)`); + } + if (fs.existsSync(path.join(__dirname, 'logs'))) { + console.log(` - 脚本日志目录 (logs/)`); + } + + // 上传文件 + console.log(`\n ${colors.bright}6. 上传文件${colors.reset}`); + if (fs.existsSync(path.join(__dirname, 'backend', 'uploads'))) { + console.log(` - 上传文件目录 (backend/uploads/)`); + } + + // 依赖和构建产物 + console.log(`\n ${colors.bright}7. 依赖和构建产物${colors.reset}`); + const depDirs = [ + { path: path.join(__dirname, 'backend', 'node_modules'), name: '后端 node_modules' }, + { path: path.join(__dirname, 'frontend', 'node_modules'), name: '前端 node_modules' }, + { path: path.join(__dirname, 'frontend', 'node_modules', '.vite'), name: '前端 Vite 缓存' }, + { path: path.join(__dirname, 'frontend', 'dist'), name: '前端构建产物' }, + { path: path.join(__dirname, 'backend', 'temp'), name: '后端临时文件' } + ]; + depDirs.forEach(d => { + if (fs.existsSync(d.path)) { + console.log(` - 删除: ${d.name}`); + } + }); + + console.log(`\n${colors.gray}${'─'.repeat(60)}${colors.reset}`); + console.log(` ${colors.yellow}提示: 移除 --dry-run 参数以实际执行卸载${colors.reset}`); + log.divider(); +} + function printSummary() { const duration = ((Date.now() - uninstallStartTime) / 1000).toFixed(1); @@ -613,9 +922,13 @@ ${colors.reset}`); log.info('运行模式: 强制卸载(无需确认)'); } + if (cmdArgs.dryRun) { + log.info('运行模式: 预览模式(不实际执行)'); + } + log.divider(); - if (!cmdArgs.force) { + if (!cmdArgs.force && !cmdArgs.dryRun) { const confirm = await ask('确认要开始卸载? (y/N)', 'N'); if (confirm.toLowerCase() !== 'y') { log.info('已取消卸载'); @@ -625,25 +938,38 @@ ${colors.reset}`); } } + if (cmdArgs.dryRun) { + showDryRunPreview(cmdArgs); + rl.close(); + closeLogFile(); + return; + } + try { if (cmdArgs.backup) { await backupDatabase(); } await stopAndDeleteServices(); - await cleanupNginxConfig(); + await cleanupNginxConfig(cmdArgs); await cleanupConfigFiles(); if (!cmdArgs.skipDb) { - await cleanupDatabase(); + await cleanupDatabase(cmdArgs); } else { log.info('已跳过数据库删除'); } - await cleanupLogs(); + await cleanupLogs(cmdArgs); + + if (!cmdArgs.skipUploads) { + await cleanupUploads(cmdArgs); + } else { + log.info('已跳过上传文件目录删除'); + } if (!cmdArgs.skipDeps) { - await cleanupDependencies(); + await cleanupDependencies(cmdArgs); } else { log.info('已跳过依赖删除'); }