import { useEffect, useRef, useState } from 'react'; import type { OutputTongyishuFileInfo } from '../../api'; import { getTongyishuPdf, signInMedicalExamCenter, submitTongyishuSign } from '../../api'; import { setExamActionRecord, setTongyishuPdfList, getTongyishuPdfList, type TongyishuPdfInfo, } from '../../utils/examActions'; import type { SignaturePadHandle } from '../ui'; import { Button, SignaturePad } from '../ui'; 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 busy = signLoading || submitLoading || consentLoading; 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, })); 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); getTongyishuPdf({ exam_id: examId }) .then((res) => { const list = res.Data?.list_pdf_url || []; // 先拉取接口返回的全部 list_pdf_url,再用本地已保存的已签名 PDF 覆盖 let mergedList = list; const storedList = getTongyishuPdfList(examId); 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, ); if (matched && matched.pdf_url) { 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]); return (
身份证拍照与签到
拍照身份证后点击签到按钮完成签到。
{previewImage && (
setShowImagePreview(true)} > 身份证预览
)}
{message && (
{message}
)}
{showImagePreview && previewImage && (
setShowImagePreview(false)}>
身份证预览
)}
体检知情同意书
点击后弹出知情同意书全文及签名区域,签署完成后回到签到页面。
{consentLoading &&
加载中...
} {!consentLoading && consentMessage &&
{consentMessage}
} {!consentLoading && consentList.length > 0 && (
{consentList.map((item) => (
{item.pdf_name} {item.combination_code !== undefined && signedCombinationCodes.includes(Number(item.combination_code)) && ( 已签名 )}
))}
)}
{previewPdf && (
{previewPdf.pdf_name}