Files
yuanhe-checkin-electron/electron/main.js

419 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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可能文件损坏`);
}
log.info(
`准备打印PDF: ${pdfFilePath}, 打印机: ${
targetPrinterName || "默认打印机"
}`
);
// 使用 pdf-to-printer 打印
const printOptions_ptp = {
printer: targetPrinterName || undefined,
silent: true, // 静默打印
};
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();
}
});