diff --git a/build/icons/256x256.png b/build/icons/256x256.png index da8b5a2..802a3e1 100644 Binary files a/build/icons/256x256.png and b/build/icons/256x256.png differ diff --git a/build/icons/512x512.png b/build/icons/512x512.png index da8b5a2..1c2bbe7 100644 Binary files a/build/icons/512x512.png and b/build/icons/512x512.png differ diff --git a/build/icons/icon.png b/build/icons/icon.png index da8b5a2..802a3e1 100644 Binary files a/build/icons/icon.png and b/build/icons/icon.png differ diff --git a/cmd/bin.js b/cmd/bin.js index 04ec18c..fc72035 100644 --- a/cmd/bin.js +++ b/cmd/bin.js @@ -45,7 +45,7 @@ module.exports = { win64: { cmd: 'electron-builder', directory: './', - args: ['--config=./cmd/builder.json', '-w=nsis', '--x64'], + args: ['--config=./cmd/builder.json', '-w=dir', '--x64'], }, win32: { args: ['--config=./cmd/builder.json', '-w=nsis', '--ia32'], diff --git a/cmd/builder.json b/cmd/builder.json index f0c2166..7d7581b 100644 --- a/cmd/builder.json +++ b/cmd/builder.json @@ -6,6 +6,9 @@ "output": "out" }, "asar": true, + "asarUnpack": [ + "public/images/**/*" + ], "files": [ "**/*", "!cmd/", @@ -17,28 +20,47 @@ "!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": "灿能检测" - }, + "extraResources": [ + { + "from": "build/extraResources/dll", + "to": "extraResources/dll", + "filter": ["**/*"] + }, + { + "from": "build/extraResources/java", + "to": "extraResources/java", + "filter": ["**/*"] + }, + { + "from": "build/extraResources/read.txt", + "to": "extraResources/read.txt" + }, + { + "from": "scripts/", + "to": "scripts", + "filter": ["**/*"] + } + ], + "extraFiles": [ + { + "from": "build/extraResources/mysql", + "to": "mysql", + "filter": ["**/*"] + }, + { + "from": "build/extraResources/jre", + "to": "jre", + "filter": ["**/*"] + } + ], "win": { - "icon": "build/icons/icon.ico", "artifactName": "${productName}-${os}-${version}-${arch}.${ext}", "target": [ { - "target": "portable" + "target": "dir", + "arch": ["x64"] } ] - } + }, + "compression": "store" } \ No newline at end of file diff --git a/electron/config/config.default.js b/electron/config/config.default.js index e7728c4..1d4b156 100644 --- a/electron/config/config.default.js +++ b/electron/config/config.default.js @@ -12,7 +12,7 @@ module.exports = () => { singleLock: true, windowsOption: { title: 'NPQS9100-自动检测平台', - menuBarVisible: false, + menuBarVisible: true, // 显示菜单栏,方便查看日志 width: 1920, height: 1000, minWidth: 1024, @@ -24,7 +24,7 @@ module.exports = () => { //preload: path.join(getElectronDir(), 'preload', 'bridge.js'), }, frame: true, - show: true, + show: false, // 初始不显示,等待服务启动完成后再显示 icon: path.join(getBaseDir(), 'public', 'images', 'logo-32.png'), }, logger: { @@ -66,6 +66,20 @@ module.exports = () => { mainServer: { indexPath: '/public/dist/index.html', channelSeparator: '/', + }, + // MySQL配置 + mysql: { + enable: true, + autoStart: true, // 应用启动时自动启动MySQL + path: path.join(getBaseDir(), 'mysql'), + connection: { + host: 'localhost', + port: 3306, + user: 'root', + password: 'njcnpqs', + database: 'npqs9100_db', + charset: 'utf8mb4' + } } } } diff --git a/electron/main.js b/electron/main.js index b005896..a552255 100644 --- a/electron/main.js +++ b/electron/main.js @@ -1,19 +1,87 @@ const { ElectronEgg } = require('ee-core'); -const { Lifecycle } = require('./preload/lifecycle'); +const { app, Menu, ipcMain } = require('electron'); +const lifecycle = require('./preload/lifecycle'); const { preload } = require('./preload'); // new app -const app = new ElectronEgg(); +const electronApp = new ElectronEgg(); -// register lifecycle -const life = new Lifecycle(); -app.register("ready", life.ready); -app.register("electron-app-ready", life.electronAppReady); -app.register("window-ready", life.windowReady); -app.register("before-close", life.beforeClose); +// 创建应用菜单 +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' }, + { role: 'toggleDevTools', label: '开发者工具' } + ] + }, + { + label: '帮助', + submenu: [ + { + label: '使用说明', + click: () => { + // 可以打开帮助文档 + } + }, + { type: 'separator' }, + { + label: '关于', + click: () => { + // 可以显示关于信息 + } + } + ] + } + ]; + + const menu = Menu.buildFromTemplate(template); + Menu.setApplicationMenu(menu); +} + +// 注册 IPC 处理器 +ipcMain.handle('show-log-window', () => { + if (lifecycle.logWindowManager) { + lifecycle.logWindowManager.show(); + } +}); + +ipcMain.handle('hide-log-window', () => { + if (lifecycle.logWindowManager) { + lifecycle.logWindowManager.hide(); + } +}); + +ipcMain.handle('toggle-log-window', () => { + if (lifecycle.logWindowManager) { + lifecycle.logWindowManager.toggle(); + } +}); + +// register lifecycle (绑定 this 上下文) +electronApp.register("ready", lifecycle.ready.bind(lifecycle)); +electronApp.register("electron-app-ready", () => { + lifecycle.electronAppReady.bind(lifecycle)(); + createApplicationMenu(); +}); +electronApp.register("window-ready", lifecycle.windowReady.bind(lifecycle)); +electronApp.register("before-close", lifecycle.beforeClose.bind(lifecycle)); // register preload -app.register("preload", preload); +electronApp.register("preload", preload); // run -app.run(); \ No newline at end of file +electronApp.run(); \ No newline at end of file diff --git a/electron/preload/bridge.js b/electron/preload/bridge.js index c9b86db..eb0a24d 100644 --- a/electron/preload/bridge.js +++ b/electron/preload/bridge.js @@ -7,4 +7,8 @@ const { contextBridge, ipcRenderer } = require('electron') contextBridge.exposeInMainWorld('electron', { ipcRenderer: ipcRenderer, + // 日志窗口控制 + showLogWindow: () => ipcRenderer.invoke('show-log-window'), + hideLogWindow: () => ipcRenderer.invoke('hide-log-window'), + toggleLogWindow: () => ipcRenderer.invoke('toggle-log-window'), }) \ No newline at end of file diff --git a/electron/preload/lifecycle.js b/electron/preload/lifecycle.js index 927d3f2..67bbda1 100644 --- a/electron/preload/lifecycle.js +++ b/electron/preload/lifecycle.js @@ -1,20 +1,484 @@ 'use strict'; +const path = require('path'); const { logger } = require('ee-core/log'); const { getConfig } = require('ee-core/config'); const { getMainWindow } = require('ee-core/electron'); +const { getBaseDir } = require('ee-core/ps'); +const { app } = require('electron'); + +// 动态获取 scripts 目录路径 +function getScriptsPath(scriptName) { + const fs = require('fs'); + + // 判断是否是打包后的环境 + const isProd = process.resourcesPath && process.resourcesPath.includes('win-unpacked'); + + if (isProd) { + // 生产环境(打包后):从 resources 目录 + const prodPath = path.join(process.resourcesPath, 'scripts', scriptName); + console.log(`[getScriptsPath] Production mode, using: ${prodPath}`); + return prodPath; + } else { + // 开发环境:从项目根目录 + // __dirname 是 electron/preload 或 public/electron/preload + // 需要找到项目根目录 + let currentDir = __dirname; + let scriptsPath = null; + + // 向上查找,直到找到 scripts 目录 + for (let i = 0; i < 5; i++) { + currentDir = path.join(currentDir, '..'); + const testPath = path.join(currentDir, 'scripts', scriptName + '.js'); + if (fs.existsSync(testPath)) { + scriptsPath = path.join(currentDir, 'scripts', scriptName); + console.log(`[getScriptsPath] Development mode, found at: ${scriptsPath}`); + return scriptsPath; + } + } + + // 如果找不到,返回一个默认路径 + console.warn(`[getScriptsPath] Cannot find ${scriptName}, returning default path`); + return path.join(__dirname, '../../../scripts', scriptName); + } +} + +// 延迟加载 scripts +let MySQLManager, JavaRunner, ConfigGenerator, PortChecker, StartupManager, LogWindowManager; + +function loadScripts() { + if (!MySQLManager) { + MySQLManager = require(getScriptsPath('start-mysql')); + JavaRunner = require(getScriptsPath('java-runner')); + ConfigGenerator = require(getScriptsPath('config-generator')); + PortChecker = require(getScriptsPath('port-checker')); + StartupManager = require(getScriptsPath('startup-manager')); + LogWindowManager = require(getScriptsPath('log-window-manager')); + } +} class Lifecycle { + constructor() { + this.mysqlManager = null; + this.javaRunner = null; + this.startupManager = null; + this.logWindowManager = null; + this.mysqlPort = null; + this.javaPort = null; + this.autoRefreshTimer = null; + } /** * core app have been loaded */ async ready() { logger.info('[lifecycle] ready'); - // 在这里可以做: - // - 初始化数据库连接 - // - 加载配置文件 - // - 初始化全局变量 + // 延迟加载 scripts + loadScripts(); + } + + /** + * 完整的应用启动流程 + */ + async startApplication() { + const config = getConfig(); + + // 步骤1: 初始化 + this.startupManager.updateProgress('init'); + await this.sleep(500); + + // 步骤2: 检测 MySQL 端口 + this.startupManager.updateProgress('check-mysql-port'); + this.logWindowManager.addLog('system', '正在检测可用的 MySQL 端口(从3306开始)...'); + + 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.mysqlManager = new MySQLManager(); + + // 监听 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} 未响应)`); + } + + logger.info('[lifecycle] MySQL is ready'); + this.logWindowManager.addLog('success', 'MySQL 已就绪!'); + await this.sleep(500); + } + + // 步骤5: 检测 Java 端口 + this.startupManager.updateProgress('check-java-port', { mysqlPort: this.mysqlPort }); + this.logWindowManager.addLog('system', '正在检测可用的 Java 端口(从18092开始)...'); + + this.javaPort = await PortChecker.findAvailablePort(18092, 100); + + if (this.javaPort === -1) { + this.logWindowManager.addLog('error', 'Java 端口检测失败:18092-18191 全部被占用'); + throw new Error('无法找到可用的后端服务端口(18092-18191 全部被占用)'); + } + + if (this.javaPort !== 18092) { + this.logWindowManager.addLog('warn', `Java 默认端口 18092 已被占用,自动切换到端口: ${this.javaPort}`); + } else { + this.logWindowManager.addLog('success', `Java 将使用默认端口: ${this.javaPort}`); + } + + logger.info(`[lifecycle] Spring Boot will use port: ${this.javaPort}`); + this.startupManager.updateProgress('check-java-port', { + mysqlPort: this.mysqlPort, + javaPort: this.javaPort + }); + await this.sleep(500); + + // 步骤6: 生成配置文件 + this.startupManager.updateProgress('generate-config', { + mysqlPort: this.mysqlPort, + javaPort: this.javaPort + }); + + const configGenerator = new ConfigGenerator(); + const { configPath, dataPath } = await configGenerator.generateConfig({ + mysqlPort: this.mysqlPort, + javaPort: this.javaPort, + mysqlPassword: 'njcnpqs' + }); + + logger.info(`[lifecycle] Configuration generated at: ${configPath}`); + logger.info(`[lifecycle] Data directory: ${dataPath}`); + this.startupManager.updateProgress('generate-config', { + mysqlPort: this.mysqlPort, + javaPort: this.javaPort, + dataPath: dataPath + }); + await this.sleep(500); + + // 步骤7: 启动 Spring Boot + this.startupManager.updateProgress('start-java', { + mysqlPort: this.mysqlPort, + javaPort: this.javaPort, + dataPath: dataPath + }); + + await this.startSpringBoot(configPath); + + // 步骤8: 等待 Spring Boot 就绪 + this.startupManager.updateProgress('wait-java', { + mysqlPort: this.mysqlPort, + javaPort: this.javaPort, + dataPath: dataPath + }); + + const javaReady = await PortChecker.waitForPort(this.javaPort, 60000); + + if (!javaReady) { + logger.warn(`[lifecycle] Spring Boot 启动超时,但继续启动应用`); + } else { + logger.info('[lifecycle] Spring Boot is ready'); + } + + await this.sleep(1000); + + // 步骤9: 完成 + this.startupManager.updateProgress('done', { + mysqlPort: this.mysqlPort, + javaPort: this.javaPort, + dataPath: dataPath + }); + + logger.info('[lifecycle] Application startup completed'); + this.logWindowManager.addLog('system', '='.repeat(60)); + this.logWindowManager.addLog('success', '✓ NPQS9100 启动完成!所有服务正常运行'); + this.logWindowManager.addLog('system', `✓ MySQL 端口: ${this.mysqlPort}`); + this.logWindowManager.addLog('system', `✓ Java 端口: ${this.javaPort}`); + this.logWindowManager.addLog('system', `✓ 数据目录: ${dataPath}`); + this.logWindowManager.addLog('system', '='.repeat(60)); + this.logWindowManager.addLog('system', '应用即将启动...'); + + // 先关闭 Loading 窗口 + this.startupManager.closeLoadingWindow(); + + // 等待3秒,让用户看清日志信息,然后再显示主窗口 + await this.sleep(3000); + + // 显示主窗口 + const win = getMainWindow(); + win.show(); + win.focus(); + + // 添加主窗口关闭事件监听 + win.on('close', async () => { + logger.info('[lifecycle] Main window closing, cleaning up...'); + await this.cleanup(); + }); + + // 立即刷新一次,确保显示最新内容 + setTimeout(() => { + if (win && !win.isDestroyed()) { + logger.info('[lifecycle] Reloading main window to ensure fresh content'); + this.logWindowManager.addLog('system', '正在加载应用界面...'); + win.reload(); + } + }, 500); + + // 设置主窗口加载超时自动刷新(30秒) + this.setupAutoRefresh(win); + + // 提示用户 + this.logWindowManager.addLog('system', '主应用已打开!此日志窗口可随时关闭'); + } + + /** + * 设置主窗口自动刷新机制 + */ + setupAutoRefresh(win) { + // 30秒后检查页面是否还在loading,如果是则刷新 + this.autoRefreshTimer = setTimeout(() => { + if (win && !win.isDestroyed()) { + // 检查页面是否还在loading状态 + win.webContents.executeJavaScript('document.readyState').then(state => { + logger.info(`[lifecycle] Page readyState after 30s: ${state}`); + if (state !== 'complete') { + logger.warn('[lifecycle] Page still loading after 30s, reloading...'); + if (this.logWindowManager && this.logWindowManager.logWindow && !this.logWindowManager.logWindow.isDestroyed()) { + this.logWindowManager.addLog('warn', '页面加载超时,自动刷新...'); + } + win.reload(); + } else { + logger.info('[lifecycle] Page loaded successfully'); + } + }).catch(err => { + logger.error('[lifecycle] Failed to check page state:', err); + }); + } + }, 30000); // 30秒超时 + } + + /** + * 清理所有资源 + */ + async cleanup() { + logger.info('[lifecycle] Starting cleanup...'); + + // 清除自动刷新定时器 + if (this.autoRefreshTimer) { + clearTimeout(this.autoRefreshTimer); + this.autoRefreshTimer = null; + } + + // 在日志窗口显示清理信息(在关闭之前) + if (this.logWindowManager && this.logWindowManager.logWindow && !this.logWindowManager.logWindow.isDestroyed()) { + this.logWindowManager.addLog('system', '='.repeat(60)); + this.logWindowManager.addLog('system', '应用正在关闭,清理资源中...'); + } + + // 关闭 Loading 窗口 + if (this.startupManager) { + try { + this.startupManager.closeLoadingWindow(); + } catch (error) { + // 忽略 + } + } + + // 停止 Spring Boot + if (this.javaRunner) { + try { + logger.info('[lifecycle] Stopping Spring Boot...'); + if (this.logWindowManager && this.logWindowManager.logWindow && !this.logWindowManager.logWindow.isDestroyed()) { + 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]); + + logger.info('[lifecycle] Spring Boot stopped'); + if (this.logWindowManager && this.logWindowManager.logWindow && !this.logWindowManager.logWindow.isDestroyed()) { + this.logWindowManager.addLog('success', 'Spring Boot 已停止'); + } + } catch (error) { + logger.error('[lifecycle] Failed to stop Spring Boot:', error); + } + } + + // 停止 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); + } + } + } + + // 等待500ms让用户看到清理日志 + await this.sleep(500); + + // 最后关闭日志窗口 + if (this.logWindowManager) { + try { + logger.info('[lifecycle] Closing log window...'); + this.logWindowManager.close(); + } catch (error) { + logger.error('[lifecycle] Failed to close log window:', error); + } + } + + 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 应用 + */ + async startSpringBoot(configPath) { + try { + logger.info('[lifecycle] Starting Spring Boot application...'); + this.logWindowManager.addLog('java', '正在启动 Spring Boot 应用...'); + + // 启动Java应用 + this.javaRunner = new JavaRunner(); + + // 开发环境:build/extraResources/java/entrance.jar + // 打包后:resources/extraResources/java/entrance.jar + const isDev = !process.resourcesPath; + const jarPath = isDev + ? path.join(__dirname, '..', 'build', 'extraResources', 'java', 'entrance.jar') + : path.join(process.resourcesPath, 'extraResources', 'java', 'entrance.jar'); + + const javaProcess = this.javaRunner.runSpringBoot(jarPath, configPath, { + javaPort: this.javaPort + }); + + // 监听Java进程输出 + javaProcess.stdout.on('data', (data) => { + const output = data.toString().trim(); + if (output) { + logger.info('[SpringBoot]', output); + + // 根据内容判断日志类型 + if (output.includes('ERROR') || output.includes('Exception')) { + this.logWindowManager.addLog('error', `[Java] ${output}`); + } else if (output.includes('WARN')) { + this.logWindowManager.addLog('warn', `[Java] ${output}`); + } else if (output.includes('Started') || output.includes('Completed')) { + this.logWindowManager.addLog('success', `[Java] ${output}`); + } else { + this.logWindowManager.addLog('java', `[Java] ${output}`); + } + } + }); + + javaProcess.stderr.on('data', (data) => { + const output = data.toString().trim(); + if (output) { + logger.error('[SpringBoot]', output); + this.logWindowManager.addLog('error', `[Java Error] ${output}`); + } + }); + + javaProcess.on('close', (code) => { + logger.info(`[SpringBoot] Process exited with code ${code}`); + this.logWindowManager.addLog('system', `Spring Boot 进程退出,代码: ${code}`); + }); + + logger.info('[lifecycle] Spring Boot application started'); + this.logWindowManager.addLog('success', 'Spring Boot 应用已启动!'); + } catch (error) { + logger.error('[lifecycle] Failed to start Spring Boot:', error); + this.logWindowManager.addLog('error', `Spring Boot 启动失败: ${error.message}`); + throw error; + } + } + + /** + * 睡眠函数 + */ + sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); } /** @@ -30,29 +494,58 @@ class Lifecycle { async windowReady() { logger.info('[lifecycle] window-ready'); - // 延迟加载,无白屏 - const win = getMainWindow(); - const { windowsOption } = getConfig(); - if (windowsOption.show == false) { - win.once('ready-to-show', () => { + // 创建日志窗口 + this.logWindowManager = new LogWindowManager(); + this.logWindowManager.createLogWindow(); + this.logWindowManager.addLog('system', '='.repeat(60)); + this.logWindowManager.addLog('system', 'NPQS9100 启动中...'); + this.logWindowManager.addLog('system', '='.repeat(60)); + + // 创建 Loading 窗口 + this.startupManager = new StartupManager(); + this.startupManager.createLoadingWindow(); + + // 开始启动流程 + 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(); - }) - } else { - 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); } + + // 主窗口初始化但不显示,等待启动流程完成后再显示 + // 在 startApplication() 中会调用 win.show() } /** - * before app close + * before app close (框架生命周期钩子) */ async beforeClose() { - logger.info('[lifecycle] before-close'); + logger.info('[lifecycle] before-close hook triggered'); + await this.cleanup(); } } Lifecycle.toString = () => '[class Lifecycle]'; -module.exports = { - Lifecycle -}; \ No newline at end of file +// 导出实例而不是类 +module.exports = new Lifecycle(); diff --git a/frontend/.env.production b/frontend/.env.production index 1650ed9..0d6f9c7 100644 --- a/frontend/.env.production +++ b/frontend/.env.production @@ -23,6 +23,6 @@ VITE_PWA=true # 线上环境接口地址 #VITE_API_URL="/api" # 打包时用 -VITE_API_URL="http://192.168.1.125:18092/" +VITE_API_URL="http://127.0.0.1:18092/" # 开启激活验证 VITE_ACTIVATE_OPEN=false \ No newline at end of file diff --git a/frontend/src/api/check/test/index.ts b/frontend/src/api/check/test/index.ts index c5474c3..942471d 100644 --- a/frontend/src/api/check/test/index.ts +++ b/frontend/src/api/check/test/index.ts @@ -1,4 +1,3 @@ -import { pa } from 'element-plus/es/locale/index.mjs'; import http from '@/api' import {CheckData} from '@/api/check/interface' diff --git a/frontend/src/api/device/device/index.ts b/frontend/src/api/device/device/index.ts index a25c194..b8c1fc9 100644 --- a/frontend/src/api/device/device/index.ts +++ b/frontend/src/api/device/device/index.ts @@ -1,4 +1,3 @@ -import { pa } from 'element-plus/es/locale/index.mjs'; import type {Device} from '@/api/device/interface/device' import http from '@/api' diff --git a/frontend/src/api/device/icd/index.ts b/frontend/src/api/device/icd/index.ts index 3eb5f1e..0830ffd 100644 --- a/frontend/src/api/device/icd/index.ts +++ b/frontend/src/api/device/icd/index.ts @@ -1,6 +1,5 @@ import type { ICD } from '@/api/device/interface/icd' import http from '@/api' -import { pa } from 'element-plus/es/locale/index.mjs' /** * @name ICD管理模块 diff --git a/frontend/src/api/user/login/index.ts b/frontend/src/api/user/login/index.ts index 1ab485c..a316ef0 100644 --- a/frontend/src/api/user/login/index.ts +++ b/frontend/src/api/user/login/index.ts @@ -1,4 +1,3 @@ -import { pa } from 'element-plus/es/locale/index.mjs'; import type {Login} from '@/api/user/interface/user' import {ADMIN as rePrefix} from '@/api/system/config/serviceName' import http from '@/api' diff --git a/frontend/src/views/home/components/compareTest.vue b/frontend/src/views/home/components/compareTest.vue index a89627b..f317dac 100644 --- a/frontend/src/views/home/components/compareTest.vue +++ b/frontend/src/views/home/components/compareTest.vue @@ -131,8 +131,6 @@ import {getBigTestItem} from '@/api/check/test' import {getAutoGenerate} from '@/api/user/login' import {useModeStore} from '@/stores/modules/mode' // 引入模式 store import {useDictStore} from '@/stores/modules/dict' -import { ca } from 'element-plus/es/locale' - const checkStore = useCheckStore() const modeStore = useModeStore() const dictStore = useDictStore() diff --git a/frontend/src/views/home/components/deviceConnectionPopup.vue b/frontend/src/views/home/components/deviceConnectionPopup.vue index c8e2bcb..4243a40 100644 --- a/frontend/src/views/home/components/deviceConnectionPopup.vue +++ b/frontend/src/views/home/components/deviceConnectionPopup.vue @@ -65,8 +65,6 @@ import CustomEdge from './RemoveableEdge.vue' // 导入自定义连接线组件 import { jwtUtil } from '@/utils/jwtUtil' import { useCheckStore } from '@/stores/modules/check' import { Plan } from '@/api/plan/interface' -import { fa } from 'element-plus/es/locale' - const checkStore = useCheckStore() const dialogVisible = ref(false) const selectTestItemPopupRef = ref>() diff --git a/frontend/src/views/machine/controlSource/components/machineTree.vue b/frontend/src/views/machine/controlSource/components/machineTree.vue index 95296fe..95cbe7a 100644 --- a/frontend/src/views/machine/controlSource/components/machineTree.vue +++ b/frontend/src/views/machine/controlSource/components/machineTree.vue @@ -26,8 +26,6 @@ import { ref, reactive, onMounted, watch, nextTick } from 'vue' import { CheckData } from '@/api/check/interface' -import { da } from 'element-plus/es/locale' -import { on } from 'events' const props = defineProps({ treeData: { type: Array, diff --git a/public/html/loading.html b/public/html/loading.html index 9d1520b..9cde441 100644 --- a/public/html/loading.html +++ b/public/html/loading.html @@ -1,94 +1,215 @@ - - - - - - - -
-
-
-
-
-
-
+ .loading-container { + width: 480px; + padding: 40px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 20px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + text-align: center; + color: white; + } + + .logo { + font-size: 32px; + font-weight: bold; + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); + } + + .subtitle { + font-size: 14px; + opacity: 0.9; + margin-bottom: 30px; + } + + .status-text { + font-size: 16px; + margin-bottom: 20px; + min-height: 24px; + font-weight: 500; + } + + .progress-bar-container { + width: 100%; + height: 8px; + background: rgba(255, 255, 255, 0.3); + border-radius: 4px; + overflow: hidden; + margin-bottom: 15px; + } + + .progress-bar { + height: 100%; + background: linear-gradient(90deg, #ffffff 0%, #f0f0f0 100%); + border-radius: 4px; + transition: width 0.3s ease; + width: 0%; + box-shadow: 0 0 10px rgba(255, 255, 255, 0.5); + } + + .progress-percent { + font-size: 14px; + opacity: 0.8; + } + + .spinner { + display: inline-block; + width: 40px; + height: 40px; + margin-top: 10px; + } + + .spinner div { + box-sizing: border-box; + display: block; + position: absolute; + width: 32px; + height: 32px; + margin: 4px; + border: 3px solid #fff; + border-radius: 50%; + animation: spinner 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; + border-color: #fff transparent transparent transparent; + } + + .spinner div:nth-child(1) { + animation-delay: -0.45s; + } + + .spinner div:nth-child(2) { + animation-delay: -0.3s; + } + + .spinner div:nth-child(3) { + animation-delay: -0.15s; + } + + @keyframes spinner { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + + .error-container { + display: none; + margin-top: 20px; + padding: 15px; + background: rgba(255, 0, 0, 0.2); + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.3); + } + + .error-container.show { + display: block; + } + + .error-title { + font-size: 16px; + font-weight: bold; + margin-bottom: 8px; + } + + .error-message { + font-size: 14px; + opacity: 0.9; + } + + .extra-info { + font-size: 12px; + opacity: 0.7; + margin-top: 15px; + font-style: italic; + } + + + +
+ +
南京灿能电气自动化 · 自动检测平台
+ +
正在初始化应用...
+ +
+
- + +
0%
+ +
+
+
+
+
+
+ +
+
启动失败
+
+
+ +
+
+ + +