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, signIn, submitDaojiandanSign, submitTongyishuSign, } from "../../api/hisApi"; // 独立的 PDF 渲染组件,使用 React.memo 避免不必要的重新渲染 const PdfRenderer = React.memo<{ pdfFile: Uint8Array | null; loading: boolean; error: string; pageCount: number; onPageCountUpdate: (numPages: number) => void; loadMessage: string; documentKey: string; }>( ({ pdfFile, loading, error, pageCount, onPageCountUpdate, loadMessage, documentKey, }) => { const pagesToRender = Math.max(pageCount, 1); return ( <> {loading && (
{loadMessage}
)} {error && (
{error}
)} {!loading && !error && !pdfFile && (
暂无可展示的 PDF,等待加载完成
)} {!loading && !error && pdfFile && (
onPageCountUpdate(numPages)} > {Array.from({ length: pagesToRender }, (_, pageIndex) => ( ))}
)} ); } ); PdfRenderer.displayName = "PdfRenderer"; type PdfMeta = { pdf_url: string; pdf_name: string; combination_code: number | null; }; 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 [pdfInfoList, setPdfInfoList] = 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); const [currentStep, setCurrentStep] = useState(0); const [currentPdfPageCount, setCurrentPdfPageCount] = useState(0); const totalRequiredSignatures = pdfInfoList.length + 1; const isInstructionStep = currentStep === 0; const currentPdfIndex = isInstructionStep ? -1 : currentStep - 1; const currentPdfFile = currentPdfIndex >= 0 && currentPdfIndex < pdfFiles.length ? pdfFiles[currentPdfIndex] : null; const currentPdfMeta = currentPdfIndex >= 0 && currentPdfIndex < pdfInfoList.length ? pdfInfoList[currentPdfIndex] : null; const currentPdfName = currentPdfMeta?.pdf_name || ""; // 辅助函数:从 URL 获取 PDF 的 Uint8Array const fetchPdfBytes = async (url: string) => { try { 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 rawList = res.Data?.list_pdf_url || []; const normalizedList = rawList .map((item, idx): PdfMeta => { if (typeof item === "string") { return { pdf_url: item, pdf_name: `知情同意书${idx + 1}`, combination_code: null, }; } if (item && typeof item === "object") { const codeValue = item.combination_code != null ? Number(item.combination_code) : null; return { pdf_url: item.pdf_url || "", pdf_name: item.pdf_name || `知情同意书${idx + 1}`, combination_code: codeValue != null && !Number.isNaN(codeValue) ? codeValue : null, }; } return { pdf_url: "", pdf_name: `知情同意书${idx + 1}`, combination_code: null, }; }) .filter( (item): item is PdfMeta => typeof item.pdf_url === "string" && item.pdf_url.length > 0 ); if (!normalizedList.length) { throw new Error("未获取到任何 PDF 链接"); } setLoading(true); setError(""); setPdfFiles([]); setPdfInfoList([]); window.electronAPI?.log("info", "[UI7] 开始获取 PDF 文件"); const files: Uint8Array[] = []; for (let idx = 0; idx < normalizedList.length; idx++) { const { pdf_url: url } = normalizedList[idx]; window.electronAPI?.log( "info", `[UI7] 下载第 ${idx + 1} 份 PDF: ${url}` ); const pdfBytes = await fetchPdfBytes(url); files.push(pdfBytes); } setPdfFiles(files); setPdfInfoList(normalizedList); setLoading(false); setError(""); } catch (err) { const errorMsg = `PDF获取失败: ${err}`; console.error(errorMsg); window.electronAPI?.log("error", `[UI7] ${errorMsg}`); setError("PDF 获取失败,请检查文件"); setLoading(false); } }; fetchPdfs(); }, []); useEffect(() => { localStorage.removeItem("consentSignatureList"); }, []); useEffect(() => { if (countdown > 0) { const timer = setTimeout(() => { setCountdown(countdown - 1); }, 1000); return () => clearTimeout(timer); } else { setShowWaitButton(false); } }, [countdown]); useEffect(() => { setCurrentPdfPageCount(0); }, [currentStep, currentPdfFile]); const resetCountdown = useCallback(() => { setCountdown(5); setShowWaitButton(true); }, []); 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"); const storedRaw = localStorage.getItem("consentSignatureList"); const storedList: string[] = storedRaw ? JSON.parse(storedRaw) : []; storedList[currentStep] = dataUrl; localStorage.setItem("consentSignatureList", JSON.stringify(storedList)); } catch (cacheErr) { window.electronAPI?.log( "warn", `[UI7] 签名缓存失败: ${(cacheErr as Error).message}` ); } if (currentStep === 0) { window.electronAPI?.log( "info", `[UI7] 提交导检单签名(第 1 次),exam_id=${examId}` ); const daojiandanRes = await submitDaojiandanSign(examId, blob); if (daojiandanRes.Status !== 200) { throw new Error(daojiandanRes.Message || "提交导检单签名失败"); } localStorage.setItem("consentSignature", daojiandanRes.Data.pdf_url || ""); } else { const meta = pdfInfoList[currentStep - 1]; if (!meta || meta.combination_code == null) { throw new Error("当前知情同意书缺少组合代码,无法提交签名"); } window.electronAPI?.log( "info", `[UI7] 提交知情同意书签名,exam_id=${examId}, combination_code=${meta.combination_code}` ); const tongyishuRes = await submitTongyishuSign( examId, meta.combination_code, blob ); if (tongyishuRes.Status !== 200) { throw new Error(tongyishuRes.Message || "提交知情同意书签名失败"); } } if (currentStep >= pdfInfoList.length) { await sign(); navigate("/UI8"); return; } clearCanvas(); setCurrentStep((prev) => prev + 1); resetCountdown(); window.electronAPI?.log( "info", `[UI7] 完成第 ${currentStep + 1} 次签名,进入下一份 PDF` ); } catch (err) { const msg = (err as Error).message || "签名提交失败,请稍后重试"; window.electronAPI?.log("error", `[UI7] ${msg}`); alert(msg); } finally { setIsSubmitting(false); } }; execute(); }, [ canvasToBlob, clearCanvas, currentStep, isCanvasBlank, isSubmitting, navigate, pdfInfoList, resetCountdown, totalRequiredSignatures, ]); const handlePageCountUpdate = useCallback((numPages: number) => { setCurrentPdfPageCount(numPages); }, []); const sign = async () => { const physical_exam_id = localStorage.getItem("selectedExamId"); const package_code = localStorage.getItem("package_code"); if (!physical_exam_id || !package_code) { alert("体检ID或套餐代码不存在"); return; } const res = await signIn(Number(physical_exam_id), Number(package_code)); if (res.Status === 200) { if (res.Data.is_success === 0) { return; } else { console.log(res.Data); alert(res.Message); } } else { alert(res.Message); } }; return (
{!isInstructionStep && currentPdfName ? `${currentPdfName}` : "检测项目知情同意书确认"} {isInstructionStep && (



{/*

请仔细阅读后签字确认

*/}
)} {!isInstructionStep && (
)} 请仔细阅读后签字确认
{showWaitButton ? ( ) : ( )}
); }; export default UI7;