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 (
{/* 左侧导航 */}
{/* 右侧主区域 */}
{/* 顶部工具条 */}
{/* 页面内容 */}
{/* 首页 */}
{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) => (
setSelectedDay(day)}
className={cls(
'h-9 rounded-2xl border flex items-center justify-center',
selectedDay === day
? 'bg-gray-900 text-white border-gray-900'
: 'bg-white hover:bg-gray-50',
)}
>
{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 (
setBookingDoctor(d)}
className='text-left w-full'
>
当日号源:{d.total} 个
剩余预约号 {d.remain}
);
})}
{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 (
onTabChange(t.key)}
className={cls(
'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 && (
({client.addonCount})
)}
{((t.key === 'sign' && signDone) || (t.key === 'print' && printDone)) && (
✅
)}
);
})}
{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) ? '已检查' : '未检查'}
))}
);
}
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}
setPhoneEditing(true)}
>
✏️ 编辑
) : (
setPhone(e.target.value)}
/>
setPhoneEditing(false)}
>
保存
)}
性别/年龄:
{client.gender} / {client.age}
客户级别:{client.level}
所属渠道:{customerChannel}
婚姻状况:
{!maritalEditing ? (
{marital}
setMaritalEditing(true)}
>
✏️ 编辑
) : (
setMarital(e.target.value)}
/>
setMaritalEditing(false)}
>
保存
)}
家医:{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}
))}
未查项目 共 {client.pendingItems.length} 项
{client.pendingItems.map((i) => (
{i}
))}
);
}
// 导检单预览(纯预览,无状态)
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) => (
| {idx + 1} |
{item} |
— |
已预约 |
))}
{client.pendingItems.map((item, idx) => (
| {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 (
{action === 'meal' && (
选择已用餐客户进行登记:
{EXAM_CLIENTS.map((c) => {
const checked = mealDoneIds.includes(c.id);
return (
);
})}
)}
{action === 'vip' && (
通过「太平 VIP 认证」二维码,可完成太平渠道客户的身份绑定与权益确认。
- 客户出示太平 APP 内会员二维码,由工作人员扫码完成认证。
- 认证成功后,系统会自动标记为「太平 VIP 客户」,并记录在体检档案中。
- 支持后续报告寄送、复查预约等专属服务。
太平认证二维码占位
)}
{action === 'delivery' && (
)}
{action === 'note' && (
)}
);
}
// 预约申请弹层
interface BookingModalProps {
doctor: (typeof BOOKING_DOCTORS)[number];
onClose: () => void;
}
function BookingModal({ doctor, onClose }: BookingModalProps) {
return (
备注
医生:{doctor.name}({doctor.dept})
提交预约申请
);
}
export default App;