diff --git a/electron/main.js b/electron/main.js index a7aae9c..8016f80 100644 --- a/electron/main.js +++ b/electron/main.js @@ -147,10 +147,31 @@ ipcMain.handle("fetch-pdf", async (event, pdfUrl) => { }); }); +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) => { +ipcMain.handle("print-pdf", async (event, pdfDataOrUrl, printOptions = {}) => { let printWindow = null; let tempFilePath = null; + const targetPrinterName = printOptions?.printerName; try { let pdfPath = pdfDataOrUrl; @@ -210,14 +231,20 @@ ipcMain.handle("print-pdf", async (event, pdfDataOrUrl) => { // 静默打印(直接调用系统打印对话框) return new Promise((resolve) => { - printWindow.webContents.print( - { - silent: false, // 显示打印对话框 - printBackground: true, - margins: { - marginType: "none", - }, + const basePrintOptions = { + silent: Boolean(targetPrinterName), + printBackground: true, + margins: { + marginType: "none", }, + }; + + if (targetPrinterName) { + basePrintOptions.deviceName = targetPrinterName; + } + + printWindow.webContents.print( + basePrintOptions, (success, errorType) => { // 清理临时文件 if (tempFilePath && fs.existsSync(tempFilePath)) { diff --git a/electron/preload.js b/electron/preload.js index 5c78634..1965a88 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -4,7 +4,9 @@ contextBridge.exposeInMainWorld("electronAPI", { // 获取PDF(绕过CORS) fetchPdf: (pdfUrl) => ipcRenderer.invoke("fetch-pdf", pdfUrl), // 打印PDF - printPdf: (pdfUrl) => ipcRenderer.invoke("print-pdf", pdfUrl), + printPdf: (pdfUrl, options) => ipcRenderer.invoke("print-pdf", pdfUrl, options), + // 获取打印机列表 + getPrinters: () => ipcRenderer.invoke("get-printers"), startIdCardListen: () => ipcRenderer.invoke("start_idcard_listen"), stopIdCardListen: () => ipcRenderer.invoke("stop_idcard_listen"), onIdCardData: (callback) => diff --git a/src/electron.d.ts b/src/electron.d.ts index 1f871a3..5661719 100644 --- a/src/electron.d.ts +++ b/src/electron.d.ts @@ -1,9 +1,26 @@ // Electron API 类型声明 +interface ElectronPrinterInfo { + name: string; + displayName?: string; + description?: string; + status?: number; + isDefault?: boolean; + options?: Record; +} + interface ElectronAPI { fetchPdf: ( pdfUrl: string ) => Promise<{ success: boolean; data?: string; error?: string }>; - printPdf: (pdfUrl: string) => Promise<{ success: boolean; error?: string }>; + printPdf: ( + pdfUrl: string, + options?: { printerName?: string } + ) => Promise<{ success: boolean; error?: string }>; + getPrinters: () => Promise<{ + success: boolean; + printers?: ElectronPrinterInfo[]; + error?: string; + }>; startIdCardListen: () => Promise; stopIdCardListen: () => Promise; onIdCardData: (callback: (data: any) => void) => void; diff --git a/src/pages/UI7/UI7.css b/src/pages/UI7/UI7.css index bac3758..913f436 100644 --- a/src/pages/UI7/UI7.css +++ b/src/pages/UI7/UI7.css @@ -1,5 +1,5 @@ .ui7-text-wrapper { - height: 1080px; + height: 670px; width: 975px; border: 2px solid #000; border-radius: 30px; @@ -7,12 +7,15 @@ overflow-y: auto; overflow-x: hidden; /* 隐藏滚动条但保持滚动功能 */ - scrollbar-width: none; /* Firefox */ - -ms-overflow-style: none; /* IE 和 Edge */ + scrollbar-width: none; + /* Firefox */ + -ms-overflow-style: none; + /* IE 和 Edge */ } .ui7-text-wrapper::-webkit-scrollbar { - display: none; /* Chrome, Safari, Opera */ + display: none; + /* Chrome, Safari, Opera */ } .ui7-text-content { @@ -97,4 +100,4 @@ border: none; outline: none; background-color: transparent; -} +} \ No newline at end of file diff --git a/src/pages/UI7/UI7.tsx b/src/pages/UI7/UI7.tsx index ad80149..1d03bb1 100644 --- a/src/pages/UI7/UI7.tsx +++ b/src/pages/UI7/UI7.tsx @@ -8,7 +8,7 @@ import ConfirmButton from "../../components/ConfirmButton"; import DecorLine from "../../components/DecorLine"; import WaitButton from "../../components/WaitButton"; -import { getTongyishuPdf } from "../../api/hisApi"; +import { getTongyishuPdf, submitDaojiandanSign } from "../../api/hisApi"; // 独立的 PDF 渲染组件,使用 React.memo 避免不必要的重新渲染 const PdfRenderer = React.memo<{ @@ -22,7 +22,7 @@ const PdfRenderer = React.memo<{ return ( <> {loading && ( -
+
{loadMessage}
)} @@ -64,6 +64,9 @@ PdfRenderer.displayName = "PdfRenderer"; const UI7: React.FC = () => { const navigate = useNavigate(); const hasFetchedRef = useRef(false); + const canvasRef = useRef(null); + const dprRef = useRef(1); + const lastPointRef = useRef<{ x: number; y: number } | null>(null); const [countdown, setCountdown] = useState(5); const [showWaitButton, setShowWaitButton] = useState(true); const [pdfFiles, setPdfFiles] = useState([]); @@ -72,6 +75,8 @@ const UI7: React.FC = () => { const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [loadMessage, setLoadMessage] = useState("PDF加载中..."); + const [isDrawing, setIsDrawing] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); // 辅助函数:从 URL 获取 PDF 的 Uint8Array @@ -143,7 +148,7 @@ const UI7: React.FC = () => { const pdfBytes = await fetchPdfBytes(url); files.push(pdfBytes); } - + setPdfFiles(files); setLoading(false); setError(""); @@ -170,13 +175,205 @@ const UI7: React.FC = () => { } }, [countdown]); + useEffect(() => { + const initCanvas = () => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d", { + willReadFrequently: true, + alpha: true, + }); + if (!ctx) return; + const rect = canvas.getBoundingClientRect(); + const scale = 3; + dprRef.current = scale; + canvas.width = rect.width * scale; + canvas.height = rect.height * scale; + ctx.setTransform(scale, 0, 0, scale, 0, 0); + canvas.style.width = `${rect.width}px`; + canvas.style.height = `${rect.height}px`; + ctx.strokeStyle = "#000000"; + ctx.lineWidth = 5; + ctx.lineCap = "round"; + ctx.lineJoin = "round"; + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = "high"; + }; + + const timer = setTimeout(initCanvas, 150); + window.addEventListener("resize", initCanvas); + return () => { + clearTimeout(timer); + window.removeEventListener("resize", initCanvas); + }; + }, []); + + const getCoordinates = useCallback( + (event: React.PointerEvent) => { + const canvas = canvasRef.current; + if (!canvas) return { x: 0, y: 0 }; + const rect = canvas.getBoundingClientRect(); + return { + x: event.clientX - rect.left, + y: event.clientY - rect.top, + }; + }, + [] + ); + + const startDrawing = useCallback( + (event: React.PointerEvent) => { + if (event.cancelable) { + event.preventDefault(); + } + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + const { x, y } = getCoordinates(event); + lastPointRef.current = { x, y }; + ctx.beginPath(); + ctx.moveTo(x, y); + setIsDrawing(true); + canvas.setPointerCapture(event.pointerId); + }, + [getCoordinates] + ); + + const draw = useCallback( + (event: React.PointerEvent) => { + if (event.cancelable) { + event.preventDefault(); + } + if (!isDrawing || !lastPointRef.current) return; + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + const { x, y } = getCoordinates(event); + const lastPoint = lastPointRef.current; + const midX = (lastPoint.x + x) / 2; + const midY = (lastPoint.y + y) / 2; + ctx.quadraticCurveTo(lastPoint.x, lastPoint.y, midX, midY); + ctx.stroke(); + lastPointRef.current = { x, y }; + }, + [getCoordinates, isDrawing] + ); + + const stopDrawing = useCallback( + (event?: React.PointerEvent) => { + setIsDrawing(false); + lastPointRef.current = null; + if (event && canvasRef.current?.hasPointerCapture(event.pointerId)) { + canvasRef.current.releasePointerCapture(event.pointerId); + } + }, + [] + ); + + const clearCanvas = useCallback(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + ctx.save(); + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.restore(); + ctx.strokeStyle = "#000000"; + ctx.lineWidth = 2; + ctx.lineCap = "round"; + ctx.lineJoin = "round"; + }, []); + + const isCanvasBlank = useCallback(() => { + const canvas = canvasRef.current; + if (!canvas) return true; + const ctx = canvas.getContext("2d"); + if (!ctx) return true; + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const pixelBuffer = new Uint32Array(imageData.data.buffer); + return !pixelBuffer.some((color) => color !== 0); + }, []); + + const canvasToBlob = useCallback((canvas: HTMLCanvasElement) => { + return new Promise((resolve, reject) => { + canvas.toBlob( + (blob) => { + if (blob) { + resolve(blob); + } else { + reject(new Error("签名生成失败,请重试")); + } + }, + "image/png", + 1 + ); + }); + }, []); + const handleBack = useCallback(() => { navigate(-1); }, [navigate]); const handleConfirm = useCallback(() => { - navigate("/UI8"); - }, [navigate]); + const execute = async () => { + if (isSubmitting) { + return; + } + const canvas = canvasRef.current; + if (!canvas) { + alert("画布初始化失败,请刷新页面"); + return; + } + if (isCanvasBlank()) { + alert("请先完成签名后再继续"); + return; + } + const examIdStr = localStorage.getItem("selectedExamId"); + const examId = Number(examIdStr); + if (!examIdStr || Number.isNaN(examId)) { + alert("未找到体检ID,请返回重试"); + return; + } + + setIsSubmitting(true); + try { + const blob = await canvasToBlob(canvas); + + // 仍在本地缓存一下,方便 UI8 等页面复用 + try { + const dataUrl = canvas.toDataURL("image/png"); + localStorage.setItem("consentSignature", dataUrl); + } catch (cacheErr) { + window.electronAPI?.log( + "warn", + `[UI7] 签名缓存失败: ${(cacheErr as Error).message}` + ); + } + + window.electronAPI?.log( + "info", + `[UI7] 提交导检单签名,exam_id=${examId}` + ); + const res = await submitDaojiandanSign(examId, blob); + if (res.Status !== 200) { + throw new Error(res.Message || "提交签名失败"); + } + window.electronAPI?.log("info", "[UI7] 签名提交成功"); + navigate("/UI8"); + } catch (err) { + const msg = (err as Error).message || "签名提交失败,请稍后重试"; + window.electronAPI?.log("error", `[UI7] ${msg}`); + alert(msg); + } finally { + setIsSubmitting(false); + } + }; + + execute(); + }, [canvasToBlob, isCanvasBlank, isSubmitting, navigate]); // 稳定 PDF 页数更新回调 const handlePageCountUpdate = useCallback((index: number, numPages: number) => { @@ -206,12 +403,32 @@ const UI7: React.FC = () => {
+ 请阅读后在下方签名确认 + +
+ + +
+
{showWaitButton ? ( ) : ( - + )}
diff --git a/src/pages/UI9/UI9.tsx b/src/pages/UI9/UI9.tsx index 8667a4a..56d53f0 100644 --- a/src/pages/UI9/UI9.tsx +++ b/src/pages/UI9/UI9.tsx @@ -24,8 +24,8 @@ const UI9: React.FC = () => { navigate("/"); }, [navigate]); - const [countdown, setCountdown] = useState(10); - const [backTime, setBackTime] = useState("确认(10S)"); + const [countdown, setCountdown] = useState(30); + const [backTime, setBackTime] = useState("确认(30S)"); useEffect(() => { if (countdown > 0) {