878 lines
28 KiB
TypeScript
878 lines
28 KiB
TypeScript
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 && (
|
||
<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 && !pdfFile && (
|
||
<div style={{ padding: "20px", textAlign: "center", fontSize: "28px" }}>
|
||
暂无可展示的 PDF,等待加载完成
|
||
</div>
|
||
)}
|
||
{!loading && !error && pdfFile && (
|
||
<div style={{ marginBottom: "20px" }}>
|
||
<Document
|
||
key={documentKey}
|
||
file={{ data: pdfFile }}
|
||
loading=""
|
||
onLoadSuccess={({ numPages }) => onPageCountUpdate(numPages)}
|
||
>
|
||
{Array.from({ length: pagesToRender }, (_, pageIndex) => (
|
||
<Page
|
||
key={`pdf-page-${pageIndex + 1}`}
|
||
pageNumber={pageIndex + 1}
|
||
renderTextLayer={false}
|
||
renderAnnotationLayer={false}
|
||
width={900}
|
||
/>
|
||
))}
|
||
</Document>
|
||
</div>
|
||
)}
|
||
</>
|
||
);
|
||
}
|
||
);
|
||
|
||
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<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 [pdfInfoList, setPdfInfoList] = useState<PdfMeta[]>([]);
|
||
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);
|
||
const [currentStep, setCurrentStep] = useState(0);
|
||
const [hasSignature, setHasSignature] = useState(false);
|
||
const [currentPdfPageCount, setCurrentPdfPageCount] = useState(0);
|
||
const titleRef = useRef<HTMLSpanElement | null>(null);
|
||
const [isTitleCompact, setIsTitleCompact] = useState(false);
|
||
|
||
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 () => {
|
||
const examId = Number(localStorage.getItem("selectedExamId"));
|
||
window.electronAPI?.log("info", `[UI7] 开始获取知情同意书PDF列表,exam_id=${examId}`);
|
||
|
||
try {
|
||
const res = await getTongyishuPdf(examId);
|
||
window.electronAPI?.log("info", `[UI7] 获取PDF列表响应: Status=${res.Status}, Message=${res.Message || "null"}`);
|
||
|
||
if (res.Status !== 200) {
|
||
window.electronAPI?.log("error", `[UI7] 获取PDF数据失败: ${res.Message}`);
|
||
setLoadMessage(res.Message || "获取PDF数据失败");
|
||
return;
|
||
}
|
||
|
||
const rawList = res.Data?.list_pdf_url || [];
|
||
window.electronAPI?.log("info", `[UI7] 原始PDF列表数量: ${rawList.length}`);
|
||
|
||
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
|
||
);
|
||
|
||
window.electronAPI?.log("info", `[UI7] 规范化后PDF列表数量: ${normalizedList.length}`);
|
||
|
||
if (normalizedList.length === 0) {
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] 没有获取到知情同意书,将只进行导检单签名`
|
||
);
|
||
setPdfFiles([]);
|
||
setPdfInfoList([]);
|
||
setLoading(false);
|
||
setError("");
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] PDF 获取完成,共 0 份知情同意书,总签名步骤: 1 (仅导检单)`
|
||
);
|
||
return;
|
||
}
|
||
|
||
normalizedList.forEach((item, idx) => {
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] PDF[${idx}]: name="${item.pdf_name}", combination_code=${item.combination_code || "null"}, url=${item.pdf_url.substring(0, 80)}...`
|
||
);
|
||
});
|
||
|
||
setLoading(true);
|
||
setError("");
|
||
setPdfFiles([]);
|
||
setPdfInfoList([]);
|
||
window.electronAPI?.log("info", `[UI7] 开始下载 ${normalizedList.length} 份 PDF 文件`);
|
||
|
||
const files: Uint8Array[] = [];
|
||
for (let idx = 0; idx < normalizedList.length; idx++) {
|
||
const { pdf_url: url, pdf_name: name } = normalizedList[idx];
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] 下载第 ${idx + 1}/${normalizedList.length} 份 PDF: ${name}`
|
||
);
|
||
const startTime = Date.now();
|
||
const pdfBytes = await fetchPdfBytes(url);
|
||
const duration = Date.now() - startTime;
|
||
files.push(pdfBytes);
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] 第 ${idx + 1} 份 PDF 下载完成,大小: ${pdfBytes.length} bytes,耗时: ${duration}ms`
|
||
);
|
||
}
|
||
|
||
setPdfFiles(files);
|
||
setPdfInfoList(normalizedList);
|
||
setLoading(false);
|
||
setError("");
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] PDF 获取完成,共 ${normalizedList.length} 份,总签名步骤: ${normalizedList.length + 1} (导检单 + ${normalizedList.length} 份知情同意书)`
|
||
);
|
||
} catch (err) {
|
||
const errorMsg = `PDF获取失败: ${err}`;
|
||
console.error(errorMsg);
|
||
window.electronAPI?.log("error", `[UI7] ${errorMsg}`);
|
||
setError("PDF 获取失败,请检查文件");
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
fetchPdfs();
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
window.electronAPI?.log("info", "[UI7] 初始化:清理旧的签名缓存数据");
|
||
localStorage.removeItem("consentSignatureList");
|
||
localStorage.removeItem("tongyishuSignedPdfUrls");
|
||
window.electronAPI?.log("info", "[UI7] 初始化完成:签名缓存已清理");
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (countdown > 0) {
|
||
const timer = setTimeout(() => {
|
||
setCountdown(countdown - 1);
|
||
}, 1000);
|
||
return () => clearTimeout(timer);
|
||
} else {
|
||
setShowWaitButton(false);
|
||
}
|
||
}, [countdown]);
|
||
|
||
useEffect(() => {
|
||
setCurrentPdfPageCount(0);
|
||
if (currentStep > 0) {
|
||
const meta = pdfInfoList[currentStep - 1];
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] 步骤切换: currentStep=${currentStep}, 当前PDF: ${meta?.pdf_name || "未知"}`
|
||
);
|
||
} else {
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] 步骤切换: currentStep=${currentStep} (导检单说明页)`
|
||
);
|
||
}
|
||
}, [currentStep, currentPdfFile, pdfInfoList]);
|
||
|
||
useEffect(() => {
|
||
setHasSignature(false);
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] 步骤 ${currentStep} 签名状态已重置`
|
||
);
|
||
}, [currentStep]);
|
||
|
||
// 检测标题文本是否超过一行,如果超过则应用缩小样式
|
||
useEffect(() => {
|
||
const checkTitleOverflow = () => {
|
||
if (!titleRef.current) return;
|
||
|
||
const element = titleRef.current;
|
||
|
||
// 先移除 compact 类,以正常大小检测
|
||
element.classList.remove('compact');
|
||
|
||
// 使用 requestAnimationFrame 确保 DOM 已更新
|
||
requestAnimationFrame(() => {
|
||
if (!element) return;
|
||
|
||
// 获取实际内容高度
|
||
const scrollHeight = element.scrollHeight;
|
||
// 单行高度为 91px(line-height)
|
||
const singleLineHeight = 91;
|
||
|
||
// 如果内容高度超过单行高度,说明换行了
|
||
const needsCompact = scrollHeight > singleLineHeight;
|
||
setIsTitleCompact(needsCompact);
|
||
|
||
// 如果需要 compact,添加类名
|
||
if (needsCompact) {
|
||
element.classList.add('compact');
|
||
}
|
||
});
|
||
};
|
||
|
||
// 延迟执行,确保 DOM 已渲染
|
||
const timer = setTimeout(() => {
|
||
checkTitleOverflow();
|
||
}, 100);
|
||
|
||
// 监听窗口大小变化
|
||
window.addEventListener('resize', checkTitleOverflow);
|
||
|
||
return () => {
|
||
clearTimeout(timer);
|
||
window.removeEventListener('resize', checkTitleOverflow);
|
||
};
|
||
}, [currentStep, currentPdfName, isInstructionStep]);
|
||
|
||
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<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 };
|
||
setHasSignature(true);
|
||
},
|
||
[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";
|
||
setHasSignature(false);
|
||
}, []);
|
||
|
||
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) {
|
||
window.electronAPI?.log("warn", "[UI7] 检测到重复提交,已忽略");
|
||
return;
|
||
}
|
||
const canvas = canvasRef.current;
|
||
if (!canvas) {
|
||
window.electronAPI?.log("error", "[UI7] 画布初始化失败");
|
||
alert("画布初始化失败,请刷新页面");
|
||
return;
|
||
}
|
||
if (isCanvasBlank()) {
|
||
window.electronAPI?.log("warn", "[UI7] 画布为空,无法提交");
|
||
alert("请先完成签名后再继续");
|
||
return;
|
||
}
|
||
const examIdStr = localStorage.getItem("selectedExamId");
|
||
const examId = Number(examIdStr);
|
||
if (!examIdStr || Number.isNaN(examId)) {
|
||
window.electronAPI?.log("error", `[UI7] 体检ID无效: ${examIdStr}`);
|
||
alert("未找到体检ID,请返回重试");
|
||
return;
|
||
}
|
||
|
||
const totalSteps = pdfInfoList.length + 1;
|
||
const currentStepNum = currentStep + 1;
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] ========== 开始提交签名 ==========`
|
||
);
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] 当前步骤: ${currentStepNum}/${totalSteps}, currentStep=${currentStep}, pdfInfoList.length=${pdfInfoList.length}`
|
||
);
|
||
|
||
setIsSubmitting(true);
|
||
try {
|
||
const blob = await canvasToBlob(canvas);
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] 签名图片生成成功,大小: ${blob.size} bytes`
|
||
);
|
||
|
||
// 本地缓存成列表,方便 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));
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] 签名已缓存到 localStorage[${currentStep}]`
|
||
);
|
||
} catch (cacheErr) {
|
||
window.electronAPI?.log(
|
||
"warn",
|
||
`[UI7] 签名缓存失败: ${(cacheErr as Error).message}`
|
||
);
|
||
}
|
||
|
||
if (currentStep === 0) {
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] >>> 提交导检单签名(步骤 ${currentStepNum}/${totalSteps}),exam_id=${examId}`
|
||
);
|
||
const startTime = Date.now();
|
||
const daojiandanRes = await submitDaojiandanSign(examId, blob);
|
||
const duration = Date.now() - startTime;
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] 导检单签名响应: Status=${daojiandanRes.Status}, Message=${daojiandanRes.Message || "null"}, 耗时: ${duration}ms`
|
||
);
|
||
|
||
if (daojiandanRes.Status !== 200) {
|
||
throw new Error(daojiandanRes.Message || "提交导检单签名失败");
|
||
}
|
||
|
||
const pdfUrl = daojiandanRes.Data.pdf_url || "";
|
||
localStorage.setItem("consentSignature", pdfUrl);
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] 导检单签名成功,PDF URL已保存: ${pdfUrl.substring(0, 80)}...`
|
||
);
|
||
|
||
// 如果没有知情同意书,导检单签名完成后直接跳转
|
||
if (pdfInfoList.length === 0) {
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] 没有知情同意书需要签名,导检单签名完成后直接跳转`
|
||
);
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] ✓ 所有签名已完成!准备跳转到UI8(仅导检单)`
|
||
);
|
||
sign();
|
||
navigate("/UI8");
|
||
return;
|
||
}
|
||
} else {
|
||
const meta = pdfInfoList[currentStep - 1];
|
||
if (!meta) {
|
||
window.electronAPI?.log(
|
||
"error",
|
||
`[UI7] 无法找到当前PDF元信息,currentStep=${currentStep}, pdfInfoList.length=${pdfInfoList.length}`
|
||
);
|
||
throw new Error("当前PDF信息缺失,无法提交签名");
|
||
}
|
||
if (meta.combination_code == null) {
|
||
window.electronAPI?.log(
|
||
"error",
|
||
`[UI7] 当前知情同意书缺少组合代码: pdf_name="${meta.pdf_name}", pdf_url=${meta.pdf_url.substring(0, 80)}...`
|
||
);
|
||
throw new Error("当前知情同意书缺少组合代码,无法提交签名");
|
||
}
|
||
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] >>> 提交知情同意书签名(步骤 ${currentStepNum}/${totalSteps})`
|
||
);
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] PDF信息: name="${meta.pdf_name}", combination_code=${meta.combination_code}, exam_id=${examId}`
|
||
);
|
||
|
||
const startTime = Date.now();
|
||
const tongyishuRes = await submitTongyishuSign(
|
||
examId,
|
||
meta.combination_code,
|
||
blob
|
||
);
|
||
const duration = Date.now() - startTime;
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] 知情同意书签名响应: Status=${tongyishuRes.Status}, Message=${tongyishuRes.Message || "null"}, 耗时: ${duration}ms`
|
||
);
|
||
|
||
if (tongyishuRes.Status !== 200) {
|
||
throw new Error(tongyishuRes.Message || "提交知情同意书签名失败");
|
||
}
|
||
|
||
// 保存签名后的知情同意书 PDF URL(可能返回多个)
|
||
try {
|
||
const returnedList = Array.isArray(
|
||
tongyishuRes.Data?.list_pdf_url
|
||
)
|
||
? tongyishuRes.Data.list_pdf_url
|
||
: [];
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] 返回的PDF URL列表数量: ${returnedList.length}`
|
||
);
|
||
|
||
const urls = returnedList
|
||
.map((item) =>
|
||
typeof item === "string" ? item : item?.pdf_url || ""
|
||
)
|
||
.filter((url): url is string => Boolean(url));
|
||
|
||
urls.forEach((url, idx) => {
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] 返回的PDF URL[${idx}]: ${url.substring(0, 80)}...`
|
||
);
|
||
});
|
||
|
||
const storedRaw = localStorage.getItem("tongyishuSignedPdfUrls");
|
||
const storedList: string[] = storedRaw ? JSON.parse(storedRaw) : [];
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] 原有缓存的PDF URL数量: ${storedList.length}`
|
||
);
|
||
|
||
const merged = [...storedList];
|
||
let addedCount = 0;
|
||
urls.forEach((url) => {
|
||
if (!merged.includes(url)) {
|
||
merged.push(url);
|
||
addedCount++;
|
||
}
|
||
});
|
||
|
||
localStorage.setItem(
|
||
"tongyishuSignedPdfUrls",
|
||
JSON.stringify(merged)
|
||
);
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] 知情同意书PDF URL已保存,新增: ${addedCount}, 总计: ${merged.length}`
|
||
);
|
||
} catch (cacheErr) {
|
||
window.electronAPI?.log(
|
||
"warn",
|
||
`[UI7] 知情同意书 PDF URL 缓存失败: ${(cacheErr as Error).message}`
|
||
);
|
||
}
|
||
}
|
||
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] 检查是否完成所有签名: currentStep=${currentStep}, pdfInfoList.length=${pdfInfoList.length}, 条件: currentStep > 0 && currentStep >= pdfInfoList.length`
|
||
);
|
||
|
||
if (currentStep > 0 && currentStep >= pdfInfoList.length) {
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] ✓ 所有签名已完成!准备跳转到UI8`
|
||
);
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] 已签名: 导检单(1份) + 知情同意书(${pdfInfoList.length}份) = 共${totalSteps}份`
|
||
);
|
||
sign();
|
||
navigate("/UI8");
|
||
return;
|
||
}
|
||
|
||
const nextStep = currentStep + 1;
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] 继续下一步: 从步骤 ${currentStepNum} 进入步骤 ${nextStep + 1}/${totalSteps}`
|
||
);
|
||
|
||
clearCanvas();
|
||
setCurrentStep((prev) => prev + 1);
|
||
resetCountdown();
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] ========== 步骤 ${currentStepNum} 完成,进入步骤 ${nextStep + 1} ==========`
|
||
);
|
||
} catch (err) {
|
||
const msg = (err as Error).message || "签名提交失败,请稍后重试";
|
||
window.electronAPI?.log("error", `[UI7] 签名提交失败: ${msg}`);
|
||
window.electronAPI?.log("error", `[UI7] 错误堆栈: ${(err as Error).stack || "无"}`);
|
||
alert(msg);
|
||
} finally {
|
||
setIsSubmitting(false);
|
||
window.electronAPI?.log("info", `[UI7] 提交流程结束,isSubmitting=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");
|
||
if (!physical_exam_id) {
|
||
window.electronAPI?.log("error", "[UI7] 签到失败:体检ID不存在");
|
||
alert("体检ID不存在");
|
||
return;
|
||
}
|
||
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] 开始执行签到,exam_id=${physical_exam_id}`
|
||
);
|
||
|
||
const startTime = Date.now();
|
||
const res = await signIn(Number(physical_exam_id));
|
||
const duration = Date.now() - startTime;
|
||
|
||
window.electronAPI?.log(
|
||
"info",
|
||
`[UI7] 签到响应: Status=${res.Status}, Message=${res.Message || "null"}, is_success=${res.Data?.is_success}, 耗时: ${duration}ms`
|
||
);
|
||
|
||
if (res.Status === 200) {
|
||
if (res.Data.is_success === 0) {
|
||
window.electronAPI?.log("info", "[UI7] 签到成功");
|
||
return;
|
||
} else {
|
||
window.electronAPI?.log("warn", `[UI7] 签到返回异常: ${res.Message}`);
|
||
console.log(res.Data);
|
||
alert(res.Message);
|
||
}
|
||
} else {
|
||
window.electronAPI?.log("error", `[UI7] 签到失败: ${res.Message}`);
|
||
alert(res.Message);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="basic-root">
|
||
<div className="basic-white-block">
|
||
<div className="basic-content">
|
||
<span
|
||
ref={titleRef}
|
||
className={`ui7-basic-title ${isTitleCompact ? 'compact' : ''}`}
|
||
>
|
||
{!isInstructionStep && currentPdfName ? `${currentPdfName}` : "检测项目知情同意书确认"}
|
||
</span>
|
||
<DecorLine />
|
||
|
||
{isInstructionStep && (
|
||
<div
|
||
style={{
|
||
padding: "40px",
|
||
fontSize: "28px",
|
||
lineHeight: "48px",
|
||
color: "#333",
|
||
}}
|
||
>
|
||
<br />
|
||
<br />
|
||
<br />
|
||
{/* <p style={{ textAlign: "center", fontSize: "36px" }}>
|
||
请仔细阅读后签字确认
|
||
</p> */}
|
||
</div>
|
||
)}
|
||
|
||
{!isInstructionStep && (
|
||
<div className="ui7-text-wrapper">
|
||
<div className="ui7-text-content">
|
||
<PdfRenderer
|
||
pdfFile={currentPdfFile}
|
||
loading={loading}
|
||
error={error}
|
||
pageCount={currentPdfPageCount}
|
||
onPageCountUpdate={handlePageCountUpdate}
|
||
loadMessage={loadMessage}
|
||
documentKey={`pdf-step-${currentStep}`}
|
||
/>
|
||
</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}
|
||
disabled={!hasSignature || isSubmitting}
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default UI7;
|