import { useEffect, useMemo, useRef, useState } from 'react'; import type { ExamClient, ExamModalTab } from '../../data/mockData'; import type { CustomerAppointmentInfo, CustomerExamAddItem, CustomerInfo, PhysicalExamProgressItem, } from '../../api'; import { getCustomerDetail, getPhysicalExamProgressDetail, signInMedicalExamCenter, getTongyishuPdf } from '../../api'; import { Button, Input } from '../ui'; interface ExamModalProps { client: ExamClient; tab: ExamModalTab; onTabChange: (key: ExamModalTab) => void; onClose: () => void; } export const ExamModal = ({ client, tab, onTabChange, onClose }: ExamModalProps) => { const tabs: { key: ExamModalTab; label: string }[] = [ { key: 'detail', label: '详情' }, { key: 'sign', label: '签到' }, { key: 'addon', label: '加项' }, { key: 'print', label: '打印导检单' }, { key: 'delivery', label: '报告寄送' }, ]; const signDone = client.signStatus === '已登记' || client.checkedItems.includes('签到'); const addonDone = (client.addonCount || 0) > 0; const printDone = !!client.guidePrinted; const deliveryDone = !!client.deliveryDone; const tabDone: Record = { detail: false, sign: signDone, addon: addonDone, print: printDone, delivery: deliveryDone, }; const handleDoubleClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); }; const handleTouchStart = (e: React.TouchEvent) => { if (e.touches.length > 1) { e.preventDefault(); } }; const [detailLoading, setDetailLoading] = useState(false); const [customerInfo, setCustomerInfo] = useState(null); const [appointmentInfo, setAppointmentInfo] = useState(null); const [addItemInfoList, setAddItemInfoList] = useState(null); const [progressList, setProgressList] = useState(null); useEffect(() => { const physical_exam_id = Number(client.id); if (!physical_exam_id) return; setDetailLoading(true); Promise.all([ getCustomerDetail({ physical_exam_id }), getPhysicalExamProgressDetail({ physical_exam_id }), ]) .then(([detailRes, progressRes]) => { setCustomerInfo(detailRes.Data?.customerInfo ?? null); setAppointmentInfo(detailRes.Data?.appointmentInfo ?? null); setAddItemInfoList(detailRes.Data?.addItemInfoList ?? null); setProgressList(progressRes.Data?.examProgressesList ?? null); }) .catch((err) => { console.error('获取客户详情/进度失败', err); }) .finally(() => setDetailLoading(false)); }, [client.id]); return (
{/* Header 区域:增加了内边距 padding */}
{/* 左侧:姓名 + VIP + 体检号 */}
{client.name} {/* VIP 徽章样式优化:紫色背景 + 左右内边距 */} VIP 体检号:{client.id}
{/* 右侧:仅保留关闭按钮 */}
{/* Tabs 区域 */}
{tabs.map((t) => { const isActive = tab === t.key; const isDone = tabDone[t.key]; return ( ); })}
{/* 按钮部分:将原本在 Tab 右侧的任何操作可以放这里,或者留空 */}
{tab === 'detail' && ( )} {tab === 'sign' && } {tab === 'addon' && } {tab === 'print' && } {tab === 'delivery' && }
); }; const ExamSignPanel = () => { const [idNo, setIdNo] = useState(''); const [ocrLoading, setOcrLoading] = useState(false); const [signLoading, setSignLoading] = useState(false); const [message, setMessage] = useState(null); const fileInputRef = useRef(null); const handlePickFile = () => { fileInputRef.current?.click(); }; const mockOcr = async (file: File) => { // 简单模拟 OCR:提取文件名中的数字,或返回示例身份证 const match = file.name.match(/\d{6,18}/); await new Promise((r) => setTimeout(r, 600)); return match?.[0] || '440101199001010010'; }; const handleFileChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setOcrLoading(true); setMessage(null); try { const ocrId = await mockOcr(file); setIdNo(ocrId); } catch (err) { console.error(err); setMessage('OCR 识别失败,请重试或手动输入'); } finally { setOcrLoading(false); e.target.value = ''; } }; const handleSign = async () => { const trimmed = idNo.trim(); if (!trimmed) { setMessage('请输入身份证号'); return; } setSignLoading(true); setMessage(null); try { const res = await signInMedicalExamCenter({ id_no: trimmed }); const ok = res.Status === 200 && res.Data?.is_success === 0; setMessage(ok ? '签到成功' : res.Message || '签到失败'); } catch (err) { console.error(err); setMessage('签到请求失败,请稍后重试'); } finally { setSignLoading(false); } }; return (
身份证扫描与签到
支持扫描身份证识别证号,确认后再点击签到。
setIdNo(e.target.value)} className='flex-1' />
{message &&
{message}
}
如识别不准,可手动修改后再签到。
体检知情同意书
点击后弹出知情同意书全文及签名区域,签署完成后回到签到页面。
); }; interface AddonTag { title: string; type: 1 | 2 | 3 | 4; // 1: 热门(红), 2: 普通(灰), 3: 医生推荐(蓝), 4: 折扣信息 } interface AddonItem { id?: string; name: string; paid?: boolean; tags?: AddonTag[]; originalPrice?: string; currentPrice?: string; price?: number; // 兼容 addonOptions 结构 } const ExamAddonPanel = ({ client }: { client: ExamClient }) => { // 从 client 获取加项选项数据 const addonOptions = (client['addonOptions' as keyof ExamClient] as AddonItem[] | undefined) || []; const addonSummary = (client['addonSummary' as keyof ExamClient] as AddonItem[] | undefined) || []; // 合并数据,优先使用 addonOptions,如果没有则使用 addonSummary const allAddons: AddonItem[] = addonOptions.length > 0 ? addonOptions.map(item => ({ id: item.id || `addon_${item.name}`, name: item.name, paid: item.paid || false, tags: item.tags || [], originalPrice: item.originalPrice || (item.price ? item.price.toFixed(2) : '0.00'), currentPrice: item.currentPrice || (item.price ? item.price.toFixed(2) : '0.00'), })) : addonSummary; const [selectedIds, setSelectedIds] = useState>( new Set(allAddons.filter(item => item.paid).map(item => item.id || item.name)) ); const maxSelect = 15; const selectedCount = selectedIds.size; const [addonSearch, setAddonSearch] = useState(''); const filteredAddons = addonSearch.trim() ? allAddons.filter(item => item.name.toLowerCase().includes(addonSearch.toLowerCase()) ) : allAddons; const toggleSelect = (id: string) => { if (selectedIds.has(id)) { setSelectedIds(prev => { const next = new Set(prev); next.delete(id); return next; }); } else { if (selectedCount < maxSelect) { setSelectedIds(prev => new Set(prev).add(id)); } } }; // 计算价格汇总 const selectedItems = allAddons.filter(item => selectedIds.has(item.id || item.name)); const totalOriginal = selectedItems.reduce((sum, item) => { return sum + parseFloat(item.originalPrice || item.currentPrice || '0'); }, 0); const totalCurrent = selectedItems.reduce((sum, item) => { return sum + parseFloat(item.currentPrice || item.originalPrice || '0'); }, 0); const discount = totalOriginal - totalCurrent; // 获取标签样式 const getTagStyle = (tag: AddonTag) => { switch (tag.type) { case 1: // 热门 return 'bg-[#FDF0F0] text-[#BC4845]'; case 3: // 医生推荐 return 'bg-[#ECF0FF] text-[#6A6AE5]'; case 4: // 折扣信息 return 'bg-[#4C5460] text-[#F1F2F5]'; default: // 2: 普通 return 'bg-[#F1F2F5] text-[#464E5B]'; } }; // 获取折扣信息文字(从 tags 中提取 type 4 的标签,或计算折扣) const getDiscountText = (item: AddonItem) => { const discountTag = item.tags?.find(t => t.type === 4); if (discountTag) return discountTag.title; const orig = parseFloat(item.originalPrice || '0'); const curr = parseFloat(item.currentPrice || '0'); if (orig > 0 && curr < orig) { const percent = Math.round((curr / orig) * 100); return `渠道 ${percent} 折`; } return '渠道价'; }; return (
{/* 标题和说明 */}

体检套餐加项选择

setAddonSearch(e.target.value)} className='text-sm' />
最多可选 {maxSelect} 项 · 一排 5 个
已勾选 {selectedCount} 项,自费加项费用按渠道折扣价结算。
{/* 加项网格 */}
{filteredAddons.map((item) => { const id = item.id || item.name; const isSelected = selectedIds.has(id); const origPrice = parseFloat(item.originalPrice || '0'); const currPrice = parseFloat(item.currentPrice || '0'); return (
toggleSelect(id)} > {/* 复选框 */}
toggleSelect(id)} onClick={(e) => e.stopPropagation()} className='mt-0.5 w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500' />
{/* 项目名称 */}
{item.name}
{/* 标签 */} {item.tags && item.tags.length > 0 && (
{item.tags .filter(t => t.type !== 4) // 折扣信息单独显示 .map((tag, idx) => ( {tag.title} ))}
)}
{/* 价格信息 */}
{origPrice > 0 && origPrice > currPrice && ( ¥{origPrice.toFixed(0)} )}
¥{currPrice.toFixed(0)} {getDiscountText(item)}
); })}
{/* 底部汇总和支付 */}
加项原价合计: ¥{totalOriginal.toFixed(0)}
渠道折扣价: ¥{totalCurrent.toFixed(0)} {discount > 0 && ( 已优惠 ¥{discount.toFixed(0)} )}
结算方式: 个人支付 (微信 / 支付宝)
); }; const ExamDetailInfo = ({ client, customerInfo, appointmentInfo, addItemInfoList, progressList, loading, }: { client: ExamClient; customerInfo: CustomerInfo | null; appointmentInfo: CustomerAppointmentInfo | null; addItemInfoList: CustomerExamAddItem[] | null; progressList: PhysicalExamProgressItem[] | null; loading: boolean; }) => { const basePhone = customerInfo?.phone || (client['mobile' as keyof ExamClient] as string | undefined) || ''; const baseMarital = customerInfo?.patient_marital_status_name || (client['maritalStatus' as keyof ExamClient] as string | undefined) || '—'; const [phone, setPhone] = useState(basePhone || '—'); const [marital, setMarital] = useState(baseMarital); const [phoneEditing, setPhoneEditing] = useState(false); const [maritalEditing, setMaritalEditing] = useState(false); const customerChannel = client.customerType === '团客' ? '团体客户' : '散客客户'; const familyDoctor = customerInfo?.family_doctor_name || (client['familyDoctor' as keyof ExamClient] as string | undefined) || '—'; const groupTag = client['groupTag' as keyof ExamClient] || (client.customerType === '团客' ? '团检' : '—'); const bookingTime = appointmentInfo?.appointment_time || (client['bookingTime' as keyof ExamClient] || '—'); const signTime = appointmentInfo?.sign_in_time || (client['signTime' as keyof ExamClient] || '—'); const addonSummary = addItemInfoList && addItemInfoList.length > 0 ? addItemInfoList.map((i) => `${i.dept_name ?? ''} ${i.combination_name ?? ''}`.trim()).join('、') : client['addonSummary' as keyof ExamClient] || '—'; const progressGroups = useMemo(() => { const checked: string[] = []; const abandoned: string[] = []; const pending: string[] = []; const deferred: string[] = []; (progressList || []).forEach((p) => { const name = p.project_name || p.department_name || '项目'; switch (p.exam_status) { case 1: checked.push(name); break; case 2: abandoned.push(name); break; case 4: deferred.push(name); break; default: pending.push(name); } }); return { checked, abandoned, pending, deferred }; }, [progressList]); return (
头像
{loading ? '加载中…' : '基础信息:头像、姓名、证件号、手机号等(点击图标可进行编辑)'}
基础信息
姓名:{customerInfo?.customer_name || client.name}
证件号:{customerInfo?.id_no || '—'}
手机号: {!phoneEditing ? ( {phone} ) : ( setPhone(e.target.value)} /> )}
性别/年龄: {customerInfo?.gender_name || client.gender} / {client.age}
客户级别:{client.level}
所属渠道:{customerChannel}
婚姻状况: {!maritalEditing ? ( {marital} ) : ( setMarital(e.target.value)} /> )}
家医:{familyDoctor}
团标签:{groupTag as string}
预约信息
预约时间:{bookingTime as string}
签到时间:{signTime as string}
已消耗时长: {appointmentInfo?.physical_exam_complete_time && appointmentInfo?.sign_in_time ? `${appointmentInfo.physical_exam_complete_time} - ${appointmentInfo.sign_in_time}` : client.elapsed}
体检套餐名称:{client.packageName}
加项内容:{addonSummary as string}
已查项目 共 {progressGroups.checked.length} 项
{(progressGroups.checked.length ? progressGroups.checked : client.checkedItems).map((i) => ( {i} ))}
弃检项目 共 {progressGroups.abandoned.length} 项
{progressGroups.abandoned.length ? ( progressGroups.abandoned.map((i) => ( {i} )) ) : ( 暂无弃检项目 )}
未查项目 共 {progressGroups.pending.length} 项
{(progressGroups.pending.length ? progressGroups.pending : client.pendingItems).map((i) => ( {i} ))}
延期项目 共 {progressGroups.deferred.length} 项
{progressGroups.deferred.length ? ( progressGroups.deferred.map((i) => ( {i} )) ) : ( 暂无延期项目 )}
); }; const ExamDeliveryPanel = ({ client }: { client: ExamClient }) => (
报告寄送
收件人姓名
联系电话
寄送地址
备注说明