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 ? ( ) : ( - + )}