main.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764
  1. // main.js (ESM 版本)
  2. import { app, BrowserWindow, Menu, ipcMain, shell, dialog } from 'electron';
  3. import { fileURLToPath } from 'url';
  4. import { dirname, join } from 'path';
  5. import { exec } from 'child_process';
  6. import { promisify } from 'util';
  7. import { writeLog } from './src/log/log-writer.js';
  8. import { tmpdir } from 'os';
  9. import { writeFileSync } from 'fs';
  10. import fs from 'fs';
  11. import path from 'path';
  12. // 导入 safeStringify 函数来处理对象序列化
  13. function safeStringify(obj) {
  14. const cache = new Set();
  15. return JSON.stringify(obj, (key, value) => {
  16. if (typeof value === 'object' && value !== null) {
  17. if (cache.has(value)) {
  18. return '[Circular]';
  19. }
  20. cache.add(value);
  21. }
  22. return value;
  23. });
  24. }
  25. // -------------- 全局错误处理 --------------
  26. process.on('uncaughtException', (error) => {
  27. writeLog('fatal', `未捕获的异常: ${error.message}\n${error.stack}`);
  28. // 在生产环境中,可以选择退出应用
  29. if (process.env.NODE_ENV === 'production') {
  30. app.quit();
  31. }
  32. });
  33. process.on('unhandledRejection', (reason, promise) => {
  34. writeLog('error', `未处理的Promise拒绝: ${reason}\nPromise: ${promise}`);
  35. });
  36. // 监听应用崩溃
  37. app.on('render-process-gone', (event, webContents, details) => {
  38. writeLog('fatal', `渲染进程崩溃: ${safeStringify(details)}`);
  39. });
  40. app.on('child-process-gone', (event, childProcess, details) => {
  41. writeLog('error', `子进程崩溃: ${safeStringify(details)}`);
  42. });
  43. // -------------- 构造 ESM 版 __dirname --------------
  44. const __filename = fileURLToPath(import.meta.url);
  45. const __dirname = dirname(__filename);
  46. const execAsync = promisify(exec);
  47. // -------------- 导入测试模块 --------------
  48. import { parseCommandLineArgs, MonkeyTester, createDevMenuTemplate } from './electron/test.js';
  49. // -------------- 全局Monkey Tester实例 --------------
  50. let monkeyTester = null;
  51. let win=null;
  52. // -------------- 解析启动参数 --------------
  53. writeLog('info', `启动参数-解析前: ${safeStringify(process.argv)}`);
  54. const launchOptions = parseCommandLineArgs();
  55. writeLog('info', `启动参数: ${safeStringify(launchOptions)}`);
  56. // -------------- 单实例检查 ------------
  57. // 1. 请求单实例锁
  58. const gotTheLock = app.requestSingleInstanceLock();
  59. if (!gotTheLock) {
  60. // 抢锁失败 -> 立即退出
  61. app.quit();
  62. }
  63. // 2. 抢锁成功 -> 监听后续启动事件
  64. app.on('second-instance', (ev, cmd, cwd, additionalData) => {
  65. // 把已存在的窗口提到最前
  66. if (win) {
  67. if (win.isMinimized()) win.restore();
  68. win.focus();
  69. }
  70. // todo 也可以在这里解析 cmd 中的参数,做打开文件等操作
  71. });
  72. // -------------- 创建窗口 --------------
  73. function createWindow() {
  74. const isMac = process.platform === 'darwin';
  75. // 根据启动参数调整窗口配置
  76. const windowConfig = {
  77. show: false,
  78. webPreferences: {
  79. nodeIntegration: false,
  80. contextIsolation: true,
  81. preload: join(__dirname, 'preload.js'),
  82. devTools: launchOptions.enableDevTools, // 根据启动参数启用开发者工具
  83. },
  84. };
  85. // 如果启用了开发者工具或开发者菜单,需要显示框架和默认标题栏样式
  86. if (launchOptions.enableDevTools || launchOptions.enableDevMenu) {
  87. windowConfig.frame = true;
  88. windowConfig.titleBarStyle = 'default';
  89. } else {
  90. windowConfig.frame = false;
  91. windowConfig.titleBarStyle = 'hidden';
  92. }
  93. win = new BrowserWindow(windowConfig);
  94. // 根据启动参数设置应用菜单
  95. if (launchOptions.enableDevMenu) {
  96. // 启用开发者菜单
  97. const template = createDevMenuTemplate(isMac, win);
  98. const menu = Menu.buildFromTemplate(template);
  99. Menu.setApplicationMenu(menu);
  100. } else {
  101. // 去掉应用菜单栏
  102. Menu.setApplicationMenu(null);
  103. if (!isMac) win.removeMenu();
  104. }
  105. win.maximize();
  106. // 加载外置 H5 页面
  107. win.loadFile(join(process.cwd(), 'h5/index.html'));
  108. win.once('ready-to-show', () => {
  109. win.show();
  110. });
  111. //渲染进程死了,纪录日志
  112. win.webContents.on('render-process-gone', (event, details) => {
  113. writeLog('error',`渲染进程崩溃 ${details}`);
  114. // writeLog('error', JSON.stringify(details));
  115. });
  116. }
  117. // -------------- 应用生命周期 --------------
  118. app.whenReady().then(createWindow);
  119. app.on('window-all-closed', () => {
  120. if (process.platform !== 'darwin') app.quit();
  121. });
  122. app.on('activate', () => {
  123. if (BrowserWindow.getAllWindows().length === 0) createWindow();
  124. });
  125. // -------------- 系统操作函数 --------------
  126. const getSystemCommands = () => {
  127. const platform = process.platform;
  128. switch (platform) {
  129. case 'win32': // Windows
  130. return {
  131. shutdown: 'shutdown /s /t 0'
  132. };
  133. case 'darwin': // macOS
  134. return {
  135. shutdown: 'sudo shutdown -h now'
  136. };
  137. case 'linux': // Linux
  138. return {
  139. shutdown: 'systemctl poweroff'
  140. };
  141. default:
  142. return {
  143. shutdown: null
  144. };
  145. }
  146. };
  147. const executeSystemCommand = async (command, requiresAdmin = false) => {
  148. try {
  149. if (!command) {
  150. throw new Error('当前平台不支持该操作');
  151. }
  152. // Windows平台的权限检查
  153. if (process.platform === 'win32' && requiresAdmin) {
  154. // 检查是否以管理员身份运行
  155. try {
  156. await execAsync('net session', { timeout: 5000 });
  157. } catch (adminCheckError) {
  158. writeLog('warn', '操作可能需要管理员权限');
  159. return {
  160. success: false,
  161. error: '该操作需要管理员权限,请以管理员身份运行程序后重试',
  162. requiresAdmin: true
  163. };
  164. }
  165. }
  166. const { stdout, stderr } = await execAsync(command, { timeout: 10000 });
  167. writeLog('info', `系统命令执行成功: ${command}`);
  168. if (stderr && stderr.trim()) {
  169. writeLog('warn', `系统命令有警告信息: ${stderr}`);
  170. }
  171. return { success: true, stdout, stderr };
  172. } catch (error) {
  173. let errorMessage = error.message;
  174. // 特定错误类型的处理
  175. if (error.code === 'EACCES') {
  176. errorMessage = '权限不足,请以管理员身份运行程序';
  177. } else if (error.code === 'ENOENT') {
  178. errorMessage = '系统命令不存在或路径错误';
  179. } else if (error.code === 'ETIMEDOUT') {
  180. errorMessage = '操作超时,请重试';
  181. }
  182. writeLog('error', `系统命令执行失败: ${command}, 错误: ${errorMessage}`);
  183. return { success: false, error: errorMessage };
  184. }
  185. };
  186. // -------------- IPC --------------
  187. // 存储相关 IPC 处理器
  188. // 获取配置文件路径
  189. function getConfigPath() {
  190. const userDataPath = app.getPath('userData');
  191. return path.join(userDataPath, 'config.json');
  192. }
  193. // 确保配置文件所在目录存在
  194. function ensureConfigDir() {
  195. const configPath = getConfigPath();
  196. const dir = path.dirname(configPath);
  197. if (!fs.existsSync(dir)) {
  198. fs.mkdirSync(dir, { recursive: true });
  199. }
  200. }
  201. ipcMain.handle('storage-get', async (event, key) => {
  202. try {
  203. const configPath = getConfigPath();
  204. if (!fs.existsSync(configPath)) {
  205. return null;
  206. }
  207. const content = fs.readFileSync(configPath, 'utf-8');
  208. const data = JSON.parse(content);
  209. return data[key] || null;
  210. } catch (error) {
  211. console.error('读取配置文件失败:', error);
  212. return null;
  213. }
  214. });
  215. ipcMain.handle('storage-set', async (event, key, value) => {
  216. try {
  217. ensureConfigDir();
  218. const configPath = getConfigPath();
  219. let data = {};
  220. if (fs.existsSync(configPath)) {
  221. const content = fs.readFileSync(configPath, 'utf-8');
  222. data = JSON.parse(content);
  223. }
  224. data[key] = value;
  225. fs.writeFileSync(configPath, JSON.stringify(data, null, 2), 'utf-8');
  226. } catch (error) {
  227. console.error('保存配置文件失败:', error);
  228. throw error;
  229. }
  230. });
  231. ipcMain.handle('storage-remove', async (event, key) => {
  232. try {
  233. const configPath = getConfigPath();
  234. if (!fs.existsSync(configPath)) {
  235. return;
  236. }
  237. const content = fs.readFileSync(configPath, 'utf-8');
  238. const data = JSON.parse(content);
  239. delete data[key];
  240. fs.writeFileSync(configPath, JSON.stringify(data, null, 2), 'utf-8');
  241. } catch (error) {
  242. console.error('删除配置项失败:', error);
  243. throw error;
  244. }
  245. });
  246. // 日志功能
  247. ipcMain.handle('write-log', async (_, level, msg) => {
  248. writeLog(level, msg);
  249. });
  250. // Monkey Test 启动
  251. ipcMain.on('start-monkey-test', (event, options) => {
  252. if (!win) return;
  253. // 如果已经有实例在运行,先停止
  254. if (monkeyTester && monkeyTester.isActive()) {
  255. monkeyTester.stop();
  256. }
  257. const isMac = process.platform === 'darwin';
  258. monkeyTester = new MonkeyTester(win, isMac, win);
  259. monkeyTester.start(options);
  260. });
  261. // 退出应用
  262. ipcMain.handle('exit-close', async () => {
  263. writeLog('info', '用户选择关闭应用程序');
  264. app.quit();
  265. return { success: true };
  266. });
  267. // 关机
  268. ipcMain.handle('exit-shutdown', async () => {
  269. writeLog('info', '用户选择关机');
  270. const commands = getSystemCommands();
  271. // 关机通常需要管理员权限
  272. const requiresAdmin = true;
  273. return await executeSystemCommand(commands.shutdown, requiresAdmin);
  274. });
  275. // ========== 打印相关 IPC 处理器 ==========
  276. /**
  277. * 打印HTML内容
  278. */
  279. // 工具:一次性事件监听
  280. const once = (emitter, name) =>
  281. new Promise(resolve => emitter.once(name, resolve));
  282. // 工具:生成不二次编码的 data URL
  283. const makeDataURL = html =>
  284. `data:text/html;charset=utf-8,${Buffer.from(html, 'utf-8')}`;
  285. ipcMain.handle('print-film', async (event, options) => {
  286. let printWindow = null;
  287. try {
  288. writeLog('info', `开始打印,选项: ${JSON.stringify(options)}`);
  289. const { html, orientation, paperSize, silent, margins } = options;
  290. // 1. 创建隐藏窗口
  291. printWindow = new BrowserWindow({
  292. show: true, // true 仅调试时打开
  293. webPreferences: {
  294. nodeIntegration: false,
  295. contextIsolation: false,
  296. //sandbox: true
  297. }
  298. });
  299. // 2. 加载 HTML
  300. const loaded = once(printWindow.webContents, 'did-finish-load');
  301. await printWindow.loadURL(makeDataURL(html));
  302. await loaded;
  303. // 3. 打印参数
  304. const printOpts = {
  305. silent: Boolean(silent),
  306. printBackground: true,
  307. color: true,
  308. margins: {
  309. marginType: 'custom',
  310. top: margins?.top ?? 10,
  311. bottom: margins?.bottom ?? 10,
  312. left: margins?.left ?? 10,
  313. right: margins?.right ?? 10
  314. },
  315. landscape: orientation === 'landscape',
  316. pageSize: paperSize || 'A4'
  317. };
  318. // 4. 执行打印(带 60 秒超时)
  319. const [success, reason] = await Promise.race([
  320. new Promise(res => {
  321. printWindow.webContents.print(printOpts, (ok, err) => res([ok, err]));
  322. }),
  323. new Promise((_, rej) =>
  324. setTimeout(() => rej(new Error('打印超时(60s)')), 60_000)
  325. )
  326. ]);
  327. // 5. 清理窗口
  328. if (printWindow && !printWindow.isDestroyed()) {
  329. printWindow.close();
  330. printWindow = null;
  331. }
  332. if (success) {
  333. writeLog('info', '打印成功');
  334. return { success: true };
  335. }
  336. throw new Error(reason || '打印失败');
  337. } catch (err) {
  338. writeLog('error', `打印出错: ${err.message}`);
  339. // 确保异常时窗口也关掉
  340. if (printWindow && !printWindow.isDestroyed()) {
  341. printWindow.close();
  342. }
  343. return { success: false, error: err.message };
  344. }
  345. });
  346. /**
  347. * 打印HTML内容到文件(使用File System API)
  348. */
  349. ipcMain.handle('print-film-to-file', async (event, options) => {
  350. let printWindow = null;
  351. const tempFiles = [];
  352. try {
  353. writeLog('info', `开始文件系统打印,选项: ${JSON.stringify(options)}`);
  354. const { html, imageDataList, orientation, paperSize, silent, margins } = options;
  355. // 1. 创建临时图片文件
  356. const fs = await import('fs/promises');
  357. const os = await import('os');
  358. const path = await import('path');
  359. const { fileURLToPath, pathToFileURL } = await import('url');
  360. const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'electron-print-'));
  361. // 当前时间戳用于文件名
  362. const timestamp = Date.now();
  363. // 处理每个图片数据
  364. const imageUrlMap = new Map();
  365. for (let i = 0; i < imageDataList.length; i++) {
  366. const { canvasId, data: base64Data } = imageDataList[i];
  367. // 将base64转换为buffer
  368. const buffer = Buffer.from(base64Data, 'base64');
  369. // 生成临时文件路径
  370. const tempFilePath = path.join(tempDir, `print-image-${timestamp}-${i}.png`);
  371. // 写入文件
  372. await fs.writeFile(tempFilePath, buffer);
  373. tempFiles.push(tempFilePath);
  374. writeLog('info', `临时图片文件已保存: ${tempFilePath}`);
  375. // 使用Node.js内置方法构造file:// URL
  376. const fileUrl = pathToFileURL(tempFilePath).href;
  377. writeLog('info', `File URL构造: ${fileUrl}`);
  378. writeLog('info', `原始路径: ${tempFilePath}`);
  379. // 记录文件路径映射
  380. imageUrlMap.set(canvasId, fileUrl);
  381. }
  382. // 2. 生成最终HTML,将占位符替换为文件URL
  383. let finalHtml = html;
  384. for (const [canvasId, fileUrl] of imageUrlMap.entries()) {
  385. finalHtml = finalHtml.replace(
  386. new RegExp(`<img id="${canvasId}"[^>]*>`, 'g'),
  387. `<img id="${canvasId}" src="${fileUrl}" style="max-width: 100%;" alt="打印图片" />`
  388. );
  389. }
  390. writeLog('info', `HTML占位符已替换完成,临时文件数量: ${tempFiles.length}`);
  391. // 3. 包装完整的HTML文档
  392. const fullHtml = `
  393. <!DOCTYPE html>
  394. <html>
  395. <head>
  396. <meta charset="UTF-8">
  397. <title>打印胶片</title>
  398. <style>
  399. * {
  400. margin: 0;
  401. padding: 0;
  402. box-sizing: border-box;
  403. }
  404. body {
  405. width: 100%;
  406. height: 100%;
  407. background: white;
  408. }
  409. @page {
  410. size: ${paperSize} ${orientation};
  411. margin: ${margins?.top ?? 10}mm ${margins?.right ?? 10}mm ${margins?.bottom ?? 10}mm ${margins?.left ?? 10}mm;
  412. }
  413. .print-only {
  414. display: block !important;
  415. }
  416. .screen-only {
  417. display: none !important;
  418. }
  419. </style>
  420. </head>
  421. <body>
  422. 235689
  423. <div id="film-print-area">
  424. ${finalHtml}
  425. </div>
  426. </body>
  427. </html>
  428. `;
  429. // 4. 创建隐藏窗口(调试时可以设为true查看窗口内容)
  430. printWindow = new BrowserWindow({
  431. show: true, // 设为true调试内容
  432. webPreferences: {
  433. nodeIntegration: false,
  434. contextIsolation: false,
  435. }
  436. });
  437. writeLog('info', '打印窗口已创建,开始加载HTML');
  438. writeLog('info',`把完整的html文件保存起来:${fullHtml}`)
  439. // 5. 加载 HTML
  440. const loaded = once(printWindow.webContents, 'did-finish-load');
  441. // const htmlDataURL = makeDataURL(fullHtml);
  442. // await printWindow.loadURL(htmlDataURL);
  443. // 1. 把完整 HTML 写入临时文件
  444. const tmpHtml = join(tmpdir(), `electron-print-${Date.now()}.html`);
  445. await writeFileSync(tmpHtml, fullHtml, 'utf8');
  446. // 2. 用 file:// 协议加载
  447. await printWindow.loadURL(`file://${tmpHtml.replace(/\\/g, '/')}`);
  448. await loaded;
  449. writeLog('info', 'HTML加载完成,等待图片加载...');
  450. // 6. 等待所有图片加载完成(重要的是这个步骤)
  451. await new Promise((resolve, reject) => {
  452. let loadedCount = 0;
  453. const totalImages = imageDataList.length;
  454. if (totalImages === 0) {
  455. // 没有图片,直接继续
  456. resolve();
  457. return;
  458. }
  459. const checkLoaded = () => {
  460. loadedCount++;
  461. writeLog('info', `图片已加载 ${loadedCount}/${totalImages}`);
  462. if (loadedCount >= totalImages) {
  463. writeLog('info', '所有图片加载完成');
  464. resolve();
  465. }
  466. };
  467. printWindow.webContents.on('console-message', (event, level, message) => {
  468. // 监控图片加载的调试信息
  469. if (message.includes('图片加载完成')) {
  470. checkLoaded();
  471. }
  472. });
  473. // 设置超时(20秒)
  474. setTimeout(() => {
  475. writeLog('warn', `图片加载超时 (已加载 ${loadedCount}/${totalImages}),仍继续打印`);
  476. resolve();
  477. }, 10000);
  478. });
  479. // 7. 执行打印
  480. const printOpts = {
  481. silent: false,//Boolean(silent),
  482. printBackground: true,
  483. color: true,
  484. margins: {
  485. marginType: 'custom',
  486. top: margins?.top ?? 10,
  487. bottom: margins?.bottom ?? 10,
  488. left: margins?.left ?? 10,
  489. right: margins?.right ?? 10
  490. },
  491. landscape: orientation === 'landscape',
  492. pageSize: paperSize || 'A4'
  493. };
  494. writeLog('info', '开始执行打印');
  495. const [success, reason] = await Promise.race([
  496. new Promise( res => {
  497. // 使用 printDialog 显示对话框
  498. // printWindow.webContents.printDialog(printOpts);
  499. printWindow.webContents.print(printOpts, (ok, err) => res([ok, err]));
  500. }),
  501. new Promise((_, rej) =>
  502. setTimeout(() => rej(new Error('打印超时(60s)')), 60_000)
  503. )
  504. ]);
  505. // 8. 清理临时文件
  506. // try {
  507. // for (const tempFile of tempFiles) {
  508. // await fs.unlink(tempFile);
  509. // writeLog('info', `临时文件已删除: ${tempFile}`);
  510. // }
  511. // // 删除临时目录
  512. // await fs.rmdir(tempDir, { recursive: true });
  513. // writeLog('info', `临时目录已删除: ${tempDir}`);
  514. // } catch (cleanupError) {
  515. // writeLog('warn', `清理临时文件失败: ${cleanupError.message}`);
  516. // }
  517. if (success) {
  518. writeLog('info', '文件系统打印成功');
  519. return { success: true };
  520. }
  521. throw new Error(reason || '打印失败');
  522. } catch (err) {
  523. writeLog('error', `文件系统打印出错: ${err.message}`);
  524. // 清理资源
  525. if (printWindow && !printWindow.isDestroyed()) {
  526. printWindow.close();
  527. }
  528. // // 清理临时文件
  529. // try {
  530. // const fs = await import('fs/promises');
  531. // for (const tempFile of tempFiles) {
  532. // try {
  533. // await fs.unlink(tempFile);
  534. // } catch (e) {
  535. // // 忽略单个文件删除错误
  536. // }
  537. // }
  538. // } catch (cleanupError) {
  539. // writeLog('warn', `清理临时文件失败: ${cleanupError.message}`);
  540. // }
  541. return { success: false, error: err.message };
  542. }
  543. });
  544. /**
  545. * 保存 Blob 数据到文件
  546. * 通用的文件保存功能,支持任意二进制数据
  547. */
  548. ipcMain.handle('save-blob', async (event, options) => {
  549. try {
  550. const { data, defaultFilename, filters, title } = options;
  551. writeLog('info', `开始保存文件,默认文件名: ${defaultFilename}`);
  552. // 显示保存对话框
  553. const { filePath, canceled } = await dialog.showSaveDialog(win, {
  554. title: title || '保存文件',
  555. defaultPath: defaultFilename,
  556. filters: filters || [{ name: '所有文件', extensions: ['*'] }]
  557. });
  558. if (canceled || !filePath) {
  559. writeLog('info', '用户取消保存');
  560. return { success: false, canceled: true };
  561. }
  562. // 将 ArrayBuffer 转换为 Buffer
  563. const buffer = Buffer.from(data);
  564. // 保存文件
  565. const fs = await import('fs/promises');
  566. await fs.writeFile(filePath, buffer);
  567. writeLog('info', `文件已保存到: ${filePath}`);
  568. return { success: true, filePath };
  569. } catch (error) {
  570. writeLog('error', `保存文件出错: ${error.message}`);
  571. return { success: false, error: error.message };
  572. }
  573. });
  574. /**
  575. * 导出为PDF
  576. */
  577. ipcMain.handle('print-to-pdf', async (event, options) => {
  578. try {
  579. writeLog('info', `开始导出PDF,选项: ${JSON.stringify(options)}`);
  580. const { html, orientation, paperSize, margins } = options;
  581. // 创建隐藏窗口
  582. const pdfWindow = new BrowserWindow({
  583. show: false,
  584. webPreferences: {
  585. nodeIntegration: false,
  586. contextIsolation: true,
  587. },
  588. });
  589. // 加载HTML内容
  590. await pdfWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`);
  591. // 等待加载完成
  592. await new Promise(resolve => {
  593. pdfWindow.webContents.on('did-finish-load', resolve);
  594. });
  595. // PDF选项
  596. const pdfOptions = {
  597. printBackground: true,
  598. landscape: orientation === 'landscape',
  599. pageSize: paperSize || 'A4',
  600. margins: {
  601. top: margins?.top || 10,
  602. bottom: margins?.bottom || 10,
  603. left: margins?.left || 10,
  604. right: margins?.right || 10,
  605. },
  606. };
  607. // 生成PDF
  608. const data = await pdfWindow.webContents.printToPDF(pdfOptions);
  609. // 关闭窗口
  610. pdfWindow.close();
  611. // 显示保存对话框
  612. const { filePath } = await dialog.showSaveDialog(win, {
  613. title: '保存PDF',
  614. defaultPath: `film-${Date.now()}.pdf`,
  615. filters: [
  616. { name: 'PDF文件', extensions: ['pdf'] }
  617. ]
  618. });
  619. if (filePath) {
  620. const fs = await import('fs/promises');
  621. await fs.writeFile(filePath, data);
  622. writeLog('info', `PDF已保存到: ${filePath}`);
  623. // 询问是否打开文件
  624. const { response } = await dialog.showMessageBox(win, {
  625. type: 'info',
  626. message: 'PDF导出成功',
  627. detail: `文件已保存到:\n${filePath}`,
  628. buttons: ['打开文件', '关闭'],
  629. defaultId: 0
  630. });
  631. if (response === 0) {
  632. shell.openPath(filePath);
  633. }
  634. return { success: true, filePath };
  635. }
  636. return { success: false, error: '用户取消保存' };
  637. } catch (error) {
  638. writeLog('error', `导出PDF出错: ${error.message}`);
  639. return { success: false, error: error.message };
  640. }
  641. });