const { app, BrowserWindow, ipcMain } = require("electron"); const path = require("path"); const https = require("https"); const http = require("http"); const fs = require("fs"); const os = require("os"); const ptp = require("pdf-to-printer"); let mainWindow = null; const { Worker } = require("worker_threads"); const log = require("electron-log"); // 配置日志输出 log.transports.file.level = "info"; log.transports.file.resolvePath = () => path.join(path.dirname(app.getPath("exe")), "logs", "main.log"); log.info("App starting..."); // 监听渲染进程日志 ipcMain.on("log-message", (event, { level, message }) => { if (log[level]) { log[level](`[Renderer] ${message}`); } else { log.info(`[Renderer] ${message}`); } }); let idCardWorker = null; function createIdCardWorker() { if (idCardWorker) return; log.info("Creating IDCard Worker..."); idCardWorker = new Worker(path.join(__dirname, "idcard-worker.js")); idCardWorker.on("message", (msg) => { if (!mainWindow) return; if (msg.type === "data") { log.info("IDCard data received"); mainWindow.webContents.send("idcard-data", { payload: msg.payload }); } else if (msg.type === "error") { log.error("IDCard worker error message:", msg.payload); mainWindow.webContents.send("idcard-error", { payload: msg.payload }); } else if (msg.type === "log") { log.info("[Worker]", msg.payload); } }); idCardWorker.on("error", (err) => { log.error("Worker thread error:", err); }); idCardWorker.on("exit", (code) => { if (code !== 0) log.error(`Worker stopped with exit code ${code}`); else log.info("Worker stopped gracefully"); idCardWorker = null; }); } function createWindow() { mainWindow = new BrowserWindow({ width: 1080, height: 1920, fullscreen: true, // 全屏 frame: false, // 无边框 icon: path.join(__dirname, "../resources/icon.ico"), // 设置窗口图标 webPreferences: { preload: path.join(__dirname, "preload.js"), nodeIntegration: false, contextIsolation: true, }, }); // 启用触摸事件支持(Windows 触摸屏) mainWindow.webContents.on("did-finish-load", () => { // 注入触摸事件支持脚本 mainWindow.webContents .executeJavaScript( ` // 确保触摸事件可用 if (navigator.maxTouchPoints === 0) { console.log('[Electron] 未检测到触摸设备,将使用鼠标事件模拟'); } else { console.log('[Electron] 检测到触摸设备,触摸点数:', navigator.maxTouchPoints); } ` ) .catch((err) => log.error("Failed to inject touch support:", err)); }); const isDev = !app.isPackaged; if (isDev) { mainWindow.loadURL("http://localhost:5173"); // 打开开发者工具 mainWindow.webContents.openDevTools(); } else { mainWindow.loadFile(path.join(__dirname, "../dist/index.html")); } } // 处理PDF获取请求(绕过CORS) ipcMain.handle("fetch-pdf", async (event, pdfUrl) => { return new Promise((resolve, reject) => { const protocol = pdfUrl.startsWith("https") ? https : http; protocol .get(pdfUrl, (response) => { // 处理重定向 if (response.statusCode === 301 || response.statusCode === 302) { const redirectUrl = response.headers.location; protocol .get(redirectUrl, (redirectResponse) => { const chunks = []; redirectResponse.on("data", (chunk) => chunks.push(chunk)); redirectResponse.on("end", () => { const buffer = Buffer.concat(chunks); resolve({ success: true, data: buffer.toString("base64"), }); }); redirectResponse.on("error", (error) => { reject({ success: false, error: error.message }); }); }) .on("error", (error) => { reject({ success: false, error: error.message }); }); } else { const chunks = []; response.on("data", (chunk) => chunks.push(chunk)); response.on("end", () => { const buffer = Buffer.concat(chunks); resolve({ success: true, data: buffer.toString("base64"), }); }); response.on("error", (error) => { reject({ success: false, error: error.message }); }); } }) .on("error", (error) => { reject({ success: false, error: error.message }); }); }); }); ipcMain.handle("get-printers", async (event) => { try { const webContents = event.sender; if (!webContents) { return { success: false, error: "No webContents available" }; } if (typeof webContents.getPrintersAsync === "function") { const printers = await webContents.getPrintersAsync(); return { success: true, printers }; } const printers = webContents.getPrinters(); return { success: true, printers }; } catch (error) { log.error("Failed to get printers:", error); return { success: false, error: error.message }; } }); // 处理PDF打印请求 ipcMain.handle("print-pdf", async (event, pdfDataOrUrl, printOptions = {}) => { let tempFilePath = null; const targetPrinterName = printOptions?.printerName; try { let pdfFilePath = null; // 如果是 base64 data URI,转换为临时文件 if (pdfDataOrUrl.startsWith("data:application/pdf;base64,")) { const base64Data = pdfDataOrUrl.replace( "data:application/pdf;base64,", "" ); // 清理base64字符串中的空白字符 const cleanBase64 = base64Data.replace(/\s/g, ""); try { const buffer = Buffer.from(cleanBase64, "base64"); // 验证PDF文件头(%PDF) if (buffer.length < 4 || buffer.toString("ascii", 0, 4) !== "%PDF") { throw new Error("无效的PDF数据:文件头不正确"); } // 创建临时文件 tempFilePath = path.join( os.tmpdir(), `print_${Date.now()}_${Math.random().toString(36).substring(7)}.pdf` ); fs.writeFileSync(tempFilePath, buffer); pdfFilePath = tempFilePath; const fileSize = fs.statSync(tempFilePath).size; log.info( `PDF written to temp file: ${tempFilePath}, size: ${fileSize} bytes` ); if (fileSize < 1024) { throw new Error( `PDF文件大小异常(${fileSize} bytes),可能数据不完整` ); } } catch (err) { log.error("Base64解码或文件写入失败:", err); throw new Error(`PDF数据处理失败: ${err.message}`); } } // 如果是HTTP/HTTPS URL,先下载 else if ( pdfDataOrUrl.startsWith("http://") || pdfDataOrUrl.startsWith("https://") ) { log.info("Downloading PDF from URL:", pdfDataOrUrl); const protocol = pdfDataOrUrl.startsWith("https") ? https : http; const buffer = await new Promise((resolve, reject) => { protocol .get(pdfDataOrUrl, (response) => { // 处理重定向 if (response.statusCode === 301 || response.statusCode === 302) { const redirectUrl = response.headers.location; protocol .get(redirectUrl, (redirectResponse) => { const chunks = []; redirectResponse.on("data", (chunk) => chunks.push(chunk)); redirectResponse.on("end", () => { resolve(Buffer.concat(chunks)); }); redirectResponse.on("error", reject); }) .on("error", reject); } else { const chunks = []; response.on("data", (chunk) => chunks.push(chunk)); response.on("end", () => { resolve(Buffer.concat(chunks)); }); response.on("error", reject); } }) .on("error", reject); }); // 验证PDF文件头 if (buffer.length < 4 || buffer.toString("ascii", 0, 4) !== "%PDF") { throw new Error("下载的文件不是有效的PDF"); } tempFilePath = path.join( os.tmpdir(), `print_${Date.now()}_${Math.random().toString(36).substring(7)}.pdf` ); fs.writeFileSync(tempFilePath, buffer); pdfFilePath = tempFilePath; const fileSize = fs.statSync(tempFilePath).size; log.info( `PDF downloaded to temp file: ${tempFilePath}, size: ${fileSize} bytes` ); } // 如果是本地文件路径 else if (pdfDataOrUrl.startsWith("file://")) { pdfFilePath = pdfDataOrUrl.replace("file://", ""); } // 如果是相对路径 else if (pdfDataOrUrl.startsWith("/assets/")) { const isDev = !app.isPackaged; if (!isDev) { pdfFilePath = path.join(__dirname, "..", "dist", pdfDataOrUrl); } else { throw new Error("开发环境不支持相对路径PDF打印,请使用完整URL或base64"); } } // 直接作为文件路径 else { pdfFilePath = pdfDataOrUrl; } // 验证文件存在 if (!pdfFilePath || !fs.existsSync(pdfFilePath)) { throw new Error(`PDF文件不存在: ${pdfFilePath}`); } // 验证文件大小 const stats = fs.statSync(pdfFilePath); if (stats.size < 1024) { throw new Error(`PDF文件大小异常(${stats.size} bytes),可能文件损坏`); } const duplexMode = printOptions?.duplex; log.info( `准备打印PDF: ${pdfFilePath}, 打印机: ${targetPrinterName || "默认打印机" }, 双面打印: ${duplexMode ? "是" : "否"}` ); // 使用 pdf-to-printer 打印 const printOptions_ptp = { printer: targetPrinterName || undefined, silent: true, // 静默打印 side: duplexMode ? "duplex" : "simplex", }; await ptp.print(pdfFilePath, printOptions_ptp); log.info("PDF打印请求已发送成功"); // 清理临时文件 if (tempFilePath && fs.existsSync(tempFilePath)) { try { // 延迟删除,确保打印任务已提交 setTimeout(() => { fs.unlinkSync(tempFilePath); log.info("Temp file deleted"); }, 5000); } catch (err) { log.error("Failed to delete temp file:", err); } } return { success: true }; } catch (error) { log.error("Print error:", error); // 清理临时文件 if (tempFilePath && fs.existsSync(tempFilePath)) { try { fs.unlinkSync(tempFilePath); } catch (err) { log.error("Failed to delete temp file:", err); } } return { success: false, error: error.message || String(error) }; } }); app.whenReady().then(() => { createWindow(); createIdCardWorker(); // IPC ���� ipcMain.handle("start_idcard_listen", () => { if (idCardWorker) { idCardWorker.postMessage("start"); } else { createIdCardWorker(); idCardWorker.postMessage("start"); } return "started"; }); ipcMain.handle("stop_idcard_listen", () => { if (idCardWorker) { idCardWorker.postMessage("stop"); } return "stopped"; }); ipcMain.on("restart-app", () => { app.relaunch(); app.exit(0); }); ipcMain.on("quit-app", () => { try { app.quit(); } catch (err) { // 最后手段:强制退出 app.exit(0); } }); // 读取本地文件并以 base64 返回(用于渲染进程获取 id card 照片) ipcMain.handle("read-local-file", async (event, filePath) => { try { if (!filePath || typeof filePath !== "string") { return { success: false, error: "invalid filePath" }; } if (!fs.existsSync(filePath)) { return { success: false, error: `file not exists: ${filePath}` }; } const buffer = fs.readFileSync(filePath); const base64 = buffer.toString("base64"); const mime = "image/bmp"; return { success: true, data: base64, mime }; } catch (err) { log.error("read-local-file error:", err); return { success: false, error: err.message || String(err) }; } }); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); }); app.on("window-all-closed", () => { if (idCardWorker) { idCardWorker.terminate(); } if (process.platform !== "darwin") { app.quit(); } });