feat(electron): 添加Electron桌面应用支持
- 新增Electron主进程、预加载脚本和构建配置 - 修改前端配置以支持Electron打包 - 更新项目文档和依赖 - 重构API调用使用统一axios实例
This commit is contained in:
39
app/electron-builder.json
Normal file
39
app/electron-builder.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"productName": "投标应用",
|
||||
"appId": "com.bidding.app",
|
||||
"directories": {
|
||||
"output": "dist-electron",
|
||||
"app": "./app"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"frontend/**/*",
|
||||
".env",
|
||||
"node_modules/**/*",
|
||||
"package.json",
|
||||
"app/**/*"
|
||||
],
|
||||
"win": {
|
||||
"target": "nsis",
|
||||
"icon": "frontend/public/favicon.ico",
|
||||
"requestedExecutionLevel": "asInvoker"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"createDesktopShortcut": true,
|
||||
"createStartMenuShortcut": true,
|
||||
"shortcutName": "投标应用"
|
||||
},
|
||||
"extraResources": [
|
||||
{
|
||||
"from": ".env",
|
||||
"to": ".env",
|
||||
"filter": ["**/*"]
|
||||
}
|
||||
],
|
||||
"publish": {
|
||||
"provider": "generic",
|
||||
"url": "http://localhost:3000/"
|
||||
}
|
||||
}
|
||||
172
app/main.js
Normal file
172
app/main.js
Normal file
@@ -0,0 +1,172 @@
|
||||
const { app, BrowserWindow, ipcMain } = require('electron');
|
||||
const path = require('path');
|
||||
const { spawn } = require('child_process');
|
||||
const dotenv = require('dotenv');
|
||||
const fs = require('fs');
|
||||
|
||||
// 加载环境变量
|
||||
const envPath = path.join(__dirname, '..', '.env');
|
||||
if (fs.existsSync(envPath)) {
|
||||
dotenv.config({ path: envPath });
|
||||
}
|
||||
|
||||
let mainWindow;
|
||||
let backendProcess;
|
||||
|
||||
/**
|
||||
* 创建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 (process.env.NODE_ENV === 'development') {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
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() {
|
||||
const backendPath = path.join(__dirname, '..', 'dist', 'main.js');
|
||||
|
||||
// 检查后端构建文件是否存在
|
||||
if (!fs.existsSync(backendPath)) {
|
||||
console.error('后端服务构建文件不存在,请先执行 npm run build');
|
||||
return;
|
||||
}
|
||||
|
||||
// 启动后端服务
|
||||
backendProcess = spawn('node', [backendPath], {
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: process.env.NODE_ENV || 'production',
|
||||
},
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
backendProcess.on('error', (error) => {
|
||||
console.error('后端服务启动失败:', error);
|
||||
});
|
||||
|
||||
backendProcess.on('exit', (code) => {
|
||||
console.log(`后端服务退出,退出码: ${code}`);
|
||||
backendProcess = null;
|
||||
});
|
||||
|
||||
// 等待后端服务启动完成
|
||||
try {
|
||||
await waitForBackend();
|
||||
} catch (error) {
|
||||
console.error('等待后端服务启动失败:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止后端服务
|
||||
*/
|
||||
function stopBackend() {
|
||||
if (backendProcess) {
|
||||
console.log('正在停止后端服务...');
|
||||
backendProcess.kill();
|
||||
backendProcess = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 应用就绪时启动后端服务,然后创建窗口
|
||||
app.on('ready', async () => {
|
||||
await startBackend();
|
||||
createWindow();
|
||||
});
|
||||
|
||||
// 所有窗口关闭时退出应用
|
||||
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];
|
||||
});
|
||||
13
app/package.json
Normal file
13
app/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "bidding-app",
|
||||
"version": "0.0.1",
|
||||
"description": "投标应用Electron版本",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"build": "electron-builder"
|
||||
},
|
||||
"keywords": ["electron", "bidding", "app"],
|
||||
"author": "",
|
||||
"license": "UNLICENSED"
|
||||
}
|
||||
14
app/preload.js
Normal file
14
app/preload.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
/**
|
||||
* 预加载脚本,用于在渲染进程和主进程之间通信
|
||||
* 提供安全的API给渲染进程访问主进程功能
|
||||
*/
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
/**
|
||||
* 获取环境变量值
|
||||
* @param {string} key - 环境变量名称
|
||||
* @returns {Promise<string>} - 环境变量值
|
||||
*/
|
||||
getEnv: (key) => ipcRenderer.invoke('get-env', key),
|
||||
});
|
||||
Reference in New Issue
Block a user