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 } from '../../api'; import { signInMedicalExamCenter, submitTongyishuSign, submitDaojiandanSign, editDaojiandanPrintStatus, submitAddItemBillSign, getTijianPdfFile } 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 [consentMessage, setConsentMessage] = useState(null); 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 [daojiandanPdfReady, setDaojiandanPdfReady] = useState(false); const [daojiandanPdfBlobUrl, setDaojiandanPdfBlobUrl] = useState(null); 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 [addItemBillPdfReady, setAddItemBillPdfReady] = useState(false); const [addItemBillPdfBlobUrl, setAddItemBillPdfBlobUrl] = useState(null); const addItemBillCanvasContainerRef = useRef(null); const busy = signLoading || submitLoading || consentLoading || pdfLoading || daojiandanSubmitLoading || addItemBillSubmitLoading; useEffect(() => { onBusyChange?.(busy); return () => onBusyChange?.(false); }, [busy, onBusyChange]); const refreshTijianPdfs = async (examIdValue: number) => { setConsentLoading(true); setConsentMessage(null); 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); if (!mappedConsent.length) { setConsentMessage(res.Message || '暂无知情同意书'); } // 导检单(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 { setDaojiandanUrl(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: item.combination_code ?? 0, combinationCode: 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); setConsentMessage('知情同意书加载失败,请稍后重试'); } finally { setConsentLoading(false); } }; // 初始化加载体检 PDF 列表(导检单、知情同意书、加项单) useEffect(() => { if (!examId) { setConsentList([]); setConsentMessage('缺少体检ID,无法获取知情同意书'); setDaojiandanUrl(null); setIsDaojiandanSigned(false); return; } refreshTijianPdfs(examId); }, [examId]); const handlePickFile = () => { 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; } 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 handleSubmitSign = async () => { 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 列表) if (examId) { refreshTijianPdfs(examId); } }, 2000); } 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); setDaojiandanPdfBlobUrl(null); setDaojiandanPdfReady(false); return; } let objectUrl: string | null = null; setDaojiandanPdfReady(false); setDaojiandanPdfLoading(true); setDaojiandanPdfData(null); fetch(daojiandanUrl) .then((resp) => { if (!resp.ok) throw new Error('获取PDF文件失败'); return resp.blob(); }) .then((blob) => { objectUrl = URL.createObjectURL(blob); setDaojiandanPdfBlobUrl(objectUrl); return blob.arrayBuffer(); }) .then((arrayBuffer) => { setDaojiandanPdfData(arrayBuffer); setDaojiandanPdfLoading(false); }) .catch((err) => { console.error('导检单PDF 拉取失败', err); setDaojiandanPdfLoading(false); }); return () => { if (objectUrl) { URL.revokeObjectURL(objectUrl); } }; }, [showDaojiandanPreview, daojiandanUrl]); // 渲染导检单 PDF useEffect(() => { if (!daojiandanPdfData || !daojiandanCanvasContainerRef.current) return; setDaojiandanPdfReady(false); 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; } setDaojiandanPdfReady(true); } catch (err) { console.error('导检单PDF 渲染失败', err); setDaojiandanPdfLoading(false); } }; renderAllPages(); }, [daojiandanPdfData]); // 加载加项单 PDF 数据(当前选中的加项单) useEffect(() => { if (!showAddItemBillPreview || !currentAddItemBill?.pdf_url) { setAddItemBillPdfData(null); setAddItemBillPdfBlobUrl(null); setAddItemBillPdfReady(false); return; } let objectUrl: string | null = null; setAddItemBillPdfReady(false); 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); setAddItemBillPdfBlobUrl(objectUrl); 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]); // 渲染加项单 PDF useEffect(() => { if (!addItemBillPdfData || !addItemBillCanvasContainerRef.current) return; setAddItemBillPdfReady(false); 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; } setAddItemBillPdfReady(true); } catch (err) { console.error('加项单PDF 渲染失败', err); setAddItemBillPdfLoading(false); } }; renderAllPages(); }, [addItemBillPdfData]); // 导检单预览:使用 pdfjs 渲染到 canvas useEffect(() => { if (!showDaojiandanPreview || !daojiandanUrl || !daojiandanCanvasContainerRef.current) return; const container = daojiandanCanvasContainerRef.current; let cancelled = false; const renderDaojiandan = async () => { try { setDaojiandanPdfLoading(true); 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) { setDaojiandanPdfLoading(false); } } }; 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 (!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; const pdfNameValue = res.Data.pdf_name || '导检单'; 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); }, 2000); } 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; } 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 列表,获取最新的加项单签名状态和地址 if (examId) { 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) => ({ pdf_sort: item.combination_code ?? 0, combinationCode: 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 (e) { console.error('刷新加项单列表失败', e); } } setTimeout(() => { setShowAddItemBillSignature(false); setAddItemBillSubmitMessage(null); addItemBillSignaturePadRef.current?.clear(); setShowAddItemBillPreview(true); }, 2000); } 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) return; 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('一键打印失败,请稍后重试'); } }; return (
身份证拍照与签到
拍照身份证后点击签到按钮完成签到。
{previewImage && (
setShowImagePreview(true)} > 身份证预览
)}
{message && (
{message}
)}
{showImagePreview && previewImage && (
setShowImagePreview(false)}>
身份证预览
)}
体检知情同意书
{checkAllSigned() && ( )}
{/*
点击后弹出知情同意书全文及签名区域,签署完成后回到签到页面。
*/}
{consentLoading &&
加载中...
} {!consentLoading && consentMessage &&
{consentMessage}
} {!consentLoading && (consentList.length > 0 || true) && (
{consentList.map((item) => (
{item.pdf_name.length > 12 ? item.pdf_name.slice(0, 12) + "..." : item.pdf_name} {item.combination_code !== undefined && signedCombinationCodes.includes(Number(item.combination_code)) && ( 已签名 )}
{item.combination_code !== undefined && signedCombinationCodes.includes(Number(item.combination_code)) && ( )}
))}
导检单 {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.pdf_sort})` : ''} {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加载失败
)}
) }
); };