Files
yuanhe-checkin-electron/src/pages/UI7/UI7.tsx
2025-11-27 17:36:37 +08:00

442 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<number, number>;
onPageCountUpdate: (index: number, numPages: number) => void;
loadMessage: string;
}>(({ pdfFiles, loading, error, pageCounts, onPageCountUpdate, loadMessage }) => {
return (
<>
{loading && (
<div style={{ padding: "20px", textAlign: "center", fontSize: "32px" }}>
<span style={{ fontSize: "32px" }}>{loadMessage}</span>
</div>
)}
{error && (
<div style={{ padding: "20px", color: "red", textAlign: "center" }}>
{error}
</div>
)}
{!loading &&
!error &&
pdfFiles.map((fileData, index) => (
<div key={index} style={{ marginBottom: "20px" }}>
<Document
file={{ data: fileData }}
loading=""
onLoadSuccess={({ numPages }) => onPageCountUpdate(index, numPages)}
>
{Array.from(
{ length: pageCounts[index] || 0 },
(_, pageIndex) => (
<Page
key={`pdf-${index}-page-${pageIndex + 1}`}
pageNumber={pageIndex + 1}
renderTextLayer={false}
renderAnnotationLayer={false}
width={900}
/>
)
)}
</Document>
</div>
))}
</>
);
});
PdfRenderer.displayName = "PdfRenderer";
const UI7: React.FC = () => {
const navigate = useNavigate();
const hasFetchedRef = useRef(false);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const dprRef = useRef<number>(1);
const lastPointRef = useRef<{ x: number; y: number } | null>(null);
const [countdown, setCountdown] = useState(5);
const [showWaitButton, setShowWaitButton] = useState(true);
const [pdfFiles, setPdfFiles] = useState<Uint8Array[]>([]);
const [pageCounts, setPageCounts] =
useState<Record<number, number>>({});
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
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<HTMLCanvasElement>) => {
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<HTMLCanvasElement>) => {
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<HTMLCanvasElement>) => {
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<HTMLCanvasElement>) => {
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<Blob>((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 (
<div className="basic-root">
<div className="basic-white-block">
<div className="basic-content">
<span className="basic-title"></span>
<DecorLine />
<div className="ui7-text-wrapper">
<div className="ui7-text-content">
<PdfRenderer
pdfFiles={pdfFiles}
loading={loading}
error={error}
pageCounts={pageCounts}
onPageCountUpdate={handlePageCountUpdate}
loadMessage={loadMessage}
/>
</div>
</div>
<span className="ui7-text_4"></span>
<div className="ui7-signature-wrapper">
<canvas
ref={canvasRef}
className="ui7-signature-canvas"
onPointerDown={startDrawing}
onPointerMove={draw}
onPointerUp={stopDrawing}
onPointerLeave={stopDrawing}
onPointerCancel={stopDrawing}
/>
<button className="ui7-clear-button" onClick={clearCanvas}>
</button>
</div>
<div className="basic-confirm-section">
<BackButton text="返回" onClick={handleBack} />
{showWaitButton ? (
<WaitButton text={`等待(${countdown})S`} />
) : (
<ConfirmButton
text={isSubmitting ? "提交中..." : "确定"}
onClick={handleConfirm}
/>
)}
</div>
</div>
</div>
</div>
);
};
export default UI7;