419 lines
12 KiB
JavaScript
419 lines
12 KiB
JavaScript
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 <20><><EFBFBD><EFBFBD>
|
||
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();
|
||
}
|
||
});
|