import { useEffect, useRef, useState } from 'react'; import * as pdfjsLib from 'pdfjs-dist'; import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?url'; import type { OutputTongyishuFileInfo, OutputTijianPdfFileInfo, OutputPhysicalExamItemInfo } from '../../api'; import { signInMedicalExamCenter, submitTongyishuSign, submitDaojiandanSign, editDaojiandanPrintStatus, submitAddItemBillSign, getTijianPdfFile, getTongyishuPdf, getDaojiandanPdf, getExamOptionalItemList, removeOptionalPackage, getExamOptionRecordList, } from '../../api'; import type { SignaturePadHandle } from '../ui'; import { Button, SignaturePad } from '../ui'; // Polyfill for Promise.withResolvers if (typeof (Promise as any).withResolvers === 'undefined') { (Promise as any).withResolvers = function () { let resolve!: (value: T | PromiseLike) => void; let reject!: (reason?: any) => void; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve, reject }; }; } // 配置 PDF.js worker pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker; interface ExamSignPanelProps { examId?: number; onBusyChange?: (busy: boolean) => void; } export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => { const [idCardFile, setIdCardFile] = useState(null); const [previewImage, setPreviewImage] = useState(null); const [showImagePreview, setShowImagePreview] = useState(false); const [signLoading, setSignLoading] = useState(false); const [message, setMessage] = useState(null); const fileInputRef = useRef(null); const [consentList, setConsentList] = useState([]); const [consentLoading, setConsentLoading] = useState(false); const [previewPdf, setPreviewPdf] = useState(null); const [showSignature, setShowSignature] = useState(false); const signaturePadRef = useRef(null); const [submitLoading, setSubmitLoading] = useState(false); const [submitMessage, setSubmitMessage] = useState(null); const [signedCombinationCodes, setSignedCombinationCodes] = useState([]); const [pdfData, setPdfData] = useState(null); const [pdfReady, setPdfReady] = useState(false); const [pdfLoading, setPdfLoading] = useState(false); const [pdfBlobUrl, setPdfBlobUrl] = useState(null); const canvasContainerRef = useRef(null); // 导检单相关状态 const [daojiandanUrl, setDaojiandanUrl] = useState(null); const [isDaojiandanSigned, setIsDaojiandanSigned] = useState(false); // 导检单是否已签名 const [showDaojiandanSignature, setShowDaojiandanSignature] = useState(false); const daojiandanSignaturePadRef = useRef(null); const [daojiandanSubmitLoading, setDaojiandanSubmitLoading] = useState(false); const [daojiandanSubmitMessage, setDaojiandanSubmitMessage] = useState(null); const [showDaojiandanPreview, setShowDaojiandanPreview] = useState(false); const [daojiandanPdfData, setDaojiandanPdfData] = useState(null); const [daojiandanPdfLoading, setDaojiandanPdfLoading] = useState(false); const daojiandanCanvasContainerRef = useRef(null); const [showAddItemBillPreview, setShowAddItemBillPreview] = useState(false); const [showAddItemBillSignature, setShowAddItemBillSignature] = useState(false); const addItemBillSignaturePadRef = useRef(null); const [addItemBillSubmitLoading, setAddItemBillSubmitLoading] = useState(false); const [addItemBillSubmitMessage, setAddItemBillSubmitMessage] = useState(null); type AddItemBillItem = { pdf_sort: number; combinationCode: string; payment_status?: string | null; payment_status_name?: string | null; pdf_name: string; pdf_url: string; is_signed: boolean; }; const [addItemBillList, setAddItemBillList] = useState([]); const [currentAddItemBill, setCurrentAddItemBill] = useState(null); const [addItemBillPdfData, setAddItemBillPdfData] = useState(null); const [addItemBillPdfLoading, setAddItemBillPdfLoading] = useState(false); const [batchPrintLoading, setBatchPrintLoading] = useState(false); const addItemBillCanvasContainerRef = useRef(null); // 可选项目列表相关状态 const [optionalItemList, setOptionalItemList] = useState([]); const [optionalItemLoading, setOptionalItemLoading] = useState(false); 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 || submitLoading || consentLoading || pdfLoading || daojiandanSubmitLoading || addItemBillSubmitLoading || batchPrintLoading || optionalItemLoading; useEffect(() => { onBusyChange?.(busy); return () => onBusyChange?.(false); }, [busy, onBusyChange]); 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 { const res = await getTijianPdfFile({ exam_id: examIdValue }); const list: OutputTijianPdfFileInfo[] = res.Data || []; // 知情同意书列表(pdf_type = 2) const consentItems = list.filter((item) => item.pdf_type === 2); const mappedConsent: OutputTongyishuFileInfo[] = consentItems.map((item) => ({ pdf_name: item.pdf_name || '', pdf_url: item.sign_pdf_url || item.pdf_url || '', combination_code: item.combination_code ?? null, is_signed: item.is_sign === 1, })); setConsentList(mappedConsent); // 已签名的组合码,用于一键打印等逻辑 const signedCodes = consentItems .filter((item) => item.is_sign === 1 && item.combination_code !== null && item.combination_code !== undefined) .map((item) => Number(item.combination_code)) .filter((n) => Number.isFinite(n)) || []; setSignedCombinationCodes(signedCodes); // 导检单(pdf_type = 1,取第一条) const daojiandan = list.find((item) => item.pdf_type === 1); if (daojiandan) { const url = daojiandan.sign_pdf_url || daojiandan.pdf_url || null; setDaojiandanUrl(url); setIsDaojiandanSigned(daojiandan.is_sign === 1); } else { // 如果 getTijianPdfFile 没有返回导检单,保留之前通过 getDaojiandanPdf 获取的URL(如果存在) // 不设置为 null,这样未签名的导检单也能显示"查看"按钮 setDaojiandanUrl((prev) => (prev !== null ? prev : null)); setIsDaojiandanSigned(false); } // 加项单(pdf_type = 3):完全由新接口提供列表和签名状态(直接使用接口返回的字段) const addItemFromApi = list.filter((item) => item.pdf_type === 3); if (addItemFromApi.length > 0) { const addItemList: AddItemBillItem[] = addItemFromApi.map((item) => ({ // 这里直接使用接口返回的 pdf_sort / combination_code / is_pay / is_pay_name pdf_sort: item.pdf_sort ?? 0, combinationCode: item.combination_code != null ? String(item.combination_code) : '', payment_status: item.is_pay != null ? String(item.is_pay) : null, payment_status_name: item.is_pay_name ?? null, pdf_name: item.pdf_name || '加项单', pdf_url: item.sign_pdf_url || item.pdf_url || '', is_signed: item.is_sign === 1, })); setAddItemBillList(addItemList); const unsigned = addItemList.find((bill) => bill.is_signed !== true); setCurrentAddItemBill(unsigned || addItemList[0] || null); } else { setAddItemBillList([]); setCurrentAddItemBill(null); } } catch (err) { console.error('获取体检PDF列表失败', err); } finally { setConsentLoading(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: currentExamId }); if (res.Status === 200 && res.Data?.list_pdf_url) { const list = res.Data.list_pdf_url; const mappedConsent: OutputTongyishuFileInfo[] = list.map((item) => ({ pdf_name: item.pdf_name || '', pdf_url: item.pdf_url || '', combination_code: item.combination_code ?? null, })); setConsentList(mappedConsent); } } catch (err) { console.error('获取知情同意书失败', err); } }; // 初始化时加载导检单(未签名版本也可以查看) const loadDaojiandan = async () => { try { console.log("未签名导检单请求"); const res = await getDaojiandanPdf({ exam_id: currentExamId }); if (res.Status === 200 && res.Data?.pdf_url) { setDaojiandanUrl((prev) => prev || res.Data?.pdf_url || null); } } catch (err) { console.error('获取导检单失败', err); } }; // 加载 PDF loadTongyishu().then(() => { // 调用 refreshTijianPdfs 前再次检查可选项目状态(使用 ref 确保读取最新值) if (optionalItemListRef.current.length === 0 || optionalConfirmedRef.current) { refreshTijianPdfs(currentExamId, true); } }); loadDaojiandan(); }; // 初始化:先检查可选项 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) { setMessage('请先确认体检项目'); setShowOptionalConfirmTip(true); return; } fileInputRef.current?.click(); }; const convertToJpg = async (file: File): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (event) => { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext('2d'); if (!ctx) { reject(new Error('无法创建画布上下文')); return; } ctx.drawImage(img, 0, 0); canvas.toBlob( (blob) => { if (!blob) { reject(new Error('图片转换失败')); return; } const jpgFile = new File([blob], 'id_card.jpg', { type: 'image/jpeg' }); resolve(jpgFile); }, 'image/jpeg', 0.92 ); }; img.onerror = () => reject(new Error('图片加载失败')); img.src = event.target?.result as string; }; reader.onerror = () => reject(new Error('文件读取失败')); reader.readAsDataURL(file); }); }; const handleFileChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setMessage(null); try { const jpgFile = await convertToJpg(file); setIdCardFile(jpgFile); const reader = new FileReader(); reader.onload = (event) => { setPreviewImage(event.target?.result as string); }; reader.readAsDataURL(jpgFile); } catch (err) { console.error('图片转换失败', err); setMessage('图片处理失败,请重试'); } e.target.value = ''; }; const handleSign = async () => { if (!idCardFile) { setMessage('请先上传身份证照片'); return; } if (!examId) { setMessage('缺少体检ID'); return; } // 如果存在可选项目但尚未确认,则不允许直接签到 if (optionalItemList.length > 0 && !optionalConfirmed) { setMessage('请先确认体检项目'); setShowOptionalConfirmTip(true); return; } setSignLoading(true); setMessage(null); try { const res = await signInMedicalExamCenter({ id_no_pic: idCardFile }); const ok = res.Status === 200 && res.Data?.is_success === 0; if (ok) { setMessage('签到成功'); } else { setMessage(res.Message || '签到失败'); } } catch (err) { console.error(err); // setMessage('签到请求失败,请稍后重试'); } finally { setSignLoading(false); } }; // 确认当前选择的可选项目:移除未选择的项目 const handleConfirmOptionalSelection = async () => { if (!examId) { setMessage('缺少体检ID'); return; } if (optionalItemList.length === 0) { setOptionalConfirmed(true); optionalConfirmedRef.current = true; setShowOptionalConfirmTip(false); return; } if (selectedOptionalItem == null) { setMessage('请选择一个可选项目'); setShowOptionalConfirmTip(true); return; } const unselectedCodes = optionalItemList .map((item) => item.combination_code) .filter((code): code is number => code != null && code !== selectedOptionalItem); // 没有未选项目需要移除,直接标记为已确认 if (unselectedCodes.length === 0) { setOptionalConfirmed(true); optionalConfirmedRef.current = true; setShowOptionalConfirmTip(false); setMessage('可选项目已确定'); setTimeout(() => setMessage(null), 3000); return; } setOptionalItemLoading(true); try { const combinationCodeIds = unselectedCodes.join(','); const res = await removeOptionalPackage({ physical_exam_id: examId, combination_code_ids: combinationCodeIds, }); if (res.Status === 200 && res.Data?.is_success === 1) { setOptionalConfirmed(true); optionalConfirmedRef.current = true; setShowOptionalConfirmTip(false); // 刷新操作记录列表(不再调用 optional-item-list 接口,避免恢复已移除的项目) await loadOptionalItems(); setMessage('可选项目已确定'); setTimeout(() => setMessage(null), 3000); } else { setMessage('可选项目确定失败:' + (res.Message || '未知错误')); setTimeout(() => setMessage(null), 3000); } } catch (err) { console.error('可选项目确定失败', err); setMessage('可选项目确定失败,请稍后重试'); setTimeout(() => setMessage(null), 3000); } finally { setOptionalItemLoading(false); } }; const handleSubmitSign = async () => { // 有可选项目但尚未确认时,禁止签名并提示先确认项目 if (optionalItemList.length > 0 && !optionalConfirmed) { setSubmitMessage('请先确认体检项目'); setShowOptionalConfirmTip(true); return; } if (!examId || !previewPdf?.combination_code) { setSubmitMessage('缺少必要信息,无法提交签名'); return; } const dataUrl = signaturePadRef.current?.toDataURL('image/png'); if (!dataUrl) { setSubmitMessage('请先完成签名'); return; } setSubmitLoading(true); setSubmitMessage(null); try { const blob = await fetch(dataUrl).then((r) => r.blob()); const res = await submitTongyishuSign({ exam_id: examId, combination_code: previewPdf.combination_code, sign_file: blob, }); if (res.Status === 200) { setSubmitMessage('签名提交成功'); // 更新本地已签名组合码(立刻刷新按钮状态) setSignedCombinationCodes((prev) => { const code = Number(previewPdf.combination_code); if (!Number.isFinite(code)) return prev || []; return Array.from(new Set([...(prev || []), code])); }); setTimeout(() => { setShowSignature(false); setPreviewPdf(null); setSubmitMessage(null); signaturePadRef.current?.clear(); // 更新签名状态(刷新统一 PDF 列表) // 如果还有可选项目未确认,不执行接口请求(使用 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 { setSubmitMessage(res.Message || '签名提交失败'); } } catch (err) { console.error('提交签名失败', err); setSubmitMessage('签名提交失败,请稍后重试'); } finally { setSubmitLoading(false); } }; // 加载 PDF 数据 useEffect(() => { if (!previewPdf?.pdf_url) { setPdfData(null); setPdfBlobUrl(null); setPdfReady(false); return; } let objectUrl: string | null = null; setPdfReady(false); setPdfLoading(true); setPdfData(null); fetch(previewPdf.pdf_url) .then((resp) => { if (!resp.ok) throw new Error('获取PDF文件失败'); return resp.blob(); }) .then((blob) => { objectUrl = URL.createObjectURL(blob); setPdfBlobUrl(objectUrl); return blob.arrayBuffer(); }) .then((arrayBuffer) => { setPdfData(arrayBuffer); setPdfLoading(false); }) .catch((err) => { console.error('PDF 拉取失败', err); setPdfLoading(false); }); return () => { if (objectUrl) { URL.revokeObjectURL(objectUrl); } }; }, [previewPdf?.pdf_url]); // 渲染 PDF useEffect(() => { if (!pdfData || !canvasContainerRef.current) return; setPdfReady(false); const renderAllPages = async () => { try { const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise; if (!canvasContainerRef.current) return; // 清空容器 canvasContainerRef.current.innerHTML = ''; const scale = 3; 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'; canvasContainerRef.current.appendChild(canvas); const renderContext = { canvasContext: context, viewport: viewport, } as any; await page.render(renderContext).promise; } setPdfReady(true); } catch (err) { console.error('PDF 渲染失败', err); setPdfLoading(false); } }; renderAllPages(); }, [pdfData]); // 加载导检单 PDF 数据 useEffect(() => { if (!showDaojiandanPreview || !daojiandanUrl) { setDaojiandanPdfData(null); return; } let objectUrl: string | null = null; setDaojiandanPdfLoading(true); setDaojiandanPdfData(null); fetch(daojiandanUrl) .then((resp) => { if (!resp.ok) throw new Error('获取PDF文件失败'); return resp.blob(); }) .then((blob) => { objectUrl = URL.createObjectURL(blob); return blob.arrayBuffer(); }) .then((arrayBuffer) => { setDaojiandanPdfData(arrayBuffer); setDaojiandanPdfLoading(false); }) .catch((err) => { console.error('导检单PDF 拉取失败', err); }); return () => { if (objectUrl) { URL.revokeObjectURL(objectUrl); } }; }, [showDaojiandanPreview, daojiandanUrl]); // 渲染导检单 PDF useEffect(() => { if (!daojiandanPdfData || !daojiandanCanvasContainerRef.current) return; const renderAllPages = async () => { try { const pdf = await pdfjsLib.getDocument({ data: daojiandanPdfData }).promise; if (!daojiandanCanvasContainerRef.current) return; // 清空容器 daojiandanCanvasContainerRef.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'; daojiandanCanvasContainerRef.current.appendChild(canvas); const renderContext = { canvasContext: context, viewport: viewport, } as any; await page.render(renderContext).promise; } } catch (err) { console.error('导检单PDF 渲染失败', err); } }; renderAllPages(); }, [daojiandanPdfData]); // 加载加项单 PDF 数据(当前选中的加项单) useEffect(() => { if (!showAddItemBillPreview || !currentAddItemBill?.pdf_url) { setAddItemBillPdfData(null); setAddItemBillPdfLoading(false); return; } let objectUrl: string | null = null; setAddItemBillPdfLoading(true); setAddItemBillPdfData(null); fetch(currentAddItemBill.pdf_url) .then((resp) => { if (!resp.ok) throw new Error('获取PDF文件失败'); return resp.blob(); }) .then((blob) => { objectUrl = URL.createObjectURL(blob); return blob.arrayBuffer(); }) .then((arrayBuffer) => { setAddItemBillPdfData(arrayBuffer); setAddItemBillPdfLoading(false); }) .catch((err) => { console.error('加项单PDF 拉取失败', err); setAddItemBillPdfLoading(false); }); return () => { if (objectUrl) { URL.revokeObjectURL(objectUrl); } }; }, [showAddItemBillPreview, currentAddItemBill?.pdf_url]); // 导检单预览:使用 pdfjs 渲染到 canvas useEffect(() => { if (!showDaojiandanPreview || !daojiandanUrl || !daojiandanCanvasContainerRef.current) return; const container = daojiandanCanvasContainerRef.current; let cancelled = false; const renderDaojiandan = async () => { try { container.innerHTML = ''; const resp = await fetch(daojiandanUrl); if (!resp.ok) throw new Error('获取PDF文件失败'); const blob = await resp.blob(); const arrayBuffer = await blob.arrayBuffer(); const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; const scale = 3.0; for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) { if (cancelled) return; 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'; container.appendChild(canvas); const renderContext = { canvasContext: context, viewport: viewport, } as any; await page.render(renderContext).promise; } } catch (err) { console.error('导检单PDF 渲染失败', err); } finally { if (!cancelled) { } } }; renderDaojiandan(); return () => { cancelled = true; container.innerHTML = ''; }; }, [showDaojiandanPreview, daojiandanUrl]); // 加项单预览:使用 pdfjs 渲染到 canvas(依赖加载好的 addItemBillPdfData) useEffect(() => { if (!showAddItemBillPreview || !addItemBillPdfData || !addItemBillCanvasContainerRef.current) return; const container = addItemBillCanvasContainerRef.current; container.innerHTML = ''; const renderAddItemBill = async () => { try { const pdf = await pdfjsLib.getDocument({ data: addItemBillPdfData }).promise; 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'; container.appendChild(canvas); const renderContext = { canvasContext: context, viewport: viewport, } as any; await page.render(renderContext).promise; } } catch (err) { console.error('加项单PDF 渲染失败', err); } }; renderAddItemBill(); return () => { container.innerHTML = ''; }; }, [showAddItemBillPreview, addItemBillPdfData]); const handlePrint = () => { if (!pdfBlobUrl || !pdfReady || !canvasContainerRef.current || !previewPdf) return; // 创建打印窗口 const printWindow = window.open('', '_blank'); if (!printWindow) return; // 获取所有的 canvas const canvases = canvasContainerRef.current.querySelectorAll('canvas'); // 构建打印页面 printWindow.document.write(` ${previewPdf.pdf_name || '知情同意书打印'}
`); // 将每个 canvas 转换为图片并添加到打印窗口 canvases.forEach((canvas) => { const imgData = (canvas as HTMLCanvasElement).toDataURL('image/png'); printWindow.document.write(``); }); printWindow.document.write(` `); printWindow.document.close(); // 等待图片加载完成后执行打印 printWindow.onload = () => { printWindow.focus(); setTimeout(() => { printWindow.print(); }, 1000); }; }; const handleDirectPrint = async (pdfItem: OutputTongyishuFileInfo) => { if (!pdfItem.pdf_url) return; try { // 加载PDF const response = await fetch(pdfItem.pdf_url); if (!response.ok) throw new Error('获取PDF文件失败'); const blob = await response.blob(); const arrayBuffer = await blob.arrayBuffer(); // 渲染PDF到临时canvas const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; const scale = 1.0; const canvasImages: string[] = []; 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; const renderContext = { canvasContext: context, viewport: viewport, } as any; await page.render(renderContext).promise; canvasImages.push(canvas.toDataURL('image/png')); } // 创建打印窗口 const printWindow = window.open('', '_blank'); if (!printWindow) return; printWindow.document.write(` ${pdfItem.pdf_name || '知情同意书打印'}
`); canvasImages.forEach((imgData) => { printWindow.document.write(``); }); printWindow.document.write(` `); printWindow.document.close(); // 等待图片加载完成后执行打印 printWindow.onload = () => { printWindow.focus(); setTimeout(() => { printWindow.print(); }, 1000); }; } catch (err) { console.error('打印失败', err); alert('打印失败,请稍后重试'); } }; // 导检单签名提交 const handleSubmitDaojiandanSign = async () => { // 有可选项目但尚未确认时,禁止签名并提示先确认项目 if (optionalItemList.length > 0 && !optionalConfirmed) { setDaojiandanSubmitMessage('请先确认体检项目'); setShowOptionalConfirmTip(true); return; } if (!examId) { setDaojiandanSubmitMessage('缺少必要信息,无法提交签名'); return; } const dataUrl = daojiandanSignaturePadRef.current?.toDataURL('image/png'); if (!dataUrl) { setDaojiandanSubmitMessage('请先完成签名'); return; } setDaojiandanSubmitLoading(true); setDaojiandanSubmitMessage(null); try { const blob = await fetch(dataUrl).then((r) => r.blob()); const res = await submitDaojiandanSign({ exam_id: examId, sign_file: blob, }); if (res.Status === 200 && res.Data?.pdf_url) { setDaojiandanSubmitMessage('签名提交成功'); const pdfUrlValue = res.Data.pdf_url; setDaojiandanUrl(pdfUrlValue); setIsDaojiandanSigned(true); // 签名成功后标记为已签名 // 更新导检单打印状态 try { await editDaojiandanPrintStatus({ exam_id: examId }); } catch (err) { console.error('更新导检单打印状态失败', err); } setTimeout(() => { setShowDaojiandanSignature(false); setDaojiandanSubmitMessage(null); daojiandanSignaturePadRef.current?.clear(); setShowDaojiandanPreview(true); }, 500); } else { setDaojiandanSubmitMessage(res.Message || '签名提交失败'); } } catch (err) { console.error('提交签名失败', err); setDaojiandanSubmitMessage('签名提交失败,请稍后重试'); } finally { setDaojiandanSubmitLoading(false); } }; // 加项单签名提交(针对当前选中的加项单) const handleSubmitAddItemBillSign = async () => { if (!examId) { setAddItemBillSubmitMessage('缺少必要信息,无法提交签名'); return; } if (!currentAddItemBill) { setAddItemBillSubmitMessage('没有可签名的加项单'); return; } const dataUrl = addItemBillSignaturePadRef.current?.toDataURL('image/png'); if (!dataUrl) { setAddItemBillSubmitMessage('请先完成签名'); return; } // 记录当前正在签名的加项单,用于签名成功后重新选中它 const justSignedCombinationCode = currentAddItemBill.combinationCode; setAddItemBillSubmitLoading(true); setAddItemBillSubmitMessage(null); try { const blob = await fetch(dataUrl).then((r) => r.blob()); const res = await submitAddItemBillSign({ exam_id: examId, pdf_sort: currentAddItemBill.pdf_sort, combinationCode: currentAddItemBill.combinationCode, sign_file: blob, }); if (res.Status === 200) { setAddItemBillSubmitMessage('签名提交成功'); // 签名成功后刷新统一 PDF 列表,获取最新的加项单签名状态和地址 // 如果还有可选项目未确认,不执行接口请求(使用 ref 确保读取最新值) if (examId && (optionalItemListRef.current.length === 0 || optionalConfirmedRef.current)) { try { const refreshRes = await getTijianPdfFile({ exam_id: examId as number }); const list: OutputTijianPdfFileInfo[] = refreshRes.Data || []; const addItemFromApi = list.filter((item) => item.pdf_type === 3); if (addItemFromApi.length > 0) { const addItemList: AddItemBillItem[] = addItemFromApi.map((item) => ({ // 同样直接使用接口返回的字段,保持与 refreshTijianPdfs 一致 pdf_sort: item.pdf_sort ?? 0, combinationCode: item.combination_code != null ? String(item.combination_code) : '', payment_status: item.is_pay != null ? String(item.is_pay) : null, payment_status_name: item.is_pay_name ?? null, pdf_name: item.pdf_name || '加项单', pdf_url: item.sign_pdf_url || item.pdf_url || '', is_signed: item.is_sign === 1, })); setAddItemBillList(addItemList); // 优先选中刚刚签名的那一张,其次选未签名的,其次第一张 const justSigned = addItemList.find( (bill) => bill.combinationCode === justSignedCombinationCode ); const unsigned = addItemList.find((bill) => bill.is_signed !== true); setCurrentAddItemBill(justSigned || unsigned || addItemList[0] || null); } else { setAddItemBillList([]); setCurrentAddItemBill(null); } } catch (e) { console.error('刷新加项单列表失败', e); } } setTimeout(() => { setShowAddItemBillSignature(false); setAddItemBillSubmitMessage(null); addItemBillSignaturePadRef.current?.clear(); setShowAddItemBillPreview(true); }, 500); } else { setAddItemBillSubmitMessage(res.Message || '签名提交失败'); } } catch (err) { console.error('提交签名失败', err); setAddItemBillSubmitMessage('签名提交失败,请稍后重试'); } finally { setAddItemBillSubmitLoading(false); } }; // 加项单直接打印 const handleAddItemBillDirectPrint = async (target: AddItemBillItem | null) => { if (!target?.pdf_url) return; try { const response = await fetch(target.pdf_url); if (!response.ok) throw new Error('获取PDF文件失败'); const blob = await response.blob(); const arrayBuffer = await blob.arrayBuffer(); const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; const scale = 3.0; const canvasImages: string[] = []; 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; const renderContext = { canvasContext: context, viewport: viewport, } as any; await page.render(renderContext).promise; canvasImages.push(canvas.toDataURL('image/png')); } const printWindow = window.open('', '_blank'); if (!printWindow) return; printWindow.document.write(` ${target.pdf_name || '加项单打印'}
`); canvasImages.forEach((imgData) => { printWindow.document.write(``); }); printWindow.document.write(` `); printWindow.document.close(); printWindow.onload = () => { printWindow.focus(); setTimeout(() => { printWindow.print(); }, 1000); }; } catch (err) { console.error('打印失败', err); alert('打印失败,请稍后重试'); } }; // 导检单直接打印 const handleDaojiandanDirectPrint = async () => { if (!daojiandanUrl) return; try { const response = await fetch(daojiandanUrl); if (!response.ok) throw new Error('获取PDF文件失败'); const blob = await response.blob(); const arrayBuffer = await blob.arrayBuffer(); const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; const scale = 3.0; const canvasImages: string[] = []; 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; const renderContext = { canvasContext: context, viewport: viewport, } as any; await page.render(renderContext).promise; canvasImages.push(canvas.toDataURL('image/png')); } const printWindow = window.open('', '_blank'); if (!printWindow) return; printWindow.document.write(` 导检单打印
`); canvasImages.forEach((imgData) => { printWindow.document.write(``); }); printWindow.document.write(` `); // 在关闭文档前设置 onload 事件 printWindow.onload = () => { printWindow.focus(); setTimeout(() => { printWindow.print(); }, 1000); }; printWindow.document.close(); // 更新导检单打印状态(不阻塞打印流程) if (examId) { editDaojiandanPrintStatus({ exam_id: examId }).catch((err) => { console.error('更新导检单打印状态失败', err); }); } } catch (err) { console.error('打印失败', err); alert('打印失败,请稍后重试'); } }; // 检查所有文档是否都已签名 const checkAllSigned = () => { // 检查所有知情同意书是否都已签名 const allConsentsSigned = consentList.every((item) => { if (item.combination_code === undefined || item.combination_code === null) return true; return signedCombinationCodes.includes(Number(item.combination_code)); }); // 检查导检单是否已签名(只有 localStorage 中的或签名后的才是已签名的) const daojiandanSigned = isDaojiandanSigned; return allConsentsSigned && consentList.length > 0 && daojiandanSigned; }; // 一键打印所有文档 const handleBatchPrint = async () => { if (busy || batchPrintLoading) return; setBatchPrintLoading(true); try { const allImages: string[] = []; const scale = 3.0; // 处理所有已签名的知情同意书 for (const item of consentList) { if (item.combination_code === undefined || item.combination_code === null) continue; if (!signedCombinationCodes.includes(Number(item.combination_code))) continue; const targetUrl = item.pdf_url; if (!targetUrl) continue; try { const response = await fetch(targetUrl); if (!response.ok) continue; const blob = await response.blob(); const arrayBuffer = await blob.arrayBuffer(); const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; 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; const renderContext = { canvasContext: context, viewport: viewport, } as any; await page.render(renderContext).promise; allImages.push(canvas.toDataURL('image/png')); } } catch (err) { console.error(`处理知情同意书 ${item.pdf_name} 失败`, err); } } // 处理导检单 if (daojiandanUrl) { try { const response = await fetch(daojiandanUrl); if (response.ok) { const blob = await response.blob(); const arrayBuffer = await blob.arrayBuffer(); const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; 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; const renderContext = { canvasContext: context, viewport: viewport, } as any; await page.render(renderContext).promise; allImages.push(canvas.toDataURL('image/png')); } } } catch (err) { console.error('处理导检单失败', err); } } // 处理加项单(如果有且已签名) if (addItemBillList.length > 0) { try { for (const bill of addItemBillList) { if (!bill.pdf_url || bill.is_signed !== true) continue; const response = await fetch(bill.pdf_url); if (response.ok) { const blob = await response.blob(); const arrayBuffer = await blob.arrayBuffer(); const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; 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; const renderContext = { canvasContext: context, viewport: viewport, } as any; await page.render(renderContext).promise; allImages.push(canvas.toDataURL('image/png')); } } } } catch (err) { console.error('处理加项单失败', err); } } if (allImages.length === 0) { alert('没有可打印的内容'); return; } // 创建打印窗口 const printWindow = window.open('', '_blank'); if (!printWindow) { alert('无法打开打印窗口,请检查浏览器弹窗设置'); return; } printWindow.document.write(` 一键打印
`); allImages.forEach((imgData) => { printWindow.document.write(``); }); printWindow.document.write(` `); printWindow.document.close(); // 如果包含导检单,更新导检单打印状态 if (examId && daojiandanUrl) { try { await editDaojiandanPrintStatus({ exam_id: examId }); } catch (err) { console.error('更新导检单打印状态失败', err); // 不阻塞打印流程,仅记录错误 } } // 等待图片加载完成后自动弹出打印窗口 printWindow.onload = () => { printWindow.focus(); setTimeout(() => { printWindow.print(); }, 1000); }; } catch (err) { console.error('一键打印失败', err); alert('一键打印失败,请稍后重试'); } finally { setBatchPrintLoading(false); } }; return (
身份证拍照与签到
拍照身份证后点击签到按钮完成签到。
{previewImage && (
setShowImagePreview(true)} > 身份证预览
)}
{message && (
{message}
)} {/* 可选项目列表 */} {optionalItemLoading ? (
加载可选项目...
) : optionalItemList.length > 0 ? (
可选项目(必选其一){optionalConfirmed ? '(已确定)' : ''}
{optionalItemList.map((item) => { const combinationCode = item.combination_code; const isSelected = combinationCode != null && combinationCode === selectedOptionalItem; return ( ); })}
{!optionalConfirmed && (
{showOptionalConfirmTip && ( 请先确认体检项目,确认后不可修改 )}
)}
) : null}
{showImagePreview && previewImage && (
setShowImagePreview(false)}>
身份证预览
)}
体检知情同意书
{checkAllSigned() && ( )}
{/*
点击后弹出知情同意书全文及签名区域,签署完成后回到签到页面。
*/}
{consentLoading &&
加载中...
} {/* {!consentLoading && consentMessage &&
{consentMessage}
} */} {!consentLoading && (consentList.length > 0 || true) && (
{consentList.map((item) => (
{item.pdf_name.length > 10 ? item.pdf_name.slice(0, 10) + "..." : item.pdf_name} {item.combination_code !== undefined && signedCombinationCodes.includes(Number(item.combination_code)) && ( 已签名 )}
{item.combination_code !== undefined && signedCombinationCodes.includes(Number(item.combination_code)) && ( )}
))} {/* 只有在没有可选项目,或者有可选项目但已确认时,才显示导检单 */} {(optionalItemList.length === 0 || optionalConfirmed) && (
导检单 {isDaojiandanSigned && ( 已签名 )}
{isDaojiandanSigned ? ( // 已签名:显示打印和查看按钮 <> ) : daojiandanUrl ? ( // 未签名但有导检单:显示查看和签名按钮 <> ) : ( // 没有导检单:只显示签名按钮 )}
)} {/* 加项单列表(可能有多个) */} {addItemBillList.length > 0 && addItemBillList.map((bill) => { const isSigned = bill.is_signed === true; const displayName = bill.pdf_name && bill.pdf_name.length > 12 ? bill.pdf_name.slice(0, 12) + '...' : bill.pdf_name || '加项单'; return (
{displayName} {typeof bill.pdf_sort === 'number' ? `` : ''} {bill.payment_status_name && ( {bill.payment_status_name} )} {isSigned && ( 已签名 )}
{isSigned && bill.pdf_url && ( )} {bill.pdf_url && ( )} {!isSigned && ( )}
); })}
)}
{ previewPdf && (
{previewPdf.pdf_name}
{pdfLoading ? (
正在加载PDF...
) : pdfData ? (
) : (
PDF加载失败
)}
) } { showSignature && (
签署知情同意书
请在上方区域签名完成签到
{submitMessage && (
{submitMessage}
)}
) } { showDaojiandanSignature && (
签署导检单
请在上方区域签名
{daojiandanSubmitMessage && (
{daojiandanSubmitMessage}
)}
) } { showDaojiandanPreview && daojiandanUrl && (
导检单
{daojiandanPdfLoading ? (
正在加载PDF...
) : daojiandanPdfData ? (
) : (
PDF加载失败
)}
) } { showAddItemBillSignature && (
签署加项单
请在上方区域签名
{addItemBillSubmitMessage && (
{addItemBillSubmitMessage}
)}
) } { showAddItemBillPreview && currentAddItemBill && currentAddItemBill.pdf_url && (
{currentAddItemBill.pdf_name || '加项单'}
{currentAddItemBill.is_signed && ( )}
{addItemBillPdfLoading ? (
正在加载PDF...
) : addItemBillPdfData ? (
) : (
PDF加载失败
)}
) }
); };