4 changed files with 402 additions and 0 deletions
@ -0,0 +1,11 @@ |
|||||
|
@echo off |
||||
|
chcp 65001 >nul |
||||
|
:: 前端开发服务启动脚本 |
||||
|
:: 用法: |
||||
|
:: dev.bat 同时启动 web + mall |
||||
|
:: dev.bat web 仅启动健康APP |
||||
|
:: dev.bat mall 仅启动商城 |
||||
|
:: dev.bat --kill 仅关闭端口占用进程 |
||||
|
|
||||
|
cd /d "%~dp0.." |
||||
|
node scripts\dev.js %* |
||||
@ -0,0 +1,374 @@ |
|||||
|
#!/usr/bin/env node
|
||||
|
/** |
||||
|
* 前端开发服务启动脚本 |
||||
|
* |
||||
|
* 用法: |
||||
|
* node scripts/dev.js # 同时启动 web + mall |
||||
|
* node scripts/dev.js web # 仅启动健康APP (web) |
||||
|
* node scripts/dev.js mall # 仅启动商城 (mall) |
||||
|
* node scripts/dev.js --kill # 仅关闭占用端口的进程 |
||||
|
* |
||||
|
* 启动前会自动检查端口占用并清理。 |
||||
|
*/ |
||||
|
|
||||
|
const { execSync, spawn } = require('child_process'); |
||||
|
const path = require('path'); |
||||
|
const os = require('os'); |
||||
|
|
||||
|
// ─── 配置 ────────────────────────────────────────────
|
||||
|
const PROJECTS = { |
||||
|
web: { |
||||
|
name: '健康APP (web)', |
||||
|
dir: 'web', |
||||
|
port: 5173, |
||||
|
color: '\x1b[36m', // cyan
|
||||
|
url: 'http://localhost:5173' |
||||
|
}, |
||||
|
mall: { |
||||
|
name: '健康商城 (mall)', |
||||
|
dir: 'mall', |
||||
|
port: 5174, |
||||
|
color: '\x1b[32m', // green
|
||||
|
url: 'http://localhost:5174' |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const RESET = '\x1b[0m'; |
||||
|
const BOLD = '\x1b[1m'; |
||||
|
const RED = '\x1b[31m'; |
||||
|
const YELLOW = '\x1b[33m'; |
||||
|
const GREEN = '\x1b[32m'; |
||||
|
const CYAN = '\x1b[36m'; |
||||
|
const DIM = '\x1b[2m'; |
||||
|
|
||||
|
// ─── 工具函数 ────────────────────────────────────────
|
||||
|
function log(msg) { console.log(msg); } |
||||
|
function info(msg) { log(`${CYAN}ℹ${RESET} ${msg}`); } |
||||
|
function success(msg) { log(`${GREEN}✓${RESET} ${msg}`); } |
||||
|
function warn(msg) { log(`${YELLOW}⚠${RESET} ${msg}`); } |
||||
|
function error(msg) { log(`${RED}✗${RESET} ${msg}`); } |
||||
|
|
||||
|
function banner() { |
||||
|
log(''); |
||||
|
log(`${BOLD}╔═══════════════════════════════════════════════╗${RESET}`); |
||||
|
log(`${BOLD}║ 健康AI助手 · 前端开发服务启动脚本 ║${RESET}`); |
||||
|
log(`${BOLD}╚═══════════════════════════════════════════════╝${RESET}`); |
||||
|
log(''); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取占用指定端口的进程 PID |
||||
|
* 使用 netstat(Windows/Linux 通用)进行精确检测 |
||||
|
* 返回 PID 或 null |
||||
|
*/ |
||||
|
function getPortPid(port) { |
||||
|
try { |
||||
|
const isWin = os.platform() === 'win32'; |
||||
|
if (isWin) { |
||||
|
// Windows: netstat -ano,查找 LISTENING 状态
|
||||
|
const result = execSync(`netstat -ano`, { encoding: 'utf-8', timeout: 5000 }); |
||||
|
const lines = result.split('\n'); |
||||
|
for (const line of lines) { |
||||
|
// 匹配精确端口(避免 51730 匹配 5173)
|
||||
|
const match = line.match(new RegExp(`:\\s*${port}\\s+.*LISTENING\\s+(\\d+)`)); |
||||
|
if (match) { |
||||
|
const pid = parseInt(match[1], 10); |
||||
|
if (pid > 0) return pid; |
||||
|
} |
||||
|
} |
||||
|
// 兜底:用 findstr
|
||||
|
try { |
||||
|
const result2 = execSync(`netstat -ano | findstr ":${port} "`, { encoding: 'utf-8', timeout: 5000 }); |
||||
|
const lines2 = result2.trim().split('\n'); |
||||
|
for (const line of lines2) { |
||||
|
if (line.includes('LISTENING')) { |
||||
|
const parts = line.trim().split(/\s+/); |
||||
|
const pid = parseInt(parts[parts.length - 1], 10); |
||||
|
if (pid > 0) return pid; |
||||
|
} |
||||
|
} |
||||
|
} catch { /* no match */ } |
||||
|
} else { |
||||
|
const result = execSync(`lsof -ti:${port}`, { encoding: 'utf-8', timeout: 5000 }); |
||||
|
const pid = parseInt(result.trim().split('\n')[0], 10); |
||||
|
if (pid > 0) return pid; |
||||
|
} |
||||
|
} catch { |
||||
|
// 没有找到进程
|
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取进程名称 |
||||
|
*/ |
||||
|
function getProcessName(pid) { |
||||
|
try { |
||||
|
const isWin = os.platform() === 'win32'; |
||||
|
if (isWin) { |
||||
|
const result = execSync(`tasklist /FI "PID eq ${pid}" /FO CSV /NH`, { encoding: 'utf-8', timeout: 5000 }); |
||||
|
const match = result.match(/"([^"]+)"/); |
||||
|
return match ? match[1] : '未知进程'; |
||||
|
} else { |
||||
|
return execSync(`ps -p ${pid} -o comm=`, { encoding: 'utf-8', timeout: 5000 }).trim(); |
||||
|
} |
||||
|
} catch { |
||||
|
return '未知进程'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 终止进程 |
||||
|
*/ |
||||
|
function killProcess(pid) { |
||||
|
try { |
||||
|
const isWin = os.platform() === 'win32'; |
||||
|
if (isWin) { |
||||
|
// 先尝试终止进程树
|
||||
|
execSync(`taskkill /PID ${pid} /T /F`, { encoding: 'utf-8', timeout: 5000, stdio: 'ignore' }); |
||||
|
} else { |
||||
|
execSync(`kill -9 ${pid}`, { encoding: 'utf-8', timeout: 5000, stdio: 'ignore' }); |
||||
|
} |
||||
|
return true; |
||||
|
} catch { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 检查并释放端口 |
||||
|
*/ |
||||
|
async function ensurePortFree(port, projectName) { |
||||
|
const pid = getPortPid(port); |
||||
|
if (!pid) { |
||||
|
success(`端口 ${BOLD}${port}${RESET} 空闲 ${DIM}(${projectName})${RESET}`); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
const procName = getProcessName(pid); |
||||
|
warn(`端口 ${BOLD}${port}${RESET} 被占用 → PID: ${pid} (${procName}) ${DIM}[${projectName}]${RESET}`); |
||||
|
|
||||
|
if (killProcess(pid)) { |
||||
|
// 等待端口释放(最多重试 3 次)
|
||||
|
for (let i = 0; i < 3; i++) { |
||||
|
await new Promise(resolve => setTimeout(resolve, 1000)); |
||||
|
if (!getPortPid(port)) { |
||||
|
success(`已终止进程 PID ${pid},端口 ${port} 已释放`); |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
error(`端口 ${port} 仍被占用,无法释放`); |
||||
|
return false; |
||||
|
} else { |
||||
|
error(`无法终止进程 PID ${pid}`); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 检查项目依赖是否已安装 |
||||
|
*/ |
||||
|
function checkNodeModules(projectDir) { |
||||
|
const nmPath = path.join(projectDir, 'node_modules'); |
||||
|
try { |
||||
|
require('fs').accessSync(nmPath); |
||||
|
return true; |
||||
|
} catch { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 安装项目依赖 |
||||
|
*/ |
||||
|
function installDeps(projectDir, projectName) { |
||||
|
info(`${projectName}: 正在安装依赖 (npm install)...`); |
||||
|
try { |
||||
|
execSync('npm install', { |
||||
|
cwd: projectDir, |
||||
|
stdio: 'inherit', |
||||
|
timeout: 120000 |
||||
|
}); |
||||
|
success(`${projectName}: 依赖安装完成`); |
||||
|
return true; |
||||
|
} catch { |
||||
|
error(`${projectName}: 依赖安装失败`); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 启动项目开发服务 |
||||
|
*/ |
||||
|
function startProject(project, rootDir) { |
||||
|
const projectDir = path.join(rootDir, project.dir); |
||||
|
const label = `${project.color}[${project.name}]${RESET}`; |
||||
|
|
||||
|
info(`${label} 启动中...`); |
||||
|
|
||||
|
const child = spawn('npm', ['run', 'dev'], { |
||||
|
cwd: projectDir, |
||||
|
stdio: ['ignore', 'pipe', 'pipe'], |
||||
|
env: { ...process.env }, |
||||
|
shell: true |
||||
|
}); |
||||
|
|
||||
|
child.stdout.on('data', (data) => { |
||||
|
const lines = data.toString().split('\n').filter(l => l.trim()); |
||||
|
for (const line of lines) { |
||||
|
log(` ${label} ${line}`); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
child.stderr.on('data', (data) => { |
||||
|
const lines = data.toString().split('\n').filter(l => l.trim()); |
||||
|
for (const line of lines) { |
||||
|
// Vite 的 deprecation warning 降级显示
|
||||
|
if (line.includes('Deprecation Warning') || line.includes('More info:')) { |
||||
|
log(` ${label} ${DIM}${line}${RESET}`); |
||||
|
} else { |
||||
|
log(` ${label} ${YELLOW}${line}${RESET}`); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
child.on('error', (err) => { |
||||
|
error(`${label} 启动失败: ${err.message}`); |
||||
|
}); |
||||
|
|
||||
|
child.on('close', (code) => { |
||||
|
if (code !== null && code !== 0) { |
||||
|
warn(`${label} 进程退出,退出码: ${code}`); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return child; |
||||
|
} |
||||
|
|
||||
|
// ─── 主流程 ──────────────────────────────────────────
|
||||
|
async function main() { |
||||
|
banner(); |
||||
|
|
||||
|
const args = process.argv.slice(2); |
||||
|
const killOnly = args.includes('--kill'); |
||||
|
const targets = []; |
||||
|
|
||||
|
if (args.includes('web')) targets.push('web'); |
||||
|
if (args.includes('mall')) targets.push('mall'); |
||||
|
if (targets.length === 0 && !killOnly) { |
||||
|
targets.push('web', 'mall'); |
||||
|
} |
||||
|
|
||||
|
const rootDir = path.resolve(__dirname, '..'); |
||||
|
|
||||
|
// ── 步骤 1:显示计划 ──
|
||||
|
if (killOnly) { |
||||
|
info('模式: 仅关闭端口占用进程'); |
||||
|
} else if (targets.length === 2) { |
||||
|
info('模式: 同时启动 健康APP + 商城'); |
||||
|
} else { |
||||
|
info(`模式: 仅启动 ${PROJECTS[targets[0]].name}`); |
||||
|
} |
||||
|
log(''); |
||||
|
|
||||
|
// ── 步骤 2:检查端口 ──
|
||||
|
log(`${BOLD}[1/3] 检查端口占用${RESET}`); |
||||
|
log('─'.repeat(48)); |
||||
|
|
||||
|
const portProjects = killOnly |
||||
|
? Object.values(PROJECTS) |
||||
|
: targets.map(t => PROJECTS[t]); |
||||
|
|
||||
|
for (const proj of portProjects) { |
||||
|
const freed = await ensurePortFree(proj.port, proj.name); |
||||
|
if (!freed && !killOnly) { |
||||
|
error(`无法释放端口 ${proj.port},请手动处理后重试`); |
||||
|
process.exit(1); |
||||
|
} |
||||
|
} |
||||
|
log(''); |
||||
|
|
||||
|
if (killOnly) { |
||||
|
success('端口清理完成'); |
||||
|
process.exit(0); |
||||
|
} |
||||
|
|
||||
|
// ── 步骤 3:检查依赖 ──
|
||||
|
log(`${BOLD}[2/3] 检查项目依赖${RESET}`); |
||||
|
log('─'.repeat(48)); |
||||
|
|
||||
|
for (const key of targets) { |
||||
|
const proj = PROJECTS[key]; |
||||
|
const projDir = path.join(rootDir, proj.dir); |
||||
|
if (checkNodeModules(projDir)) { |
||||
|
success(`${proj.name}: node_modules 已就绪`); |
||||
|
} else { |
||||
|
warn(`${proj.name}: 未安装依赖`); |
||||
|
const ok = installDeps(projDir, proj.name); |
||||
|
if (!ok) { |
||||
|
error('依赖安装失败,退出'); |
||||
|
process.exit(1); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
log(''); |
||||
|
|
||||
|
// ── 步骤 4:启动服务 ──
|
||||
|
log(`${BOLD}[3/3] 启动开发服务${RESET}`); |
||||
|
log('─'.repeat(48)); |
||||
|
|
||||
|
const children = []; |
||||
|
for (const key of targets) { |
||||
|
const child = startProject(PROJECTS[key], rootDir); |
||||
|
children.push(child); |
||||
|
} |
||||
|
|
||||
|
// 等待所有服务就绪后显示汇总
|
||||
|
await new Promise(resolve => setTimeout(resolve, 3000)); |
||||
|
log(''); |
||||
|
log(`${BOLD}╔═══════════════════════════════════════════════╗${RESET}`); |
||||
|
log(`${BOLD}║ 开发服务已启动 ║${RESET}`); |
||||
|
log(`${BOLD}╠═══════════════════════════════════════════════╣${RESET}`); |
||||
|
for (const key of targets) { |
||||
|
const proj = PROJECTS[key]; |
||||
|
log(`${BOLD}║${RESET} ${proj.color}${proj.name}${RESET}: ${BOLD}${proj.url}${RESET}`); |
||||
|
} |
||||
|
log(`${BOLD}╠═══════════════════════════════════════════════╣${RESET}`); |
||||
|
log(`${BOLD}║${RESET} 后端 API: ${BOLD}http://localhost:8080${RESET}`); |
||||
|
log(`${BOLD}║${RESET} 测试账号: ${DIM}13800138000 / 123456${RESET}`); |
||||
|
log(`${BOLD}╠═══════════════════════════════════════════════╣${RESET}`); |
||||
|
log(`${BOLD}║${RESET} 按 ${RED}Ctrl+C${RESET} 停止所有服务`); |
||||
|
log(`${BOLD}╚═══════════════════════════════════════════════╝${RESET}`); |
||||
|
log(''); |
||||
|
|
||||
|
// ── 优雅退出处理 ──
|
||||
|
function cleanup() { |
||||
|
log(''); |
||||
|
info('正在停止所有服务...'); |
||||
|
for (const child of children) { |
||||
|
try { |
||||
|
const isWin = os.platform() === 'win32'; |
||||
|
if (isWin) { |
||||
|
execSync(`taskkill /PID ${child.pid} /T /F`, { stdio: 'ignore', timeout: 5000 }); |
||||
|
} else { |
||||
|
child.kill('SIGTERM'); |
||||
|
} |
||||
|
} catch { |
||||
|
// 进程可能已退出
|
||||
|
} |
||||
|
} |
||||
|
success('所有服务已停止'); |
||||
|
process.exit(0); |
||||
|
} |
||||
|
|
||||
|
process.on('SIGINT', cleanup); |
||||
|
process.on('SIGTERM', cleanup); |
||||
|
|
||||
|
// 保持主进程运行
|
||||
|
await new Promise(() => {}); |
||||
|
} |
||||
|
|
||||
|
main().catch(err => { |
||||
|
error(`脚本执行出错: ${err.message}`); |
||||
|
process.exit(1); |
||||
|
}); |
||||
@ -0,0 +1,11 @@ |
|||||
|
#!/bin/bash |
||||
|
# 前端开发服务启动脚本 |
||||
|
# 用法: |
||||
|
# ./scripts/dev.sh 同时启动 web + mall |
||||
|
# ./scripts/dev.sh web 仅启动健康APP |
||||
|
# ./scripts/dev.sh mall 仅启动商城 |
||||
|
# ./scripts/dev.sh --kill 仅关闭端口占用进程 |
||||
|
|
||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" |
||||
|
cd "$SCRIPT_DIR/.." |
||||
|
node scripts/dev.js "$@" |
||||
Loading…
Reference in new issue