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 } from '../../api'; import { getTongyishuPdf, signInMedicalExamCenter, submitTongyishuSign, submitDaojiandanSign, editDaojiandanPrintStatus, getDaojiandanPdf as getDaojiandanPdfApi, getAddItemBillPdf as getAddItemBillPdfApi, submitAddItemBillSign } from '../../api'; import { setExamActionRecord, setTongyishuPdfList, getTongyishuPdfList, setDaojiandanPdf, getDaojiandanPdf as getDaojiandanPdfFromStorage, setAddItemBillPdf, getAddItemBillPdf as getAddItemBillPdfFromStorage, type TongyishuPdfInfo, type AddItemBillPdfInfo, } from '../../utils/examActions'; 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 [addItemBillList, setAddItemBillList] = useState([]); const [currentAddItemBill, setCurrentAddItemBill] = useState(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); 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 SIGN_STORAGE_KEY = `yh_signed_consents_${new Date().toISOString().slice(0, 10)}`; const handlePickFile = () => { fileInputRef.current?.click(); }; useEffect(() => { if (typeof window === 'undefined') return; const raw = localStorage.getItem(SIGN_STORAGE_KEY); if (raw) { try { const parsed = JSON.parse(raw); if (Array.isArray(parsed)) { setSignedCombinationCodes(parsed.filter((x) => typeof x === 'number')); } } catch (err) { console.warn('签名记录解析失败', err); } } }, []); 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('签到成功'); // 记录身份证拍照与签到操作 if (examId) { setExamActionRecord(examId, 'idCardSignIn', true); } } 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('签名提交成功'); // 记录体检知情同意书的签字操作 if (examId) { setExamActionRecord(examId, 'consentSign', true); } // 存储返回的PDF列表(标记为已签名) if (res.Data?.list_pdf_url && Array.isArray(res.Data.list_pdf_url) && examId) { const pdfList: TongyishuPdfInfo[] = res.Data.list_pdf_url.map((item) => ({ pdf_name: item.pdf_name || '', pdf_url: item.pdf_url || '', combination_code: item.combination_code ?? null, is_signed: true, })); setTongyishuPdfList(examId, pdfList); } setSignedCombinationCodes((prev) => { const code = Number(previewPdf.combination_code); if (!Number.isFinite(code)) return prev || []; const next = Array.from(new Set([...(prev || []), code])); if (typeof window !== 'undefined') { localStorage.setItem(SIGN_STORAGE_KEY, JSON.stringify(next)); } return next; }); setTimeout(() => { setShowSignature(false); setPreviewPdf(null); setSubmitMessage(null); signaturePadRef.current?.clear(); }, 2000); } else { setSubmitMessage(res.Message || '签名提交失败'); } } catch (err) { console.error('提交签名失败', err); setSubmitMessage('签名提交失败,请稍后重试'); } finally { setSubmitLoading(false); } }; useEffect(() => { if (!examId) { setConsentList([]); setConsentMessage('缺少体检ID,无法获取知情同意书'); return; } setConsentLoading(true); setConsentMessage(null); // 先检查 localStorage 中是否有已签名的知情同意书 const storedList = getTongyishuPdfList(examId); const allSigned = storedList && storedList.length > 0 && storedList.every((pdf) => pdf.is_signed === true); // 如果所有知情同意书都已签名,直接使用 localStorage 中的数据,不请求接口 if (allSigned && storedList) { const mergedList = storedList.map((pdf) => ({ pdf_name: pdf.pdf_name, pdf_url: pdf.pdf_url, combination_code: pdf.combination_code ?? null, })); setConsentList(mergedList); setConsentLoading(false); return; } // 如果有未签名的,请求接口获取最新列表 getTongyishuPdf({ exam_id: examId }) .then((res) => { const list = res.Data?.list_pdf_url || []; // 合并接口返回的数据和本地已保存的已签名 PDF let mergedList = list; if (storedList && storedList.length > 0) { mergedList = list.map((item) => { if (item.combination_code === undefined || item.combination_code === null) return item; const code = Number(item.combination_code); if (!Number.isFinite(code)) return item; const matched = storedList.find( (pdf) => pdf.combination_code !== null && Number(pdf.combination_code) === code && pdf.is_signed === true, ); // 如果本地有已签名的版本,优先使用 if (matched && matched.pdf_url && matched.is_signed === true) { return { ...item, pdf_url: matched.pdf_url, pdf_name: matched.pdf_name || item.pdf_name, }; } return item; }); } setConsentList(mergedList); if (!list.length) { setConsentMessage(res.Data?.message || '暂无知情同意书'); } }) .catch((err) => { console.error('获取知情同意书失败', err); setConsentMessage('知情同意书加载失败,请稍后重试'); }) .finally(() => setConsentLoading(false)); }, [examId]); // 组件加载时检查导检单localStorage,如果没有则调用接口获取未签名的导检单 useEffect(() => { if (!examId) return; // 先检查 localStorage 中的导检单 const storedPdf = getDaojiandanPdfFromStorage(examId); if (storedPdf && storedPdf.pdf_url) { setDaojiandanUrl(storedPdf.pdf_url); // 使用 localStorage 中保存的 is_signed 字段判断是否已签名 setIsDaojiandanSigned(storedPdf.is_signed === true); } else { // 如果 localStorage 中没有导检单,调用接口获取未签名的导检单用于查看和签名 const fetchDaojiandan = async () => { try { const res = await getDaojiandanPdfApi({ exam_id: examId }); if (res.Status === 200 && res.Data?.pdf_url) { const pdfUrlValue = res.Data.pdf_url; // 不保存到 localStorage,只用于显示和签名 setDaojiandanUrl(pdfUrlValue); setIsDaojiandanSigned(false); // 接口获取的是未签名的 } } catch (err) { console.error('获取导检单失败', err); } }; fetchDaojiandan(); } // 检查加项单PDF(可能有多个) const storedAddItemBillList = getAddItemBillPdfFromStorage(examId); if (storedAddItemBillList && storedAddItemBillList.length > 0) { setAddItemBillList(storedAddItemBillList); // 默认选中第一个未签名的加项单,如果都已签名则选中第一个 const unsigned = storedAddItemBillList.find((item) => item.is_signed !== true); setCurrentAddItemBill(unsigned || storedAddItemBillList[0]); } else { // 没有加项单:不再主动调用接口获取,因为新接口需要 CombinationCode(仅支付时知道) setAddItemBillList([]); setCurrentAddItemBill(null); } }, [examId]); // 加载 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); // 签名成功后标记为已签名 // 保存导检单PDF信息到localStorage(标记为已签名) setDaojiandanPdf(examId, { pdf_name: pdfNameValue, pdf_url: pdfUrlValue, is_signed: true, }); // 记录打印导检单是否签名操作 setExamActionRecord(examId, 'printSign', 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 && res.Data?.pdf_url) { setAddItemBillSubmitMessage('签名提交成功'); const pdfUrlValue = res.Data.pdf_url; const pdfNameValue = res.Data.pdf_name || '加项单'; // 更新当前加项单为已签名,并更新本地列表与 localStorage setAddItemBillList((prev) => { const next = prev.map((item) => { if (item.pdf_sort === currentAddItemBill.pdf_sort) { const updated: AddItemBillPdfInfo = { ...item, pdf_name: pdfNameValue, pdf_url: pdfUrlValue, payment_status: res.Data?.payment_status ?? item.payment_status ?? null, payment_status_name: res.Data?.payment_status_name ?? item.payment_status_name ?? null, is_signed: true, }; // 同步写入 localStorage setAddItemBillPdf(examId, updated); return updated; } return item; }); return next; }); setCurrentAddItemBill((prev) => prev && prev.pdf_sort === currentAddItemBill.pdf_sort ? { ...prev, pdf_name: pdfNameValue, pdf_url: pdfUrlValue, payment_status: res.Data?.payment_status ?? prev.payment_status ?? null, payment_status_name: res.Data?.payment_status_name ?? prev.payment_status_name ?? null, is_signed: true, } : prev ); 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: AddItemBillPdfInfo | 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; let targetUrl = item.pdf_url; // 如果本地已保存签名后的PDF列表,则优先使用签名后的PDF地址 if (examId) { const storedList = getTongyishuPdfList(examId); if (storedList && item.combination_code !== undefined && item.combination_code !== null) { const code = Number(item.combination_code); const matched = storedList.find( (pdf) => pdf.combination_code !== null && Number(pdf.combination_code) === code, ); if (matched && matched.pdf_url) { targetUrl = matched.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 isCurrent = currentAddItemBill && currentAddItemBill.pdf_sort === bill.pdf_sort; 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 && ( )} {!currentAddItemBill.is_signed && ( )}
{addItemBillPdfLoading ? (
正在加载PDF...
) : addItemBillPdfData ? (
) : (
PDF加载失败
)}
) }
); };