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