diff --git a/src/components/exam/ExamModal.tsx b/src/components/exam/ExamModal.tsx index a8f0a6c..b707f52 100644 --- a/src/components/exam/ExamModal.tsx +++ b/src/components/exam/ExamModal.tsx @@ -1,13 +1,13 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useState } from 'react'; import type { ExamClient, ExamModalTab } from '../../data/mockData'; -import type { CustomerAppointmentInfo, CustomerExamAddItem, CustomerInfo, OutputTongyishuFileInfo, PhysicalExamProgressItem } from '../../api'; -import { getCustomerDetail, getPhysicalExamProgressDetail, getTongyishuPdf, signInMedicalExamCenter, submitTongyishuSign } from '../../api'; -import { Button, SignaturePad, type SignaturePadHandle } from '../ui'; +import type { CustomerAppointmentInfo, CustomerExamAddItem, CustomerInfo, PhysicalExamProgressItem } from '../../api'; +import { getCustomerDetail, getPhysicalExamProgressDetail } from '../../api'; import { ExamDetailPanel } from './ExamDetailPanel'; import { ExamAddonPanel } from './ExamAddonPanel'; import { ExamPrintPanel } from './ExamPrintPanel'; import { ExamDeliveryPanel } from './ExamDeliveryPanel'; +import { ExamSignPanel } from './ExamSignPanel'; interface ExamModalProps { client: ExamClient; @@ -54,6 +54,7 @@ export const ExamModal = ({ client, tab, onTabChange, onClose }: ExamModalProps) const [appointmentInfo, setAppointmentInfo] = useState(null); const [addItemInfoList, setAddItemInfoList] = useState(null); const [progressList, setProgressList] = useState(null); + const [signBusy, setSignBusy] = useState(false); useEffect(() => { const physical_exam_id = Number(client.id); @@ -88,16 +89,12 @@ export const ExamModal = ({ client, tab, onTabChange, onClose }: ExamModalProps) onDoubleClick={handleDoubleClick} onTouchStart={handleTouchStart} > - - {/* Header 区域:增加了内边距 padding */}
- {/* 左侧:姓名 + VIP + 体检号 */}
{client.name} - {/* VIP 徽章样式优化:紫色背景 + 左右内边距 */} VIP @@ -105,18 +102,16 @@ export const ExamModal = ({ client, tab, onTabChange, onClose }: ExamModalProps) 体检号:{client.id}
- - {/* 右侧:仅保留关闭按钮 */}
- {/* Tabs 区域 */}
{tabs.map((t) => { @@ -126,30 +121,24 @@ export const ExamModal = ({ client, tab, onTabChange, onClose }: ExamModalProps) ); })}
- - {/* 按钮部分:将原本在 Tab 右侧的任何操作可以放这里,或者留空 */}
@@ -163,402 +152,13 @@ export const ExamModal = ({ client, tab, onTabChange, onClose }: ExamModalProps) loading={detailLoading} /> )} - {tab === 'sign' && } + {tab === 'sign' && } {tab === 'addon' && } {tab === 'print' && } {tab === 'delivery' && }
- ); }; -const ExamSignPanel = ({ examId }: { examId?: number }) => { - 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 SIGN_STORAGE_KEY = 'yh_signed_consents'; - - 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 // 质量 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 { - // 转换为 JPG 格式 - 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; - setMessage(ok ? '签到成功' : 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 { - // 将 base64 转换为 Blob - 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 || []; - const next = Array.from(new Set([...(prev || []), code])); - if (typeof window !== 'undefined') { - localStorage.setItem(SIGN_STORAGE_KEY, JSON.stringify(next)); - } - return next; - }); - // 2秒后关闭弹窗 - 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 || []; - setConsentList(list); - 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}
-
- - -
-
-
-