2025-12-09 12:59:04 +08:00
|
|
|
|
const { app, BrowserWindow, ipcMain } = require('electron');
|
2025-08-15 10:12:38 +08:00
|
|
|
|
const path = require('path');
|
|
|
|
|
|
const http = require('http');
|
|
|
|
|
|
const fs = require('fs');
|
|
|
|
|
|
const url = require('url');
|
|
|
|
|
|
const { spawn } = require('child_process');
|
|
|
|
|
|
let mainWindow;
|
|
|
|
|
|
let localServer;
|
|
|
|
|
|
let backendProcess;
|
2025-12-11 09:49:02 +08:00
|
|
|
|
// app.disableHardwareAcceleration();
|
2025-12-12 13:12:39 +08:00
|
|
|
|
app.disableDomainBlockingFor3DAPIs();
|
2025-12-09 12:59:04 +08:00
|
|
|
|
ipcMain.handle('generate-report-pdf', async (event, payload) => {
|
|
|
|
|
|
const win = BrowserWindow.fromWebContents(event.sender);
|
|
|
|
|
|
if (!win) throw new Error('窗口未找到');
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 准备打印环境:克隆节点到独立的打印容器,确保流式布局
|
|
|
|
|
|
await win.webContents.executeJavaScript(`
|
|
|
|
|
|
(function(){
|
|
|
|
|
|
const selector = '${(payload && payload.selector) ? payload.selector : '#report-root'}';
|
|
|
|
|
|
const root = document.querySelector(selector);
|
|
|
|
|
|
if (!root) throw new Error('报告根节点缺失');
|
|
|
|
|
|
|
|
|
|
|
|
// 创建打印容器
|
|
|
|
|
|
const printContainer = document.createElement('div');
|
|
|
|
|
|
printContainer.id = 'electron-print-container';
|
|
|
|
|
|
|
|
|
|
|
|
// 样式设置:绝对定位覆盖全屏,背景白,z-index最高
|
|
|
|
|
|
printContainer.style.position = 'absolute';
|
|
|
|
|
|
printContainer.style.top = '0';
|
|
|
|
|
|
printContainer.style.left = '0';
|
|
|
|
|
|
printContainer.style.width = '100%';
|
|
|
|
|
|
printContainer.style.minHeight = '100vh';
|
|
|
|
|
|
printContainer.style.background = '#ffffff';
|
|
|
|
|
|
printContainer.style.zIndex = '2147483647';
|
|
|
|
|
|
printContainer.style.display = 'block';
|
|
|
|
|
|
printContainer.style.overflow = 'visible'; // 关键:允许内容溢出以触发分页
|
|
|
|
|
|
|
|
|
|
|
|
// 克隆目标节点
|
|
|
|
|
|
const cloned = root.cloneNode(true);
|
|
|
|
|
|
|
|
|
|
|
|
// 强制重置克隆节点的关键样式,确保它是流式布局
|
|
|
|
|
|
cloned.style.position = 'static'; // 必须是 static 或 relative
|
|
|
|
|
|
cloned.style.display = 'block';
|
|
|
|
|
|
cloned.style.width = '100%';
|
|
|
|
|
|
cloned.style.height = 'auto';
|
|
|
|
|
|
cloned.style.overflow = 'visible';
|
|
|
|
|
|
cloned.style.margin = '0';
|
|
|
|
|
|
cloned.style.transform = 'none'; // 移除可能影响打印的变换
|
|
|
|
|
|
|
|
|
|
|
|
// 将克隆节点加入容器
|
|
|
|
|
|
printContainer.appendChild(cloned);
|
|
|
|
|
|
|
|
|
|
|
|
// 将容器加入 body
|
|
|
|
|
|
document.body.appendChild(printContainer);
|
|
|
|
|
|
|
|
|
|
|
|
// 关键修复:打印时只保留我们的打印容器可见,其他所有元素隐藏
|
|
|
|
|
|
// 我们创建一个 style 标签来强制隐藏除了 printContainer 以外的所有 body 直接子元素
|
|
|
|
|
|
const style = document.createElement('style');
|
|
|
|
|
|
style.id = 'print-style-override';
|
|
|
|
|
|
style.innerHTML = \`
|
|
|
|
|
|
@media print {
|
|
|
|
|
|
body > *:not(#electron-print-container) {
|
|
|
|
|
|
display: none !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
#electron-print-container {
|
|
|
|
|
|
display: block !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
\`;
|
|
|
|
|
|
document.head.appendChild(style);
|
|
|
|
|
|
|
|
|
|
|
|
document.body.classList.add('print-mode');
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
})()
|
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const pdf = await win.webContents.printToPDF({
|
|
|
|
|
|
pageSize: (payload && payload.pageSize) ? payload.pageSize : 'A4',
|
|
|
|
|
|
landscape: !!(payload && payload.landscape),
|
|
|
|
|
|
printBackground: !(payload && payload.printBackground === false),
|
|
|
|
|
|
marginsType: 0,
|
|
|
|
|
|
displayHeaderFooter: true, // 启用页眉页脚
|
|
|
|
|
|
headerTemplate: '<div></div>', // 空页眉
|
|
|
|
|
|
footerTemplate: `
|
|
|
|
|
|
<div style="width: 100%; text-align: center; font-size: 10px; font-family: 'Microsoft YaHei', sans-serif; padding-top: 5px; padding-bottom: 5px;">
|
|
|
|
|
|
第 <span class="pageNumber"></span> 页 / 共 <span class="totalPages"></span> 页
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`
|
|
|
|
|
|
});
|
|
|
|
|
|
return pdf;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
// 3. 清理环境
|
|
|
|
|
|
await win.webContents.executeJavaScript(`
|
|
|
|
|
|
(function(){
|
|
|
|
|
|
const container = document.getElementById('electron-print-container');
|
|
|
|
|
|
if (container) container.remove();
|
|
|
|
|
|
document.body.classList.remove('print-mode');
|
|
|
|
|
|
return true;
|
|
|
|
|
|
})()
|
|
|
|
|
|
`);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-08-15 10:12:38 +08:00
|
|
|
|
function startBackendService() {
|
|
|
|
|
|
// 在打包后的应用中,使用process.resourcesPath获取resources目录
|
|
|
|
|
|
const resourcesPath = process.resourcesPath || path.join(__dirname, '../..');
|
2025-08-16 12:11:08 +08:00
|
|
|
|
const backendPath = path.join(resourcesPath, 'backend/BodyBalanceBackend/BodyBalanceBackend.exe');
|
2025-08-15 10:12:38 +08:00
|
|
|
|
const backendDir = path.join(resourcesPath, 'backend/');
|
|
|
|
|
|
|
|
|
|
|
|
console.log('Resources path:', resourcesPath);
|
|
|
|
|
|
console.log('Backend path:', backendPath);
|
|
|
|
|
|
console.log('Backend directory:', backendDir);
|
|
|
|
|
|
|
|
|
|
|
|
// 检查后端可执行文件是否存在
|
|
|
|
|
|
if (!fs.existsSync(backendPath)) {
|
|
|
|
|
|
console.error('Backend executable not found:', backendPath);
|
|
|
|
|
|
// 尝试备用路径
|
2025-08-16 12:11:08 +08:00
|
|
|
|
const fallbackPath = path.join(__dirname, 'resources/backend/BodyBalanceBackend/BodyBalanceBackend.exe');
|
2025-08-15 10:12:38 +08:00
|
|
|
|
console.log('Trying fallback path:', fallbackPath);
|
|
|
|
|
|
if (!fs.existsSync(fallbackPath)) {
|
|
|
|
|
|
console.error('Fallback backend executable not found:', fallbackPath);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 使用备用路径
|
2025-08-16 12:11:08 +08:00
|
|
|
|
const fallbackDir = path.join(__dirname, 'resources/backend/BodyBalanceBackend/');
|
|
|
|
|
|
|
2025-08-15 10:12:38 +08:00
|
|
|
|
backendProcess = spawn(fallbackPath, [], {
|
|
|
|
|
|
cwd: fallbackDir,
|
|
|
|
|
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
|
|
|
|
});
|
|
|
|
|
|
console.log('Starting backend service with fallback path:', fallbackPath);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log('Starting backend service:', backendPath);
|
|
|
|
|
|
|
|
|
|
|
|
// 启动后端进程
|
|
|
|
|
|
backendProcess = spawn(backendPath, [], {
|
|
|
|
|
|
cwd: backendDir,
|
|
|
|
|
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
backendProcess.stdout.on('data', (data) => {
|
|
|
|
|
|
console.log('Backend stdout:', data.toString());
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
backendProcess.stderr.on('data', (data) => {
|
|
|
|
|
|
console.error('Backend stderr:', data.toString());
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
backendProcess.on('close', (code) => {
|
|
|
|
|
|
console.log('Backend process exited with code:', code);
|
|
|
|
|
|
backendProcess = null;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
backendProcess.on('error', (err) => {
|
|
|
|
|
|
console.error('Failed to start backend process:', err);
|
|
|
|
|
|
backendProcess = null;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function stopBackendService() {
|
|
|
|
|
|
if (backendProcess) {
|
|
|
|
|
|
console.log('Stopping backend service...');
|
|
|
|
|
|
backendProcess.kill('SIGTERM');
|
|
|
|
|
|
backendProcess = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 强制杀死所有BodyBalanceBackend.exe进程
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { exec } = require('child_process');
|
|
|
|
|
|
exec('taskkill /f /im BodyBalanceBackend.exe', (error, stdout, stderr) => {
|
|
|
|
|
|
if (error) {
|
|
|
|
|
|
// 如果没有找到进程,taskkill会返回错误,这是正常的
|
|
|
|
|
|
if (!error.message.includes('not found')) {
|
|
|
|
|
|
console.error('Error killing BodyBalanceBackend processes:', error.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log('Successfully killed all BodyBalanceBackend processes:', stdout);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('Failed to execute taskkill command:', err);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createWindow() {
|
|
|
|
|
|
mainWindow = new BrowserWindow({
|
|
|
|
|
|
width: 1920,
|
|
|
|
|
|
height: 1080,
|
|
|
|
|
|
|
|
|
|
|
|
webPreferences: {
|
|
|
|
|
|
nodeIntegration: false,
|
|
|
|
|
|
contextIsolation: true,
|
2025-12-10 15:35:50 +08:00
|
|
|
|
// sandbox: false, // 显式关闭沙盒,避免 preload 加载问题
|
2025-08-15 10:12:38 +08:00
|
|
|
|
preload: path.join(__dirname, 'preload.js')
|
|
|
|
|
|
},
|
2025-10-14 11:32:35 +08:00
|
|
|
|
icon: path.join(__dirname, '../public/logo.png'),
|
2025-08-15 10:12:38 +08:00
|
|
|
|
show: false,
|
|
|
|
|
|
fullscreen: false,
|
|
|
|
|
|
frame: true,
|
|
|
|
|
|
autoHideMenuBar: true,
|
|
|
|
|
|
backgroundColor: '#000000'
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-08-28 16:37:26 +08:00
|
|
|
|
// 窗口创建后立即最大化
|
|
|
|
|
|
mainWindow.maximize();
|
|
|
|
|
|
|
2025-08-15 10:12:38 +08:00
|
|
|
|
// 开发环境加载本地服务器,生产环境加载打包后的文件
|
|
|
|
|
|
const isDev = process.env.NODE_ENV === 'development';
|
|
|
|
|
|
if (isDev) {
|
|
|
|
|
|
mainWindow.loadURL('http://localhost:3000');
|
|
|
|
|
|
// mainWindow.webContents.openDevTools(); // 如需调试,请手动打开或在 did-finish-load 之后打开
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 启动后端服务
|
|
|
|
|
|
startBackendService();
|
2025-10-11 16:58:52 +08:00
|
|
|
|
// 延迟2秒后启动本地HTTP服务器
|
2025-08-15 10:12:38 +08:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
startLocalServer(() => {
|
|
|
|
|
|
mainWindow.loadURL('http://localhost:3000');
|
|
|
|
|
|
});
|
2025-10-11 16:58:52 +08:00
|
|
|
|
}, 2000);
|
2025-08-15 10:12:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 窗口就绪后再显示,避免白屏/闪烁
|
|
|
|
|
|
mainWindow.once('ready-to-show', () => {
|
|
|
|
|
|
mainWindow.show();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 监听页面加载完成事件
|
|
|
|
|
|
mainWindow.webContents.once('did-finish-load', () => {
|
|
|
|
|
|
console.log('Page loaded completely');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 添加加载失败的处理
|
|
|
|
|
|
mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL) => {
|
|
|
|
|
|
console.log('Failed to load:', errorDescription, 'URL:', validatedURL);
|
|
|
|
|
|
mainWindow.show(); // 即使加载失败也显示窗口
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
mainWindow.on('closed', () => {
|
|
|
|
|
|
mainWindow = null;
|
|
|
|
|
|
if (localServer) {
|
|
|
|
|
|
localServer.close();
|
|
|
|
|
|
}
|
|
|
|
|
|
// 关闭后端服务
|
|
|
|
|
|
stopBackendService();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function startLocalServer(callback) {
|
|
|
|
|
|
const staticPath = path.join(__dirname, '../dist/');
|
|
|
|
|
|
|
|
|
|
|
|
localServer = http.createServer((req, res) => {
|
|
|
|
|
|
const parsedUrl = url.parse(req.url);
|
|
|
|
|
|
let pathname = parsedUrl.pathname;
|
|
|
|
|
|
|
|
|
|
|
|
// 默认加载index.html
|
|
|
|
|
|
if (pathname === '/') {
|
|
|
|
|
|
pathname = '/index.html';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const filePath = path.join(staticPath, pathname);
|
|
|
|
|
|
|
|
|
|
|
|
fs.readFile(filePath, (err, data) => {
|
|
|
|
|
|
if (err) {
|
|
|
|
|
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
|
|
|
|
res.end('File not found');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 设置正确的Content-Type
|
|
|
|
|
|
const ext = path.extname(filePath);
|
|
|
|
|
|
let contentType = 'text/html';
|
|
|
|
|
|
|
|
|
|
|
|
switch (ext) {
|
|
|
|
|
|
case '.js':
|
|
|
|
|
|
contentType = 'application/javascript';
|
|
|
|
|
|
break;
|
|
|
|
|
|
case '.css':
|
|
|
|
|
|
contentType = 'text/css';
|
|
|
|
|
|
break;
|
|
|
|
|
|
case '.json':
|
|
|
|
|
|
contentType = 'application/json';
|
|
|
|
|
|
break;
|
|
|
|
|
|
case '.png':
|
|
|
|
|
|
contentType = 'image/png';
|
|
|
|
|
|
break;
|
|
|
|
|
|
case '.jpg':
|
|
|
|
|
|
case '.jpeg':
|
|
|
|
|
|
contentType = 'image/jpeg';
|
|
|
|
|
|
break;
|
|
|
|
|
|
case '.svg':
|
|
|
|
|
|
contentType = 'image/svg+xml';
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
res.writeHead(200, { 'Content-Type': contentType });
|
|
|
|
|
|
res.end(data);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
localServer.listen(3000, 'localhost', () => {
|
|
|
|
|
|
console.log('Local server started on http://localhost:3000');
|
|
|
|
|
|
callback();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 应用事件处理
|
|
|
|
|
|
// 关闭硬件加速以规避 GPU 进程异常导致的闪烁
|
2025-12-11 09:49:02 +08:00
|
|
|
|
// app.disableHardwareAcceleration();
|
2025-08-15 10:12:38 +08:00
|
|
|
|
app.whenReady().then(createWindow);
|
|
|
|
|
|
|
|
|
|
|
|
app.on('window-all-closed', () => {
|
|
|
|
|
|
if (process.platform !== 'darwin') {
|
|
|
|
|
|
if (localServer) {
|
|
|
|
|
|
localServer.close();
|
|
|
|
|
|
}
|
|
|
|
|
|
// 关闭后端服务
|
|
|
|
|
|
stopBackendService();
|
|
|
|
|
|
app.quit();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.on('activate', () => {
|
|
|
|
|
|
if (BrowserWindow.getAllWindows().length === 0) {
|
|
|
|
|
|
createWindow();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 应用退出前清理资源
|
|
|
|
|
|
app.on('before-quit', () => {
|
|
|
|
|
|
stopBackendService();
|
2025-12-11 09:49:02 +08:00
|
|
|
|
});
|