Files
ipad/src/components/exam/ExamModal.tsx
2026-02-05 11:25:03 +08:00

179 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react';
import type { ExamClient, ExamModalTab } from '../../data/mockData';
import type { CustomerAppointmentInfo, CustomerExamAddItem, CustomerInfo, PhysicalExamProgressItem } from '../../api';
import { getCustomerDetail, getPhysicalExamProgress } 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;
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 as any).is_sign_in === 1) || client.signStatus === '已签到' || client.checkedItems.includes('签到');
const addonDone = (client.addonCount || 0) > 0;
const printDone = !!client.guidePrinted;
const deliveryDone = !!client.deliveryDone;
const tabDone: Record<ExamModalTab, boolean> = {
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<CustomerInfo | null>(null);
const [appointmentInfo, setAppointmentInfo] = useState<CustomerAppointmentInfo | null>(null);
const [addItemInfoList, setAddItemInfoList] = useState<CustomerExamAddItem[] | null>(null);
const [progressList, setProgressList] = useState<PhysicalExamProgressItem[] | null>(null);
const [signBusy, setSignBusy] = useState(false);
useEffect(() => {
const physical_exam_id = Number(client.id);
if (!physical_exam_id) return;
setDetailLoading(true);
Promise.all([
getCustomerDetail({ physical_exam_id }),
getPhysicalExamProgress({ physical_exam_id }),
])
.then(([detailRes, progressRes]) => {
setCustomerInfo(detailRes.Data?.customerInfo ?? null);
setAppointmentInfo(detailRes.Data?.appointmentInfo ?? null);
setAddItemInfoList(detailRes.Data?.addItemInfoList ?? null);
setProgressList(progressRes.Data ?? null);
})
.catch((err) => {
console.error('获取客户详情/进度失败', err);
})
.finally(() => setDetailLoading(false));
}, [client.id]);
const refetchDetail = () => {
const physical_exam_id = Number(client.id);
if (!physical_exam_id) return;
getCustomerDetail({ physical_exam_id }).then((detailRes) => {
setCustomerInfo(detailRes.Data?.customerInfo ?? null);
setAppointmentInfo(detailRes.Data?.appointmentInfo ?? null);
setAddItemInfoList(detailRes.Data?.addItemInfoList ?? null);
});
};
return (
<div
className='fixed inset-0 z-40 flex items-center justify-center bg-black/50'
style={{ touchAction: 'none' }}
onDoubleClick={handleDoubleClick}
onTouchStart={handleTouchStart}
>
<div
className='w-[960px] max-w-[95vw] max-h-[95vh] bg-white rounded-2xl shadow-xl overflow-hidden text-sm'
style={{ touchAction: 'none' }}
onDoubleClick={handleDoubleClick}
onTouchStart={handleTouchStart}
>
<div className='px-8 pt-6 pb-2'>
<div className='flex items-center justify-between'>
<div className='flex items-end gap-3'>
<span className='text-2xl font-bold text-gray-900 leading-none'>
{client.name}
</span>
<span className='inline-flex items-center justify-center bg-indigo-100 text-indigo-600 text-[11px] font-bold px-2 py-0.5 rounded h-5 align-bottom'>
VIP
</span>
<span className='text-sm text-gray-400 ml-1 leading-none mb-0.5'>
{client.id}
</span>
</div>
<button
className={`text-sm transition-colors ${signBusy ? 'text-gray-300 cursor-not-allowed' : 'text-gray-400 hover:text-gray-600'}`}
onClick={!signBusy ? onClose : undefined}
disabled={signBusy}
>
</button>
</div>
</div>
<div className='px-8 py-4 flex items-center justify-between'>
<div className='flex items-center gap-3'>
{tabs.map((t) => {
const isActive = tab === t.key;
const isDone = tabDone[t.key];
return (
<button
key={t.key}
onClick={() => onTabChange(t.key)}
className={`px-5 py-2 rounded-2xl border text-sm transition-all duration-200 ${isActive
? 'bg-blue-600 text-white border-blue-600 shadow-md shadow-blue-200'
: isDone
? 'bg-gray-50 text-gray-400 border-gray-100'
: 'bg-white text-gray-600 border-gray-200 hover:border-gray-300'
}`}
disabled={signBusy}
>
<span className='flex items-center gap-1'>
{t.label}
{isDone && <span></span>}
{t.key === 'addon' && (client.addonCount || 0) > 0 && (
<span className={`text-xs ${isActive ? 'text-blue-100' : 'text-gray-400'}`}>
({client.addonCount})
</span>
)}
</span>
</button>
);
})}
</div>
</div>
<div className='px-4 py-4 bg-gray-50/60'>
{tab === 'detail' && (
<ExamDetailPanel
client={client}
customerInfo={customerInfo}
appointmentInfo={appointmentInfo}
addItemInfoList={addItemInfoList}
progressList={progressList}
loading={detailLoading}
onCustomerUpdated={refetchDetail}
/>
)}
{tab === 'sign' && <ExamSignPanel examId={Number(client.id)} onBusyChange={setSignBusy} />}
{tab === 'addon' && <ExamAddonPanel client={client} onGoToSign={() => onTabChange('sign')} />}
{tab === 'print' && <ExamPrintPanel client={client} />}
{tab === 'delivery' && <ExamDeliveryPanel client={client} />}
</div>
</div>
</div>
);
};