调整C端展示

This commit is contained in:
2025-11-26 08:50:22 +08:00
parent f987e1c625
commit 92a3076638
23 changed files with 715 additions and 1309 deletions

View File

@@ -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',

View File

@@ -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;

View File

@@ -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);

View File

@@ -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]';