This commit is contained in:
YI FANG
2025-11-26 09:50:49 +08:00
commit 8155c9f95d
43 changed files with 7687 additions and 0 deletions

View File

@@ -0,0 +1,375 @@
import { useState } from 'react';
import type { ExamClient, ExamModalTab } from '../../data/mockData';
import { Badge, Button } 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: '打印导检单' },
];
const signDone = client.signStatus === '已登记' || client.checkedItems.includes('签到');
const addonDone = (client.addonCount || 0) > 0;
const printDone = !!client.guidePrinted;
const tabDone: Record<ExamModalTab, boolean> = {
detail: false,
sign: signDone,
addon: addonDone,
print: printDone,
};
return (
<div className='fixed inset-0 z-40 flex items-center justify-center bg-black/30'>
<div className='w-[720px] max-w-[95vw] bg-white rounded-3xl shadow-xl overflow-hidden text-sm'>
<div className='px-4 py-3 border-b flex items-center justify-between'>
<div className='flex items-center gap-3'>
<span className='font-semibold'>{client.name}</span>
<span className='text-xs text-gray-500'>{client.id}</span>
<Badge>{client.level}</Badge>
</div>
<button className='text-xs text-gray-500' onClick={onClose}>
</button>
</div>
<div className='px-4 py-2 border-b flex items-center gap-2 text-xs'>
{tabs.map((t) => {
const isActive = tab === t.key;
const isDone = tabDone[t.key];
return (
<button
key={t.key}
onClick={() => onTabChange(t.key)}
className={`px-3 py-1 rounded-2xl border text-xs ${
isActive
? 'bg-gray-900 text-white border-gray-900'
: isDone
? 'bg-gray-100 text-gray-400 border-gray-200'
: 'bg-white text-gray-700'
}`}
>
{t.label}
{t.key === 'addon' && (client.addonCount || 0) > 0 && (
<span className='ml-1 text-[10px] opacity-80'>({client.addonCount})</span>
)}
{((t.key === 'sign' && signDone) || (t.key === 'print' && printDone)) && <span className='ml-1'></span>}
</button>
);
})}
</div>
<div className='px-4 py-4 bg-gray-50/60'>
{tab === 'detail' && <ExamDetailInfo client={client} />}
{tab === 'sign' && <ExamSignPanel />}
{tab === 'addon' && <ExamAddonPanel client={client} />}
{tab === 'print' && <ExamPrintPanel client={client} />}
</div>
</div>
</div>
);
};
const ExamSignPanel = () => (
<div className='grid grid-cols-2 gap-4 text-sm'>
<div className='p-4 rounded-2xl border bg-gray-50/60 flex flex-col gap-3'>
<div className='font-medium'></div>
<div className='text-xs text-gray-500'></div>
<div className='flex gap-2 text-xs'>
<Button className='py-1.5 px-3'></Button>
<Button className='py-1.5 px-3'></Button>
</div>
<div className='text-[11px] text-gray-400'></div>
</div>
<div className='p-4 rounded-2xl border bg-gray-50/60 flex flex-col gap-3'>
<div className='font-medium'></div>
<div className='text-xs text-gray-500'></div>
<div className='flex gap-2 text-xs text-gray-600'>
<Badge></Badge>
<Badge></Badge>
</div>
<Button className='py-1.5 px-3'></Button>
</div>
</div>
);
const ExamAddonPanel = ({ client }: { client: ExamClient }) => (
<div className='grid grid-cols-2 gap-6 text-sm'>
<div>
<div className='font-medium mb-2'></div>
<ul className='space-y-1 text-xs text-gray-600'>
{client.checkedItems.concat(client.pendingItems).map((item, idx) => (
<li key={idx} className='flex items-center justify-between'>
<span>{item}</span>
<span className='text-gray-400 text-[11px]'>{client.checkedItems.includes(item) ? '已检查' : '未检查'}</span>
</li>
))}
</ul>
</div>
<div>
<div className='font-medium mb-2'></div>
<div className='space-y-2 text-xs text-gray-600'>
{['肿瘤标志物筛查', '甲状腺彩超', '骨密度检测'].map((label) => (
<label key={label} className='flex items-center gap-2'>
<input type='checkbox' className='rounded' /> {label}
</label>
))}
</div>
<Button className='mt-3'></Button>
</div>
</div>
);
const ExamDetailInfo = ({ client }: { client: ExamClient }) => {
const [phone, setPhone] = useState((client['mobile' as keyof ExamClient] as string | undefined) || '137****9988');
const [marital, setMarital] = useState(
(client['maritalStatus' as keyof ExamClient] as string | undefined) || '未婚',
);
const [phoneEditing, setPhoneEditing] = useState(false);
const [maritalEditing, setMaritalEditing] = useState(false);
const customerChannel = client.customerType === '团客' ? '团体客户' : '散客客户';
const familyDoctor = (client['familyDoctor' as keyof ExamClient] as string | undefined) || '—';
const groupTag = client['groupTag' as keyof ExamClient] || (client.customerType === '团客' ? '团检' : '—');
const bookingTime = client['bookingTime' as keyof ExamClient] || '—';
const signTime = client['signTime' as keyof ExamClient] || '—';
const addonSummary = client['addonSummary' as keyof ExamClient] || '—';
return (
<div className='space-y-4 text-sm'>
<div className='flex items-center gap-4'>
<div className='w-14 h-14 rounded-full bg-gray-200 flex-shrink-0 overflow-hidden'>
<div className='w-full h-full rounded-full bg-gray-300 flex items-center justify-center text-[10px] text-gray-500'>
</div>
</div>
<div className='text-xs text-gray-500'></div>
</div>
<div className='space-y-2 text-xs text-gray-700'>
<div className='font-medium text-gray-900'></div>
<div className='grid grid-cols-2 gap-x-8 gap-y-1'>
<div>
<span className='text-gray-900'>{client.name}</span>
</div>
<div>
<span className='text-gray-900'>4401********1234</span>
</div>
<div className='flex items-center'>
<span></span>
{!phoneEditing ? (
<span className='text-gray-900 flex items-center'>
{phone}
<button className='ml-1 text-blue-500 text-[11px] hover:underline' onClick={() => setPhoneEditing(true)}>
</button>
</span>
) : (
<span className='flex items-center gap-1'>
<input
className='w-28 rounded-xl border px-2 py-0.5 text-[11px] outline-none'
value={phone}
onChange={(e) => setPhone(e.target.value)}
/>
<button className='px-2 py-0.5 rounded-xl border text-[11px]' onClick={() => setPhoneEditing(false)}>
</button>
</span>
)}
</div>
<div>
/
<span className='text-gray-900'>
{client.gender} / {client.age}
</span>
</div>
<div>
<span className='text-gray-900'>{client.level}</span>
</div>
<div>
<span className='text-gray-900'>{customerChannel}</span>
</div>
<div className='flex items-center'>
<span></span>
{!maritalEditing ? (
<span className='text-gray-900 flex items-center'>
{marital}
<button className='ml-1 text-blue-500 text-[11px] hover:underline' onClick={() => setMaritalEditing(true)}>
</button>
</span>
) : (
<span className='flex items-center gap-1'>
<input
className='w-20 rounded-xl border px-2 py-0.5 text-[11px] outline-none'
value={marital}
onChange={(e) => setMarital(e.target.value)}
/>
<button className='px-2 py-0.5 rounded-xl border text-[11px]' onClick={() => setMaritalEditing(false)}>
</button>
</span>
)}
</div>
<div>
<span className='text-gray-900'>{familyDoctor}</span>
</div>
<div>
<span className='text-gray-900'>{groupTag as string}</span>
</div>
</div>
</div>
<div className='space-y-2 text-xs text-gray-700'>
<div className='font-medium text-gray-900'></div>
<div className='grid grid-cols-2 gap-x-8 gap-y-1'>
<div>
<span className='text-gray-900'>{bookingTime as string}</span>
</div>
<div>
<span className='text-gray-900'>{signTime as string}</span>
</div>
<div>
<span className='text-gray-900'>{client.elapsed}</span>
</div>
<div className='col-span-2'>
<span className='text-gray-900'>{client.packageName}</span>
</div>
<div className='col-span-2'>
<span className='text-gray-900'>{addonSummary as string}</span>
</div>
</div>
</div>
<div className='grid grid-cols-2 gap-4 text-xs'>
<div className='p-3 rounded-2xl bg-green-50 border'>
<div className='font-medium mb-2'> {client.checkedItems.length} </div>
<div className='flex flex-wrap gap-2'>
{client.checkedItems.map((i) => (
<span key={i} className='px-2 py-0.5 rounded-full bg-white border text-[11px]'>
{i}
</span>
))}
</div>
</div>
<div className='p-3 rounded-2xl bg-red-50 border'>
<div className='font-medium mb-2'> 0 </div>
<div className='flex flex-wrap gap-2'>
<span className='text-gray-400 text-[11px]'></span>
</div>
</div>
<div className='p-3 rounded-2xl bg-yellow-50 border'>
<div className='font-medium mb-2'> {client.pendingItems.length} </div>
<div className='flex flex-wrap gap-2'>
{client.pendingItems.map((i) => (
<span key={i} className='px-2 py-0.5 rounded-full bg-white border text-[11px]'>
{i}
</span>
))}
</div>
</div>
<div className='p-3 rounded-2xl bg-blue-50 border'>
<div className='font-medium mb-2'> 0 </div>
<div className='flex flex-wrap gap-2'>
<span className='text-gray-400 text-[11px]'></span>
</div>
</div>
</div>
</div>
);
};
const ExamPrintPanel = ({ client }: { client: ExamClient }) => (
<div className='flex justify-center'>
<div className='w-[520px] max-w-[95%] bg-white rounded-2xl border shadow-sm px-6 py-4 text-xs text-gray-800'>
<div className='flex items-center justify-between border-b pb-3 mb-3'>
<div>
<div className='text-sm font-semibold'> · </div>
<div className='text-[11px] text-gray-500 mt-1'></div>
</div>
<div className='text-right text-[11px] text-gray-500'>
<div>{client.id}</div>
<div>2025-11-18</div>
</div>
</div>
<div className='grid grid-cols-2 gap-y-1 gap-x-6 mb-3'>
<div>
<span className='font-medium'>{client.name}</span>
</div>
<div>
/
<span className='font-medium'>
{client.gender} / {client.age}
</span>
</div>
<div>
<span className='font-medium'>{client.packageName}</span>
</div>
<div>
<span className='font-medium'>{client.customerType}</span>
</div>
</div>
<div className='mb-2 font-medium'></div>
<table className='w-full border text-[11px] mb-3'>
<thead>
<tr className='bg-gray-50'>
<th className='border px-2 py-1 text-left'></th>
<th className='border px-2 py-1 text-left'></th>
<th className='border px-2 py-1 text-left'></th>
<th className='border px-2 py-1 text-left'></th>
</tr>
</thead>
<tbody>
{client.checkedItems.map((item, idx) => (
<tr key={`c-${idx}`}>
<td className='border px-2 py-1'>{idx + 1}</td>
<td className='border px-2 py-1'>{item}</td>
<td className='border px-2 py-1'></td>
<td className='border px-2 py-1'></td>
</tr>
))}
{client.pendingItems.map((item, idx) => (
<tr key={`p-${idx}`}>
<td className='border px-2 py-1'>{client.checkedItems.length + idx + 1}</td>
<td className='border px-2 py-1'>{item}</td>
<td className='border px-2 py-1'></td>
<td className='border px-2 py-1'></td>
</tr>
))}
</tbody>
</table>
<div className='grid grid-cols-2 gap-4 text-[11px] text-gray-600'>
<div>
<div className='mb-1 font-medium text-gray-800'></div>
<ul className='list-disc ml-4 space-y-0.5'>
<li></li>
<li></li>
<li>尿</li>
</ul>
</div>
<div className='flex flex-col items-end justify-between'>
<div className='text-right'>
<div>________________</div>
<div className='mt-2'>2025-11-18 09:30</div>
</div>
<div className='mt-4 w-24 h-24 border border-dashed flex items-center justify-center text-[10px] text-gray-400'>
/
</div>
</div>
</div>
</div>
</div>
);

