const { app, BrowserWindow, ipcMain, Menu } = require('electron'); const path = require('path'); const { spawn } = require('child_process'); const dotenv = require('dotenv'); const fs = require('fs'); // 设置控制台输出编码为 UTF-8(Windows 兼容) if (process.platform === 'win32') { // 设置 stdout 和 stderr 的编码 if (process.stdout.setDefaultEncoding) { process.stdout.setDefaultEncoding('utf8'); } if (process.stderr.setDefaultEncoding) { process.stderr.setDefaultEncoding('utf8'); } // 尝试设置控制台代码页为 UTF-8 try { require('child_process').exec('chcp 65001', { encoding: 'utf8' }); } catch (e) { // 忽略错误 } } // 加载环境变量 const envPath = path.join(__dirname, '..', '.env'); if (fs.existsSync(envPath)) { dotenv.config({ path: envPath }); } let mainWindow; let backendProcess; // 判断是否为开发模式 // 主要依赖 app.isPackaged,如果为 false 则是开发环境 // 或者检查路径是否包含 app.asar(打包后的应用) const isDevelopment = !app.isPackaged || process.env.NODE_ENV === 'development'; console.log('运行模式检测:'); console.log(' - app.isPackaged:', app.isPackaged); console.log(' - process.resourcesPath:', process.resourcesPath); console.log(' - process.env.NODE_ENV:', process.env.NODE_ENV); console.log(' - isDevelopment:', isDevelopment); /** * 创建Electron主窗口 */ function createWindow() { mainWindow = new BrowserWindow({ width: 1200, height: 800, webPreferences: { preload: path.join(__dirname, 'preload.js'), nodeIntegration: false, contextIsolation: true, }, autoHideMenuBar: false, title: '投标应用', }); // 加载前端页面 const indexPath = path.join(__dirname, '..', 'frontend', 'dist', 'index.html'); mainWindow.loadFile(indexPath); // 开发环境下打开开发者工具 if (isDevelopment || process.env.NODE_ENV === 'development') { console.log('开发模式:打开开发者工具'); mainWindow.webContents.openDevTools(); // 过滤掉 DevTools 的 Autofill 相关错误 mainWindow.webContents.on('console-message', (event, level, message, line, sourceId) => { if (message.includes('Autofill.enable') || message.includes('Autofill.setAddresses')) { event.preventDefault(); } }); } mainWindow.on('closed', () => { mainWindow = null; }); } /** * 等待后端服务启动 */ function waitForBackend(port = 3000, maxRetries = 30, interval = 1000) { return new Promise((resolve, reject) => { let retries = 0; const checkBackend = () => { const net = require('net'); const client = new net.Socket(); client.once('connect', () => { client.destroy(); console.log('后端服务已启动'); resolve(); }); client.once('error', () => { client.destroy(); retry(); }); client.once('timeout', () => { client.destroy(); retry(); }); client.connect(port, 'localhost'); client.setTimeout(1000); function retry() { retries++; if (retries >= maxRetries) { reject(new Error('后端服务启动超时')); } else { console.log(`等待后端服务启动... (${retries}/${maxRetries})`); setTimeout(checkBackend, interval); } } }; checkBackend(); }); } /** * 启动后端服务 */ async function startBackend() { let backendPath; if (app.isPackaged) { // 生产环境:使用 app.asar.unpacked 中的文件 backendPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'dist', 'main.js'); } else { // 开发环境:使用项目根目录下的 dist 文件夹 backendPath = path.join(__dirname, '..', 'dist', 'main.js'); } // 检查后端构建文件是否存在 if (!fs.existsSync(backendPath)) { console.error('后端服务构建文件不存在,路径:', backendPath); console.error('请先执行 npm run build'); return; } console.log('启动后端服务:', backendPath); // 启动后端服务 backendProcess = spawn('node', [backendPath], { env: { ...process.env, NODE_ENV: isDevelopment ? 'development' : (process.env.NODE_ENV || 'production'), // 设置编码环境变量(Windows) ...(process.platform === 'win32' && { PYTHONIOENCODING: 'utf-8', LANG: 'en_US.UTF-8' }), }, stdio: 'pipe', // Windows 上设置 shell 选项以确保编码正确 ...(process.platform === 'win32' && { shell: false }), }); // 捕获并显示后端进程的输出 backendProcess.stdout.on('data', (data) => { // 确保正确解码 UTF-8 编码的数据 const output = Buffer.isBuffer(data) ? data.toString('utf8') : data.toString(); console.log(`[后端输出] ${output}`); }); backendProcess.stderr.on('data', (data) => { // 确保正确解码 UTF-8 编码的数据 const output = Buffer.isBuffer(data) ? data.toString('utf8') : data.toString(); console.error(`[后端错误] ${output}`); }); backendProcess.on('error', (error) => { console.error('后端服务启动失败:', error); }); let backendExited = false; backendProcess.on('exit', (code, signal) => { backendExited = true; if (code !== 0 && code !== null) { console.error(`后端服务异常退出,退出码: ${code}, 信号: ${signal}`); } else { console.log(`后端服务退出,退出码: ${code}`); } backendProcess = null; }); // 等待后端服务启动完成 try { await waitForBackend(); // 检查后端是否在等待期间就退出了 if (backendExited) { throw new Error('后端服务在启动过程中退出'); } } catch (error) { console.error('等待后端服务启动失败:', error.message); if (backendProcess) { console.error('正在停止后端进程...'); backendProcess.kill(); backendProcess = null; } throw error; // 重新抛出错误,让调用者知道启动失败 } } /** * 停止后端服务 */ function stopBackend() { if (backendProcess) { console.log('正在停止后端服务...'); backendProcess.kill(); backendProcess = null; } } // 应用就绪时启动后端服务,然后创建窗口 app.on('ready', async () => { try { await startBackend(); createWindow(); Menu.setApplicationMenu(null); } catch (error) { console.error('应用启动失败:', error); // 显示错误对话框 const { dialog } = require('electron'); dialog.showErrorBox( '启动失败', `后端服务启动失败: ${error.message}\n\n请检查控制台输出以获取更多信息。` ); } }); // 所有窗口关闭时退出应用 app.on('window-all-closed', () => { stopBackend(); if (process.platform !== 'darwin') { app.quit(); } }); // MacOS上点击dock图标时重新创建窗口 app.on('activate', async () => { if (BrowserWindow.getAllWindows().length === 0) { if (!backendProcess) { await startBackend(); } createWindow(); } }); // 应用退出前停止后端服务 app.on('before-quit', () => { stopBackend(); }); // 处理来自渲染进程的IPC消息 ipcMain.handle('get-env', (event, key) => { return process.env[key]; });