分离体检详情面板

This commit is contained in:
xianyi
2025-12-15 15:29:51 +08:00
parent 2a62fecfb1
commit 4741a437a4
2 changed files with 378 additions and 363 deletions

View File

@@ -0,0 +1,373 @@
import { useMemo, useState } from 'react';
import type {
CustomerAppointmentInfo,
CustomerExamAddItem,
CustomerInfo,
PhysicalExamProgressItem,
} from '../../api';
import { editCustomerDetail } from '../../api';
import type { ExamClient } from '../../data/mockData';
import { Button, Input } from '../ui';
interface ExamDetailPanelProps {
client: ExamClient;
customerInfo: CustomerInfo | null;
appointmentInfo: CustomerAppointmentInfo | null;
addItemInfoList: CustomerExamAddItem[] | null;
progressList: PhysicalExamProgressItem[] | null;
loading: boolean;
}
const getMaritalCodeFromText = (text: string): number => {
if (text.includes('未婚') || text === '未婚') return 10;
if (text.includes('已婚') || text === '已婚') return 20;
return 20;
};
const getMaritalText = (code: number): string => (code === 10 ? '未婚' : '已婚');
export const ExamDetailPanel = ({
client,
customerInfo,
appointmentInfo,
addItemInfoList,
progressList,
loading,
}: ExamDetailPanelProps) => {
const basePhone = customerInfo?.phone || (client['mobile' as keyof ExamClient] as string | undefined) || '';
const baseMaritalText =
customerInfo?.patient_marital_status_name ||
(client['maritalStatus' as keyof ExamClient] as string | undefined) ||
'—';
const baseMaritalCode = baseMaritalText === '—' ? 20 : getMaritalCodeFromText(baseMaritalText);
const [phone, setPhone] = useState(basePhone || '—');
const [maritalCode, setMaritalCode] = useState(baseMaritalCode);
const [phoneEditing, setPhoneEditing] = useState(false);
const [maritalEditing, setMaritalEditing] = useState(false);
const [editLoading, setEditLoading] = useState(false);
const [editMessage, setEditMessage] = useState<string | null>(null);
const customerChannel = client.customerType === '团客' ? '团体客户' : '散客客户';
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 = 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 handleSavePhone = async () => {
if (!phone || phone.trim() === '' || phone === '—') {
setEditMessage('请输入联系电话');
return;
}
setEditLoading(true);
setEditMessage(null);
try {
const res = await editCustomerDetail({
marital_status: maritalCode,
phone: phone.trim(),
});
if (res.Status === 200) {
setEditMessage('保存成功');
setPhoneEditing(false);
setTimeout(() => setEditMessage(null), 2000);
} else {
setEditMessage(res.Message || '保存失败');
}
} catch (err) {
console.error('保存客户信息失败', err);
setEditMessage('保存失败,请稍后重试');
} finally {
setEditLoading(false);
}
};
const handleSaveMarital = async () => {
const phoneValue = phone === '—' ? '' : phone.trim();
if (!phoneValue) {
setEditMessage('联系电话不能为空');
return;
}
setEditLoading(true);
setEditMessage(null);
try {
const res = await editCustomerDetail({
marital_status: maritalCode,
phone: phoneValue,
});
if (res.Status === 200) {
setEditMessage('保存成功');
setMaritalEditing(false);
setTimeout(() => setEditMessage(null), 2000);
} else {
setEditMessage(res.Message || '保存失败');
}
} catch (err) {
console.error('保存客户信息失败', err);
setEditMessage('保存失败,请稍后重试');
} finally {
setEditLoading(false);
}
};
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 (
<div className='space-y-4 text-sm'>
<div className='flex items-center gap-4'>
<div className='w-14 h-14 rounded-full bg-gray-200 flex-shrink-0 overflow-hidden'>
<div className='w-full h-full rounded-full bg-gray-300 flex items-center justify-center text-[10px] text-gray-500'>
</div>
</div>
<div className='text-xs text-gray-500'>
{loading ? '加载中…' : '基础信息:头像、姓名、证件号、手机号等(点击图标可进行编辑)'}
</div>
</div>
{editMessage && (
<div
className={`text-xs px-3 py-2 rounded-lg ${
editMessage.includes('成功') ? 'bg-green-50 text-green-600' : 'bg-amber-50 text-amber-600'
}`}
>
{editMessage}
</div>
)}
<div className='space-y-2 text-xs text-gray-700'>
<div className='font-medium text-gray-900'></div>
<div className='grid grid-cols-2 gap-x-8 gap-y-1'>
<div>
<span className='text-gray-900'>{customerInfo?.customer_name || client.name}</span>
</div>
<div>
<span className='text-gray-900'>{customerInfo?.id_no || '—'}</span>
</div>
<div className='flex items-center'>
<span></span>
{!phoneEditing ? (
<span className='text-gray-900 flex items-center'>
{phone}
<button className='ml-1 text-blue-500 text-[11px] hover:underline' onClick={() => setPhoneEditing(true)}>
</button>
</span>
) : (
<span className='flex items-center gap-1'>
<input
className='w-28 rounded-xl border px-2 py-0.5 text-[11px] outline-none'
value={phone}
onChange={(e) => setPhone(e.target.value)}
disabled={editLoading}
/>
<button
className='px-2 py-0.5 rounded-xl border text-[11px] disabled:opacity-50'
onClick={handleSavePhone}
disabled={editLoading}
>
{editLoading ? '保存中...' : '保存'}
</button>
<button
className='px-2 py-0.5 rounded-xl border text-[11px]'
onClick={() => {
setPhoneEditing(false);
setPhone(basePhone || '—');
setEditMessage(null);
}}
disabled={editLoading}
>
</button>
</span>
)}
</div>
<div>
/
<span className='text-gray-900'>
{customerInfo?.gender_name || client.gender} / {client.age}
</span>
</div>
<div>
<span className='text-gray-900'>{client.level}</span>
</div>
<div>
<span className='text-gray-900'>{customerChannel}</span>
</div>
<div className='flex items-center'>
<span></span>
{!maritalEditing ? (
<span className='text-gray-900 flex items-center'>
{getMaritalText(maritalCode)}
<button className='ml-1 text-blue-500 text-[11px] hover:underline' onClick={() => setMaritalEditing(true)}>
</button>
</span>
) : (
<span className='flex items-center gap-2'>
<label className='flex items-center gap-1 cursor-pointer'>
<input
type='radio'
name='marital'
value='10'
checked={maritalCode === 10}
onChange={(e) => setMaritalCode(Number(e.target.value))}
disabled={editLoading}
className='w-3 h-3'
/>
<span className='text-[11px]'></span>
</label>
<label className='flex items-center gap-1 cursor-pointer'>
<input
type='radio'
name='marital'
value='20'
checked={maritalCode === 20}
onChange={(e) => setMaritalCode(Number(e.target.value))}
disabled={editLoading}
className='w-3 h-3'
/>
<span className='text-[11px]'></span>
</label>
<button
className='px-2 py-0.5 rounded-xl border text-[11px] disabled:opacity-50'
onClick={handleSaveMarital}
disabled={editLoading}
>
{editLoading ? '保存中...' : '保存'}
</button>
<button
className='px-2 py-0.5 rounded-xl border text-[11px]'
onClick={() => {
setMaritalEditing(false);
setMaritalCode(baseMaritalCode);
setEditMessage(null);
}}
disabled={editLoading}
>
</button>
</span>
)}
</div>
<div>
<span className='text-gray-900'>{familyDoctor}</span>
</div>
<div>
<span className='text-gray-900'>{groupTag as string}</span>
</div>
</div>
</div>
<div className='space-y-2 text-xs text-gray-700'>
<div className='font-medium text-gray-900'></div>
<div className='grid grid-cols-2 gap-x-8 gap-y-1'>
<div>
<span className='text-gray-900'>{bookingTime as string}</span>
</div>
<div>
<span className='text-gray-900'>{signTime as string}</span>
</div>
<div>
<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 className='col-span-2'>
<span className='text-gray-900'>{client.packageName}</span>
</div>
<div className='col-span-2'>
<span className='text-gray-900'>{addonSummary as string}</span>
</div>
</div>
</div>
<div className='grid grid-cols-2 gap-4 text-xs'>
<div className='p-3 rounded-2xl bg-green-50 border max-h-48 overflow-auto custom-scroll'>
<div className='font-medium mb-2'> {progressGroups.checked.length} </div>
<div className='flex flex-wrap gap-2'>
{(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]'>
{i}
</span>
))}
</div>
</div>
<div className='p-3 rounded-2xl bg-red-50 border max-h-48 overflow-auto custom-scroll'>
<div className='font-medium mb-2'> {progressGroups.abandoned.length} </div>
<div className='flex flex-wrap gap-2'>
{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 className='p-3 rounded-2xl bg-yellow-50 border max-h-48 overflow-auto custom-scroll'>
<div className='font-medium mb-2'> {progressGroups.pending.length} </div>
<div className='flex flex-wrap gap-2'>
{(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]'>
{i}
</span>
))}
</div>
</div>
<div className='p-3 rounded-2xl bg-blue-50 border max-h-48 overflow-auto custom-scroll'>
<div className='font-medium mb-2'> {progressGroups.deferred.length} </div>
<div className='flex flex-wrap gap-2'>
{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>
);
};

