加回签名

This commit is contained in:
xianyi
2025-11-27 15:22:58 +08:00
parent 08ea84b690
commit b192e1bae9

View File

@@ -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 && (
<div style={{ padding: "20px", textAlign: "center" , fontSize: "32px" }}>
<div style={{ padding: "20px", textAlign: "center", fontSize: "32px" }}>
<span style={{ fontSize: "32px" }}>{loadMessage}</span>
</div>
)}
@@ -64,6 +64,9 @@ 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[]>([]);
@@ -72,6 +75,8 @@ const UI7: React.FC = () => {
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
@@ -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<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 = 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<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(() => {
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 = () => {
</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="确定" onClick={handleConfirm} />
<ConfirmButton
text={isSubmitting ? "提交中..." : "确定"}
onClick={handleConfirm}
/>
)}
</div>
</div>