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 [hasSignature, setHasSignature] = useState(false); const [currentPdfPageCount, setCurrentPdfPageCount] = useState(0); const titleRef = useRef(null); const [isTitleCompact, setIsTitleCompact] = useState(false); 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 () => { const examId = Number(localStorage.getItem("selectedExamId")); window.electronAPI?.log("info", `[UI7] 开始获取知情同意书PDF列表,exam_id=${examId}`); try { const res = await getTongyishuPdf(examId); window.electronAPI?.log("info", `[UI7] 获取PDF列表响应: Status=${res.Status}, Message=${res.Message || "null"}`); if (res.Status !== 200) { window.electronAPI?.log("error", `[UI7] 获取PDF数据失败: ${res.Message}`); setLoadMessage(res.Message || "获取PDF数据失败"); return; } const rawList = res.Data?.list_pdf_url || []; window.electronAPI?.log("info", `[UI7] 原始PDF列表数量: ${rawList.length}`); 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 ); window.electronAPI?.log("info", `[UI7] 规范化后PDF列表数量: ${normalizedList.length}`); if (normalizedList.length === 0) { window.electronAPI?.log( "info", `[UI7] 没有获取到知情同意书,将只进行导检单签名` ); setPdfFiles([]); setPdfInfoList([]); setLoading(false); setError(""); window.electronAPI?.log( "info", `[UI7] PDF 获取完成,共 0 份知情同意书,总签名步骤: 1 (仅导检单)` ); return; } normalizedList.forEach((item, idx) => { window.electronAPI?.log( "info", `[UI7] PDF[${idx}]: name="${item.pdf_name}", combination_code=${item.combination_code || "null"}, url=${item.pdf_url.substring(0, 80)}...` ); }); setLoading(true); setError(""); setPdfFiles([]); setPdfInfoList([]); window.electronAPI?.log("info", `[UI7] 开始下载 ${normalizedList.length} 份 PDF 文件`); const files: Uint8Array[] = []; for (let idx = 0; idx < normalizedList.length; idx++) { const { pdf_url: url, pdf_name: name } = normalizedList[idx]; window.electronAPI?.log( "info", `[UI7] 下载第 ${idx + 1}/${normalizedList.length} 份 PDF: ${name}` ); const startTime = Date.now(); const pdfBytes = await fetchPdfBytes(url); const duration = Date.now() - startTime; files.push(pdfBytes); window.electronAPI?.log( "info", `[UI7] 第 ${idx + 1} 份 PDF 下载完成,大小: ${pdfBytes.length} bytes,耗时: ${duration}ms` ); } setPdfFiles(files); setPdfInfoList(normalizedList); setLoading(false); setError(""); window.electronAPI?.log( "info", `[UI7] PDF 获取完成,共 ${normalizedList.length} 份,总签名步骤: ${normalizedList.length + 1} (导检单 + ${normalizedList.length} 份知情同意书)` ); } catch (err) { const errorMsg = `PDF获取失败: ${err}`; console.error(errorMsg); window.electronAPI?.log("error", `[UI7] ${errorMsg}`); setError("PDF 获取失败,请检查文件"); setLoading(false); } }; fetchPdfs(); }, []); useEffect(() => { window.electronAPI?.log("info", "[UI7] 初始化:清理旧的签名缓存数据"); localStorage.removeItem("consentSignatureList"); localStorage.removeItem("tongyishuSignedPdfUrls"); window.electronAPI?.log("info", "[UI7] 初始化完成:签名缓存已清理"); }, []); useEffect(() => { if (countdown > 0) { const timer = setTimeout(() => { setCountdown(countdown - 1); }, 1000); return () => clearTimeout(timer); } else { setShowWaitButton(false); } }, [countdown]); useEffect(() => { setCurrentPdfPageCount(0); if (currentStep > 0) { const meta = pdfInfoList[currentStep - 1]; window.electronAPI?.log( "info", `[UI7] 步骤切换: currentStep=${currentStep}, 当前PDF: ${meta?.pdf_name || "未知"}` ); } else { window.electronAPI?.log( "info", `[UI7] 步骤切换: currentStep=${currentStep} (导检单说明页)` ); } }, [currentStep, currentPdfFile, pdfInfoList]); useEffect(() => { setHasSignature(false); window.electronAPI?.log( "info", `[UI7] 步骤 ${currentStep} 签名状态已重置` ); }, [currentStep]); // 检测标题文本是否超过一行,如果超过则应用缩小样式 useEffect(() => { const checkTitleOverflow = () => { if (!titleRef.current) return; const element = titleRef.current; // 先移除 compact 类,以正常大小检测 element.classList.remove('compact'); // 使用 requestAnimationFrame 确保 DOM 已更新 requestAnimationFrame(() => { if (!element) return; // 获取实际内容高度 const scrollHeight = element.scrollHeight; // 单行高度为 91px(line-height) const singleLineHeight = 91; // 如果内容高度超过单行高度,说明换行了 const needsCompact = scrollHeight > singleLineHeight; setIsTitleCompact(needsCompact); // 如果需要 compact,添加类名 if (needsCompact) { element.classList.add('compact'); } }); }; // 延迟执行,确保 DOM 已渲染 const timer = setTimeout(() => { checkTitleOverflow(); }, 100); // 监听窗口大小变化 window.addEventListener('resize', checkTitleOverflow); return () => { clearTimeout(timer); window.removeEventListener('resize', checkTitleOverflow); }; }, [currentStep, currentPdfName, isInstructionStep]); 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 }; setHasSignature(true); }, [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"; setHasSignature(false); }, []); 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) { window.electronAPI?.log("warn", "[UI7] 检测到重复提交,已忽略"); return; } const canvas = canvasRef.current; if (!canvas) { window.electronAPI?.log("error", "[UI7] 画布初始化失败"); alert("画布初始化失败,请刷新页面"); return; } if (isCanvasBlank()) { window.electronAPI?.log("warn", "[UI7] 画布为空,无法提交"); alert("请先完成签名后再继续"); return; } const examIdStr = localStorage.getItem("selectedExamId"); const examId = Number(examIdStr); if (!examIdStr || Number.isNaN(examId)) { window.electronAPI?.log("error", `[UI7] 体检ID无效: ${examIdStr}`); alert("未找到体检ID,请返回重试"); return; } const totalSteps = pdfInfoList.length + 1; const currentStepNum = currentStep + 1; window.electronAPI?.log( "info", `[UI7] ========== 开始提交签名 ==========` ); window.electronAPI?.log( "info", `[UI7] 当前步骤: ${currentStepNum}/${totalSteps}, currentStep=${currentStep}, pdfInfoList.length=${pdfInfoList.length}` ); setIsSubmitting(true); try { const blob = await canvasToBlob(canvas); window.electronAPI?.log( "info", `[UI7] 签名图片生成成功,大小: ${blob.size} bytes` ); // 本地缓存成列表,方便 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)); window.electronAPI?.log( "info", `[UI7] 签名已缓存到 localStorage[${currentStep}]` ); } catch (cacheErr) { window.electronAPI?.log( "warn", `[UI7] 签名缓存失败: ${(cacheErr as Error).message}` ); } if (currentStep === 0) { window.electronAPI?.log( "info", `[UI7] >>> 提交导检单签名(步骤 ${currentStepNum}/${totalSteps}),exam_id=${examId}` ); const startTime = Date.now(); const daojiandanRes = await submitDaojiandanSign(examId, blob); const duration = Date.now() - startTime; window.electronAPI?.log( "info", `[UI7] 导检单签名响应: Status=${daojiandanRes.Status}, Message=${daojiandanRes.Message || "null"}, 耗时: ${duration}ms` ); if (daojiandanRes.Status !== 200) { throw new Error(daojiandanRes.Message || "提交导检单签名失败"); } const pdfUrl = daojiandanRes.Data.pdf_url || ""; localStorage.setItem("consentSignature", pdfUrl); window.electronAPI?.log( "info", `[UI7] 导检单签名成功,PDF URL已保存: ${pdfUrl.substring(0, 80)}...` ); // 如果没有知情同意书,导检单签名完成后直接跳转 if (pdfInfoList.length === 0) { window.electronAPI?.log( "info", `[UI7] 没有知情同意书需要签名,导检单签名完成后直接跳转` ); window.electronAPI?.log( "info", `[UI7] ✓ 所有签名已完成!准备跳转到UI8(仅导检单)` ); sign(); navigate("/UI8"); return; } } else { const meta = pdfInfoList[currentStep - 1]; if (!meta) { window.electronAPI?.log( "error", `[UI7] 无法找到当前PDF元信息,currentStep=${currentStep}, pdfInfoList.length=${pdfInfoList.length}` ); throw new Error("当前PDF信息缺失,无法提交签名"); } if (meta.combination_code == null) { window.electronAPI?.log( "error", `[UI7] 当前知情同意书缺少组合代码: pdf_name="${meta.pdf_name}", pdf_url=${meta.pdf_url.substring(0, 80)}...` ); throw new Error("当前知情同意书缺少组合代码,无法提交签名"); } window.electronAPI?.log( "info", `[UI7] >>> 提交知情同意书签名(步骤 ${currentStepNum}/${totalSteps})` ); window.electronAPI?.log( "info", `[UI7] PDF信息: name="${meta.pdf_name}", combination_code=${meta.combination_code}, exam_id=${examId}` ); const startTime = Date.now(); const tongyishuRes = await submitTongyishuSign( examId, meta.combination_code, blob ); const duration = Date.now() - startTime; window.electronAPI?.log( "info", `[UI7] 知情同意书签名响应: Status=${tongyishuRes.Status}, Message=${tongyishuRes.Message || "null"}, 耗时: ${duration}ms` ); if (tongyishuRes.Status !== 200) { throw new Error(tongyishuRes.Message || "提交知情同意书签名失败"); } // 保存签名后的知情同意书 PDF URL(可能返回多个) try { const returnedList = Array.isArray( tongyishuRes.Data?.list_pdf_url ) ? tongyishuRes.Data.list_pdf_url : []; window.electronAPI?.log( "info", `[UI7] 返回的PDF URL列表数量: ${returnedList.length}` ); const urls = returnedList .map((item) => typeof item === "string" ? item : item?.pdf_url || "" ) .filter((url): url is string => Boolean(url)); urls.forEach((url, idx) => { window.electronAPI?.log( "info", `[UI7] 返回的PDF URL[${idx}]: ${url.substring(0, 80)}...` ); }); const storedRaw = localStorage.getItem("tongyishuSignedPdfUrls"); const storedList: string[] = storedRaw ? JSON.parse(storedRaw) : []; window.electronAPI?.log( "info", `[UI7] 原有缓存的PDF URL数量: ${storedList.length}` ); const merged = [...storedList]; let addedCount = 0; urls.forEach((url) => { if (!merged.includes(url)) { merged.push(url); addedCount++; } }); localStorage.setItem( "tongyishuSignedPdfUrls", JSON.stringify(merged) ); window.electronAPI?.log( "info", `[UI7] 知情同意书PDF URL已保存,新增: ${addedCount}, 总计: ${merged.length}` ); } catch (cacheErr) { window.electronAPI?.log( "warn", `[UI7] 知情同意书 PDF URL 缓存失败: ${(cacheErr as Error).message}` ); } } window.electronAPI?.log( "info", `[UI7] 检查是否完成所有签名: currentStep=${currentStep}, pdfInfoList.length=${pdfInfoList.length}, 条件: currentStep > 0 && currentStep >= pdfInfoList.length` ); if (currentStep > 0 && currentStep >= pdfInfoList.length) { window.electronAPI?.log( "info", `[UI7] ✓ 所有签名已完成!准备跳转到UI8` ); window.electronAPI?.log( "info", `[UI7] 已签名: 导检单(1份) + 知情同意书(${pdfInfoList.length}份) = 共${totalSteps}份` ); sign(); navigate("/UI8"); return; } const nextStep = currentStep + 1; window.electronAPI?.log( "info", `[UI7] 继续下一步: 从步骤 ${currentStepNum} 进入步骤 ${nextStep + 1}/${totalSteps}` ); clearCanvas(); setCurrentStep((prev) => prev + 1); resetCountdown(); window.electronAPI?.log( "info", `[UI7] ========== 步骤 ${currentStepNum} 完成,进入步骤 ${nextStep + 1} ==========` ); } catch (err) { const msg = (err as Error).message || "签名提交失败,请稍后重试"; window.electronAPI?.log("error", `[UI7] 签名提交失败: ${msg}`); window.electronAPI?.log("error", `[UI7] 错误堆栈: ${(err as Error).stack || "无"}`); alert(msg); } finally { setIsSubmitting(false); window.electronAPI?.log("info", `[UI7] 提交流程结束,isSubmitting=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"); if (!physical_exam_id) { window.electronAPI?.log("error", "[UI7] 签到失败:体检ID不存在"); alert("体检ID不存在"); return; } window.electronAPI?.log( "info", `[UI7] 开始执行签到,exam_id=${physical_exam_id}` ); const startTime = Date.now(); const res = await signIn(Number(physical_exam_id)); const duration = Date.now() - startTime; window.electronAPI?.log( "info", `[UI7] 签到响应: Status=${res.Status}, Message=${res.Message || "null"}, is_success=${res.Data?.is_success}, 耗时: ${duration}ms` ); if (res.Status === 200) { if (res.Data.is_success === 0) { window.electronAPI?.log("info", "[UI7] 签到成功"); return; } else { window.electronAPI?.log("warn", `[UI7] 签到返回异常: ${res.Message}`); console.log(res.Data); alert(res.Message); } } else { window.electronAPI?.log("error", `[UI7] 签到失败: ${res.Message}`); alert(res.Message); } }; return (
{!isInstructionStep && currentPdfName ? `${currentPdfName}` : "检测项目知情同意书确认"} {isInstructionStep && (



{/*

请仔细阅读后签字确认

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