View File

@@ -1,16 +1,17 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import type { ExamClient, ExamModalTab } from '../../data/mockData';
import type {
CustomerAppointmentInfo,
CustomerExamAddItem,
CustomerInfo,
OutputTongyishuFileInfo,
PhysicalExamProgressItem,
OutputTongyishuFileInfo,
} from '../../api';
import { getCustomerDetail, getPhysicalExamProgressDetail, signInMedicalExamCenter, getTongyishuPdf, submitTongyishuSign, editCustomerDetail } from '../../api';
import { getCustomerDetail, getPhysicalExamProgressDetail, signInMedicalExamCenter, getTongyishuPdf, submitTongyishuSign } from '../../api';
import type { SignaturePadHandle } from '../ui';
import { Button, Input, SignaturePad } from '../ui';
import { ExamDetailPanel } from './ExamDetailPanel';
interface ExamModalProps {
client: ExamClient;
@@ -157,7 +158,7 @@ export const ExamModal = ({ client, tab, onTabChange, onClose }: ExamModalProps)
<div className='px-4 py-4 bg-gray-50/60'>
{tab === 'detail' && (
<ExamDetailInfo
<ExamDetailPanel
client={client}
customerInfo={customerInfo}
appointmentInfo={appointmentInfo}
@@ -772,365 +773,6 @@ const ExamAddonPanel = ({ client }: { client: ExamClient }) => {
);
};
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 baseMaritalText =
customerInfo?.patient_marital_status_name ||
(client['maritalStatus' as keyof ExamClient] as string | undefined) ||
'—';
// 将文本转换为数字10-未婚20-已婚
const getMaritalCodeFromText = (text: string): number => {
if (text.includes('未婚') || text === '未婚') return 10;
if (text.includes('已婚') || text === '已婚') return 20;
return 20; // 默认已婚
};
const baseMaritalCode = baseMaritalText === '—' ? 20 : getMaritalCodeFromText(baseMaritalText);
const [phone, setPhone] = useState(basePhone || '—');
const [maritalCode, setMaritalCode] = useState(baseMaritalCode);
const [phoneEditing, setPhoneEditing] = useState(false);
const [maritalEditing, setMaritalEditing] = useState(false);
const [editLoading, setEditLoading] = useState(false);
const [editMessage, setEditMessage] = useState<string | null>(null);
const getMaritalText = (code: number): string => {
return code === 10 ? '未婚' : '已婚';
};
const customerChannel = client.customerType === '团客' ? '团体客户' : '散客客户';
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 = 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 handleSavePhone = async () => {
if (!phone || phone.trim() === '' || phone === '—') {
setEditMessage('请输入联系电话');
return;
}
setEditLoading(true);
setEditMessage(null);
try {
const res = await editCustomerDetail({
marital_status: maritalCode,
phone: phone.trim(),
});
if (res.Status === 200) {
setEditMessage('保存成功');
setPhoneEditing(false);
// 2秒后清除消息
setTimeout(() => setEditMessage(null), 2000);
} else {
setEditMessage(res.Message || '保存失败');
}
} catch (err) {
console.error('保存客户信息失败', err);
setEditMessage('保存失败,请稍后重试');
} finally {
setEditLoading(false);
}
};
const handleSaveMarital = async () => {
const phoneValue = phone === '—' ? '' : phone.trim();
if (!phoneValue) {
setEditMessage('联系电话不能为空');
return;
}
setEditLoading(true);
setEditMessage(null);
try {
const res = await editCustomerDetail({
marital_status: maritalCode,
phone: phoneValue,
});
if (res.Status === 200) {
setEditMessage('保存成功');
setMaritalEditing(false);
// 2秒后清除消息
setTimeout(() => setEditMessage(null), 2000);
} else {
setEditMessage(res.Message || '保存失败');
}
} catch (err) {
console.error('保存客户信息失败', err);
setEditMessage('保存失败,请稍后重试');
} finally {
setEditLoading(false);
}
};
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 (
<div className='space-y-4 text-sm'>
<div className='flex items-center gap-4'>
<div className='w-14 h-14 rounded-full bg-gray-200 flex-shrink-0 overflow-hidden'>
<div className='w-full h-full rounded-full bg-gray-300 flex items-center justify-center text-[10px] text-gray-500'>
</div>
</div>
<div className='text-xs text-gray-500'>
{loading ? '加载中…' : '基础信息:头像、姓名、证件号、手机号等(点击图标可进行编辑)'}
</div>
</div>
{editMessage && (
<div className={`text-xs px-3 py-2 rounded-lg ${editMessage.includes('成功') ? 'bg-green-50 text-green-600' : 'bg-amber-50 text-amber-600'
}`}>
{editMessage}
</div>
)}
<div className='space-y-2 text-xs text-gray-700'>
<div className='font-medium text-gray-900'></div>
<div className='grid grid-cols-2 gap-x-8 gap-y-1'>
<div>
<span className='text-gray-900'>{customerInfo?.customer_name || client.name}</span>
</div>
<div>
<span className='text-gray-900'>{customerInfo?.id_no || '—'}</span>
</div>
<div className='flex items-center'>
<span></span>
{!phoneEditing ? (
<span className='text-gray-900 flex items-center'>
{phone}
<button className='ml-1 text-blue-500 text-[11px] hover:underline' onClick={() => setPhoneEditing(true)}>
</button>
</span>
) : (
<span className='flex items-center gap-1'>
<input
className='w-28 rounded-xl border px-2 py-0.5 text-[11px] outline-none'
value={phone}
onChange={(e) => setPhone(e.target.value)}
disabled={editLoading}
/>
<button
className='px-2 py-0.5 rounded-xl border text-[11px] disabled:opacity-50'
onClick={handleSavePhone}
disabled={editLoading}
>
{editLoading ? '保存中...' : '保存'}
</button>
<button
className='px-2 py-0.5 rounded-xl border text-[11px]'
onClick={() => {
setPhoneEditing(false);
setPhone(basePhone || '—');
setEditMessage(null);
}}
disabled={editLoading}
>
</button>
</span>
)}
</div>
<div>
/
<span className='text-gray-900'>
{customerInfo?.gender_name || client.gender} / {client.age}
</span>
</div>
<div>
<span className='text-gray-900'>{client.level}</span>
</div>
<div>
<span className='text-gray-900'>{customerChannel}</span>
</div>
<div className='flex items-center'>
<span></span>
{!maritalEditing ? (
<span className='text-gray-900 flex items-center'>
{getMaritalText(maritalCode)}
<button className='ml-1 text-blue-500 text-[11px] hover:underline' onClick={() => setMaritalEditing(true)}>
</button>
</span>
) : (
<span className='flex items-center gap-2'>
<label className='flex items-center gap-1 cursor-pointer'>
<input
type='radio'
name='marital'
value='10'
checked={maritalCode === 10}
onChange={(e) => setMaritalCode(Number(e.target.value))}
disabled={editLoading}
className='w-3 h-3'
/>
<span className='text-[11px]'></span>
</label>
<label className='flex items-center gap-1 cursor-pointer'>
<input
type='radio'
name='marital'
value='20'
checked={maritalCode === 20}
onChange={(e) => setMaritalCode(Number(e.target.value))}
disabled={editLoading}
className='w-3 h-3'
/>
<span className='text-[11px]'></span>
</label>
<button
className='px-2 py-0.5 rounded-xl border text-[11px] disabled:opacity-50'
onClick={handleSaveMarital}
disabled={editLoading}
>
{editLoading ? '保存中...' : '保存'}
</button>
<button
className='px-2 py-0.5 rounded-xl border text-[11px]'
onClick={() => {
setMaritalEditing(false);
setMaritalCode(baseMaritalCode);
setEditMessage(null);
}}
disabled={editLoading}
>
</button>
</span>
)}
</div>
<div>
<span className='text-gray-900'>{familyDoctor}</span>
</div>
<div>
<span className='text-gray-900'>{groupTag as string}</span>
</div>
</div>
</div>
<div className='space-y-2 text-xs text-gray-700'>
<div className='font-medium text-gray-900'></div>
<div className='grid grid-cols-2 gap-x-8 gap-y-1'>
<div>
<span className='text-gray-900'>{bookingTime as string}</span>
</div>
<div>
<span className='text-gray-900'>{signTime as string}</span>
</div>
<div>
<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 className='col-span-2'>
<span className='text-gray-900'>{client.packageName}</span>
</div>
<div className='col-span-2'>
<span className='text-gray-900'>{addonSummary as string}</span>
</div>
</div>
</div>
<div className='grid grid-cols-2 gap-4 text-xs'>
<div className='p-3 rounded-2xl bg-green-50 border max-h-48 overflow-auto custom-scroll'>
<div className='font-medium mb-2'> {progressGroups.checked.length} </div>
<div className='flex flex-wrap gap-2'>
{(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]'>
{i}
</span>
))}
</div>
</div>
<div className='p-3 rounded-2xl bg-red-50 border max-h-48 overflow-auto custom-scroll'>
<div className='font-medium mb-2'> {progressGroups.abandoned.length} </div>
<div className='flex flex-wrap gap-2'>
{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 className='p-3 rounded-2xl bg-yellow-50 border max-h-48 overflow-auto custom-scroll'>
<div className='font-medium mb-2'> {progressGroups.pending.length} </div>
<div className='flex flex-wrap gap-2'>
{(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]'>
{i}
</span>
))}
</div>
</div>
<div className='p-3 rounded-2xl bg-blue-50 border max-h-48 overflow-auto custom-scroll'>
<div className='font-medium mb-2'> {progressGroups.deferred.length} </div>
<div className='flex flex-wrap gap-2'>
{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>
);
};
const ExamDeliveryPanel = ({ client }: { client: ExamClient }) => (
<div className='flex justify-center'>
<div className='w-full rounded-2xl border shadow-sm px-6 py-4 text-xs text-gray-800'>