View File

@@ -0,0 +1,131 @@
import type { ExamClient, ExamModalTab } from '../../data/mockData';
import { EXAM_STATS, EXAM_TAGS } from '../../data/mockData';
import { Badge, Card, CardContent, CardHeader, InfoCard } from '../ui';
import { cls } from '../../utils/cls';
interface ExamSectionProps {
filteredClients: ExamClient[];
selectedExamClient: ExamClient;
examFilterTag: (typeof EXAM_TAGS)[number];
onFilterChange: (tag: (typeof EXAM_TAGS)[number]) => void;
onOpenModal: (id: string, tab: ExamModalTab) => void;
}
export const ExamSection = ({
filteredClients,
selectedExamClient,
examFilterTag,
onFilterChange,
onOpenModal,
}: ExamSectionProps) => (
<div className='space-y-4'>
<Card>
<CardHeader></CardHeader>
<CardContent>
<div className='grid grid-cols-4 gap-3'>
{EXAM_STATS.map(([label, value]) => (
<InfoCard key={label} label={label} value={value} />
))}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<span></span>
<div className='flex items-center gap-2 text-xs'>
{EXAM_TAGS.map((tag) => (
<button
key={tag}
onClick={() => onFilterChange(tag)}
className={cls(
'px-3 py-1 rounded-2xl border',
examFilterTag === tag ? 'bg-gray-900 text-white border-gray-900' : 'bg-white text-gray-700',
)}
>
{tag}
</button>
))}
</div>
</CardHeader>
<CardContent>
<div className='grid grid-cols-3 gap-3 text-sm'>
{filteredClients.map((client) => {
const signDone = client.signStatus === '已登记' || client.checkedItems.includes('签到');
const addonCount = client.addonCount || 0;
const printDone = !!client.guidePrinted;
const openModal = (tab: ExamModalTab) => onOpenModal(client.id, tab);
return (
<button
key={client.id}
onClick={() => openModal('detail')}
className={cls(
'text-left p-3 rounded-2xl border bg-white hover:bg-gray-50 flex flex-col gap-1',
selectedExamClient.id === client.id && 'border-gray-900 bg-gray-50',
)}
>
<div className='flex items-center justify-between mb-1'>
<span className='font-medium'>{client.name}</span>
<Badge>{client.level}</Badge>
</div>
<div className='text-xs text-gray-500 truncate'>{client.packageName}</div>
<div className='flex items-center justify-between text-xs text-gray-500 mt-1'>
<span>{client.status}</span>
<span>{client.elapsed}</span>
</div>
<div className='mt-2 flex flex-wrap gap-1 text-[11px]'>
<button
type='button'
className='px-2 py-0.5 rounded-2xl border bg-white hover:bg-gray-100 flex items-center gap-1'
onClick={(e) => {
e.stopPropagation();
openModal('detail');
}}
>
<span></span>
</button>
<button
type='button'
className='px-2 py-0.5 rounded-2xl border bg-white hover:bg-gray-100 flex items-center gap-1'
onClick={(e) => {
e.stopPropagation();
openModal('sign');
}}
>
<span></span>
{signDone && <span></span>}
</button>
<button
type='button'
className='px-2 py-0.5 rounded-2xl border bg-white hover:bg-gray-100 flex items-center gap-1'
onClick={(e) => {
e.stopPropagation();
openModal('addon');
}}
>
<span></span>
{addonCount > 0 && <span className='opacity-80'>({addonCount})</span>}
</button>
<button
type='button'
className='px-2 py-0.5 rounded-2xl border bg-white hover:bg-gray-100 flex items-center gap-1'
onClick={(e) => {
e.stopPropagation();
openModal('print');
}}
>
<span></span>
{printDone && <span></span>}
</button>
</div>
</button>
);
})}
</div>
</CardContent>
</Card>
</div>
);