添加接口
- 体检客户列表 - 体检进度详情 - 客户详情
This commit is contained in:
@@ -6,16 +6,16 @@ import type {
|
|||||||
RevenueStatisticsResponse,
|
RevenueStatisticsResponse,
|
||||||
InputTodayExamProgress,
|
InputTodayExamProgress,
|
||||||
TodayExamProgressResponse,
|
TodayExamProgressResponse,
|
||||||
|
InputPhysicalExamCustomerList,
|
||||||
|
PhysicalExamCustomerListResponse,
|
||||||
|
InputPhysicalExamProgressDetail,
|
||||||
|
PhysicalExamProgressDetailResponse,
|
||||||
|
InputCustomerDetail,
|
||||||
|
CustomerDetailResponse,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自助机HIS接口
|
* 自助机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/
|
* 基础路径: /api/his-web/medical-exam-center-app/
|
||||||
*/
|
*/
|
||||||
const MEDICAL_EXAM_BASE_PATH = '/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);
|
).then(res => res.data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 体检客户列表
|
||||||
|
*/
|
||||||
|
export const getPhysicalExamCustomerList = (
|
||||||
|
data: InputPhysicalExamCustomerList
|
||||||
|
): Promise<PhysicalExamCustomerListResponse> => {
|
||||||
|
return request.post<PhysicalExamCustomerListResponse>(
|
||||||
|
`${MEDICAL_EXAM_BASE_PATH}/customer-list`,
|
||||||
|
data
|
||||||
|
).then(res => res.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 体检进度详情
|
||||||
|
*/
|
||||||
|
export const getPhysicalExamProgressDetail = (
|
||||||
|
data: InputPhysicalExamProgressDetail
|
||||||
|
): Promise<PhysicalExamProgressDetailResponse> => {
|
||||||
|
return request.post<PhysicalExamProgressDetailResponse>(
|
||||||
|
`${MEDICAL_EXAM_BASE_PATH}/progress`,
|
||||||
|
data
|
||||||
|
).then(res => res.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户详情
|
||||||
|
*/
|
||||||
|
export const getCustomerDetail = (
|
||||||
|
data: InputCustomerDetail
|
||||||
|
): Promise<CustomerDetailResponse> => {
|
||||||
|
return request.post<CustomerDetailResponse>(
|
||||||
|
`${MEDICAL_EXAM_BASE_PATH}/customer-detail`,
|
||||||
|
data
|
||||||
|
).then(res => res.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
140
src/api/types.ts
140
src/api/types.ts
@@ -111,3 +111,143 @@ export interface OutputRevenueStatisticsInfo {
|
|||||||
*/
|
*/
|
||||||
export type RevenueStatisticsResponse = CommonActionResult<OutputRevenueStatisticsInfo>;
|
export type RevenueStatisticsResponse = CommonActionResult<OutputRevenueStatisticsInfo>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 体检客户列表入参
|
||||||
|
*/
|
||||||
|
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<OutputPhysicalExamCustomerListItem[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 体检进度详情入参
|
||||||
|
*/
|
||||||
|
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<OutputPhysicalExamProgressDetail>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户详情入参
|
||||||
|
*/
|
||||||
|
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<OutputCustomerDetail>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import { useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import type { ExamClient, ExamModalTab } from '../../data/mockData';
|
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';
|
import { Button, Input } from '../ui';
|
||||||
|
|
||||||
interface ExamModalProps {
|
interface ExamModalProps {
|
||||||
@@ -43,6 +50,32 @@ export const ExamModal = ({ client, tab, onTabChange, onClose }: ExamModalProps)
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [detailLoading, setDetailLoading] = useState(false);
|
||||||
|
const [customerInfo, setCustomerInfo] = useState<CustomerInfo | null>(null);
|
||||||
|
const [appointmentInfo, setAppointmentInfo] = useState<CustomerAppointmentInfo | null>(null);
|
||||||
|
const [addItemInfoList, setAddItemInfoList] = useState<CustomerExamAddItem[] | null>(null);
|
||||||
|
const [progressList, setProgressList] = useState<PhysicalExamProgressItem[] | null>(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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className='fixed inset-0 z-40 flex items-center justify-center bg-black/50'
|
className='fixed inset-0 z-40 flex items-center justify-center bg-black/50'
|
||||||
@@ -121,7 +154,16 @@ export const ExamModal = ({ client, tab, onTabChange, onClose }: ExamModalProps)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='px-4 py-4 bg-gray-50/60'>
|
<div className='px-4 py-4 bg-gray-50/60'>
|
||||||
{tab === 'detail' && <ExamDetailInfo client={client} />}
|
{tab === 'detail' && (
|
||||||
|
<ExamDetailInfo
|
||||||
|
client={client}
|
||||||
|
customerInfo={customerInfo}
|
||||||
|
appointmentInfo={appointmentInfo}
|
||||||
|
addItemInfoList={addItemInfoList}
|
||||||
|
progressList={progressList}
|
||||||
|
loading={detailLoading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{tab === 'sign' && <ExamSignPanel />}
|
{tab === 'sign' && <ExamSignPanel />}
|
||||||
{tab === 'addon' && <ExamAddonPanel client={client} />}
|
{tab === 'addon' && <ExamAddonPanel client={client} />}
|
||||||
{tab === 'print' && <ExamPrintPanel client={client} />}
|
{tab === 'print' && <ExamPrintPanel client={client} />}
|
||||||
@@ -364,20 +406,64 @@ const ExamAddonPanel = ({ client }: { client: ExamClient }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ExamDetailInfo = ({ client }: { client: ExamClient }) => {
|
const ExamDetailInfo = ({
|
||||||
const [phone, setPhone] = useState((client['mobile' as keyof ExamClient] as string | undefined) || '137****9988');
|
client,
|
||||||
const [marital, setMarital] = useState(
|
customerInfo,
|
||||||
(client['maritalStatus' as keyof ExamClient] as string | undefined) || '未婚',
|
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 [phoneEditing, setPhoneEditing] = useState(false);
|
||||||
const [maritalEditing, setMaritalEditing] = useState(false);
|
const [maritalEditing, setMaritalEditing] = useState(false);
|
||||||
|
|
||||||
const customerChannel = client.customerType === '团客' ? '团体客户' : '散客客户';
|
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 groupTag = client['groupTag' as keyof ExamClient] || (client.customerType === '团客' ? '团检' : '—');
|
||||||
const bookingTime = client['bookingTime' as keyof ExamClient] || '—';
|
const bookingTime = appointmentInfo?.appointment_time || (client['bookingTime' as keyof ExamClient] || '—');
|
||||||
const signTime = client['signTime' as keyof ExamClient] || '—';
|
const signTime = appointmentInfo?.sign_in_time || (client['signTime' as keyof ExamClient] || '—');
|
||||||
const addonSummary = client['addonSummary' 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 (
|
return (
|
||||||
<div className='space-y-4 text-sm'>
|
<div className='space-y-4 text-sm'>
|
||||||
@@ -387,17 +473,19 @@ const ExamDetailInfo = ({ client }: { client: ExamClient }) => {
|
|||||||
头像
|
头像
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='text-xs text-gray-500'>基础信息:头像、姓名、证件号、手机号等(点击图标可进行编辑)</div>
|
<div className='text-xs text-gray-500'>
|
||||||
|
{loading ? '加载中…' : '基础信息:头像、姓名、证件号、手机号等(点击图标可进行编辑)'}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='space-y-2 text-xs text-gray-700'>
|
<div className='space-y-2 text-xs text-gray-700'>
|
||||||
<div className='font-medium text-gray-900'>基础信息</div>
|
<div className='font-medium text-gray-900'>基础信息</div>
|
||||||
<div className='grid grid-cols-2 gap-x-8 gap-y-1'>
|
<div className='grid grid-cols-2 gap-x-8 gap-y-1'>
|
||||||
<div>
|
<div>
|
||||||
姓名:<span className='text-gray-900'>{client.name}</span>
|
姓名:<span className='text-gray-900'>{customerInfo?.customer_name || client.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
证件号:<span className='text-gray-900'>4401********1234</span>
|
证件号:<span className='text-gray-900'>{customerInfo?.id_no || '—'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
<span>手机号:</span>
|
<span>手机号:</span>
|
||||||
@@ -424,7 +512,7 @@ const ExamDetailInfo = ({ client }: { client: ExamClient }) => {
|
|||||||
<div>
|
<div>
|
||||||
性别/年龄:
|
性别/年龄:
|
||||||
<span className='text-gray-900'>
|
<span className='text-gray-900'>
|
||||||
{client.gender} / {client.age}
|
{customerInfo?.gender_name || client.gender} / {client.age}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -474,7 +562,12 @@ const ExamDetailInfo = ({ client }: { client: ExamClient }) => {
|
|||||||
签到时间:<span className='text-gray-900'>{signTime as string}</span>
|
签到时间:<span className='text-gray-900'>{signTime as string}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
已消耗时长:<span className='text-gray-900'>{client.elapsed}</span>
|
已消耗时长:
|
||||||
|
<span className='text-gray-900'>
|
||||||
|
{appointmentInfo?.physical_exam_complete_time && appointmentInfo?.sign_in_time
|
||||||
|
? `${appointmentInfo.physical_exam_complete_time} - ${appointmentInfo.sign_in_time}`
|
||||||
|
: client.elapsed}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='col-span-2'>
|
<div className='col-span-2'>
|
||||||
体检套餐名称:<span className='text-gray-900'>{client.packageName}</span>
|
体检套餐名称:<span className='text-gray-900'>{client.packageName}</span>
|
||||||
@@ -486,36 +579,52 @@ const ExamDetailInfo = ({ client }: { client: ExamClient }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='grid grid-cols-2 gap-4 text-xs'>
|
<div className='grid grid-cols-2 gap-4 text-xs'>
|
||||||
<div className='p-3 rounded-2xl bg-green-50 border'>
|
<div className='p-3 rounded-2xl bg-green-50 border max-h-48 overflow-auto custom-scroll'>
|
||||||
<div className='font-medium mb-2'>已查项目 共 {client.checkedItems.length} 项</div>
|
<div className='font-medium mb-2'>已查项目 共 {progressGroups.checked.length} 项</div>
|
||||||
<div className='flex flex-wrap gap-2'>
|
<div className='flex flex-wrap gap-2'>
|
||||||
{client.checkedItems.map((i) => (
|
{(progressGroups.checked.length ? progressGroups.checked : client.checkedItems).map((i) => (
|
||||||
<span key={i} className='px-2 py-0.5 rounded-full bg-white border text-[11px]'>
|
<span key={i} className='px-2 py-0.5 rounded-full bg-white border text-[11px]'>
|
||||||
{i}
|
{i}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='p-3 rounded-2xl bg-red-50 border'>
|
<div className='p-3 rounded-2xl bg-red-50 border max-h-48 overflow-auto custom-scroll'>
|
||||||
<div className='font-medium mb-2'>弃检项目 共 0 项</div>
|
<div className='font-medium mb-2'>弃检项目 共 {progressGroups.abandoned.length} 项</div>
|
||||||
<div className='flex flex-wrap gap-2'>
|
<div className='flex flex-wrap gap-2'>
|
||||||
<span className='text-gray-400 text-[11px]'>暂无弃检项目</span>
|
{progressGroups.abandoned.length ? (
|
||||||
|
progressGroups.abandoned.map((i) => (
|
||||||
|
<span key={i} className='px-2 py-0.5 rounded-full bg-white border text-[11px]'>
|
||||||
|
{i}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span className='text-gray-400 text-[11px]'>暂无弃检项目</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='p-3 rounded-2xl bg-yellow-50 border'>
|
<div className='p-3 rounded-2xl bg-yellow-50 border max-h-48 overflow-auto custom-scroll'>
|
||||||
<div className='font-medium mb-2'>未查项目 共 {client.pendingItems.length} 项</div>
|
<div className='font-medium mb-2'>未查项目 共 {progressGroups.pending.length} 项</div>
|
||||||
<div className='flex flex-wrap gap-2'>
|
<div className='flex flex-wrap gap-2'>
|
||||||
{client.pendingItems.map((i) => (
|
{(progressGroups.pending.length ? progressGroups.pending : client.pendingItems).map((i) => (
|
||||||
<span key={i} className='px-2 py-0.5 rounded-full bg-white border text-[11px]'>
|
<span key={i} className='px-2 py-0.5 rounded-full bg-white border text-[11px]'>
|
||||||
{i}
|
{i}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='p-3 rounded-2xl bg-blue-50 border'>
|
<div className='p-3 rounded-2xl bg-blue-50 border max-h-48 overflow-auto custom-scroll'>
|
||||||
<div className='font-medium mb-2'>延期项目 共 0 项</div>
|
<div className='font-medium mb-2'>延期项目 共 {progressGroups.deferred.length} 项</div>
|
||||||
<div className='flex flex-wrap gap-2'>
|
<div className='flex flex-wrap gap-2'>
|
||||||
<span className='text-gray-400 text-[11px]'>暂无延期项目</span>
|
{progressGroups.deferred.length ? (
|
||||||
|
progressGroups.deferred.map((i) => (
|
||||||
|
<span key={i} className='px-2 py-0.5 rounded-full bg-white border text-[11px]'>
|
||||||
|
{i}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span className='text-gray-400 text-[11px]'>暂无延期项目</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,26 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@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 {
|
:root {
|
||||||
font-family: 'Inter', 'PingFang SC', 'Microsoft YaHei', system-ui, -apple-system, sans-serif;
|
font-family: 'Inter', 'PingFang SC', 'Microsoft YaHei', system-ui, -apple-system, sans-serif;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
|
|||||||
@@ -1,47 +1,97 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useOutletContext } from 'react-router-dom';
|
import { useOutletContext } from 'react-router-dom';
|
||||||
|
|
||||||
import type { ExamClient, ExamModalTab } from '../data/mockData';
|
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 { ExamSection } from '../components/exam/ExamSection';
|
||||||
import { ExamModal } from '../components/exam/ExamModal';
|
import { ExamModal } from '../components/exam/ExamModal';
|
||||||
import type { MainLayoutContext } from '../layouts/MainLayout';
|
import type { MainLayoutContext } from '../layouts/MainLayout';
|
||||||
|
import { getPhysicalExamCustomerList } from '../api';
|
||||||
|
|
||||||
export const ExamPage = () => {
|
export const ExamPage = () => {
|
||||||
const { search } = useOutletContext<MainLayoutContext>();
|
const { search } = useOutletContext<MainLayoutContext>();
|
||||||
const [examSelectedId, setExamSelectedId] = useState<string>('A001');
|
const [clients, setClients] = useState<ExamClient[]>([]);
|
||||||
|
const [examSelectedId, setExamSelectedId] = useState<string>('');
|
||||||
const [examPanelTab, setExamPanelTab] = useState<ExamModalTab>('detail');
|
const [examPanelTab, setExamPanelTab] = useState<ExamModalTab>('detail');
|
||||||
const [examModalOpen, setExamModalOpen] = useState(false);
|
const [examModalOpen, setExamModalOpen] = useState(false);
|
||||||
const [examFilterTag, setExamFilterTag] = useState<(typeof EXAM_TAGS)[number]>('全部');
|
const [examFilterTag, setExamFilterTag] = useState<(typeof EXAM_TAGS)[number]>('全部');
|
||||||
|
|
||||||
const filteredClients = useMemo(() => {
|
// 将筛选标签映射为接口 filter_type
|
||||||
return EXAM_CLIENTS.filter((c) => (c.name + c.packageName + c.id).toLowerCase().includes(search.trim().toLowerCase())).filter(
|
const filterType = useMemo(() => {
|
||||||
(c) => {
|
switch (examFilterTag) {
|
||||||
switch (examFilterTag) {
|
case '上午':
|
||||||
case '上午':
|
return 2;
|
||||||
return c.timeSlot === '上午';
|
case '下午':
|
||||||
case '下午':
|
return 3;
|
||||||
return c.timeSlot === '下午';
|
case '高客':
|
||||||
case '高客':
|
return 4;
|
||||||
return c.vipType === '高客';
|
case '普客':
|
||||||
case '普客':
|
return 5;
|
||||||
return c.vipType === '普客';
|
case '已登记':
|
||||||
case '已登记':
|
return 6;
|
||||||
return c.signStatus === '已登记';
|
case '未登记':
|
||||||
case '未登记':
|
return 7;
|
||||||
return c.signStatus !== '已登记';
|
case '散客':
|
||||||
case '散客':
|
return 8;
|
||||||
return c.customerType === '散客';
|
case '团客':
|
||||||
case '团客':
|
return 9;
|
||||||
return c.customerType === '团客';
|
default:
|
||||||
default:
|
return 1; // 全部
|
||||||
return true;
|
}
|
||||||
}
|
}, [examFilterTag]);
|
||||||
},
|
|
||||||
);
|
|
||||||
}, [search, 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) => {
|
const handleOpenModal = (id: string, tab: ExamModalTab) => {
|
||||||
setExamSelectedId(id);
|
setExamSelectedId(id);
|
||||||
@@ -51,13 +101,15 @@ export const ExamPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ExamSection
|
{selectedExamClient && (
|
||||||
filteredClients={filteredClients}
|
<ExamSection
|
||||||
selectedExamClient={selectedExamClient}
|
filteredClients={clients}
|
||||||
examFilterTag={examFilterTag}
|
selectedExamClient={selectedExamClient}
|
||||||
onFilterChange={setExamFilterTag}
|
examFilterTag={examFilterTag}
|
||||||
onOpenModal={handleOpenModal}
|
onFilterChange={setExamFilterTag}
|
||||||
/>
|
onOpenModal={handleOpenModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{examModalOpen && (
|
{examModalOpen && (
|
||||||
<ExamModal
|
<ExamModal
|
||||||
|
|||||||
Reference in New Issue
Block a user