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