import React, { useMemo, useState } from 'react'; // ===== 工具与通用组件 ===== const cls = (...xs: (string | false | null | undefined)[]) => xs.filter(Boolean).join(' '); // 简单自测(不会影响页面逻辑,仅在控制台验证 cls 的行为) (function testCls() { // 期望: 'a b c' const v = cls('a', false && 'x', null, 'b', undefined, 'c'); if (v !== 'a b c') { // eslint-disable-next-line no-console console.warn('cls 函数自测不通过,当前为:', v); } })(); const Card: React.FC<{ className?: string; children: React.ReactNode }> = ({ className = '', children }) => (
{children}
); const CardHeader: React.FC<{ children: React.ReactNode }> = ({ children }) => (
{children}
); const CardContent: React.FC<{ children: React.ReactNode }> = ({ children }) => (
{children}
); const Button: React.FC< React.ButtonHTMLAttributes & { className?: string; children: React.ReactNode } > = ({ className = '', children, ...rest }) => ( ); const Input: React.FC & { className?: string }> = ({ className = '', ...rest }) => ( ); const Badge: React.FC<{ children: React.ReactNode }> = ({ children }) => ( {children} ); const InfoCard: React.FC<{ label: React.ReactNode; value: React.ReactNode }> = ({ label, value }) => (
{label} {value}
); // 简单图标(避免外部依赖) const IconHome = () => 🏠; const IconHospital = () => 🏥; const IconCalendar = () => 📅; const IconSupport = () => 💬; // ===== 静态数据 ===== const NAV_ITEMS = [ { key: 'home', icon: IconHome, label: '首页' }, { key: 'exam', icon: IconHospital, label: '体检中心' }, { key: 'booking', icon: IconCalendar, label: '预约中心' }, { key: 'support', icon: IconSupport, label: '客服咨询' }, ] as const; const HOME_STATS: [string, number][] = [ ['今日预约', 80], ['签到人数', 60], ['在检人数', 25], ['打印导检单', 40], ['已完成人数', 30], ]; const REVENUE_STATS: [string, string][] = [ ['体检收入', '¥ 86,000'], ['加项收入', '¥ 12,400'], ['整体收入', '¥ 98,400'], ['目标收入', '¥ 120,000'], ['完成百分比', '82%'], ['缺口', '¥ 21,600'], ]; // B1:科室维度 [科室, 医生, 已检人数, 在检人数, 待检人数, 平均检查时长(分钟)] const B1_ROWS: [string, string, number, number, number, number][] = [ ['B超1', '张医生', 6, 2, 2, 15], ['B超2', '李医生', 5, 2, 1, 14], ['B超3', '王医生', 4, 2, 2, 16], ['耳鼻喉', '王医生', 10, 3, 2, 10], ['外科', '周医生', 8, 3, 2, 20], ]; const B1_SUMMARY = { totalClients: B1_ROWS.reduce((s, r) => s + r[2] + r[3] + r[4], 0), waiting: B1_ROWS.reduce((s, r) => s + r[4], 0), inExam: B1_ROWS.reduce((s, r) => s + r[3], 0), }; // 北3:家医视角 [家医, 分配客户, 面诊数] const NORTH3_ROWS: [string, number, number][] = [ ['刘医生', 15, 9], ['高医生', 12, 7], ['马医生', 18, 10], ]; const NORTH3_SUMMARY = { totalDoctor: NORTH3_ROWS.length, totalAssigned: NORTH3_ROWS.reduce((s, r) => s + r[1], 0), consult: NORTH3_ROWS.reduce((s, r) => s + r[2], 0), }; // 体检客户类型 interface ExamClient { id: string; name: string; gender: '男' | '女'; age: number; level: string; packageName: string; status: '体检中' | '已签到' | '用餐'; elapsed: string; checkedItems: string[]; pendingItems: string[]; timeSlot: '上午' | '下午'; vipType: '高客' | '普客'; signStatus: '已登记' | '未登记'; customerType: '团客' | '散客'; guidePrinted?: boolean; // 是否已打印导检单 addonCount?: number; // 加项数量 } const EXAM_CLIENTS: ExamClient[] = [ { id: 'A001', name: '张伟', gender: '男', age: 35, level: 'VIP', packageName: '高端入职体检套餐', status: '体检中', elapsed: '00:45', checkedItems: ['签到', '更衣', '预检', '抽血'], pendingItems: ['家医面诊', 'B超'], timeSlot: '上午', vipType: '高客', signStatus: '已登记', customerType: '团客', guidePrinted: true, addonCount: 2, }, { id: 'A002', name: '李静', gender: '女', age: 29, level: '普通', packageName: '基础体检套餐', status: '已签到', elapsed: '00:10', checkedItems: ['签到'], pendingItems: ['更衣', '预检', '抽血'], timeSlot: '上午', vipType: '普客', signStatus: '已登记', customerType: '散客', guidePrinted: false, addonCount: 0, }, { id: 'A003', name: '孙丽', gender: '女', age: 31, level: 'VIP', packageName: '健康管理套餐', status: '用餐', elapsed: '00:50', checkedItems: ['签到', '更衣', '预检', '抽血', '家医面诊'], pendingItems: ['B超'], timeSlot: '下午', vipType: '高客', signStatus: '已登记', customerType: '团客', guidePrinted: true, addonCount: 1, }, ]; const EXAM_STATS: [string, number][] = [ ['预约人数', EXAM_CLIENTS.length], ['已签到', EXAM_CLIENTS.filter((c) => c.status === '已签到').length], ['体检中', EXAM_CLIENTS.filter((c) => c.status === '体检中').length], ['用餐', EXAM_CLIENTS.filter((c) => c.status === '用餐').length], ]; // 额外测试:验证 EXAM_STATS 与 EXAM_CLIENTS 的数量关系(简单自测,不影响运行) (function testExamStats() { const total = EXAM_CLIENTS.length; if (EXAM_STATS[0][1] !== total) { // eslint-disable-next-line no-console console.warn('EXAM_STATS[0] 预约人数 与 EXAM_CLIENTS.length 不一致'); } })(); const EXAM_TAGS = ['全部', '上午', '下午', '高客', '普客', '已登记', '未登记', '散客', '团客'] as const; const BOOKING_DOCTORS = [ { id: 'zhang', name: '张主任', dept: '内科 · 主任医师', period: '上午', total: 20, remain: 8 }, { id: 'wang', name: '王教授', dept: '外科 · 主任医师', period: '下午', total: 16, remain: 10 }, ]; // ===== 主组件 ===== const App: React.FC = () => { const [active, setActive] = useState<'home' | 'exam' | 'booking' | 'support'>('home'); const [selectedDay, setSelectedDay] = useState(1); const [search, setSearch] = useState(''); const [examSelectedId, setExamSelectedId] = useState('A001'); const [examPanelTab, setExamPanelTab] = useState<'detail' | 'sign' | 'addon' | 'print'>('detail'); const [examModalOpen, setExamModalOpen] = useState(false); const [quickAction, setQuickAction] = useState<'none' | 'meal' | 'vip' | 'delivery' | 'note'>('none'); const [noteText, setNoteText] = useState(''); const [examFilterTag, setExamFilterTag] = useState<(typeof EXAM_TAGS)[number]>('全部'); const [mealDoneIds, setMealDoneIds] = useState( EXAM_CLIENTS.filter((c) => c.status === '用餐').map((c) => c.id), ); const [bookingDoctor, setBookingDoctor] = useState<(typeof BOOKING_DOCTORS)[number] | null>(null); const filteredClients = useMemo(() => { return EXAM_CLIENTS.filter((c) => (c.name + c.packageName + c.id).toLowerCase().includes(search.trim().toLowerCase()), ).filter((c) => { switch (examFilterTag) { case '上午': return c.timeSlot === '上午'; case '下午': return c.timeSlot === '下午'; case '高客': return c.vipType === '高客'; case '普客': return c.vipType === '普客'; case '已登记': return c.signStatus === '已登记'; case '未登记': return c.signStatus !== '已登记'; case '散客': return c.customerType === '散客'; case '团客': return c.customerType === '团客'; default: return true; } }); }, [search, examFilterTag]); const selectedExamClient: ExamClient = EXAM_CLIENTS.find((c) => c.id === examSelectedId) || EXAM_CLIENTS[0]; const totalExamCount = EXAM_CLIENTS.length; const mealCount = mealDoneIds.length; const notMealCount = totalExamCount - mealCount; return (
{/* 左侧导航 */} {/* 右侧主区域 */}
{/* 顶部工具条 */}
setSearch(e.target.value)} />
操作员 · Admin
{/* 页面内容 */}
{/* 首页 */} {active === 'home' && (
今日体检统计
{HOME_STATS.map(([label, value]) => ( ))}
营收数据统计
{REVENUE_STATS.map(([label, value]) => ( ))}
B1 服务看板
{B1_ROWS.map(([dept, doctor, done, inExam, waiting, avg]) => { const parts = done * 3; // 简化:每人 3 个部位 const totalTime = done * avg; return ( ); })}
科室 医生 已检人数 已检部位数 总时长 平均时长 在检 待检
{dept} {doctor} {done} {parts} {totalTime} {avg} {inExam} {waiting}
北3服务看板
{NORTH3_ROWS.map(([name, total, consult]) => ( ))}
家医 分配客户数 面诊数
{name} {total} {consult}
)} {/* 体检中心 */} {active === 'exam' && (
今日体检进度
{EXAM_STATS.map(([label, value]) => ( ))}
体检客户列表
{EXAM_TAGS.map((tag) => ( ))}
{filteredClients.map((c) => { const signDone = c.signStatus === '已登记' || c.checkedItems.includes('签到'); const addonCount = c.addonCount || 0; const printDone = !!c.guidePrinted; return (
); })}
{examModalOpen && ( setExamModalOpen(false)} /> )}
)} {/* 预约中心 */} {active === 'booking' && (
预约日历
{Array.from({ length: 30 }, (_, i) => i + 1).map((day) => ( ))}
当日预约数 {BOOKING_DOCTORS.length}
预约医生 · {selectedDay} 日
按科室筛选
{BOOKING_DOCTORS.map((d) => { const ratio = d.total ? (d.total - d.remain) / d.total : 0; const percent = Math.round(Math.max(0, Math.min(1, ratio)) * 100); return ( ); })}
{bookingDoctor && ( setBookingDoctor(null)} /> )}
)} {/* 客服咨询 */} {active === 'support' && ( 客服咨询 · 圆圆客服台卡

通过「圆圆客服」二维码,客户可获得一站式健康服务:包含体检预约、报告查询、报告解读等。

  • 支持体检当天现场扫码添加,绑定客户信息
  • 扫码后可在线查看体检进度、报告结果
  • 提供一对一健康咨询与报告解读服务
注:实际系统中可上传设计好的「圆圆客服二维码台卡」图片,用于前台展示与打印。
CIRCLE HARMONY · 圆和医疗
圆圆客服 · 一站式健康服务
二维码占位
一对一专属服务
服务预约 / 报告查询 / 报告解读
)} {/* 快捷操作弹层 */} {quickAction !== 'none' && ( setQuickAction('none')} totalExamCount={totalExamCount} mealCount={mealCount} notMealCount={notMealCount} mealDoneIds={mealDoneIds} onMealDone={(id) => setMealDoneIds((prev) => (prev.includes(id) ? prev : prev.concat(id))) } /> )}
); }; // ===== 体检详情弹窗 ===== interface ExamModalProps { client: ExamClient; tab: 'detail' | 'sign' | 'addon' | 'print'; onTabChange: (key: 'detail' | 'sign' | 'addon' | 'print') => void; onClose: () => void; } function ExamModal({ client, tab, onTabChange, onClose }: ExamModalProps) { const tabs: { key: ExamModalProps['tab']; 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 = { detail: false, sign: signDone, addon: addonDone, print: printDone, }; return (
{client.name} 体检号:{client.id} {client.level}
{tabs.map((t) => { const isActive = tab === t.key; const isDone = tabDone[t.key]; return ( ); })}
{tab === 'detail' && } {tab === 'sign' && } {tab === 'addon' && } {tab === 'print' && }
); } function ExamSignPanel() { return (
身份证上传
支持身份证正反面拍照或读取设备,自动识别姓名、证件号等信息。
上传后进入预览界面,确认无误后返回签到界面。
体检知情同意书
点击后弹出知情同意书全文及签名区域,签署完成后回到签到页面。
阅读记录 签名图片
); } function ExamAddonPanel({ client }: { client: ExamClient }) { return (
当前套餐项目
    {client.checkedItems.concat(client.pendingItems).map((item, idx) => (
  • {item} {client.checkedItems.includes(item) ? '已检查' : '未检查'}
  • ))}
可选加项
{['肿瘤标志物筛查', '甲状腺彩超', '骨密度检测'].map((label) => ( ))}
); } function 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 (
{/* 头像 + 提示 */}
头像
基础信息:头像、姓名、证件号、手机号等(点击图标可进行编辑)
{/* 基础信息 */}
基础信息
姓名:{client.name}
证件号:4401********1234
手机号: {!phoneEditing ? ( {phone} ) : ( setPhone(e.target.value)} /> )}
性别/年龄: {client.gender} / {client.age}
客户级别:{client.level}
所属渠道:{customerChannel}
婚姻状况: {!maritalEditing ? ( {marital} ) : ( setMarital(e.target.value)} /> )}
家医:{familyDoctor}
团标签:{groupTag as string}
{/* 预约信息 */}
预约信息
预约时间:{bookingTime as string}
签到时间:{signTime as string}
已消耗时长:{client.elapsed}
体检套餐名称:{client.packageName}
加项内容:{addonSummary as string}
{/* 项目分类:已查 / 弃检 / 未查 / 延期 */}
已查项目 共 {client.checkedItems.length} 项
{client.checkedItems.map((i) => ( {i} ))}
弃检项目 共 0 项
暂无弃检项目
未查项目 共 {client.pendingItems.length} 项
{client.pendingItems.map((i) => ( {i} ))}
延期项目 共 0 项
暂无延期项目
); } // 导检单预览(纯预览,无状态) function ExamPrintPanel({ client }: { client: ExamClient }) { return (
圆和医疗体检中心 · 导检单预览
此为预览页面,实际打印效果以院内打印机为准。
体检号:{client.id}
日期:2025-11-18
姓名:{client.name}
性别/年龄: {client.gender} / {client.age}
体检套餐:{client.packageName}
客户类型:{client.customerType}
检查项目列表(预览)
{client.checkedItems.map((item, idx) => ( ))} {client.pendingItems.map((item, idx) => ( ))}
序号 检查项目 科室 备注
{idx + 1} {item} 已预约
{client.checkedItems.length + idx + 1} {item} 待检查
导检提示
  • 请按导检单顺序前往相应科室检查。
  • 如有不适或特殊情况,请及时告知导检护士。
  • 部分检查项目需空腹或憋尿,请遵从现场指引。
