|
|
@@ -1,10 +1,12 @@
|
|
|
// main.js (ESM 版本)
|
|
|
-import { app, BrowserWindow, Menu, ipcMain, shell } from 'electron';
|
|
|
+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);
|
|
|
@@ -163,3 +165,406 @@ ipcMain.handle('exit-shutdown', async () => {
|
|
|
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 };
|
|
|
+ }
|
|
|
+});
|