From ed96d8bf1a615b4b1d4179b33b0aebc74b908674 Mon Sep 17 00:00:00 2001 From: xianyi Date: Fri, 16 Jan 2026 10:29:39 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E7=AD=BE=E5=88=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/exam/ExamSignPanel.tsx | 532 +++++++++++++++----------- 1 file changed, 299 insertions(+), 233 deletions(-) diff --git a/src/components/exam/ExamSignPanel.tsx b/src/components/exam/ExamSignPanel.tsx index 9141b0c..8c00a55 100644 --- a/src/components/exam/ExamSignPanel.tsx +++ b/src/components/exam/ExamSignPanel.tsx @@ -99,8 +99,13 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => { const [selectedOptionalItem, setSelectedOptionalItem] = useState(null); // 是否已经确认过可选项目(如果后端已有记录,视为已确认) const [optionalConfirmed, setOptionalConfirmed] = useState(false); - // 是否显示“请先确认体检项目,确认后不可修改”的提示 + // 是否显示"请先确认体检项目,确认后不可修改"的提示 const [showOptionalConfirmTip, setShowOptionalConfirmTip] = useState(false); + // 跟踪当前 examId 是否已加载过导检单和知情同意书 + const pdfsLoadedForExamIdRef = useRef(null); + // 使用 ref 存储最新的可选项目状态,确保 refreshTijianPdfs 能读取到最新值 + const optionalItemListRef = useRef([]); + const optionalConfirmedRef = useRef(false); const busy = signLoading || @@ -117,7 +122,13 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => { return () => onBusyChange?.(false); }, [busy, onBusyChange]); - const refreshTijianPdfs = async (examIdValue: number) => { + const refreshTijianPdfs = async (examIdValue: number, checkOptional: boolean = true) => { + // 如果还有可选项目未确认,不执行接口请求(使用 ref 确保读取最新值) + if (checkOptional && optionalItemListRef.current.length > 0 && !optionalConfirmedRef.current) { + console.log('跳过 pdf-file-get 请求:可选项目未确认', { optionalItemListLength: optionalItemListRef.current.length, optionalConfirmed: optionalConfirmedRef.current }); + return; + } + setConsentLoading(true); try { @@ -182,19 +193,138 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => { } }; - // 初始化加载体检 PDF 列表(导检单、知情同意书、加项单) - useEffect(() => { - if (!examId) { - setConsentList([]); - setDaojiandanUrl(null); - setIsDaojiandanSigned(false); + // 加载可选项目列表 & 操作记录(优先使用记录,无记录时才调用可选项目接口) + const loadOptionalItems = async () => { + if (!examId) return; + setOptionalItemLoading(true); + try { + // 先获取操作记录 + const recordRes = await getExamOptionRecordList({ exam_id: examId }); + + + console.log("操作记录请求", recordRes); + // 若任意记录 is_abandon=1,则视为"已确定"(已做过弃选/确认动作),不可再确认 + let hasConfirmedRecord = false; + let selectedFromRecord: number | null = null; + let itemsFromRecord: OutputPhysicalExamItemInfo[] = []; + + if (recordRes.Status === 200 && recordRes.Data && recordRes.Data.length > 0) { + hasConfirmedRecord = recordRes.Data.some((r) => r.is_abandon === 1); + + // 从记录中构建可选项目列表(只显示 is_abandon=0 的记录) + itemsFromRecord = recordRes.Data + .filter((r) => r.is_abandon === 0 && r.combination_code) + .map((r) => { + const codeNum = Number(r.combination_code); + const packageNum = r.package_code ? Number(r.package_code) : null; + if (!Number.isFinite(codeNum)) return null; + return { + combination_code: codeNum, + combination_name: r.combination_name || '', + package_code: Number.isFinite(packageNum) ? packageNum : null, + } as OutputPhysicalExamItemInfo; + }) + .filter((item): item is OutputPhysicalExamItemInfo => item !== null); + + // 预选:优先取 is_abandon=0 的组合码(若有) + const selectedRecord = recordRes.Data.find( + (r) => r.is_abandon === 0 && r.combination_code + ); + if (selectedRecord?.combination_code) { + const codeNum = Number(selectedRecord.combination_code); + if (Number.isFinite(codeNum)) { + selectedFromRecord = codeNum; + } + } + } + + // 如果有记录,使用记录构建的列表;否则调用可选项目接口 + if (itemsFromRecord.length > 0) { + // 根据 combination_code 去重,保留第一个出现的项目 + const uniqueItems = itemsFromRecord.filter((item, index, self) => + index === self.findIndex((t) => t.combination_code === item.combination_code) + ); + setOptionalItemList(uniqueItems); + optionalItemListRef.current = uniqueItems; + + if (selectedFromRecord != null && uniqueItems.some((i) => i.combination_code === selectedFromRecord)) { + setSelectedOptionalItem(selectedFromRecord); + } else { + setSelectedOptionalItem(null); + } + // 只要存在可选项目:默认未确认;但若出现 is_abandon=1 记录,则视为已确定 + setOptionalConfirmed(hasConfirmedRecord); + optionalConfirmedRef.current = hasConfirmedRecord; + setShowOptionalConfirmTip(false); + } else { + // 没有记录,调用可选项目接口 + const listRes = await getExamOptionalItemList({ physical_exam_id: examId }); + + if (listRes.Status === 200 && listRes.Data?.listOptionalItem) { + const items = listRes.Data.listOptionalItem; + // 根据 combination_code 去重,保留第一个出现的项目 + const uniqueItems = items.filter((item, index, self) => + index === self.findIndex((t) => t.combination_code === item.combination_code) + ); + setOptionalItemList(uniqueItems); + optionalItemListRef.current = uniqueItems; + + // 无历史记录,默认不选 + setSelectedOptionalItem(null); + // 只要存在可选项目:默认未确认 + setOptionalConfirmed(false); + optionalConfirmedRef.current = false; + setShowOptionalConfirmTip(false); + } else { + setOptionalItemList([]); + optionalItemListRef.current = []; + setSelectedOptionalItem(null); + setOptionalConfirmed(true); + optionalConfirmedRef.current = true; + setShowOptionalConfirmTip(false); + } + } + } catch (err) { + console.error('获取可选项目/记录失败', err); + setOptionalItemList([]); + optionalItemListRef.current = []; + setSelectedOptionalItem(null); + setOptionalConfirmed(true); + optionalConfirmedRef.current = true; + setShowOptionalConfirmTip(false); + } finally { + setOptionalItemLoading(false); + } + }; + + // 加载 PDF 的逻辑(知情同意书、导检单) + const loadPdfs = async (currentExamId: number) => { + // 如果已经为当前 examId 加载过,不再重复加载 + if (pdfsLoadedForExamIdRef.current === currentExamId) { return; } - // 初始化时加载知情同意书和导检单 + // 使用 ref 确保读取最新值:如果没有可选项目,或者有可选项目但已确认,才加载导检单和知情同意书 + console.log('检查是否应该加载 PDF,ref 值:', { + optionalItemListLength: optionalItemListRef.current.length, + optionalConfirmed: optionalConfirmedRef.current + }); + const shouldLoadPdfs = optionalItemListRef.current.length === 0 || optionalConfirmedRef.current; + + if (!shouldLoadPdfs) { + console.log('跳过所有 PDF 接口请求:可选项目未确认', { + optionalItemListLength: optionalItemListRef.current.length, + optionalConfirmed: optionalConfirmedRef.current + }); + return; + } + + pdfsLoadedForExamIdRef.current = currentExamId; + + // 初始化时加载知情同意书 const loadTongyishu = async () => { try { - const res = await getTongyishuPdf({ exam_id: examId }); + const res = await getTongyishuPdf({ exam_id: currentExamId }); if (res.Status === 200 && res.Data?.list_pdf_url) { const list = res.Data.list_pdf_url; const mappedConsent: OutputTongyishuFileInfo[] = list.map((item) => ({ @@ -212,9 +342,9 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => { // 初始化时加载导检单(未签名版本也可以查看) const loadDaojiandan = async () => { try { - const res = await getDaojiandanPdf({ exam_id: examId }); + console.log("未签名导检单请求"); + const res = await getDaojiandanPdf({ exam_id: currentExamId }); if (res.Status === 200 && res.Data?.pdf_url) { - // 如果 refreshTijianPdfs 没有设置导检单URL,则使用这里获取的未签名版本 setDaojiandanUrl((prev) => prev || res.Data?.pdf_url || null); } } catch (err) { @@ -222,85 +352,58 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => { } }; - // 加载可选项目列表 & 操作记录(用于判断是否已选择) - const loadOptionalItems = async () => { - if (!examId) return; - setOptionalItemLoading(true); - try { - const [listRes, recordRes] = await Promise.all([ - getExamOptionalItemList({ physical_exam_id: examId }), - getExamOptionRecordList({ exam_id: examId }), - ]); - - if (listRes.Status === 200 && listRes.Data?.listOptionalItem) { - const items = listRes.Data.listOptionalItem; - // 根据 combination_code 去重,保留第一个出现的项目 - const uniqueItems = items.filter((item, index, self) => - index === self.findIndex((t) => t.combination_code === item.combination_code) - ); - setOptionalItemList(uniqueItems); - - // 若任意记录 is_abandon=1,则视为“已确定”(已做过弃选/确认动作),不可再确认 - let hasConfirmedRecord = false; - let selectedFromRecord: number | null = null; - if (recordRes.Status === 200 && recordRes.Data) { - hasConfirmedRecord = recordRes.Data.some((r) => r.is_abandon === 1); - - // 预选:优先取 is_abandon=0 的组合码(若有) - const selectedRecord = recordRes.Data.find( - (r) => r.is_abandon === 0 && r.combination_code - ); - if (selectedRecord?.combination_code) { - const codeNum = Number(selectedRecord.combination_code); - if (Number.isFinite(codeNum)) { - selectedFromRecord = codeNum; - } - } - } - - if (uniqueItems.length > 0) { - if ( - selectedFromRecord != null && - uniqueItems.some((i) => i.combination_code === selectedFromRecord) - ) { - // 有历史记录时,仅做“默认选中”,不标记为已确认 - setSelectedOptionalItem(selectedFromRecord); - } else { - // 无历史记录,默认不选 - setSelectedOptionalItem(null); - } - // 只要存在可选项目:默认未确认;但若出现 is_abandon=1 记录,则视为已确定 - setOptionalConfirmed(hasConfirmedRecord); - setShowOptionalConfirmTip(false); - } else { - // 没有可选项目,则视为无需确认 - setSelectedOptionalItem(null); - setOptionalConfirmed(true); - setShowOptionalConfirmTip(false); - } - } else { - setOptionalItemList([]); - setSelectedOptionalItem(null); - setOptionalConfirmed(true); - setShowOptionalConfirmTip(false); - } - } catch (err) { - console.error('获取可选项目/记录失败', err); - setOptionalItemList([]); - setSelectedOptionalItem(null); - setOptionalConfirmed(true); - setShowOptionalConfirmTip(false); - } finally { - setOptionalItemLoading(false); + // 加载 PDF + loadTongyishu().then(() => { + // 调用 refreshTijianPdfs 前再次检查可选项目状态(使用 ref 确保读取最新值) + if (optionalItemListRef.current.length === 0 || optionalConfirmedRef.current) { + refreshTijianPdfs(currentExamId, true); } - }; + }); + loadDaojiandan(); + }; - // 先加载知情同意书和导检单,然后刷新所有PDF状态(包括签名状态) - Promise.all([loadTongyishu(), loadDaojiandan(), loadOptionalItems()]).then(() => { - refreshTijianPdfs(examId); + // 初始化:先检查可选项 + useEffect(() => { + if (!examId) { + setConsentList([]); + setDaojiandanUrl(null); + setIsDaojiandanSigned(false); + optionalItemListRef.current = []; + optionalConfirmedRef.current = true; + pdfsLoadedForExamIdRef.current = null; + setOptionalItemLoading(false); + return; + } + + // 先设置 loading 为 true + setOptionalItemLoading(true); + optionalItemListRef.current = []; + optionalConfirmedRef.current = false; + pdfsLoadedForExamIdRef.current = null; + + // 先检查可选项,选择完毕后才请求导检单接口 + loadOptionalItems().then(() => { + // loadOptionalItems 完成后,ref 已经更新,直接调用加载 PDF 的逻辑 + console.log('loadOptionalItems 完成,ref 已更新', { + optionalItemListLength: optionalItemListRef.current.length, + optionalConfirmed: optionalConfirmedRef.current + }); + // 直接调用加载 PDF 的逻辑,不依赖第二个 useEffect + loadPdfs(examId); }); }, [examId]); + // 监听可选项目确认状态变化,确认后才加载导检单和知情同意书 + useEffect(() => { + if (!examId) return; + + // 仅在可选项目状态从"未确认"变为"已确认"时,加载 PDF + if (optionalConfirmed && optionalItemList.length > 0 && pdfsLoadedForExamIdRef.current !== examId) { + console.log('可选项目已确认,开始加载 PDF'); + loadPdfs(examId); + } + }, [examId, optionalConfirmed]); + const handlePickFile = () => { // 有可选项目但尚未确认时,禁止拍照并提示先确认项目 if (optionalItemList.length > 0 && !optionalConfirmed) { @@ -413,6 +516,7 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => { } if (optionalItemList.length === 0) { setOptionalConfirmed(true); + optionalConfirmedRef.current = true; setShowOptionalConfirmTip(false); return; } @@ -429,6 +533,7 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => { // 没有未选项目需要移除,直接标记为已确认 if (unselectedCodes.length === 0) { setOptionalConfirmed(true); + optionalConfirmedRef.current = true; setShowOptionalConfirmTip(false); setMessage('可选项目已确定'); setTimeout(() => setMessage(null), 3000); @@ -444,12 +549,10 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => { }); if (res.Status === 200 && res.Data?.is_success === 1) { setOptionalConfirmed(true); + optionalConfirmedRef.current = true; setShowOptionalConfirmTip(false); - // 刷新剩余可选项目列表(理论上只剩一个) - const refreshRes = await getExamOptionalItemList({ physical_exam_id: examId }); - if (refreshRes.Status === 200 && refreshRes.Data?.listOptionalItem) { - setOptionalItemList(refreshRes.Data.listOptionalItem); - } + // 刷新操作记录列表(不再调用 optional-item-list 接口,避免恢复已移除的项目) + await loadOptionalItems(); setMessage('可选项目已确定'); setTimeout(() => setMessage(null), 3000); } else { @@ -510,8 +613,14 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => { signaturePadRef.current?.clear(); // 更新签名状态(刷新统一 PDF 列表) - if (examId) { - refreshTijianPdfs(examId); + // 如果还有可选项目未确认,不执行接口请求(使用 ref 确保读取最新值) + if (examId && (optionalItemListRef.current.length === 0 || optionalConfirmedRef.current)) { + refreshTijianPdfs(examId, true); + } else if (examId) { + console.log('跳过 pdf-file-get 请求:可选项目未确认(签名成功后)', { + optionalItemListLength: optionalItemListRef.current.length, + optionalConfirmed: optionalConfirmedRef.current + }); } }, 500); } else { @@ -708,6 +817,7 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => { useEffect(() => { if (!showAddItemBillPreview || !currentAddItemBill?.pdf_url) { setAddItemBillPdfData(null); + setAddItemBillPdfLoading(false); return; } @@ -726,9 +836,11 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => { }) .then((arrayBuffer) => { setAddItemBillPdfData(arrayBuffer); + setAddItemBillPdfLoading(false); }) .catch((err) => { console.error('加项单PDF 拉取失败', err); + setAddItemBillPdfLoading(false); }); return () => { @@ -738,56 +850,6 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => { }; }, [showAddItemBillPreview, currentAddItemBill?.pdf_url]); - // 渲染加项单 PDF - useEffect(() => { - if (!addItemBillPdfData || !addItemBillCanvasContainerRef.current) return; - - const renderAllPages = async () => { - try { - const pdf = await pdfjsLib.getDocument({ data: addItemBillPdfData }).promise; - - if (!addItemBillCanvasContainerRef.current) return; - - // 清空容器 - addItemBillCanvasContainerRef.current.innerHTML = ''; - - const scale = 3.0; - - for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) { - const page = await pdf.getPage(pageNum); - const viewport = page.getViewport({ scale }); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - - if (!context) continue; - - canvas.height = viewport.height; - canvas.width = viewport.width; - canvas.style.display = 'block'; - canvas.style.marginBottom = '10px'; - canvas.style.maxWidth = '100%'; - canvas.style.height = 'auto'; - canvas.className = 'mx-auto border rounded-lg shadow-sm'; - - addItemBillCanvasContainerRef.current.appendChild(canvas); - - const renderContext = { - canvasContext: context, - viewport: viewport, - } as any; - - await page.render(renderContext).promise; - } - - } catch (err) { - console.error('加项单PDF 渲染失败', err); - } - }; - - renderAllPages(); - }, [addItemBillPdfData]); - // 导检单预览:使用 pdfjs 渲染到 canvas useEffect(() => { if (!showDaojiandanPreview || !daojiandanUrl || !daojiandanCanvasContainerRef.current) return; @@ -1236,7 +1298,8 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => { setAddItemBillSubmitMessage('签名提交成功'); // 签名成功后刷新统一 PDF 列表,获取最新的加项单签名状态和地址 - if (examId) { + // 如果还有可选项目未确认,不执行接口请求(使用 ref 确保读取最新值) + if (examId && (optionalItemListRef.current.length === 0 || optionalConfirmedRef.current)) { try { const refreshRes = await getTijianPdfFile({ exam_id: examId as number }); const list: OutputTijianPdfFileInfo[] = refreshRes.Data || []; @@ -1997,74 +2060,95 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => { ))} -
-
- 导检单 - {isDaojiandanSigned && ( - 已签名 - )} -
-
- {isDaojiandanSigned ? ( - // 已签名:显示打印和查看按钮 - <> - - - - ) : daojiandanUrl ? ( - // 未签名但有导检单:显示查看和签名按钮 - <> - + {/* 只有在没有可选项目,或者有可选项目但已确认时,才显示导检单 */} + {(optionalItemList.length === 0 || optionalConfirmed) && ( +
+
+ 导检单 + {isDaojiandanSigned && ( + 已签名 + )} +
+
+ {isDaojiandanSigned ? ( + // 已签名:显示打印和查看按钮 + <> + + + + ) : daojiandanUrl ? ( + // 未签名但有导检单:显示查看和签名按钮 + <> + + + + ) : ( + // 没有导检单:只显示签名按钮 - - ) : ( - // 没有导检单:只显示签名按钮 - - )} + )} +
-
+ )} {/* 加项单列表(可能有多个) */} {addItemBillList.length > 0 && addItemBillList.map((bill) => {