导检护士签名:________________
打印时间:2025-11-18 09:30
条码 / 二维码
); } // 快捷操作弹层 interface QuickActionModalProps { action: 'meal' | 'vip' | 'delivery' | 'note'; noteText: string; onNoteChange: (v: string) => void; onClose: () => void; totalExamCount: number; mealCount: number; notMealCount: number; mealDoneIds: string[]; onMealDone: (id: string) => void; } function QuickActionModal({ action, noteText, onNoteChange, onClose, totalExamCount, mealCount, notMealCount, mealDoneIds, onMealDone, }: QuickActionModalProps) { const titleMap: Record = { meal: '用餐登记', vip: '太平 VIP 认证说明', delivery: '报告寄送登记', note: '备注窗', }; return (
{titleMap[action]}
{action === 'meal' && (
选择已用餐客户进行登记:
{EXAM_CLIENTS.map((c) => { const checked = mealDoneIds.includes(c.id); return ( ); })}
)} {action === 'vip' && (

通过「太平 VIP 认证」二维码,可完成太平渠道客户的身份绑定与权益确认。

  • 客户出示太平 APP 内会员二维码,由工作人员扫码完成认证。
  • 认证成功后,系统会自动标记为「太平 VIP 客户」,并记录在体检档案中。
  • 支持后续报告寄送、复查预约等专属服务。
太平认证二维码占位
)} {action === 'delivery' && (
收件人姓名
联系电话
寄送地址
备注说明