From c253c8ddcf38473f79023481c36bfce4f8d6d7c6 Mon Sep 17 00:00:00 2001 From: iven Date: Thu, 4 Jun 2026 20:35:39 +0800 Subject: [PATCH] =?UTF-8?q?chore(scripts):=20=E5=BC=80=E5=8F=91=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E6=94=AF=E6=8C=81=20Flutter=20Windows=20=E6=A1=8C?= =?UTF-8?q?=E9=9D=A2=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pnpm start:dev 默认在 Windows 上启动桌面端(非 Web) - 新增 pnpm start:dev:app:win 强制 Windows 桌面 - 新增 pnpm start:dev:app:web 强制 Chrome Web - SIGINT 清理时同时终止 nuanji_app.exe - 汇总输出区分桌面/Web 模式 --- package.json | 2 + scripts/dev.mjs | 156 ++++++++++++++++++++++++++++++++++++------------ 2 files changed, 119 insertions(+), 39 deletions(-) diff --git a/package.json b/package.json index f440baf..362b6ed 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/scripts/dev.mjs b/scripts/dev.mjs index cc617c8..1326b62 100644 --- a/scripts/dev.mjs +++ b/scripts/dev.mjs @@ -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..."); +/** 启动学生端 Flutter(Windows 桌面 或 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);