336 lines
10 KiB
JavaScript
336 lines
10 KiB
JavaScript
const { app, BrowserWindow, ipcMain } = require('electron');
|
||
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;
|
||
|
||
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;
|
||
})()
|
||
`);
|
||
}
|
||
});
|
||
|
||
function startBackendService() {
|
||
// 在打包后的应用中,使用process.resourcesPath获取resources目录
|
||
const resourcesPath = process.resourcesPath || path.join(__dirname, '../..');
|
||
const backendPath = path.join(resourcesPath, 'backend/BodyBalanceBackend/BodyBalanceBackend.exe');
|
||
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);
|
||
// 尝试备用路径
|
||
const fallbackPath = path.join(__dirname, 'resources/backend/BodyBalanceBackend/BodyBalanceBackend.exe');
|
||
console.log('Trying fallback path:', fallbackPath);
|
||
if (!fs.existsSync(fallbackPath)) {
|
||
console.error('Fallback backend executable not found:', fallbackPath);
|
||
return;
|
||
}
|
||
// 使用备用路径
|
||
const fallbackDir = path.join(__dirname, 'resources/backend/BodyBalanceBackend/');
|
||
|
||
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,
|
||
sandbox: false, // 显式关闭沙盒,避免 preload 加载问题
|
||
preload: path.join(__dirname, 'preload.js')
|
||
},
|
||
icon: path.join(__dirname, '../public/logo.png'),
|
||
show: false,
|
||
fullscreen: false,
|
||
frame: true,
|
||
autoHideMenuBar: true,
|
||
backgroundColor: '#000000'
|
||
});
|
||
|
||
// 窗口创建后立即最大化
|
||
mainWindow.maximize();
|
||
|
||
// 开发环境加载本地服务器,生产环境加载打包后的文件
|
||
const isDev = process.env.NODE_ENV === 'development';
|
||
if (isDev) {
|
||
mainWindow.loadURL('http://localhost:3000');
|
||
// mainWindow.webContents.openDevTools(); // 如需调试,请手动打开或在 did-finish-load 之后打开
|
||
} else {
|
||
// 启动后端服务
|
||
startBackendService();
|
||
// 延迟2秒后启动本地HTTP服务器
|
||
setTimeout(() => {
|
||
startLocalServer(() => {
|
||
mainWindow.loadURL('http://localhost:3000');
|
||
});
|
||
}, 2000);
|
||
}
|
||
|
||
// 窗口就绪后再显示,避免白屏/闪烁
|
||
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 进程异常导致的闪烁
|
||
app.disableHardwareAcceleration();
|
||
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();
|
||
}); |