import React, { useState, useEffect, useRef, useCallback } from "react"; import { Document, Page } from "react-pdf"; import "./UI7.css"; import "../../assets/css/basic.css"; import { useNavigate } from "react-router-dom"; import BackButton from "../../components/BackButton"; import ConfirmButton from "../../components/ConfirmButton"; import DecorLine from "../../components/DecorLine"; import WaitButton from "../../components/WaitButton"; import { getTongyishuPdf, submitDaojiandanSign } from "../../api/hisApi"; // 独立的 PDF 渲染组件,使用 React.memo 避免不必要的重新渲染 const PdfRenderer = React.memo<{ pdfFiles: Uint8Array[]; loading: boolean; error: string; pageCounts: Record; onPageCountUpdate: (index: number, numPages: number) => void; loadMessage: string; }>(({ pdfFiles, loading, error, pageCounts, onPageCountUpdate, loadMessage }) => { return ( <> {loading && (
{loadMessage}
)} {error && (
{error}
)} {!loading && !error && pdfFiles.map((fileData, index) => (
onPageCountUpdate(index, numPages)} > {Array.from( { length: pageCounts[index] || 0 }, (_, pageIndex) => ( ) )}
))} ); }); 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([]); const [pageCounts, setPageCounts] = useState>({}); 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 const fetchPdfBytes = async (url: string) => { try { if (window.electronAPI?.fetchPdf) { window.electronAPI.log("info", `[UI7] 通过 Electron 获取 PDF: ${url}`); const result = await window.electronAPI.fetchPdf(url); if (result.success && result.data) { const cleanBase64 = result.data.replace(/\s/g, ""); const binaryString = atob(cleanBase64); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes; } throw new Error(result.error || "fetchPdf 返回失败"); } window.electronAPI?.log("info", `[UI7] 通过 fetch 获取 PDF: ${url}`); const response = await fetch(url); if (!response.ok) { throw new Error(`网络请求失败: ${response.status}`); } const arrayBuffer = await response.arrayBuffer(); return new Uint8Array(arrayBuffer); } catch (error) { throw new Error(`获取 PDF 失败: ${(error as Error).message}`); } }; // 从多份 PDF 中获取数据 useEffect(() => { if (hasFetchedRef.current) { window.electronAPI?.log("info", "[UI7] 已获取过 PDF,跳过重复请求"); return; } hasFetchedRef.current = true; const fetchPdfs = async () => { try { const res = await getTongyishuPdf( Number(localStorage.getItem("selectedExamId")) ); if (res.Status !== 200) { alert(`获取PDF数据失败: ${res.Message}`); setLoadMessage(res.Message || "获取PDF数据失败"); return; } const pdfUrls: string[] = res.Data?.list_pdf_url || []; if (!pdfUrls.length) { throw new Error("未获取到任何 PDF 链接"); } setLoading(true); setError(""); setPdfFiles([]); window.electronAPI?.log("info", "[UI7] 开始获取 PDF 文件"); const files: Uint8Array[] = []; for (let idx = 0; idx < pdfUrls.length; idx++) { const url = pdfUrls[idx]; window.electronAPI?.log( "info", `[UI7] 下载第 ${idx + 1} 份 PDF: ${url}` ); const pdfBytes = await fetchPdfBytes(url); files.push(pdfBytes); } setPdfFiles(files); setLoading(false); setError(""); } catch (err) { const errorMsg = `PDF获取失败: ${err}`; console.error(errorMsg); window.electronAPI?.log("error", `[UI7] ${errorMsg}`); setError("PDF 获取失败,请检查文件"); setLoading(false); } }; fetchPdfs(); }, []); useEffect(() => { if (countdown > 0) { const timer = setTimeout(() => { setCountdown(countdown - 1); }, 1000); return () => clearTimeout(timer); } else { setShowWaitButton(false); } }, [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 = 10; 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 = 10; 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(() => { 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] 签名提交成功"); localStorage.setItem("consentSignature", res.Data.pdf_url || ""); 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) => { setPageCounts((prev) => { if (prev[index] === numPages) return prev; return { ...prev, [index]: numPages }; }); }, []); return (
体检知情同意书确认
请阅读后在下方签名确认
{showWaitButton ? ( ) : ( )}
); }; export default UI7;