chore(scripts): 开发脚本支持 Flutter Windows 桌面端
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled

- pnpm start:dev 默认在 Windows 上启动桌面端(非 Web)
- 新增 pnpm start:dev:app:win 强制 Windows 桌面
- 新增 pnpm start:dev:app:web 强制 Chrome Web
- SIGINT 清理时同时终止 nuanji_app.exe
- 汇总输出区分桌面/Web 模式
This commit is contained in:
iven
2026-06-04 20:35:39 +08:00
parent bb388ed8ff
commit c253c8ddcf
2 changed files with 119 additions and 39 deletions

View File

@@ -7,6 +7,8 @@
"start:dev:backend": "node scripts/dev.mjs backend",
"start:dev:admin": "node scripts/dev.mjs admin",
"start:dev:app": "node scripts/dev.mjs app",
"start:dev:app:win": "node scripts/dev.mjs app:win",
"start:dev:app:web": "node scripts/dev.mjs app:web",
"start:stop": "node scripts/stop.mjs"
},
"devDependencies": {

View File

@@ -6,7 +6,9 @@
* pnpm start:dev # 启动全部 (后端+管理端+学生端)
* pnpm start:dev:backend # 只启动后端
* pnpm start:dev:admin # 只启动管理端前端
* pnpm start:dev:app # 只启动学生端 Flutter
* pnpm start:dev:app # 只启动学生端Windows桌面/Web 自动检测)
* pnpm start:dev:app:win # 强制学生端 Windows 桌面
* pnpm start:dev:app:web # 强制学生端 Web (Chrome)
* pnpm start:stop # 停止所有服务
*/
@@ -299,40 +301,94 @@ async function startAdmin() {
return waitForPort(PORTS.admin, "管理端", 20, 2000);
}
/** 启动学生端 Flutter Web */
async function startApp() {
log.info("清理旧学生端进程...");
killPort(PORTS.app, "学生端前端");
log.info("编译并启动 Flutter Web...");
/** 启动学生端 FlutterWindows 桌面 或 Web */
async function startApp(mode = "auto") {
const appDir = resolve(ROOT, "app");
const flutterBin = IS_WIN ? "D:\\flutter\\bin\\flutter.bat" : "flutter";
const child = spawn(`${flutterBin} run -d chrome --web-port=${PORTS.app} --dart-define=API_BASE_URL=http://localhost:${PORTS.backend}/api/v1 --dart-define=SSE_BASE_URL=http://localhost:${PORTS.backend}/api/v1`, {
cwd: appDir,
shell: true,
stdio: "pipe",
});
// 决定运行平台auto → Windows 上用桌面,其他用 Web
const useWindows = mode === "windows" || (mode === "auto" && IS_WIN);
child.stdout?.on("data", (data) => {
const lines = data.toString().trim().split("\n");
for (const line of lines) {
if (line.trim()) console.log(color.yellow("[app]") + " " + line);
}
});
if (useWindows) {
log.info("编译并启动 Flutter Windows 桌面端...");
const child = spawn(`${flutterBin} run -d windows --dart-define=API_BASE_URL=http://localhost:${PORTS.backend}/api/v1 --dart-define=SSE_BASE_URL=http://localhost:${PORTS.backend}/api/v1`, {
cwd: appDir,
shell: true,
stdio: "pipe",
});
child.stderr?.on("data", (data) => {
const lines = data.toString().trim().split("\n");
for (const line of lines) {
if (line.trim()) console.log(color.yellow("[app]") + " " + line);
}
});
child.stdout?.on("data", (data) => {
const lines = data.toString().trim().split("\n");
for (const line of lines) {
if (line.trim()) console.log(color.yellow("[app-win]") + " " + line);
}
});
child.on("error", (err) => {
log.err(`学生端启动失败: ${err.message}`);
});
child.stderr?.on("data", (data) => {
const lines = data.toString().trim().split("\n");
for (const line of lines) {
if (line.trim()) console.log(color.yellow("[app-win]") + " " + line);
}
});
return waitForPort(PORTS.app, "学生端", 40, 3000);
child.on("error", (err) => {
log.err(`学生端启动失败: ${err.message}`);
});
// Windows 桌面不监听端口,等待进程启动即可
// flutter run 会输出 "Syncing files" 表示就绪
return new Promise((resolve) => {
let ready = false;
const check = (data) => {
if (!ready && data.toString().includes("Syncing files to device Windows")) {
ready = true;
log.ok("学生端 (Windows 桌面) 已启动");
resolve(true);
}
};
child.stdout?.on("data", check);
child.stderr?.on("data", check);
// 超时保护
setTimeout(() => {
if (!ready) {
ready = true;
log.warn("学生端启动超时,但进程仍在运行");
resolve(true);
}
}, 180_000);
});
} else {
// Web 模式
log.info("清理旧学生端进程...");
killPort(PORTS.app, "学生端前端");
log.info("编译并启动 Flutter Web...");
const child = spawn(`${flutterBin} run -d chrome --web-port=${PORTS.app} --dart-define=API_BASE_URL=http://localhost:${PORTS.backend}/api/v1 --dart-define=SSE_BASE_URL=http://localhost:${PORTS.backend}/api/v1`, {
cwd: appDir,
shell: true,
stdio: "pipe",
});
child.stdout?.on("data", (data) => {
const lines = data.toString().trim().split("\n");
for (const line of lines) {
if (line.trim()) console.log(color.yellow("[app-web]") + " " + line);
}
});
child.stderr?.on("data", (data) => {
const lines = data.toString().trim().split("\n");
for (const line of lines) {
if (line.trim()) console.log(color.yellow("[app-web]") + " " + line);
}
});
child.on("error", (err) => {
log.err(`学生端启动失败: ${err.message}`);
});
return waitForPort(PORTS.app, "学生端", 40, 3000);
}
}
// ===== 主流程 =====
@@ -351,13 +407,18 @@ function printBanner(services) {
console.log();
}
function printSummary() {
function printSummary(appMode) {
const isWindows = appMode === "windows" || (appMode === "auto" && IS_WIN);
console.log();
console.log(color.bold(color.green(" ═══ 暖记开发环境已启动 ═══")));
console.log();
console.log(` 后端 API: http://localhost:${PORTS.backend}`);
console.log(` 管理端: http://localhost:${PORTS.admin} (admin/admin123)`);
console.log(` 学生端: http://localhost:${PORTS.app}`);
if (isWindows) {
console.log(` 学生端: Flutter Windows 桌面`);
} else {
console.log(` 学生端: http://localhost:${PORTS.app}`);
}
console.log();
console.log(color.gray(" 停止所有服务: pnpm start:stop"));
console.log();
@@ -365,15 +426,31 @@ function printSummary() {
async function main() {
const arg = process.argv[2] || "all";
const services =
arg === "all"
? ["backend", "admin", "app"]
: [arg];
const validServices = ["backend", "admin", "app", "all"];
// 解析参数 → 服务列表 + app 运行模式
let services;
let appMode = "auto"; // auto | windows | web
switch (arg) {
case "all":
services = ["backend", "admin", "app"];
break;
case "app:win":
services = ["app"];
appMode = "windows";
break;
case "app:web":
services = ["app"];
appMode = "web";
break;
default:
services = [arg];
}
const validServices = ["backend", "admin", "app", "all", "app:win", "app:web"];
if (!validServices.includes(arg)) {
log.err(`未知参数: ${arg}`);
console.log("用法: pnpm start:dev [all|backend|admin|app]");
console.log("用法: pnpm start:dev [all|backend|admin|app|app:win|app:web]");
process.exit(1);
}
@@ -401,7 +478,7 @@ async function main() {
frontendPromises.push(startAdmin());
}
if (services.includes("app")) {
frontendPromises.push(startApp());
frontendPromises.push(startApp(appMode));
}
if (frontendPromises.length > 0) {
@@ -413,8 +490,8 @@ async function main() {
}
// 打印汇总
if (arg === "all") {
printSummary();
if (arg === "all" || arg === "app:win" || arg === "app:web") {
printSummary(appMode);
}
// 保持进程存活(不退出 Node让子进程继续运行
@@ -427,6 +504,7 @@ async function main() {
killPort(PORTS.app, "学生端前端");
if (IS_WIN) {
try { execSync("taskkill /F /IM erp-server.exe", { stdio: "pipe" }); } catch {}
try { execSync("taskkill /F /IM nuanji_app.exe", { stdio: "pipe" }); } catch {}
}
log.ok("所有服务已停止");
process.exit(0);