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 [currentPdfPageCount, setCurrentPdfPageCount] = useState(0);
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 () => {
try {
const res = await getTongyishuPdf(
Number(localStorage.getItem("selectedExamId"))
);
if (res.Status !== 200) {
alert(`获取PDF数据失败: ${res.Message}`);
setLoadMessage(res.Message || "获取PDF数据失败");
return;
}
const rawList = res.Data?.list_pdf_url || [];
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
);
if (!normalizedList.length) {
throw new Error("未获取到任何 PDF 链接");
}
setLoading(true);
setError("");
setPdfFiles([]);
setPdfInfoList([]);
window.electronAPI?.log("info", "[UI7] 开始获取 PDF 文件");
const files: Uint8Array[] = [];
for (let idx = 0; idx < normalizedList.length; idx++) {
const { pdf_url: url } = normalizedList[idx];
window.electronAPI?.log(
"info",
`[UI7] 下载第 ${idx + 1} 份 PDF: ${url}`
);
const pdfBytes = await fetchPdfBytes(url);
files.push(pdfBytes);
}
setPdfFiles(files);
setPdfInfoList(normalizedList);
setLoading(false);
setError("");
} catch (err) {
const errorMsg = `PDF获取失败: ${err}`;
console.error(errorMsg);
window.electronAPI?.log("error", `[UI7] ${errorMsg}`);
setError("PDF 获取失败,请检查文件");
setLoading(false);
}
};
fetchPdfs();
}, []);
useEffect(() => {
localStorage.removeItem("consentSignatureList");
}, []);
useEffect(() => {
if (countdown > 0) {
const timer = setTimeout(() => {
setCountdown(countdown - 1);
}, 1000);
return () => clearTimeout(timer);
} else {
setShowWaitButton(false);
}
}, [countdown]);
useEffect(() => {
setCurrentPdfPageCount(0);
}, [currentStep, currentPdfFile]);
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 };
},
[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";
}, []);
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) {
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");
const storedRaw = localStorage.getItem("consentSignatureList");
const storedList: string[] = storedRaw ? JSON.parse(storedRaw) : [];
storedList[currentStep] = dataUrl;
localStorage.setItem("consentSignatureList", JSON.stringify(storedList));
} catch (cacheErr) {
window.electronAPI?.log(
"warn",
`[UI7] 签名缓存失败: ${(cacheErr as Error).message}`
);
}
if (currentStep === 0) {
window.electronAPI?.log(
"info",
`[UI7] 提交导检单签名(第 1 次),exam_id=${examId}`
);
const daojiandanRes = await submitDaojiandanSign(examId, blob);
if (daojiandanRes.Status !== 200) {
throw new Error(daojiandanRes.Message || "提交导检单签名失败");
}
localStorage.setItem("consentSignature", daojiandanRes.Data.pdf_url || "");
} else {
const meta = pdfInfoList[currentStep - 1];
if (!meta || meta.combination_code == null) {
throw new Error("当前知情同意书缺少组合代码,无法提交签名");
}
window.electronAPI?.log(
"info",
`[UI7] 提交知情同意书签名,exam_id=${examId}, combination_code=${meta.combination_code}`
);
const tongyishuRes = await submitTongyishuSign(
examId,
meta.combination_code,
blob
);
if (tongyishuRes.Status !== 200) {
throw new Error(tongyishuRes.Message || "提交知情同意书签名失败");
}
}
if (currentStep >= pdfInfoList.length) {
await sign();
navigate("/UI8");
return;
}
clearCanvas();
setCurrentStep((prev) => prev + 1);
resetCountdown();
window.electronAPI?.log(
"info",
`[UI7] 完成第 ${currentStep + 1} 次签名,进入下一份 PDF`
);
} catch (err) {
const msg = (err as Error).message || "签名提交失败,请稍后重试";
window.electronAPI?.log("error", `[UI7] ${msg}`);
alert(msg);
} finally {
setIsSubmitting(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");
const package_code = localStorage.getItem("package_code");
if (!physical_exam_id || !package_code) {
alert("体检ID或套餐代码不存在");
return;
}
const res = await signIn(Number(physical_exam_id), Number(package_code));
if (res.Status === 200) {
if (res.Data.is_success === 0) {
return;
} else {
console.log(res.Data);
alert(res.Message);
}
} else {
alert(res.Message);
}
};
return (
{!isInstructionStep && currentPdfName ? `${currentPdfName}` : "检测项目知情同意书确认"}
{isInstructionStep && (
)}
{!isInstructionStep && (
)}
请仔细阅读后签字确认
{showWaitButton ? (
) : (
)}
);
};
export default UI7;