init
This commit is contained in:
375
src/components/exam/ExamModal.tsx
Normal file
375
src/components/exam/ExamModal.tsx
Normal 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>
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user