| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570 |
- // main.js (ESM 版本)
- import { app, BrowserWindow, Menu, ipcMain, shell, dialog } from 'electron';
- import { fileURLToPath } from 'url';
- import { dirname, join } from 'path';
- import { exec } from 'child_process';
- import { promisify } from 'util';
- import { writeLog } from './src/log/log-writer.js';
- import { tmpdir } from 'os';
- import { writeFileSync } from 'fs';
- // -------------- 构造 ESM 版 __dirname --------------
- const __filename = fileURLToPath(import.meta.url);
- const __dirname = dirname(__filename);
- const execAsync = promisify(exec);
- let win=null;
- // -------------- 单实例检查 ------------
- // 1. 请求单实例锁
- const gotTheLock = app.requestSingleInstanceLock();
- if (!gotTheLock) {
- // 抢锁失败 -> 立即退出
- app.quit();
- }
- // 2. 抢锁成功 -> 监听后续启动事件
- app.on('second-instance', (ev, cmd, cwd, additionalData) => {
- // 把已存在的窗口提到最前
- if (win) {
- if (win.isMinimized()) win.restore();
- win.focus();
- }
- // todo 也可以在这里解析 cmd 中的参数,做打开文件等操作
- });
- // -------------- 创建窗口 --------------
- function createWindow() {
- const isMac = process.platform === 'darwin';
- win = new BrowserWindow({
- show: false,
- frame: false,
- titleBarStyle: 'hidden',
- webPreferences: {
- nodeIntegration: false,
- contextIsolation: true,
- preload: join(__dirname, 'preload.js'),
- },
- });
- // 去掉应用菜单栏
- Menu.setApplicationMenu(null);
- if (!isMac) win.removeMenu();
- win.maximize();
- // 加载外置 H5 页面
- win.loadFile(join(process.cwd(), 'h5/index.html'));
- win.once('ready-to-show', () => win.show());
- //渲染进程死了,纪录日志
- win.webContents.on('render-process-gone', (event, details) => {
- writeLog('error',`渲染进程崩溃 ${details}`);
- // writeLog('error', JSON.stringify(details));
- });
- }
- // -------------- 应用生命周期 --------------
- app.whenReady().then(createWindow);
- app.on('window-all-closed', () => {
- if (process.platform !== 'darwin') app.quit();
- });
- app.on('activate', () => {
- if (BrowserWindow.getAllWindows().length === 0) createWindow();
- });
- // -------------- 系统操作函数 --------------
- const getSystemCommands = () => {
- const platform = process.platform;
-
- switch (platform) {
- case 'win32': // Windows
- return {
- shutdown: 'shutdown /s /t 0'
- };
- case 'darwin': // macOS
- return {
- shutdown: 'sudo shutdown -h now'
- };
- case 'linux': // Linux
- return {
- shutdown: 'systemctl poweroff'
- };
- default:
- return {
- shutdown: null
- };
- }
- };
- const executeSystemCommand = async (command, requiresAdmin = false) => {
- try {
- if (!command) {
- throw new Error('当前平台不支持该操作');
- }
-
- // Windows平台的权限检查
- if (process.platform === 'win32' && requiresAdmin) {
- // 检查是否以管理员身份运行
- try {
- await execAsync('net session', { timeout: 5000 });
- } catch (adminCheckError) {
- writeLog('warn', '操作可能需要管理员权限');
- return {
- success: false,
- error: '该操作需要管理员权限,请以管理员身份运行程序后重试',
- requiresAdmin: true
- };
- }
- }
-
- const { stdout, stderr } = await execAsync(command, { timeout: 10000 });
- writeLog('info', `系统命令执行成功: ${command}`);
-
- if (stderr && stderr.trim()) {
- writeLog('warn', `系统命令有警告信息: ${stderr}`);
- }
-
- return { success: true, stdout, stderr };
- } catch (error) {
- let errorMessage = error.message;
-
- // 特定错误类型的处理
- if (error.code === 'EACCES') {
- errorMessage = '权限不足,请以管理员身份运行程序';
- } else if (error.code === 'ENOENT') {
- errorMessage = '系统命令不存在或路径错误';
- } else if (error.code === 'ETIMEDOUT') {
- errorMessage = '操作超时,请重试';
- }
-
- writeLog('error', `系统命令执行失败: ${command}, 错误: ${errorMessage}`);
- return { success: false, error: errorMessage };
- }
- };
- // -------------- IPC --------------
- ipcMain.handle('write-log', async (_, level, msg) => {
- writeLog(level, msg);
- });
- // 退出应用
- ipcMain.handle('exit-close', async () => {
- writeLog('info', '用户选择关闭应用程序');
- app.quit();
- return { success: true };
- });
- // 关机
- ipcMain.handle('exit-shutdown', async () => {
- writeLog('info', '用户选择关机');
- const commands = getSystemCommands();
- // 关机通常需要管理员权限
- const requiresAdmin = true;
- return await executeSystemCommand(commands.shutdown, requiresAdmin);
- });
- // ========== 打印相关 IPC 处理器 ==========
- /**
- * 打印HTML内容
- */
- // 工具:一次性事件监听
- const once = (emitter, name) =>
- new Promise(resolve => emitter.once(name, resolve));
- // 工具:生成不二次编码的 data URL
- const makeDataURL = html =>
- `data:text/html;charset=utf-8,${Buffer.from(html, 'utf-8')}`;
- ipcMain.handle('print-film', async (event, options) => {
- let printWindow = null;
- try {
- writeLog('info', `开始打印,选项: ${JSON.stringify(options)}`);
- const { html, orientation, paperSize, silent, margins } = options;
- // 1. 创建隐藏窗口
- printWindow = new BrowserWindow({
- show: true, // true 仅调试时打开
- webPreferences: {
- nodeIntegration: false,
- contextIsolation: false,
- //sandbox: true
- }
- });
- // 2. 加载 HTML
- const loaded = once(printWindow.webContents, 'did-finish-load');
- await printWindow.loadURL(makeDataURL(html));
- await loaded;
- // 3. 打印参数
- const printOpts = {
- silent: Boolean(silent),
- printBackground: true,
- color: true,
- margins: {
- marginType: 'custom',
- top: margins?.top ?? 10,
- bottom: margins?.bottom ?? 10,
- left: margins?.left ?? 10,
- right: margins?.right ?? 10
- },
- landscape: orientation === 'landscape',
- pageSize: paperSize || 'A4'
- };
- // 4. 执行打印(带 60 秒超时)
- const [success, reason] = await Promise.race([
- new Promise(res => {
- printWindow.webContents.print(printOpts, (ok, err) => res([ok, err]));
- }),
- new Promise((_, rej) =>
- setTimeout(() => rej(new Error('打印超时(60s)')), 60_000)
- )
- ]);
- // 5. 清理窗口
- if (printWindow && !printWindow.isDestroyed()) {
- printWindow.close();
- printWindow = null;
- }
- if (success) {
- writeLog('info', '打印成功');
- return { success: true };
- }
- throw new Error(reason || '打印失败');
- } catch (err) {
- writeLog('error', `打印出错: ${err.message}`);
- // 确保异常时窗口也关掉
- if (printWindow && !printWindow.isDestroyed()) {
- printWindow.close();
- }
- return { success: false, error: err.message };
- }
- });
- /**
- * 打印HTML内容到文件(使用File System API)
- */
- ipcMain.handle('print-film-to-file', async (event, options) => {
- let printWindow = null;
- const tempFiles = [];
- try {
- writeLog('info', `开始文件系统打印,选项: ${JSON.stringify(options)}`);
- const { html, imageDataList, orientation, paperSize, silent, margins } = options;
- // 1. 创建临时图片文件
- const fs = await import('fs/promises');
- const os = await import('os');
- const path = await import('path');
- const { fileURLToPath, pathToFileURL } = await import('url');
- const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'electron-print-'));
- // 当前时间戳用于文件名
- const timestamp = Date.now();
- // 处理每个图片数据
- const imageUrlMap = new Map();
- for (let i = 0; i < imageDataList.length; i++) {
- const { canvasId, data: base64Data } = imageDataList[i];
- // 将base64转换为buffer
- const buffer = Buffer.from(base64Data, 'base64');
- // 生成临时文件路径
- const tempFilePath = path.join(tempDir, `print-image-${timestamp}-${i}.png`);
- // 写入文件
- await fs.writeFile(tempFilePath, buffer);
- tempFiles.push(tempFilePath);
- writeLog('info', `临时图片文件已保存: ${tempFilePath}`);
- // 使用Node.js内置方法构造file:// URL
- const fileUrl = pathToFileURL(tempFilePath).href;
- writeLog('info', `File URL构造: ${fileUrl}`);
- writeLog('info', `原始路径: ${tempFilePath}`);
- // 记录文件路径映射
- imageUrlMap.set(canvasId, fileUrl);
- }
- // 2. 生成最终HTML,将占位符替换为文件URL
- let finalHtml = html;
- for (const [canvasId, fileUrl] of imageUrlMap.entries()) {
- finalHtml = finalHtml.replace(
- new RegExp(`<img id="${canvasId}"[^>]*>`, 'g'),
- `<img id="${canvasId}" src="${fileUrl}" style="max-width: 100%;" alt="打印图片" />`
- );
- }
- writeLog('info', `HTML占位符已替换完成,临时文件数量: ${tempFiles.length}`);
- // 3. 包装完整的HTML文档
- const fullHtml = `
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="UTF-8">
- <title>打印胶片</title>
- <style>
- * {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }
- body {
- width: 100%;
- height: 100%;
- background: white;
- }
- @page {
- size: ${paperSize} ${orientation};
- margin: ${margins?.top ?? 10}mm ${margins?.right ?? 10}mm ${margins?.bottom ?? 10}mm ${margins?.left ?? 10}mm;
- }
- .print-only {
- display: block !important;
- }
- .screen-only {
- display: none !important;
- }
- </style>
- </head>
- <body>
- 235689
- <div id="film-print-area">
- ${finalHtml}
- </div>
- </body>
- </html>
- `;
- // 4. 创建隐藏窗口(调试时可以设为true查看窗口内容)
- printWindow = new BrowserWindow({
- show: true, // 设为true调试内容
- webPreferences: {
- nodeIntegration: false,
- contextIsolation: false,
- }
- });
- writeLog('info', '打印窗口已创建,开始加载HTML');
- writeLog('info',`把完整的html文件保存起来:${fullHtml}`)
- // 5. 加载 HTML
- const loaded = once(printWindow.webContents, 'did-finish-load');
- // const htmlDataURL = makeDataURL(fullHtml);
- // await printWindow.loadURL(htmlDataURL);
- // 1. 把完整 HTML 写入临时文件
- const tmpHtml = join(tmpdir(), `electron-print-${Date.now()}.html`);
- await writeFileSync(tmpHtml, fullHtml, 'utf8');
- // 2. 用 file:// 协议加载
- await printWindow.loadURL(`file://${tmpHtml.replace(/\\/g, '/')}`);
- await loaded;
- writeLog('info', 'HTML加载完成,等待图片加载...');
- // 6. 等待所有图片加载完成(重要的是这个步骤)
- await new Promise((resolve, reject) => {
- let loadedCount = 0;
- const totalImages = imageDataList.length;
- if (totalImages === 0) {
- // 没有图片,直接继续
- resolve();
- return;
- }
- const checkLoaded = () => {
- loadedCount++;
- writeLog('info', `图片已加载 ${loadedCount}/${totalImages}`);
- if (loadedCount >= totalImages) {
- writeLog('info', '所有图片加载完成');
- resolve();
- }
- };
- printWindow.webContents.on('console-message', (event, level, message) => {
- // 监控图片加载的调试信息
- if (message.includes('图片加载完成')) {
- checkLoaded();
- }
- });
- // 设置超时(20秒)
- setTimeout(() => {
- writeLog('warn', `图片加载超时 (已加载 ${loadedCount}/${totalImages}),仍继续打印`);
- resolve();
- }, 10000);
- });
- // 7. 执行打印
- const printOpts = {
- silent: false,//Boolean(silent),
- printBackground: true,
- color: true,
- margins: {
- marginType: 'custom',
- top: margins?.top ?? 10,
- bottom: margins?.bottom ?? 10,
- left: margins?.left ?? 10,
- right: margins?.right ?? 10
- },
- landscape: orientation === 'landscape',
- pageSize: paperSize || 'A4'
- };
- writeLog('info', '开始执行打印');
- const [success, reason] = await Promise.race([
- new Promise( res => {
- // 使用 printDialog 显示对话框
- // printWindow.webContents.printDialog(printOpts);
- printWindow.webContents.print(printOpts, (ok, err) => res([ok, err]));
- }),
- new Promise((_, rej) =>
- setTimeout(() => rej(new Error('打印超时(60s)')), 60_000)
- )
- ]);
- // 8. 清理临时文件
- // try {
- // for (const tempFile of tempFiles) {
- // await fs.unlink(tempFile);
- // writeLog('info', `临时文件已删除: ${tempFile}`);
- // }
- // // 删除临时目录
- // await fs.rmdir(tempDir, { recursive: true });
- // writeLog('info', `临时目录已删除: ${tempDir}`);
- // } catch (cleanupError) {
- // writeLog('warn', `清理临时文件失败: ${cleanupError.message}`);
- // }
- if (success) {
- writeLog('info', '文件系统打印成功');
- return { success: true };
- }
- throw new Error(reason || '打印失败');
- } catch (err) {
- writeLog('error', `文件系统打印出错: ${err.message}`);
- // 清理资源
- if (printWindow && !printWindow.isDestroyed()) {
- printWindow.close();
- }
- // // 清理临时文件
- // try {
- // const fs = await import('fs/promises');
- // for (const tempFile of tempFiles) {
- // try {
- // await fs.unlink(tempFile);
- // } catch (e) {
- // // 忽略单个文件删除错误
- // }
- // }
- // } catch (cleanupError) {
- // writeLog('warn', `清理临时文件失败: ${cleanupError.message}`);
- // }
- return { success: false, error: err.message };
- }
- });
- /**
- * 导出为PDF
- */
- ipcMain.handle('print-to-pdf', async (event, options) => {
- try {
- writeLog('info', `开始导出PDF,选项: ${JSON.stringify(options)}`);
- const { html, orientation, paperSize, margins } = options;
- // 创建隐藏窗口
- const pdfWindow = new BrowserWindow({
- show: false,
- webPreferences: {
- nodeIntegration: false,
- contextIsolation: true,
- },
- });
- // 加载HTML内容
- await pdfWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`);
- // 等待加载完成
- await new Promise(resolve => {
- pdfWindow.webContents.on('did-finish-load', resolve);
- });
- // PDF选项
- const pdfOptions = {
- printBackground: true,
- landscape: orientation === 'landscape',
- pageSize: paperSize || 'A4',
- margins: {
- top: margins?.top || 10,
- bottom: margins?.bottom || 10,
- left: margins?.left || 10,
- right: margins?.right || 10,
- },
- };
- // 生成PDF
- const data = await pdfWindow.webContents.printToPDF(pdfOptions);
- // 关闭窗口
- pdfWindow.close();
- // 显示保存对话框
- const { filePath } = await dialog.showSaveDialog(win, {
- title: '保存PDF',
- defaultPath: `film-${Date.now()}.pdf`,
- filters: [
- { name: 'PDF文件', extensions: ['pdf'] }
- ]
- });
- if (filePath) {
- const fs = await import('fs/promises');
- await fs.writeFile(filePath, data);
- writeLog('info', `PDF已保存到: ${filePath}`);
- // 询问是否打开文件
- const { response } = await dialog.showMessageBox(win, {
- type: 'info',
- message: 'PDF导出成功',
- detail: `文件已保存到:\n${filePath}`,
- buttons: ['打开文件', '关闭'],
- defaultId: 0
- });
- if (response === 0) {
- shell.openPath(filePath);
- }
- return { success: true, filePath };
- }
- return { success: false, error: '用户取消保存' };
- } catch (error) {
- writeLog('error', `导出PDF出错: ${error.message}`);
- return { success: false, error: error.message };
- }
- });
|