diff --git a/src/api/types.ts b/src/api/types.ts index 6ed3702..9b5bfed 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -127,9 +127,9 @@ export interface InputPhysicalExamCustomerList { is_high_customer: number; /** 是否普客 (1-是 0-否) */ is_general_customer: number; - /** 是否已登记 (1-是 0-否) */ + /** 是否已签到 (1-是 0-否) */ is_registered: number; - /** 是否未登记 (1-是 0-否) */ + /** 是否未签到 (1-是 0-否) */ is_not_registered: number; /** 是否散客 (1-是 0-否) */ is_individual_customer: number; diff --git a/src/components/exam/ExamModal.tsx b/src/components/exam/ExamModal.tsx index 372e5a6..dd08536 100644 --- a/src/components/exam/ExamModal.tsx +++ b/src/components/exam/ExamModal.tsx @@ -30,7 +30,7 @@ export const ExamModal = ({ client, tab, onTabChange, onClose }: ExamModalProps) const idCardSignInDone = isExamActionDone(client.id, 'idCardSignIn'); const printSignDone = isExamActionDone(client.id, 'printSign'); - const signDone = idCardSignInDone || client.signStatus === '已登记' || client.checkedItems.includes('签到'); + const signDone = ((client as any).is_sign_in === 1) || idCardSignInDone || client.signStatus === '已签到' || client.checkedItems.includes('签到'); const addonDone = (client.addonCount || 0) > 0; const printDone = printSignDone || !!client.guidePrinted; const deliveryDone = !!client.deliveryDone; diff --git a/src/components/exam/ExamSection.tsx b/src/components/exam/ExamSection.tsx index 5b1aa70..f40832c 100644 --- a/src/components/exam/ExamSection.tsx +++ b/src/components/exam/ExamSection.tsx @@ -219,11 +219,12 @@ export const ExamSection = ({ const idCardSignInDone = isExamActionDone(client.id, 'idCardSignIn'); const printSignDone = isExamActionDone(client.id, 'printSign'); - const signDone = idCardSignInDone || client.signStatus === '已登记' || client.checkedItems.includes('签到'); + const signDone = idCardSignInDone || client.signStatus === '已签到' || client.checkedItems.includes('签到'); const addonCount = client.addonCount || 0; const printDone = printSignDone || !!client.guidePrinted; const openModal = (tab: ExamModalTab) => onOpenModal(client.id, tab); + return (
签到 - {(signDone && printDone) && } + {((client as any).is_sign_in === 1) && } -); - -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' && ( -
-
-
- 收件人姓名 - -
-
- 联系电话 - -
-
- 寄送地址 - -
-
-
- 备注说明 -