From 0efc4c186ef08b84e4afc46ca267e8ae334825f5 Mon Sep 17 00:00:00 2001 From: xianyi Date: Wed, 10 Dec 2025 17:28:15 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=8E=A5=E5=8F=A3=20-=20?= =?UTF-8?q?=E4=BD=93=E6=A3=80=E5=AE=A2=E6=88=B7=E5=88=97=E8=A1=A8=20-=20?= =?UTF-8?q?=E4=BD=93=E6=A3=80=E8=BF=9B=E5=BA=A6=E8=AF=A6=E6=83=85=20-=20?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E8=AF=A6=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/his.ts | 48 +++++++-- src/api/types.ts | 140 +++++++++++++++++++++++++ src/components/exam/ExamModal.tsx | 165 +++++++++++++++++++++++++----- src/index.css | 20 ++++ src/pages/ExamPage.tsx | 126 ++++++++++++++++------- 5 files changed, 428 insertions(+), 71 deletions(-) diff --git a/src/api/his.ts b/src/api/his.ts index b6c4227..ee65be7 100644 --- a/src/api/his.ts +++ b/src/api/his.ts @@ -6,16 +6,16 @@ import type { RevenueStatisticsResponse, InputTodayExamProgress, TodayExamProgressResponse, + InputPhysicalExamCustomerList, + PhysicalExamCustomerListResponse, + InputPhysicalExamProgressDetail, + PhysicalExamProgressDetailResponse, + InputCustomerDetail, + CustomerDetailResponse, } from './types'; /** * 自助机HIS接口 - * 基础路径: /api/his-web/self-service-machine/ - */ -const HIS_BASE_PATH = '/api/his-web/self-service-machine'; - -/** - * 体检中心HIS接口 * 基础路径: /api/his-web/medical-exam-center-app/ */ const MEDICAL_EXAM_BASE_PATH = '/api/his-web/medical-exam-center-app'; @@ -62,3 +62,39 @@ export const getTodayExamProgress = ( ).then(res => res.data); }; +/** + * 体检客户列表 + */ +export const getPhysicalExamCustomerList = ( + data: InputPhysicalExamCustomerList +): Promise => { + return request.post( + `${MEDICAL_EXAM_BASE_PATH}/customer-list`, + data + ).then(res => res.data); +}; + +/** + * 体检进度详情 + */ +export const getPhysicalExamProgressDetail = ( + data: InputPhysicalExamProgressDetail +): Promise => { + return request.post( + `${MEDICAL_EXAM_BASE_PATH}/progress`, + data + ).then(res => res.data); +}; + +/** + * 客户详情 + */ +export const getCustomerDetail = ( + data: InputCustomerDetail +): Promise => { + return request.post( + `${MEDICAL_EXAM_BASE_PATH}/customer-detail`, + data + ).then(res => res.data); +}; + diff --git a/src/api/types.ts b/src/api/types.ts index 9f7f80c..4a47b19 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -111,3 +111,143 @@ export interface OutputRevenueStatisticsInfo { */ export type RevenueStatisticsResponse = CommonActionResult; +/** + * 体检客户列表入参 + */ +export interface InputPhysicalExamCustomerList { + /** 客户姓名 */ + customer_name?: string | null; + /** 联系电话 */ + phone?: string | null; + /** 身份证号 */ + id_no?: string | null; + /** + * 筛选类型 + * 1-全部 2-上午 3-下午 4-高客 5-普客 6-已登记 7-未登记 8-散客 9-团客 + */ + filter_type?: number | null; +} + +/** + * 体检客户列表项 + */ +export interface OutputPhysicalExamCustomerListItem { + physical_exam_status?: number | null; + physical_exam_status_name?: string | null; + physical_exam_id?: number | null; + customer_name?: string | null; + phone?: string | null; + id_no?: string | null; + family_doctor_name?: string | null; + member_level?: string | null; + is_vip?: number | null; + package_code?: string | null; + package_name?: string | null; + add_item_flag?: number | null; + add_item_count?: number | null; + channel?: string | null; + physical_exam_time?: string | null; + physical_exam_complete_time?: string | null; + customer_type?: number | null; + is_register?: number | null; + is_sign_in?: number | null; + is_print?: number | null; + appointment_remarks?: string | null; +} + +/** + * 体检客户列表接口返回 + */ +export type PhysicalExamCustomerListResponse = CommonActionResult; + +/** + * 体检进度详情入参 + */ +export interface InputPhysicalExamProgressDetail { + /** 体检ID */ + physical_exam_id: number; +} + +/** + * 体检进度信息 + */ +export interface PhysicalExamProgressItem { + department_id?: number | null; + department_name?: string | null; + project_id?: string | null; + project_name?: string | null; + /** 检查状态(1-已查 2-弃检 3-未查 4-延期) */ + exam_status?: number | null; + exam_status_name?: string | null; +} + +/** + * 客户基本信息(进度、详情共用) + */ +export interface CustomerInfo { + customer_name?: string | null; + physical_exam_number?: string | null; + is_vip?: number | null; + phone?: string | null; + id_no?: string | null; + gender_code?: number | null; + gender_name?: string | null; + patient_marital_status?: number | null; + patient_marital_status_name?: string | null; + family_doctor_name?: string | null; + customer_type?: number | null; +} + +/** + * 体检进度详情出参 + */ +export interface OutputPhysicalExamProgressDetail { + customerInfo?: CustomerInfo | null; + examProgressesList?: PhysicalExamProgressItem[] | null; +} + +/** + * 体检进度详情接口返回 + */ +export type PhysicalExamProgressDetailResponse = CommonActionResult; + +/** + * 客户详情入参 + */ +export interface InputCustomerDetail { + /** 体检ID */ + physical_exam_id: number; +} + +/** + * 客户预约信息 + */ +export interface CustomerAppointmentInfo { + appointment_time?: string | null; + sign_in_time?: string | null; + physical_exam_complete_time?: string | null; + package_name?: string | null; +} + +/** + * 客户体检加项 + */ +export interface CustomerExamAddItem { + dept_name?: string | null; + combination_name?: string | null; +} + +/** + * 客户详情出参 + */ +export interface OutputCustomerDetail { + customerInfo?: CustomerInfo | null; + appointmentInfo?: CustomerAppointmentInfo | null; + addItemInfoList?: CustomerExamAddItem[] | null; +} + +/** + * 客户详情接口返回 + */ +export type CustomerDetailResponse = CommonActionResult; + diff --git a/src/components/exam/ExamModal.tsx b/src/components/exam/ExamModal.tsx index 15f8889..634f4fb 100644 --- a/src/components/exam/ExamModal.tsx +++ b/src/components/exam/ExamModal.tsx @@ -1,6 +1,13 @@ -import { useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import type { ExamClient, ExamModalTab } from '../../data/mockData'; +import type { + CustomerAppointmentInfo, + CustomerExamAddItem, + CustomerInfo, + PhysicalExamProgressItem, +} from '../../api'; +import { getCustomerDetail, getPhysicalExamProgressDetail } from '../../api'; import { Button, Input } from '../ui'; interface ExamModalProps { @@ -43,6 +50,32 @@ export const ExamModal = ({ client, tab, onTabChange, onClose }: ExamModalProps) } }; + const [detailLoading, setDetailLoading] = useState(false); + const [customerInfo, setCustomerInfo] = useState(null); + const [appointmentInfo, setAppointmentInfo] = useState(null); + const [addItemInfoList, setAddItemInfoList] = useState(null); + const [progressList, setProgressList] = useState(null); + + useEffect(() => { + const physical_exam_id = Number(client.id); + if (!physical_exam_id) return; + setDetailLoading(true); + Promise.all([ + getCustomerDetail({ physical_exam_id }), + getPhysicalExamProgressDetail({ physical_exam_id }), + ]) + .then(([detailRes, progressRes]) => { + setCustomerInfo(detailRes.Data?.customerInfo ?? null); + setAppointmentInfo(detailRes.Data?.appointmentInfo ?? null); + setAddItemInfoList(detailRes.Data?.addItemInfoList ?? null); + setProgressList(progressRes.Data?.examProgressesList ?? null); + }) + .catch((err) => { + console.error('获取客户详情/进度失败', err); + }) + .finally(() => setDetailLoading(false)); + }, [client.id]); + return (
- {tab === 'detail' && } + {tab === 'detail' && ( + + )} {tab === 'sign' && } {tab === 'addon' && } {tab === 'print' && } @@ -364,20 +406,64 @@ const ExamAddonPanel = ({ client }: { client: ExamClient }) => { ); }; -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 ExamDetailInfo = ({ + client, + customerInfo, + appointmentInfo, + addItemInfoList, + progressList, + loading, +}: { + client: ExamClient; + customerInfo: CustomerInfo | null; + appointmentInfo: CustomerAppointmentInfo | null; + addItemInfoList: CustomerExamAddItem[] | null; + progressList: PhysicalExamProgressItem[] | null; + loading: boolean; +}) => { + const basePhone = customerInfo?.phone || (client['mobile' as keyof ExamClient] as string | undefined) || ''; + const baseMarital = + customerInfo?.patient_marital_status_name || + (client['maritalStatus' as keyof ExamClient] as string | undefined) || + '—'; + const [phone, setPhone] = useState(basePhone || '—'); + const [marital, setMarital] = useState(baseMarital); 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 familyDoctor = customerInfo?.family_doctor_name || (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] || '—'; + const bookingTime = appointmentInfo?.appointment_time || (client['bookingTime' as keyof ExamClient] || '—'); + const signTime = appointmentInfo?.sign_in_time || (client['signTime' as keyof ExamClient] || '—'); + const addonSummary = + addItemInfoList && addItemInfoList.length > 0 + ? addItemInfoList.map((i) => `${i.dept_name ?? ''} ${i.combination_name ?? ''}`.trim()).join('、') + : client['addonSummary' as keyof ExamClient] || '—'; + + const progressGroups = useMemo(() => { + const checked: string[] = []; + const abandoned: string[] = []; + const pending: string[] = []; + const deferred: string[] = []; + (progressList || []).forEach((p) => { + const name = p.project_name || p.department_name || '项目'; + switch (p.exam_status) { + case 1: + checked.push(name); + break; + case 2: + abandoned.push(name); + break; + case 4: + deferred.push(name); + break; + default: + pending.push(name); + } + }); + return { checked, abandoned, pending, deferred }; + }, [progressList]); return (
@@ -387,17 +473,19 @@ const ExamDetailInfo = ({ client }: { client: ExamClient }) => { 头像
-
基础信息:头像、姓名、证件号、手机号等(点击图标可进行编辑)
+
+ {loading ? '加载中…' : '基础信息:头像、姓名、证件号、手机号等(点击图标可进行编辑)'} +
基础信息
- 姓名:{client.name} + 姓名:{customerInfo?.customer_name || client.name}
- 证件号:4401********1234 + 证件号:{customerInfo?.id_no || '—'}
手机号: @@ -424,7 +512,7 @@ const ExamDetailInfo = ({ client }: { client: ExamClient }) => {
性别/年龄: - {client.gender} / {client.age} + {customerInfo?.gender_name || client.gender} / {client.age}
@@ -474,7 +562,12 @@ const ExamDetailInfo = ({ client }: { client: ExamClient }) => { 签到时间:{signTime as string}
- 已消耗时长:{client.elapsed} + 已消耗时长: + + {appointmentInfo?.physical_exam_complete_time && appointmentInfo?.sign_in_time + ? `${appointmentInfo.physical_exam_complete_time} - ${appointmentInfo.sign_in_time}` + : client.elapsed} +
体检套餐名称:{client.packageName} @@ -486,36 +579,52 @@ const ExamDetailInfo = ({ client }: { client: ExamClient }) => {
-
-
已查项目 共 {client.checkedItems.length} 项
+
+
已查项目 共 {progressGroups.checked.length} 项
- {client.checkedItems.map((i) => ( + {(progressGroups.checked.length ? progressGroups.checked : client.checkedItems).map((i) => ( {i} ))}
-
-
弃检项目 共 0 项
+
+
弃检项目 共 {progressGroups.abandoned.length} 项
- 暂无弃检项目 + {progressGroups.abandoned.length ? ( + progressGroups.abandoned.map((i) => ( + + {i} + + )) + ) : ( + 暂无弃检项目 + )}
-
-
未查项目 共 {client.pendingItems.length} 项
+
+
未查项目 共 {progressGroups.pending.length} 项
- {client.pendingItems.map((i) => ( + {(progressGroups.pending.length ? progressGroups.pending : client.pendingItems).map((i) => ( {i} ))}
-
-
延期项目 共 0 项
+
+
延期项目 共 {progressGroups.deferred.length} 项
- 暂无延期项目 + {progressGroups.deferred.length ? ( + progressGroups.deferred.map((i) => ( + + {i} + + )) + ) : ( + 暂无延期项目 + )}
diff --git a/src/index.css b/src/index.css index 0d96670..a686785 100644 --- a/src/index.css +++ b/src/index.css @@ -2,6 +2,26 @@ @tailwind components; @tailwind utilities; +/* 自定义滚动条(圆角、细滚动条) */ +.custom-scroll { + scrollbar-width: none; + scrollbar-color: #cbd5e1 #f8fafc; +} +.custom-scroll::-webkit-scrollbar { + width: 8px; +} +.custom-scroll::-webkit-scrollbar-track { + background: #f8fafc; + border-radius: 9999px; +} +.custom-scroll::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 9999px; +} +.custom-scroll::-webkit-scrollbar-thumb:hover { + background: #94a3b8; +} + :root { font-family: 'Inter', 'PingFang SC', 'Microsoft YaHei', system-ui, -apple-system, sans-serif; color: #0f172a; diff --git a/src/pages/ExamPage.tsx b/src/pages/ExamPage.tsx index bfdcacf..b1f96bb 100644 --- a/src/pages/ExamPage.tsx +++ b/src/pages/ExamPage.tsx @@ -1,47 +1,97 @@ -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useOutletContext } from 'react-router-dom'; import type { ExamClient, ExamModalTab } from '../data/mockData'; -import { EXAM_CLIENTS, EXAM_TAGS } from '../data/mockData'; +import { EXAM_TAGS } from '../data/mockData'; import { ExamSection } from '../components/exam/ExamSection'; import { ExamModal } from '../components/exam/ExamModal'; import type { MainLayoutContext } from '../layouts/MainLayout'; +import { getPhysicalExamCustomerList } from '../api'; export const ExamPage = () => { const { search } = useOutletContext(); - const [examSelectedId, setExamSelectedId] = useState('A001'); + const [clients, setClients] = useState([]); + const [examSelectedId, setExamSelectedId] = useState(''); const [examPanelTab, setExamPanelTab] = useState('detail'); const [examModalOpen, setExamModalOpen] = useState(false); const [examFilterTag, setExamFilterTag] = useState<(typeof EXAM_TAGS)[number]>('全部'); - 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]); + // 将筛选标签映射为接口 filter_type + const filterType = useMemo(() => { + switch (examFilterTag) { + case '上午': + return 2; + case '下午': + return 3; + case '高客': + return 4; + case '普客': + return 5; + case '已登记': + return 6; + case '未登记': + return 7; + case '散客': + return 8; + case '团客': + return 9; + default: + return 1; // 全部 + } + }, [examFilterTag]); - const selectedExamClient: ExamClient = EXAM_CLIENTS.find((c) => c.id === examSelectedId) || EXAM_CLIENTS[0]; + // 从接口拉取体检客户列表 + useEffect(() => { + const payload = { + customer_name: search.trim() || undefined, + phone: undefined, + id_no: undefined, + filter_type: filterType, + }; + getPhysicalExamCustomerList(payload) + .then((res) => { + const list = res.Data || []; + const mapped: ExamClient[] = list.map((item) => { + // 简单映射为现有 UI 使用的字段 + const statusName = item.physical_exam_status_name || ''; + const signStatus = item.is_register === 1 ? '已登记' : '未登记'; + const customerType = item.customer_type === 1 ? '团客' : '散客'; + const vipType = item.is_vip === 1 ? '高客' : '普客'; + return { + id: String(item.physical_exam_id ?? ''), + name: item.customer_name || '未知客户', + gender: '男', // 后端未提供,默认填充 + age: 0, + level: item.member_level || (item.is_vip === 1 ? 'VIP' : '普通'), + packageName: item.package_name || '未提供套餐', + status: statusName || '未开始', + elapsed: '', + checkedItems: [], + pendingItems: [], + timeSlot: '上午', + vipType, + signStatus, + customerType, + guidePrinted: item.is_print === 1, + addonCount: item.add_item_count ?? 0, + }; + }); + setClients(mapped); + if (mapped.length && !examSelectedId) { + setExamSelectedId(mapped[0].id); + } else if (mapped.length === 0) { + setExamSelectedId(''); + } + }) + .catch((err) => { + console.error('获取体检客户列表失败', err); + }); + }, [search, filterType, examSelectedId]); + + const selectedExamClient: ExamClient | undefined = useMemo( + () => clients.find((c) => c.id === examSelectedId) || clients[0], + [clients, examSelectedId], + ); const handleOpenModal = (id: string, tab: ExamModalTab) => { setExamSelectedId(id); @@ -51,13 +101,15 @@ export const ExamPage = () => { return ( <> - + {selectedExamClient && ( + + )} {examModalOpen && (