完善体检用餐登记面板

This commit is contained in:
xianyi
2025-12-16 10:17:11 +08:00
parent bf864de805
commit fcc482eac2
3 changed files with 168 additions and 74 deletions

View File

@@ -1,24 +1,101 @@
import type { ExamClient } from '../../data/mockData'; import { useEffect, useState } from 'react';
import { EXAM_CLIENTS } from '../../data/mockData';
import type { PoPhysicalExamDiningLog } from '../../api';
import { getPhysicalExamDiningList, registerPhysicalExamDining } from '../../api';
import { InfoCard } from '../ui'; import { InfoCard } from '../ui';
interface MealRegistrationModalProps { interface MealRegistrationModalProps {
onClose: () => void; onClose: () => void;
totalExamCount: number;
mealCount: number;
notMealCount: number;
mealDoneIds: string[];
onMealDone: (id: string) => void;
} }
export const MealRegistrationModal = ({ export const MealRegistrationModal = ({ onClose }: MealRegistrationModalProps) => {
onClose, const [diningType, setDiningType] = useState<number>(1); // 1-全部 2-已用餐 3-未用餐
totalExamCount, const [loading, setLoading] = useState(false);
mealCount, const [error, setError] = useState<string | null>(null);
notMealCount, const [diningData, setDiningData] = useState<{
mealDoneIds, today_exam_count: number;
onMealDone, dined_count: number;
}: MealRegistrationModalProps) => { not_dined_count: number;
DiningList: PoPhysicalExamDiningLog[];
} | null>(null);
const [registeringIds, setRegisteringIds] = useState<Set<number>>(new Set());
// 获取用餐列表
useEffect(() => {
const fetchDiningList = async () => {
setLoading(true);
setError(null);
try {
const res = await getPhysicalExamDiningList({ dining_type: diningType });
if (res.Status === 200 && res.Data) {
setDiningData({
today_exam_count: res.Data.today_exam_count || 0,
dined_count: res.Data.dined_count || 0,
not_dined_count: res.Data.not_dined_count || 0,
DiningList: res.Data.DiningList || [],
});
} else {
setError(res.Message || '获取用餐列表失败');
}
} catch (err) {
console.error('获取用餐列表失败', err);
setError('获取用餐列表失败,请稍后重试');
} finally {
setLoading(false);
}
};
fetchDiningList();
}, [diningType]);
// 处理用餐登记
const handleMealToggle = async (item: PoPhysicalExamDiningLog) => {
if (!item.physical_exam_id || !item.exam_no || !item.customer_name) {
return;
}
const examId = item.physical_exam_id;
const isCurrentlyDined = item.is_dining === 1;
// 如果已经用餐,则不处理(取消用餐可能需要其他接口)
if (isCurrentlyDined) {
return;
}
setRegisteringIds((prev) => new Set(prev).add(examId));
try {
const res = await registerPhysicalExamDining({
physical_exam_id: examId,
exam_no: item.exam_no,
customer_name: item.customer_name,
});
if (res.Status === 200) {
// 刷新列表
const refreshRes = await getPhysicalExamDiningList({ dining_type: diningType });
if (refreshRes.Status === 200 && refreshRes.Data) {
setDiningData({
today_exam_count: refreshRes.Data.today_exam_count || 0,
dined_count: refreshRes.Data.dined_count || 0,
not_dined_count: refreshRes.Data.not_dined_count || 0,
DiningList: refreshRes.Data.DiningList || [],
});
}
} else {
setError(res.Message || '用餐登记失败');
}
} catch (err) {
console.error('用餐登记失败', err);
setError('用餐登记失败,请稍后重试');
} finally {
setRegisteringIds((prev) => {
const next = new Set(prev);
next.delete(examId);
return next;
});
}
};
return ( return (
<div className='fixed inset-0 z-40 flex items-center justify-center bg-black/30'> <div className='fixed inset-0 z-40 flex items-center justify-center bg-black/30'>
<div className='w-[560px] max-w-[95vw] bg-white rounded-3xl shadow-xl overflow-hidden text-sm'> <div className='w-[560px] max-w-[95vw] bg-white rounded-3xl shadow-xl overflow-hidden text-sm'>
@@ -30,30 +107,83 @@ export const MealRegistrationModal = ({
</div> </div>
<div className='px-4 py-4 bg-gray-50/60'> <div className='px-4 py-4 bg-gray-50/60'>
<div className='space-y-3'> <div className='space-y-3'>
<div className='grid grid-cols-3 gap-3 text-xs'> {/* 筛选按钮 */}
<InfoCard label='今日体检人数' value={totalExamCount} /> <div className='flex items-center gap-2'>
<InfoCard label='已用餐人数' value={mealCount} /> <button
<InfoCard label='未用餐人数' value={notMealCount} /> className={`px-3 py-1 rounded-2xl border text-xs ${diningType === 1 ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-700'
}`}
onClick={() => setDiningType(1)}
>
</button>
<button
className={`px-3 py-1 rounded-2xl border text-xs ${diningType === 2 ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-700'
}`}
onClick={() => setDiningType(2)}
>
</button>
<button
className={`px-3 py-1 rounded-2xl border text-xs ${diningType === 3 ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-700'
}`}
onClick={() => setDiningType(3)}
>
</button>
</div>
{/* 统计信息 */}
{diningData && (
<div className='grid grid-cols-3 gap-3 text-xs'>
<InfoCard label='今日体检人数' value={diningData.today_exam_count} />
<InfoCard label='已用餐人数' value={diningData.dined_count} />
<InfoCard label='未用餐人数' value={diningData.not_dined_count} />
</div>
)}
{/* 错误提示 */}
{error && <div className='text-xs text-amber-600'>{error}</div>}
{/* 客户列表 */}
<div className='text-xs text-gray-600 mt-2 mb-1'>
{diningType === 1 && '选择已用餐客户进行登记:'}
{diningType === 2 && '已用餐客户列表:'}
{diningType === 3 && '选择未用餐客户进行登记:'}
</div> </div>
<div className='text-xs text-gray-600 mt-2 mb-1'></div>
<div className='max-h-60 overflow-auto border rounded-2xl bg-white p-2 text-xs'> <div className='max-h-60 overflow-auto border rounded-2xl bg-white p-2 text-xs'>
{EXAM_CLIENTS.map((c: ExamClient) => { {loading ? (
const checked = mealDoneIds.includes(c.id); <div className='flex items-center justify-center py-4 text-gray-500'>...</div>
) : diningData && diningData.DiningList.length > 0 ? (
diningData.DiningList.map((item) => {
const isDined = item.is_dining === 1;
const isRegistering = item.physical_exam_id ? registeringIds.has(item.physical_exam_id) : false;
const canToggle = !isDined && !isRegistering;
return ( return (
<label <label
key={c.id} key={item.physical_exam_id || item.exam_no}
className='flex items-center justify-between px-3 py-1.5 rounded-2xl hover:bg-gray-50 cursor-pointer' className={`flex items-center justify-between px-3 py-1.5 rounded-2xl ${canToggle ? 'hover:bg-gray-50 cursor-pointer' : 'cursor-not-allowed opacity-60'
}`}
> >
<span> <span>
{c.name} <span className='text-gray-400 text-[11px]'>({c.id})</span> {item.customer_name || '未知'} <span className='text-gray-400 text-[11px]'>({item.exam_no || 'N/A'})</span>
</span> </span>
<span className='flex items-center gap-2'> <span className='flex items-center gap-2'>
<span className='text-gray-400'>{c.status}</span> <span className='text-gray-400'>{item.physical_exam_status_name || '未知状态'}</span>
<input type='checkbox' checked={checked} onChange={() => onMealDone(c.id)} /> <input
type='checkbox'
checked={isDined}
disabled={!canToggle || isRegistering}
onChange={() => handleMealToggle(item)}
/>
{isRegistering && <span className='text-[11px] text-gray-400'>...</span>}
</span> </span>
</label> </label>
); );
})} })
) : (
<div className='flex items-center justify-center py-4 text-gray-500'></div>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -8,11 +8,6 @@ interface QuickActionModalProps {
noteText: string; noteText: string;
onNoteChange: (v: string) => void; onNoteChange: (v: string) => void;
onClose: () => void; onClose: () => void;
totalExamCount: number;
mealCount: number;
notMealCount: number;
mealDoneIds: string[];
onMealDone: (id: string) => void;
} }
export const QuickActionModal = ({ export const QuickActionModal = ({
@@ -20,27 +15,13 @@ export const QuickActionModal = ({
noteText, noteText,
onNoteChange, onNoteChange,
onClose, onClose,
totalExamCount,
mealCount,
notMealCount,
mealDoneIds,
onMealDone,
}: QuickActionModalProps) => { }: QuickActionModalProps) => {
if (action === 'none') { if (action === 'none') {
return null; return null;
} }
if (action === 'meal') { if (action === 'meal') {
return ( return <MealRegistrationModal onClose={onClose} />;
<MealRegistrationModal
onClose={onClose}
totalExamCount={totalExamCount}
mealCount={mealCount}
notMealCount={notMealCount}
mealDoneIds={mealDoneIds}
onMealDone={onMealDone}
/>
);
} }
if (action === 'note') { if (action === 'note') {

View File

@@ -2,7 +2,6 @@ import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import type { QuickActionType } from '../data/mockData'; import type { QuickActionType } from '../data/mockData';
import { EXAM_CLIENTS } from '../data/mockData';
import { QuickActionModal } from '../components/modals/QuickActionModal'; import { QuickActionModal } from '../components/modals/QuickActionModal';
import { LoginModal } from '../components/modals/LoginModal'; import { LoginModal } from '../components/modals/LoginModal';
import { Sidebar, type SectionKey } from '../components/layout/Sidebar'; import { Sidebar, type SectionKey } from '../components/layout/Sidebar';
@@ -34,13 +33,6 @@ export const MainLayout = () => {
const [noteText, setNoteText] = useState(''); const [noteText, setNoteText] = useState('');
const [loginModalOpen, setLoginModalOpen] = useState(false); const [loginModalOpen, setLoginModalOpen] = useState(false);
const [operatorName, setOperatorName] = useState<string>(''); const [operatorName, setOperatorName] = useState<string>('');
const [mealDoneIds, setMealDoneIds] = useState<string[]>(
EXAM_CLIENTS.filter((c) => c.status === '用餐').map((c) => c.id),
);
const totalExamCount = EXAM_CLIENTS.length;
const mealCount = mealDoneIds.length;
const notMealCount = totalExamCount - mealCount;
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
@@ -54,10 +46,6 @@ export const MainLayout = () => {
navigate(sectionToRoute[section]); navigate(sectionToRoute[section]);
}; };
const handleMealDone = (id: string) => {
setMealDoneIds((prev) => (prev.includes(id) ? prev : prev.concat(id)));
};
const handleLoginSuccess = (phone: string) => { const handleLoginSuccess = (phone: string) => {
// 实际项目中应该从后端获取用户信息 // 实际项目中应该从后端获取用户信息
// 这里暂时使用手机号后4位作为操作员名称 // 这里暂时使用手机号后4位作为操作员名称
@@ -106,11 +94,6 @@ export const MainLayout = () => {
noteText={noteText} noteText={noteText}
onNoteChange={setNoteText} onNoteChange={setNoteText}
onClose={() => setQuickAction('none')} onClose={() => setQuickAction('none')}
totalExamCount={totalExamCount}
mealCount={mealCount}
notMealCount={notMealCount}
mealDoneIds={mealDoneIds}
onMealDone={handleMealDone}
/> />
)} )}
</div> </div>