diff --git a/build/clean-and-build.bat b/build/clean-and-build.bat index 2bd9ac8..27e9ba4 100644 --- a/build/clean-and-build.bat +++ b/build/clean-and-build.bat @@ -1,52 +1,18 @@ @echo off chcp 65001 >nul echo ======================================== -echo 清理并重新打包 +echo 清理并重新打包`r echo ======================================== echo. -echo [1/5] 结束所有相关进程... +echo [1/5] 结束所有相关进�?.. echo 正在停止 NPQS9100... taskkill /F /IM NPQS9100.exe 2>nul -echo 正在停止 MySQL... -taskkill /F /IM mysqld.exe 2>nul -timeout /t 2 /nobreak >nul - -REM 验证 MySQL 是否真的停止了 -tasklist | find /I "mysqld.exe" >nul 2>&1 -if %errorlevel% equ 0 ( - echo ! MySQL 进程还在运行,再次尝试... - taskkill /F /IM mysqld.exe 2>nul - timeout /t 2 /nobreak >nul - - REM 再次验证 - tasklist | find /I "mysqld.exe" >nul 2>&1 - if %errorlevel% equ 0 ( - echo ! MySQL 进程仍在运行,使用强制方法... - REM 找出所有 mysqld.exe 的 PID 并逐个杀死 - for /f "tokens=2" %%a in ('tasklist ^| find /I "mysqld.exe"') do ( - echo 强制结束 PID: %%a - taskkill /F /PID %%a 2>nul - ) - timeout /t 2 /nobreak >nul - ) -) - -REM 最终验证 -tasklist | find /I "mysqld.exe" >nul 2>&1 -if %errorlevel% equ 0 ( - echo ✗ 警告:MySQL 进程可能仍在运行 - echo 请手动运行 build\extraResources\mysql\kill-running-port.bat - pause -) else ( - echo ✓ MySQL 已完全停止 -) - echo 正在停止 Java... taskkill /F /IM java.exe 2>nul taskkill /F /IM javaw.exe 2>nul -echo ✓ 所有进程已结束 +echo �?所有进程已结束 timeout /t 2 /nobreak >nul echo. @@ -55,50 +21,49 @@ cd /d "%~dp0.." if exist out ( rmdir /s /q out 2>nul if exist out ( - echo ✗ 删除失败,请手动删除 out 目录 + echo �?删除失败,请手动删除 out 目录 pause exit /b 1 ) else ( - echo ✓ out 目录已删除 + echo �?out 目录已删除`r ) ) else ( - echo ✓ out 目录不存在 + echo �?out 目录不存在`r ) echo. echo [3/5] 构建前端代码... call npm run build-frontend if %errorlevel% neq 0 ( - echo ✗ 前端构建失败 + echo �?前端构建失败 pause exit /b 1 ) -echo ✓ 前端代码构建完成 +echo �?前端代码构建完成 echo. echo [4/5] 构建 electron 代码... call npm run build-electron if %errorlevel% neq 0 ( - echo ✗ electron 构建失败 + echo �?electron 构建失败 pause exit /b 1 ) -echo ✓ electron 代码构建完成 +echo �?electron 代码构建完成 echo. echo [5/5] 打包 Windows 版本(包含代码加密)... call npm run build-w if %errorlevel% neq 0 ( - echo ✗ 打包失败 + echo �?打包失败 pause exit /b 1 ) echo. echo ======================================== -echo ✓ 打包完成! +echo �?打包完成!`r echo 输出目录: out\win-unpacked\ echo ======================================== echo. pause - diff --git a/build/extraResources/java/entrance.jar b/build/extraResources/java/entrance.jar index 4dd6ea9..0ce735f 100644 Binary files a/build/extraResources/java/entrance.jar and b/build/extraResources/java/entrance.jar differ diff --git a/build/extraResources/mysql/reset-init.sql b/build/extraResources/mysql/reset-init.sql deleted file mode 100644 index 8fb7075..0000000 --- a/build/extraResources/mysql/reset-init.sql +++ /dev/null @@ -1,6 +0,0 @@ -ALTER USER IF EXISTS 'root'@'localhost' IDENTIFIED BY 'njcnpqs'; -CREATE USER IF NOT EXISTS 'root'@'127.0.0.1' IDENTIFIED BY 'njcnpqs'; -GRANT ALL PRIVILEGES ON *.* TO 'root'@'127.0.0.1' WITH GRANT OPTION; -CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED BY 'njcnpqs'; -GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION; -FLUSH PRIVILEGES; diff --git a/build/icons/icon.png b/build/icons/icon.png index 802a3e1..dd02cdb 100644 Binary files a/build/icons/icon.png and b/build/icons/icon.png differ diff --git a/cmd/builder.json b/cmd/builder.json index 7d7581b..b596461 100644 --- a/cmd/builder.json +++ b/cmd/builder.json @@ -35,6 +35,10 @@ "from": "build/extraResources/read.txt", "to": "extraResources/read.txt" }, + { + "from": "build/extraResources/使用说明.txt", + "to": "extraResources/使用说明.txt" + }, { "from": "scripts/", "to": "scripts", @@ -51,9 +55,30 @@ "from": "build/extraResources/jre", "to": "jre", "filter": ["**/*"] + }, + { + "from": "build/NPQS9100-启动器.bat", + "to": "NPQS9100-启动器.bat" + }, + { + "from": "build/extraResources/使用说明.txt", + "to": "使用说明.txt" + }, + { + "from": "build/upgrade.bat", + "to": "upgrade.bat" + }, + { + "from": "build/rollback.bat", + "to": "rollback.bat" + }, + { + "from": "build/README-升级回滚.txt", + "to": "README-升级回滚.txt" } ], "win": { + "icon": "public/images/icon.png", "artifactName": "${productName}-${os}-${version}-${arch}.${ext}", "target": [ { diff --git a/doc/便携式JRE集成指南.md b/doc/便携式JRE集成指南.md deleted file mode 100644 index f9319fb..0000000 --- a/doc/便携式JRE集成指南.md +++ /dev/null @@ -1,133 +0,0 @@ -## 便携式 JRE/JDK8 集成指南 - -本指南介绍如何将 JRE 8 以“便携式”(解压即用、无需安装)的方式随应用打包,并在 Electron 主进程中通过绝对路径调用,从而避免要求用户在系统中安装 JDK/JRE。 - -### 为什么选择便携式 JRE -- 无需管理员权限与系统环境变量配置,用户无感知。 -- 不污染系统环境(不写入 JAVA_HOME/PATH)。 -- 跨平台一致,可精简体积、可控版本。 - -### 适用场景 -- 运行 Java 程序或 JAR 包(仅需运行时)。 -- 不需要 javac/jcmd/jmap 等开发/诊断工具(若需要,请改为随包便携式 JDK)。 - -### 推荐的 JRE 8 发行版(可再分发) -- Azul Zulu 8 JRE(可选 ZuluFX 含 JavaFX):[下载页面](https://www.azul.com/downloads/?version=java-8-lts&package=jre) -- BellSoft Liberica 8 JRE(Standard/Full,Full 含 JavaFX):[下载页面](https://bell-sw.com/pages/downloads/#/java-8-lts) -- Eclipse Temurin 8 JRE(Adoptium):[下载页面](https://adoptium.net/temurin/releases/?version=8) - -选择要点: -- 需要 AWT/Swing/字体/打印 → 选择非 headless 包。 -- 需要 JavaFX → 选择 Liberica Full 或 ZuluFX。 -- 仅命令行/服务端 → 任意 JRE 8(headless 也可)。 - -### 目录放置约定 -将解压后的 JRE 放入项目的 `build/extraResources/jre`,保证内部存在 `bin/java(.exe)`: - -``` -build/ - extraResources/ - jre/ - bin/ - java(.exe) - lib/ - ... -``` - -构建后在生产环境可通过 `process.resourcesPath` 访问: -`/resources/extraResources/jre/bin/java(.exe)`。 - -### 主进程调用示例 -在 `electron/preload/lifecycle.js` 或你的业务模块中封装 Java 运行工具(开发/生产两种路径): - -```js -const path = require('path'); -const { spawn } = require('child_process'); - -function getExtraResourcesDir() { - // 开发态:使用项目目录;生产态:使用 asar/resources 目录 - const isDev = !!process.env.EE_DEV || process.env.NODE_ENV === 'development'; - return isDev - ? path.join(process.cwd(), 'build', 'extraResources') - : path.join(process.resourcesPath, 'extraResources'); -} - -function getJavaBinPath() { - const extraDir = getExtraResourcesDir(); - const javaBinName = process.platform === 'win32' ? 'java.exe' : 'java'; - return path.join(extraDir, 'jre', 'bin', javaBinName); -} - -function runJavaJar(jarAbsPath, args = [], options = {}) { - const javaPath = getJavaBinPath(); - const child = spawn(javaPath, ['-jar', jarAbsPath, ...args], { - stdio: 'inherit', - ...options, - }); - return child; -} - -async function ensureJavaVersion(logger) { - return new Promise((resolve) => { - const child = spawn(getJavaBinPath(), ['-version']); - let out = ''; - let err = ''; - child.stdout && child.stdout.on('data', (d) => (out += d.toString())) - child.stderr && child.stderr.on('data', (d) => (err += d.toString())) - child.on('close', () => { - const text = (out + '\n' + err).trim(); - logger && logger.info('[java] version check:', text); - resolve(text.includes('1.8.0')); - }); - }); -} - -module.exports = { getJavaBinPath, runJavaJar, ensureJavaVersion }; -``` - -在生命周期中调用(示例): - -```js -const { logger } = require('ee-core/log'); -const path = require('path'); -const { runJavaJar, ensureJavaVersion } = require('./java-runner'); - -class Lifecycle { - async ready() { - const ok = await ensureJavaVersion(logger); - if (!ok) { - logger.error('[java] 未检测到 JRE 8,请检查 extraResources/jre 是否存在'); - } - } - - async windowReady() { - const jarPath = path.join(process.resourcesPath || process.cwd(), 'extraResources', 'tools', 'your-app.jar'); - // 示例:延后在某业务时机再启动 Java 进程 - // const proc = runJavaJar(jarPath, ['--arg1', 'value']); - } -} -``` - -注意:示例中的 `java-runner` 为上文工具函数文件,实际请按你的项目结构放置。 - -### 验证清单 -- 运行 `jre/bin/java -version` 输出包含 `1.8.0_xxx`。 -- 若涉及 GUI/字体/打印,验证 AWT/Swing 中文渲染与打印。 -- 若需 JavaFX,验证 JavaFX Demo 启动。 -- 若涉及 TLS/HTTPS,验证 SSL 通信正常。 - -### 许可与合规 -- Azul Zulu、Eclipse Temurin(Adoptium)、BellSoft Liberica 的 JRE/JDK 8 发行包均可免费再分发(GPLv2+CE 或厂商许可证)。 -- 建议在应用的“关于/许可证”中附上所选发行版的许可证链接与致谢。 - -### 常见问题 -1) 是否“阉割”? -— 上述 JRE 8 发行版均为标准运行时,通过兼容性测试;JRE 不包含开发者工具属于正常区别,不是删减。 - -2) 何时需要 JDK 而不是 JRE? -— 需要 `javac` 编译或 `jcmd/jmap` 等诊断工具,或你的 Java 组件依赖 `tools.jar` 时。 - -3) 体积如何优化? -— 选择 headless(若无 GUI 需求)、去除无用语言/字体包;或改用 JDK 9+ 使用 jlink(不适用于 8)。 - - diff --git a/doc/打包方案对比.md b/doc/打包方案对比.md deleted file mode 100644 index b7d8b41..0000000 --- a/doc/打包方案对比.md +++ /dev/null @@ -1,436 +0,0 @@ -# 应用打包方案对比与实现 - -本文档详细说明 ElectronEgg 应用的两种打包方案:纯绿色版方案 和 双版本方案。 - ---- - -## 方案对比 - -| 特性 | 方案一:纯绿色版 | 方案二:双版本打包 | -|------|-----------------|-------------------| -| **打包产物** | 单个便携版 exe | 安装版 exe + 便携版 exe | -| **安装过程** | 无需安装 | 安装版需安装,便携版无需 | -| **桌面快捷方式** | 应用内自动创建 | 安装版自动创建,便携版手动或自动 | -| **开始菜单** | 无 | 安装版有 | -| **卸载程序** | 无(直接删除) | 安装版有 | -| **适用场景** | 临时使用、U盘携带 | 正式部署、企业分发 | -| **用户体验** | 灵活、轻量 | 专业、完整 | -| **打包时间** | 快 | 较慢(打包两次) | -| **分发复杂度** | 简单(单文件) | 中等(两个文件) | - ---- - -## 方案一:纯绿色版 + 自动创建快捷方式 - -### 特点 -- ✅ 单个 exe 文件,双击即用 -- ✅ 首次启动时询问是否创建桌面快捷方式 -- ✅ 无需安装,无需卸载 -- ✅ 适合快速分发和临时使用 - -### 实现步骤 - -#### 1. 修改打包配置 - -**文件**:[cmd/builder.json](../cmd/builder.json) - -```json -{ - "productName": "南京灿能工具", - "appId": "com.canneng.tool", - "copyright": "© 2025 hongawen", - "directories": { - "output": "out" - }, - "asar": true, - "files": [ - "**/*", - "!cmd/", - "!data/", - "!electron/", - "!frontend/", - "!logs/", - "!out/", - "!go/", - "!python/" - ], - "extraResources": { - "from": "build/extraResources/", - "to": "extraResources" - }, - "publish": [ - { - "provider": "generic", - "url": "https://your-update-server.com" - } - ], - "win": { - "icon": "build/icons/icon.ico", - "artifactName": "${productName}-${os}-${version}-${arch}.${ext}", - "target": [ - { - "target": "portable" - } - ] - } -} -``` - -#### 2. 添加自动创建快捷方式功能 - -**文件**:[electron/preload/lifecycle.js](../electron/preload/lifecycle.js) - -在 `windowReady()` 钩子中添加以下代码: - -```javascript -const { logger } = require('ee-core/log'); -const { getConfig } = require('ee-core/config'); -const { getMainWindow } = require('ee-core/electron'); - -class Lifecycle { - - async ready() { - logger.info('[lifecycle] ready'); - } - - async electronAppReady() { - logger.info('[lifecycle] electron-app-ready'); - } - - async windowReady() { - logger.info('[lifecycle] window-ready'); - - // 延迟加载,无白屏 - const { windowsOption } = getConfig(); - if (windowsOption.show == false) { - const win = getMainWindow(); - win.once('ready-to-show', () => { - win.show(); - win.focus(); - }) - } - - // 绿色版自动创建桌面快捷方式 - await this.createDesktopShortcut(); - } - - /** - * 为绿色版创建桌面快捷方式 - */ - async createDesktopShortcut() { - const { app, dialog, shell } = require('electron'); - const path = require('path'); - const fs = require('fs'); - - // 判断是否为便携版(绿色版) - // 安装版通常在 C:\Program Files 或 AppData\Local\Programs - const isPortable = process.platform === 'win32' && - !process.execPath.includes('Program Files') && - !process.execPath.includes('AppData\\Local\\Programs'); - - if (!isPortable) { - logger.info('[lifecycle] 非便携版,跳过快捷方式创建'); - return; - } - - try { - const desktopPath = app.getPath('desktop'); - const shortcutPath = path.join(desktopPath, '南京灿能工具.lnk'); - - // 如果快捷方式已存在,跳过 - if (fs.existsSync(shortcutPath)) { - logger.info('[lifecycle] 桌面快捷方式已存在'); - return; - } - - // 询问用户是否创建快捷方式 - const result = await dialog.showMessageBox({ - type: 'question', - buttons: ['创建', '跳过'], - defaultId: 0, - title: '创建桌面快捷方式', - message: '是否在桌面创建快捷方式?', - detail: '方便您下次快速启动应用' - }); - - if (result.response === 0) { - // Windows 下创建快捷方式 - const success = shell.writeShortcutLink(shortcutPath, { - target: process.execPath, - cwd: path.dirname(process.execPath), - description: '南京灿能C端工具', - icon: process.execPath, - iconIndex: 0 - }); - - if (success) { - logger.info('[lifecycle] 桌面快捷方式创建成功'); - await dialog.showMessageBox({ - type: 'info', - title: '成功', - message: '桌面快捷方式已创建', - buttons: ['确定'] - }); - } else { - logger.error('[lifecycle] 桌面快捷方式创建失败'); - } - } else { - logger.info('[lifecycle] 用户跳过创建快捷方式'); - } - } catch (error) { - logger.error('[lifecycle] 创建快捷方式时出错:', error); - } - } - - async beforeClose() { - logger.info('[lifecycle] before-close'); - } -} -Lifecycle.toString = () => '[class Lifecycle]'; - -module.exports = { - Lifecycle -}; -``` - -#### 3. 打包命令 - -```bash -npm run build # 完整构建 -npm run build-w # 打包 Windows 便携版 -``` - -#### 4. 产物说明 - -打包完成后,在 `out/` 目录下会生成: -``` -out/ -└── 南京灿能工具-win-4.0.0-x64.exe (便携版,约 150-200MB) -``` - ---- - -## 方案二:双版本打包(安装版 + 便携版) - -### 特点 -- ✅ 提供两种版本供用户选择 -- ✅ 安装版:专业、完整的安装体验 -- ✅ 便携版:灵活、轻量,无需安装 -- ✅ 适合正式产品发布 - -### 实现步骤 - -#### 1. 修改打包配置 - -**文件**:[cmd/builder.json](../cmd/builder.json) - -```json -{ - "productName": "南京灿能工具", - "appId": "com.canneng.tool", - "copyright": "© 2025 hongawen", - "directories": { - "output": "out" - }, - "asar": true, - "files": [ - "**/*", - "!cmd/", - "!data/", - "!electron/", - "!frontend/", - "!logs/", - "!out/", - "!go/", - "!python/" - ], - "extraResources": { - "from": "build/extraResources/", - "to": "extraResources" - }, - "nsis": { - "oneClick": false, - "allowElevation": true, - "allowToChangeInstallationDirectory": true, - "installerIcon": "build/icons/icon.ico", - "uninstallerIcon": "build/icons/icon.ico", - "installerHeaderIcon": "build/icons/icon.ico", - "createDesktopShortcut": true, - "createStartMenuShortcut": true, - "shortcutName": "南京灿能工具", - "artifactName": "${productName}-Setup-${version}.${ext}" - }, - "portable": { - "artifactName": "${productName}-Portable-${version}.${ext}" - }, - "publish": [ - { - "provider": "generic", - "url": "https://your-update-server.com" - } - ], - "win": { - "icon": "build/icons/icon.ico", - "target": [ - { - "target": "nsis", - "arch": ["x64"] - }, - { - "target": "portable", - "arch": ["x64"] - } - ] - } -} -``` - -#### 2. 便携版快捷方式功能(可选) - -如果希望便携版也能自动创建快捷方式,使用**方案一**中的 `createDesktopShortcut()` 代码。 - -#### 3. 打包命令 - -```bash -npm run build # 完整构建 -npm run build-w # 打包两个版本 -``` - -#### 4. 产物说明 - -打包完成后,在 `out/` 目录下会生成: -``` -out/ -├── 南京灿能工具-Setup-4.0.0.exe (安装版,约 150MB) -└── 南京灿能工具-Portable-4.0.0.exe (便携版,约 150-200MB) -``` - -#### 5. 版本差异说明 - -**安装版 (NSIS)**: -- 需要安装到系统(默认 C:\Program Files) -- 自动创建桌面快捷方式 -- 自动创建开始菜单项 -- 提供卸载程序 -- 支持自动更新 -- 适合企业部署、长期使用 - -**便携版 (Portable)**: -- 单个 exe 文件 -- 双击直接运行(首次会自解压) -- 无需安装,无需卸载 -- 可放在 U 盘随身携带 -- 适合临时使用、测试环境 - ---- - -## 快捷方式创建原理(技术细节) - -### Windows 快捷方式 (.lnk) - -```javascript -shell.writeShortcutLink(shortcutPath, { - target: process.execPath, // 目标程序路径 - cwd: path.dirname(process.execPath), // 工作目录 - description: '应用描述', // 快捷方式描述 - icon: process.execPath, // 图标路径 - iconIndex: 0, // 图标索引 - args: '', // 启动参数(可选) - appUserModelId: 'com.app.id' // Windows 应用 ID(可选) -}) -``` - -### 判断是否为便携版 - -```javascript -const isPortable = process.platform === 'win32' && - !process.execPath.includes('Program Files') && - !process.execPath.includes('AppData\\Local\\Programs'); -``` - -**原理**: -- 安装版通常安装在 `C:\Program Files\YourApp\` -- 或者 `C:\Users\用户名\AppData\Local\Programs\YourApp\` -- 便携版可以在任意位置运行 - ---- - -## 推荐配置 - -### 企业级应用(推荐方案二) -``` -✅ 提供两个版本 -✅ 主推安装版(专业形象) -✅ 提供便携版作为备选 -``` - -### 轻量工具(推荐方案一) -``` -✅ 只提供便携版 -✅ 应用内自动创建快捷方式 -✅ 简化分发流程 -``` - ---- - -## 常见问题 - -### Q1: 便携版首次启动为什么慢? -**A**: 便携版是自解压程序,首次运行需要解压资源到临时目录(约 3-5 秒)。后续启动会快很多。 - -### Q2: 便携版数据存储在哪里? -**A**: -- 用户数据:`C:\Users\用户名\AppData\Roaming\你的appId\` -- 临时文件:`C:\Users\用户名\AppData\Local\Temp\` - -### Q3: 如何让便携版也支持自动更新? -**A**: 需要配置 `electron-updater`,但便携版更新体验不如安装版。建议: -- 安装版:使用自动更新 -- 便携版:提示用户下载新版本 - -### Q4: 可以同时运行两个版本吗? -**A**: 不建议。虽然技术上可行,但会导致数据冲突(共享同一个 userData 目录)。 - -### Q5: 如何自定义快捷方式图标? -**A**: 在 `build/icons/` 目录放置 `.ico` 文件,并在 `builder.json` 中配置: -```json -"win": { - "icon": "build/icons/custom-icon.ico" -} -``` - ---- - -## 测试检查清单 - -打包完成后,请进行以下测试: - -### 安装版测试 -- [ ] 安装到默认路径成功 -- [ ] 安装到自定义路径成功 -- [ ] 桌面快捷方式正常 -- [ ] 开始菜单项正常 -- [ ] 应用启动正常 -- [ ] 卸载程序正常 - -### 便携版测试 -- [ ] 双击 exe 正常启动 -- [ ] 首次启动自动创建快捷方式(如已实现) -- [ ] 桌面快捷方式可用 -- [ ] 应用功能正常 -- [ ] 关闭后再次启动正常 -- [ ] 可移动到其他目录运行 - ---- - -## 参考资源 - -- electron-builder 官方文档: https://www.electron.build/ -- NSIS 配置: https://www.electron.build/configuration/nsis -- Portable 配置: https://www.electron.build/configuration/portable -- Electron shell API: https://www.electronjs.org/docs/latest/api/shell - ---- - -*文档创建时间: 2025-10-14* -*作者: hongawen* diff --git a/electron/config/config.default.js b/electron/config/config.default.js index 1d4b156..7a3e982 100644 --- a/electron/config/config.default.js +++ b/electron/config/config.default.js @@ -25,7 +25,7 @@ module.exports = () => { }, frame: true, show: false, // 初始不显示,等待服务启动完成后再显示 - icon: path.join(getBaseDir(), 'public', 'images', 'logo-32.png'), + icon: path.join(getBaseDir(), 'public', 'images', 'icon.png'), }, logger: { level: 'INFO', diff --git a/electron/controller/mysql.js b/electron/controller/mysql.js deleted file mode 100644 index b925bca..0000000 --- a/electron/controller/mysql.js +++ /dev/null @@ -1,75 +0,0 @@ -const path = require('path'); - -// 动态获取 scripts 目录路径 -function getScriptsPath(scriptName) { - // 开发环境 - const devPath = path.join(__dirname, '../../scripts', scriptName); - // 生产环境(打包后) - const prodPath = path.join(process.resourcesPath, 'scripts', scriptName); - - try { - // 先尝试开发环境路径 - require.resolve(devPath); - return devPath; - } catch (e) { - // 如果开发环境路径不存在,使用生产环境路径 - return prodPath; - } -} - -// 延迟加载 MySQLManager -let MySQLManager = null; -function getMySQLManager() { - if (!MySQLManager) { - MySQLManager = require(getScriptsPath('start-mysql')); - } - return MySQLManager; -} - -class MySQLController { - - /** - * 启动MySQL服务 - */ - async start() { - try { - const MySQLManagerClass = getMySQLManager(); - const mysqlManager = new MySQLManagerClass(); - await mysqlManager.start(); - return { success: true, message: 'MySQL started successfully' }; - } catch (error) { - return { success: false, message: error.message }; - } - } - - /** - * 停止MySQL服务 - */ - async stop() { - try { - const MySQLManagerClass = getMySQLManager(); - const mysqlManager = new MySQLManagerClass(); - await mysqlManager.stop(); - return { success: true, message: 'MySQL stopped successfully' }; - } catch (error) { - return { success: false, message: error.message }; - } - } - - /** - * 获取MySQL连接配置 - */ - async getConnectionConfig() { - try { - const MySQLManagerClass = getMySQLManager(); - const mysqlManager = new MySQLManagerClass(); - const config = mysqlManager.getConnectionConfig(); - return { success: true, data: config }; - } catch (error) { - return { success: false, message: error.message }; - } - } -} - -MySQLController.toString = () => '[class MySQLController]'; -module.exports = MySQLController; \ No newline at end of file diff --git a/electron/main.js b/electron/main.js index a552255..c506dfc 100644 --- a/electron/main.js +++ b/electron/main.js @@ -1,27 +1,127 @@ const { ElectronEgg } = require('ee-core'); -const { app, Menu, ipcMain } = require('electron'); +const { app, Menu, ipcMain, Tray, dialog, BrowserWindow } = require('electron'); +const path = require('path'); const lifecycle = require('./preload/lifecycle'); const { preload } = require('./preload'); // new app const electronApp = new ElectronEgg(); +// 全局变量 +let tray = null; +let isQuitting = false; + +// 创建系统托盘 +function createTray() { + try { + // 开发环境和生产环境的图标路径 + const isDev = !process.resourcesPath; + const iconPath = isDev + ? path.join(__dirname, '..', 'public', 'images', 'tray.png') + : path.join(process.resourcesPath, 'app.asar.unpacked', 'public', 'images', 'tray.png'); + + console.log('[Tray] Icon path:', iconPath); + + // 检查图标文件是否存在 + const fs = require('fs'); + if (!fs.existsSync(iconPath)) { + console.error('[Tray] Icon file not found:', iconPath); + // 如果找不到,尝试使用备用路径(主图标) + const fallbackIcon = isDev + ? path.join(__dirname, '..', 'public', 'images', 'icon.png') + : path.join(process.resourcesPath, 'app.asar.unpacked', 'public', 'images', 'icon.png'); + + if (fs.existsSync(fallbackIcon)) { + console.log('[Tray] Using fallback icon:', fallbackIcon); + tray = new Tray(fallbackIcon); + } else { + console.error('[Tray] No icon available, tray not created'); + return; + } + } else { + tray = new Tray(iconPath); + } + + tray.setToolTip('NPQS-9100自动检测平台'); + console.log('[Tray] Tray created successfully'); + + // 创建托盘菜单 + const contextMenu = Menu.buildFromTemplate([ + { + label: '显示主窗口', + click: () => { + const mainWindow = BrowserWindow.getAllWindows()[0]; + if (mainWindow) { + if (mainWindow.isMinimized()) { + mainWindow.restore(); + } + mainWindow.show(); + mainWindow.focus(); + } + } + }, + { type: 'separator' }, + { + label: '退出', + click: async () => { + // 弹出确认对话框 + const { response } = await dialog.showMessageBox({ + type: 'question', + title: '退出确认', + message: '确定退出应用吗?', + buttons: ['取消', '确定退出'], + defaultId: 0, + cancelId: 0 + }); + + if (response === 1) { + // 用户点击了"确定退出" + isQuitting = true; + + // 获取主窗口 + const mainWindow = BrowserWindow.getAllWindows()[0]; + + // 移除所有 close 监听器,避免阻止关闭 + if (mainWindow) { + mainWindow.removeAllListeners('close'); + } + + // 执行清理 + await lifecycle.cleanup(); + + // 退出应用 + app.quit(); + } + } + } + ]); + + tray.setContextMenu(contextMenu); + + // 双击托盘图标显示窗口 + tray.on('double-click', () => { + const mainWindow = BrowserWindow.getAllWindows()[0]; + if (mainWindow) { + if (mainWindow.isMinimized()) { + mainWindow.restore(); + } + mainWindow.show(); + mainWindow.focus(); + } + }); + + } catch (error) { + console.error('[Tray] Failed to create tray:', error); + tray = null; + } +} + // 创建应用菜单 function createApplicationMenu() { const template = [ { label: '查看', submenu: [ - { - label: '显示/隐藏服务日志', - accelerator: 'F12', - click: () => { - if (lifecycle.logWindowManager) { - lifecycle.logWindowManager.toggle(); - } - } - }, - { type: 'separator' }, { role: 'reload', label: '刷新' }, { role: 'forceReload', label: '强制刷新' }, { type: 'separator' }, @@ -71,14 +171,87 @@ ipcMain.handle('toggle-log-window', () => { } }); +// 检查是否正在退出 +ipcMain.handle('is-quitting', () => { + return isQuitting; +}); + +// 处理单实例:当尝试启动第二个实例时,聚焦已有窗口 +app.on('second-instance', (event, commandLine, workingDirectory) => { + console.log('[Main] Second instance detected, focusing main window...'); + + // 获取主窗口 + const mainWindow = BrowserWindow.getAllWindows().find(win => { + return win.getTitle().includes('NPQS') || win.getTitle().includes('检测平台'); + }); + + if (mainWindow) { + // 如果窗口最小化,恢复它 + if (mainWindow.isMinimized()) { + mainWindow.restore(); + } + + // 如果窗口隐藏,显示它 + if (!mainWindow.isVisible()) { + mainWindow.show(); + } + + // 聚焦窗口 + mainWindow.focus(); + + console.log('[Main] Main window focused'); + } else { + console.warn('[Main] Main window not found'); + } +}); + +// 监听应用退出前事件(确保清理托盘) +app.on('before-quit', () => { + console.log('[Main] App before-quit, destroying tray...'); + + // 销毁托盘图标 + if (tray && !tray.isDestroyed()) { + tray.destroy(); + tray = null; + } +}); + +// 监听 will-quit 事件(强制退出时) +app.on('will-quit', () => { + console.log('[Main] App will-quit'); + + // 确保托盘被销毁 + if (tray && !tray.isDestroyed()) { + tray.destroy(); + tray = null; + } +}); + // register lifecycle (绑定 this 上下文) electronApp.register("ready", lifecycle.ready.bind(lifecycle)); electronApp.register("electron-app-ready", () => { lifecycle.electronAppReady.bind(lifecycle)(); createApplicationMenu(); + createTray(); // 创建系统托盘 }); electronApp.register("window-ready", lifecycle.windowReady.bind(lifecycle)); -electronApp.register("before-close", lifecycle.beforeClose.bind(lifecycle)); +electronApp.register("before-close", async () => { + // 如果不是真正退出,不执行清理 + if (!isQuitting) { + console.log('[Main] Not quitting, skip cleanup'); + return; + } + + console.log('[Main] Quitting, execute cleanup'); + + // 销毁托盘图标 + if (tray && !tray.isDestroyed()) { + tray.destroy(); + tray = null; + } + + await lifecycle.beforeClose.bind(lifecycle)(); +}); // register preload electronApp.register("preload", preload); diff --git a/electron/preload/lifecycle.js b/electron/preload/lifecycle.js index 4582273..96c7d6b 100644 --- a/electron/preload/lifecycle.js +++ b/electron/preload/lifecycle.js @@ -4,7 +4,7 @@ const path = require('path'); const { logger } = require('ee-core/log'); const { getConfig } = require('ee-core/config'); const { getMainWindow } = require('ee-core/electron'); -const ps = require('ee-core/ps'); +const { getBaseDir } = require('ee-core/ps'); const { app } = require('electron'); // 动态获取 scripts 目录路径 @@ -12,7 +12,8 @@ function getScriptsPath(scriptName) { const fs = require('fs'); // 判断是否是打包后的环境 - const isProd = process.resourcesPath && process.resourcesPath.includes('win-unpacked'); + // 只要 process.resourcesPath 存在,就是打包后的环境(无论在哪个目录) + const isProd = !!process.resourcesPath; if (isProd) { // 生产环境(打包后):从 resources 目录 @@ -44,11 +45,11 @@ function getScriptsPath(scriptName) { } // 延迟加载 scripts -let MySQLManager, JavaRunner, ConfigGenerator, PortChecker, StartupManager, LogWindowManager; +let MySQLServiceManager, JavaRunner, ConfigGenerator, PortChecker, StartupManager, LogWindowManager; function loadScripts() { - if (!MySQLManager) { - MySQLManager = require(getScriptsPath('start-mysql')); + if (!MySQLServiceManager) { + MySQLServiceManager = require(getScriptsPath('mysql-service-manager')); JavaRunner = require(getScriptsPath('java-runner')); ConfigGenerator = require(getScriptsPath('config-generator')); PortChecker = require(getScriptsPath('port-checker')); @@ -59,13 +60,14 @@ function loadScripts() { class Lifecycle { constructor() { - this.mysqlManager = null; + this.mysqlServiceManager = null; this.javaRunner = null; this.startupManager = null; this.logWindowManager = null; this.mysqlPort = null; this.javaPort = null; this.autoRefreshTimer = null; + this.isRestartingForAdmin = false; // 权限提升重启标记 } /** @@ -74,76 +76,73 @@ class Lifecycle { async ready() { logger.info('[lifecycle] ready'); // 延迟加载 scripts - if (ps.isProd()) { - loadScripts(); - } - + loadScripts(); } /** * 完整的应用启动流程 */ async startApplication() { + this.logWindowManager.addLog('system', '▶ 步骤1: 开始启动流程...'); + logger.info('[lifecycle] Starting application...'); + + this.logWindowManager.addLog('system', '▶ 步骤2: 加载配置信息...'); const config = getConfig(); + logger.info('[lifecycle] Config loaded:', JSON.stringify(config)); // 步骤1: 初始化 + this.logWindowManager.addLog('system', '▶ 步骤3: 初始化启动管理器...'); this.startupManager.updateProgress('init'); await this.sleep(500); - // 步骤2: 检测 MySQL 端口 - this.startupManager.updateProgress('check-mysql-port'); - this.logWindowManager.addLog('system', '正在检测可用的 MySQL 端口(从3306开始)...'); + // 步骤2-4: 确保 MySQL 服务运行 + this.logWindowManager.addLog('system', '▶ 步骤4: 检查 MySQL 配置...'); + logger.info('[lifecycle] MySQL config check - enable:', config.mysql?.enable, 'autoStart:', config.mysql?.autoStart); - this.mysqlPort = await PortChecker.findAvailablePort(3306, 100); - - if (this.mysqlPort === -1) { - this.logWindowManager.addLog('error', 'MySQL 端口检测失败:3306-3405 全部被占用'); - throw new Error('无法找到可用的 MySQL 端口(3306-3405 全部被占用)'); - } - - if (this.mysqlPort !== 3306) { - this.logWindowManager.addLog('warn', `MySQL 默认端口 3306 已被占用,自动切换到端口: ${this.mysqlPort}`); - } else { - this.logWindowManager.addLog('success', `MySQL 将使用默认端口: ${this.mysqlPort}`); - } - - logger.info(`[lifecycle] MySQL will use port: ${this.mysqlPort}`); - this.startupManager.updateProgress('check-mysql-port', { mysqlPort: this.mysqlPort }); - await this.sleep(500); - - // 步骤3: 启动 MySQL if (config.mysql && config.mysql.enable && config.mysql.autoStart) { - this.startupManager.updateProgress('start-mysql', { mysqlPort: this.mysqlPort }); - this.logWindowManager.addLog('mysql', `正在启动 MySQL,端口: ${this.mysqlPort}`); + this.startupManager.updateProgress('check-mysql-port'); + this.logWindowManager.addLog('system', '▶ 步骤5: 启动 MySQL 服务管理器...'); - this.mysqlManager = new MySQLManager(); + this.mysqlServiceManager = new MySQLServiceManager(this.logWindowManager); + this.logWindowManager.addLog('system', '正在检查 MySQL 服务状态...'); - // 监听 MySQL 输出 - const actualPort = await this.mysqlManager.start(this.mysqlPort); - this.setupMySQLLogging(this.mysqlManager.process); - - logger.info(`[lifecycle] MySQL started on port: ${actualPort}`); - this.logWindowManager.addLog('success', `MySQL 已启动,端口: ${actualPort}`); - - // 步骤4: 等待 MySQL 就绪 - this.startupManager.updateProgress('wait-mysql', { mysqlPort: actualPort }); - this.logWindowManager.addLog('mysql', '等待 MySQL 就绪...'); - - const mysqlReady = await PortChecker.waitForPort(actualPort, 30000); - - if (!mysqlReady) { - this.logWindowManager.addLog('error', `MySQL 启动超时(端口 ${actualPort} 未响应)`); - throw new Error(`MySQL 启动超时(端口 ${actualPort} 未响应)`); + try { + // 使用服务管理器确保MySQL服务运行 + this.logWindowManager.addLog('system', '▶ 步骤6: 确保 MySQL 服务运行中...'); + this.mysqlPort = await this.mysqlServiceManager.ensureServiceRunning( + PortChecker.findAvailablePort.bind(PortChecker), + PortChecker.waitForPort.bind(PortChecker) + ); + + logger.info(`[lifecycle] MySQL service running on port: ${this.mysqlPort}`); + this.logWindowManager.addLog('success', `✓ MySQL 服务已就绪,端口: ${this.mysqlPort}`); + this.startupManager.updateProgress('wait-mysql', { mysqlPort: this.mysqlPort }); + await this.sleep(500); + } catch (error) { + logger.error('[lifecycle] MySQL service error:', error); + this.logWindowManager.addLog('error', `MySQL 服务错误: ${error.message}`); + // 检查是否是权限问题 + if (error.message && (error.message.includes('administrator') || error.message.includes('Denied'))) { + logger.error('[lifecycle] Need administrator privileges'); + this.logWindowManager.addLog('error', '检测到需要管理员权限来安装 MySQL 服务'); + this.logWindowManager.addLog('system', '应用将自动请求管理员权限并重启...'); + + // 等待用户看清日志 + await this.sleep(2000); + + // 自动以管理员身份重启应用 + await this.restartAsAdmin(); + + // 退出当前实例 + return; + } + throw error; } - - logger.info('[lifecycle] MySQL is ready'); - this.logWindowManager.addLog('success', 'MySQL 已就绪!'); - await this.sleep(500); } // 步骤5: 检测 Java 端口 + this.logWindowManager.addLog('system', '▶ 步骤7: 检测可用的 Java 端口(从18092开始)...'); this.startupManager.updateProgress('check-java-port', { mysqlPort: this.mysqlPort }); - this.logWindowManager.addLog('system', '正在检测可用的 Java 端口(从18092开始)...'); this.javaPort = await PortChecker.findAvailablePort(18092, 100); @@ -152,13 +151,30 @@ class Lifecycle { throw new Error('无法找到可用的后端服务端口(18092-18191 全部被占用)'); } + // 步骤5.5: 检测 WebSocket 端口 + this.logWindowManager.addLog('system', '▶ 步骤8: 检测可用的 WebSocket 端口(从7777开始)...'); + + this.websocketPort = await PortChecker.findAvailablePort(7777, 100); + + if (this.websocketPort === -1) { + this.logWindowManager.addLog('error', 'WebSocket 端口检测失败:7777-7876 全部被占用'); + throw new Error('无法找到可用的 WebSocket 端口(7777-7876 全部被占用)'); + } + if (this.javaPort !== 18092) { - this.logWindowManager.addLog('warn', `Java 默认端口 18092 已被占用,自动切换到端口: ${this.javaPort}`); + this.logWindowManager.addLog('warn', `⚠ Java 默认端口 18092 已被占用,自动切换到端口: ${this.javaPort}`); } else { - this.logWindowManager.addLog('success', `Java 将使用默认端口: ${this.javaPort}`); + this.logWindowManager.addLog('success', `✓ Java 将使用默认端口: ${this.javaPort}`); + } + + if (this.websocketPort !== 7777) { + this.logWindowManager.addLog('warn', `⚠ WebSocket 默认端口 7777 已被占用,自动切换到端口: ${this.websocketPort}`); + } else { + this.logWindowManager.addLog('success', `✓ WebSocket 将使用默认端口: ${this.websocketPort}`); } logger.info(`[lifecycle] Spring Boot will use port: ${this.javaPort}`); + logger.info(`[lifecycle] WebSocket will use port: ${this.websocketPort}`); this.startupManager.updateProgress('check-java-port', { mysqlPort: this.mysqlPort, javaPort: this.javaPort @@ -166,28 +182,35 @@ class Lifecycle { await this.sleep(500); // 步骤6: 生成配置文件 + this.logWindowManager.addLog('system', '▶ 步骤9: 生成 Spring Boot 配置文件...'); this.startupManager.updateProgress('generate-config', { mysqlPort: this.mysqlPort, - javaPort: this.javaPort + javaPort: this.javaPort, + websocketPort: this.websocketPort }); const configGenerator = new ConfigGenerator(); const { configPath, dataPath } = await configGenerator.generateConfig({ mysqlPort: this.mysqlPort, javaPort: this.javaPort, + websocketPort: this.websocketPort, mysqlPassword: 'njcnpqs' }); logger.info(`[lifecycle] Configuration generated at: ${configPath}`); logger.info(`[lifecycle] Data directory: ${dataPath}`); + this.logWindowManager.addLog('success', `✓ 配置文件已生成: ${configPath}`); + this.logWindowManager.addLog('system', ` 数据目录: ${dataPath}`); this.startupManager.updateProgress('generate-config', { mysqlPort: this.mysqlPort, javaPort: this.javaPort, + websocketPort: this.websocketPort, dataPath: dataPath }); await this.sleep(500); // 步骤7: 启动 Spring Boot + this.logWindowManager.addLog('system', '▶ 步骤10: 启动 Spring Boot 应用...'); this.startupManager.updateProgress('start-java', { mysqlPort: this.mysqlPort, javaPort: this.javaPort, @@ -197,6 +220,7 @@ class Lifecycle { await this.startSpringBoot(configPath); // 步骤8: 等待 Spring Boot 就绪 + this.logWindowManager.addLog('system', '▶ 步骤11: 等待 Spring Boot 就绪(最多60秒)...'); this.startupManager.updateProgress('wait-java', { mysqlPort: this.mysqlPort, javaPort: this.javaPort, @@ -207,13 +231,16 @@ class Lifecycle { if (!javaReady) { logger.warn(`[lifecycle] Spring Boot 启动超时,但继续启动应用`); + this.logWindowManager.addLog('warn', '⚠ Spring Boot 启动超时(60秒),但应用将继续启动'); } else { logger.info('[lifecycle] Spring Boot is ready'); + this.logWindowManager.addLog('success', '✓ Spring Boot 启动成功!'); } await this.sleep(1000); // 步骤9: 完成 + this.logWindowManager.addLog('system', '▶ 步骤12: 启动完成,准备显示主窗口...'); this.startupManager.updateProgress('done', { mysqlPort: this.mysqlPort, javaPort: this.javaPort, @@ -225,6 +252,7 @@ class Lifecycle { this.logWindowManager.addLog('success', '✓ NPQS9100 启动完成!所有服务正常运行'); this.logWindowManager.addLog('system', `✓ MySQL 端口: ${this.mysqlPort}`); this.logWindowManager.addLog('system', `✓ Java 端口: ${this.javaPort}`); + this.logWindowManager.addLog('system', `✓ WebSocket 端口: ${this.websocketPort}`); this.logWindowManager.addLog('system', `✓ 数据目录: ${dataPath}`); this.logWindowManager.addLog('system', '='.repeat(60)); this.logWindowManager.addLog('system', '应用即将启动...'); @@ -241,9 +269,36 @@ class Lifecycle { win.focus(); // 添加主窗口关闭事件监听 - win.on('close', async () => { - logger.info('[lifecycle] Main window closing, cleaning up...'); - await this.cleanup(); + win.on('close', async (event) => { + // 总是弹出确认对话框 + event.preventDefault(); + + const { dialog } = require('electron'); + const { app } = require('electron'); + + const response = await dialog.showMessageBox(win, { + type: 'question', + title: '退出确认', + message: '确定要退出应用吗?', + buttons: ['取消', '退出'], + defaultId: 0, + cancelId: 0 + }); + + if (response.response === 1) { + // 用户确认退出 + logger.info('[lifecycle] User confirmed exit'); + + // 移除所有监听器避免循环 + win.removeAllListeners('close'); + + // 执行清理 + await this.cleanup(); + + // 退出应用 + app.quit(); + } + // 用户取消,什么都不做(已经 preventDefault) }); // 立即刷新一次,确保显示最新内容 @@ -294,6 +349,12 @@ class Lifecycle { async cleanup() { logger.info('[lifecycle] Starting cleanup...'); + // 如果是权限提升重启,跳过清理(服务需要继续运行) + if (this.isRestartingForAdmin) { + logger.info('[lifecycle] Restarting for admin, skip cleanup'); + return; + } + // 清除自动刷新定时器 if (this.autoRefreshTimer) { clearTimeout(this.autoRefreshTimer); @@ -323,10 +384,8 @@ class Lifecycle { this.logWindowManager.addLog('system', '正在停止 Spring Boot...'); } - // 设置超时,3秒后强制继续 - const stopPromise = this.javaRunner.stopSpringBoot(); - const timeoutPromise = new Promise(resolve => setTimeout(resolve, 3000)); - await Promise.race([stopPromise, timeoutPromise]); + // 停止 Java 进程(内部已有完整的等待和清理逻辑) + await this.javaRunner.stopSpringBoot(); logger.info('[lifecycle] Spring Boot stopped'); if (this.logWindowManager && this.logWindowManager.logWindow && !this.logWindowManager.logWindow.isDestroyed()) { @@ -337,35 +396,12 @@ class Lifecycle { } } - // 停止 MySQL - const config = getConfig(); - if (config.mysql && config.mysql.enable && config.mysql.autoStart) { - try { - logger.info('[lifecycle] Stopping MySQL...'); - if (this.logWindowManager && this.logWindowManager.logWindow && !this.logWindowManager.logWindow.isDestroyed()) { - this.logWindowManager.addLog('system', '正在停止 MySQL(最多等待10秒)...'); - } - - if (this.mysqlManager) { - // MySQL 的 stop() 方法内部使用 mysqladmin shutdown(最多10秒) - // 设置12秒超时作为保险 - const stopPromise = this.mysqlManager.stop(); - const timeoutPromise = new Promise(resolve => setTimeout(resolve, 12000)); - await Promise.race([stopPromise, timeoutPromise]); - } - - logger.info('[lifecycle] MySQL stopped'); - if (this.logWindowManager && this.logWindowManager.logWindow && !this.logWindowManager.logWindow.isDestroyed()) { - this.logWindowManager.addLog('success', 'MySQL 已停止'); - this.logWindowManager.addLog('system', '清理完成,应用即将退出'); - this.logWindowManager.addLog('system', '='.repeat(60)); - } - } catch (error) { - logger.error('[lifecycle] Failed to stop MySQL:', error); - if (this.logWindowManager && this.logWindowManager.logWindow && !this.logWindowManager.logWindow.isDestroyed()) { - this.logWindowManager.addLog('error', 'MySQL 停止失败: ' + error.message); - } - } + // MySQL 作为Windows服务运行,应用关闭时保持运行 + logger.info('[lifecycle] MySQL service keeps running as Windows service'); + if (this.logWindowManager && this.logWindowManager.logWindow && !this.logWindowManager.logWindow.isDestroyed()) { + this.logWindowManager.addLog('system', 'MySQL 服务保持运行'); + this.logWindowManager.addLog('system', '清理完成,应用即将退出'); + this.logWindowManager.addLog('system', '='.repeat(60)); } // 等待500ms让用户看到清理日志 @@ -384,35 +420,6 @@ class Lifecycle { logger.info('[lifecycle] Cleanup completed'); } - /** - * 设置 MySQL 日志监听 - */ - setupMySQLLogging(mysqlProcess) { - if (!mysqlProcess) return; - - mysqlProcess.stdout.on('data', (data) => { - const output = data.toString().trim(); - if (output) { - // 根据内容判断日志类型 - if (output.includes('[ERROR]') || output.includes('ERROR')) { - this.logWindowManager.addLog('error', `[MySQL] ${output}`); - } else if (output.includes('[Warning]') || output.includes('WARNING')) { - this.logWindowManager.addLog('warn', `[MySQL] ${output}`); - } else if (output.includes('ready for connections')) { - this.logWindowManager.addLog('success', `[MySQL] ${output}`); - } else { - this.logWindowManager.addLog('mysql', `[MySQL] ${output}`); - } - } - }); - - mysqlProcess.stderr.on('data', (data) => { - const output = data.toString().trim(); - if (output) { - this.logWindowManager.addLog('warn', `[MySQL Error] ${output}`); - } - }); - } /** * 启动 Spring Boot 应用 @@ -477,6 +484,80 @@ class Lifecycle { } } + /** + * 以管理员身份重启应用 + */ + async restartAsAdmin() { + const { app, dialog } = require('electron'); + const { spawn } = require('child_process'); + const path = require('path'); + + logger.info('[lifecycle] Requesting administrator privileges...'); + + try { + // 显示提示对话框 + const { response } = await dialog.showMessageBox({ + type: 'warning', + title: '需要管理员权限', + message: '安装 MySQL 服务需要管理员权限', + detail: '应用将以管理员身份重新启动', + buttons: ['取消', '确定'], + defaultId: 1, + cancelId: 0 + }); + + if (response === 0) { + // 用户取消,关闭应用 + logger.info('[lifecycle] User cancelled admin elevation'); + app.quit(); + return; + } + + // 获取应用可执行文件路径 + const exePath = app.getPath('exe'); + logger.info('[lifecycle] Restarting with admin privileges:', exePath); + + // 使用 PowerShell 以管理员身份启动 + const psCommand = `Start-Process -FilePath "${exePath}" -Verb RunAs`; + + const child = spawn('powershell.exe', ['-Command', psCommand], { + detached: true, + stdio: 'ignore', + windowsHide: true + }); + + // 分离子进程,父进程退出不影响子进程 + child.unref(); + + // 立即退出当前实例,释放单实例锁 + // 必须立即退出,否则新实例会因为单实例锁无法启动 + logger.info('[lifecycle] Quitting current instance to release lock...'); + + // 设置标记,跳过清理流程(这只是重启,不是真正退出) + this.isRestartingForAdmin = true; + + // 关闭所有窗口 + const BrowserWindow = require('electron').BrowserWindow; + BrowserWindow.getAllWindows().forEach(win => { + try { + win.destroy(); + } catch (e) { + // 忽略错误 + } + }); + + // 立即强制退出,释放锁 + process.exit(0); + } catch (error) { + logger.error('[lifecycle] Failed to restart as admin:', error); + this.logWindowManager.addLog('error', '自动提升权限失败,请手动以管理员身份运行'); + + // 等待5秒后关闭 + await this.sleep(5000); + app.quit(); + } + } + /** * 睡眠函数 */ @@ -495,56 +576,54 @@ class Lifecycle { * main window have been loaded */ async windowReady() { - logger.info('[lifecycle] window-ready'); - if (ps.isProd()) { - // 创建日志窗口 - this.logWindowManager = new LogWindowManager(); - this.logWindowManager.createLogWindow(); - this.logWindowManager.addLog('system', '='.repeat(60)); - this.logWindowManager.addLog('system', 'NPQS9100 启动中...'); - this.logWindowManager.addLog('system', '='.repeat(60)); + logger.info('[lifecycle] window-ready hook triggered'); - // 创建 Loading 窗口 - this.startupManager = new StartupManager(); - this.startupManager.createLoadingWindow(); + // 创建日志管理器(但不显示窗口,仅用于写日志文件) + logger.info('[lifecycle] Creating log window manager...'); + this.logWindowManager = new LogWindowManager(); + // this.logWindowManager.createLogWindow(); // ← 注释掉,不创建窗口 + this.logWindowManager.addLog('system', '='.repeat(80)); + this.logWindowManager.addLog('system', 'NPQS9100 应用启动'); + this.logWindowManager.addLog('system', '='.repeat(80)); - // 开始启动流程 - try { - await this.startApplication(); - } catch (error) { - logger.error('[lifecycle] Failed to start application:', error); - this.logWindowManager.addLog('error', `启动失败: ${error.message}`); - this.logWindowManager.addLog('system', '请检查日志窗口了解详细错误信息'); - this.startupManager.showError(error.message || '启动失败,请查看日志'); - - // 显示错误5秒后关闭Loading窗口,但不关闭日志窗口 - setTimeout(() => { - this.startupManager.closeLoadingWindow(); - - // 即使启动失败,也显示主窗口(但用户可能需要手动修复问题) - const win = getMainWindow(); - win.show(); - win.focus(); - - // 添加主窗口关闭事件监听 - win.on('close', async () => { - logger.info('[lifecycle] Main window closing (after error), cleaning up...'); - await this.cleanup(); - }); - - this.logWindowManager.addLog('warn', '应用已启动,但部分服务可能未正常运行'); - }, 5000); - } - } else { - const win = getMainWindow(); - const {windowsOption} = getConfig(); - if (windowsOption.show == false) { - win.once('ready-to-show', () => { - win.show(); - win.focus(); - }) - } + // 创建 Loading 窗口 + logger.info('[lifecycle] Creating startup manager and loading window...'); + this.startupManager = new StartupManager(); + this.startupManager.createLoadingWindow(); + this.logWindowManager.addLog('system', '='.repeat(60)); + this.logWindowManager.addLog('system', 'NPQS9100 启动中...'); + this.logWindowManager.addLog('system', '='.repeat(60)); + // 开始启动流程 + logger.info('[lifecycle] Starting application flow...'); + try { + await this.startApplication(); + } catch (error) { + logger.error('[lifecycle] Failed to start application:', error); + this.logWindowManager.addLog('error', `启动失败: ${error.message}`); + this.logWindowManager.addLog('system', '请检查日志窗口了解详细错误信息'); + this.startupManager.showError(error.message || '启动失败,请查看日志'); + + // 显示错误5秒后关闭Loading窗口,但不关闭日志窗口 + setTimeout(() => { + this.startupManager.closeLoadingWindow(); + + // 即使启动失败,也显示主窗口(但用户可能需要手动修复问题) + const win = getMainWindow(); + win.show(); + win.focus(); + + // 启动失败时,允许用户正常关闭窗口(不强制托盘) + // 因为可能托盘未创建,或用户想直接退出 + win.on('close', async (event) => { + logger.info('[lifecycle] Window closing (after error), cleaning up...'); + // 不阻止关闭,执行清理 + await this.cleanup(); + }); + + this.logWindowManager.addLog('warn', '应用已启动,但部分服务可能未正常运行'); + this.logWindowManager.addLog('system', '您可以点击 X 关闭应用'); + }, 5000); } // 主窗口初始化但不显示,等待启动流程完成后再显示 @@ -556,10 +635,7 @@ class Lifecycle { */ async beforeClose() { logger.info('[lifecycle] before-close hook triggered'); - if (ps.isProd()) { - await this.cleanup(); - } - + await this.cleanup(); } } Lifecycle.toString = () => '[class Lifecycle]'; diff --git a/frontend/.env.development b/frontend/.env.development index 775678d..f43557d 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -20,8 +20,8 @@ VITE_API_URL=/api # 开发环境跨域代理,支持配置多个 #VITE_PROXY=[["/api","http://127.0.0.1:18092/"]] -VITE_PROXY=[["/api","http://192.168.1.124:18092/"]] -#VITE_PROXY=[["/api","http://192.168.1.125:18092/"]] +#VITE_PROXY=[["/api","http://192.168.1.124:18092/"]] +VITE_PROXY=[["/api","http://192.168.1.125:18092/"]] # VITE_PROXY=[["/api","http://192.168.1.138:8080/"]]张文 # 开启激活验证 -VITE_ACTIVATE_OPEN=false \ No newline at end of file +VITE_ACTIVATE_OPEN=true \ No newline at end of file diff --git a/frontend/.env.production b/frontend/.env.production index 0d6f9c7..2822720 100644 --- a/frontend/.env.production +++ b/frontend/.env.production @@ -25,4 +25,4 @@ VITE_PWA=true #VITE_API_URL="/api" # 打包时用 VITE_API_URL="http://127.0.0.1:18092/" # 开启激活验证 -VITE_ACTIVATE_OPEN=false \ No newline at end of file +VITE_ACTIVATE_OPEN=true \ No newline at end of file diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 957bd38..b39c68c 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,5 +1,6 @@