diff --git a/apps/web/src/pages/health/AppointmentList.tsx b/apps/web/src/pages/health/AppointmentList.tsx index 8cdc822..789508d 100644 --- a/apps/web/src/pages/health/AppointmentList.tsx +++ b/apps/web/src/pages/health/AppointmentList.tsx @@ -11,9 +11,7 @@ import { Input, Dropdown, message, - Row, Alert, - Col, } from 'antd'; import { PlusOutlined, @@ -26,6 +24,7 @@ import { PatientSelect } from './components/PatientSelect'; import { DoctorSelect } from './components/DoctorSelect'; import { AuthButton } from '../../components/AuthButton'; import { PageContainer } from '../../components/PageContainer'; +import { DrawerForm } from '../../components/DrawerForm'; import { EntityName } from '../../components/EntityName'; import { formatDateTime } from '../../utils/format'; import { usePaginatedData } from '../../hooks/usePaginatedData'; @@ -83,8 +82,8 @@ interface AppointmentFilters { } export default function AppointmentList() { - const [modalOpen, setModalOpen] = useState(false); - const [form] = Form.useForm(); + const [drawerOpen, setDrawerOpen] = useState(false); + const [submitting, setSubmitting] = useState(false); // 患者选择状态(受控组件,不挂在 Form.Item 上) const [selectedPatientId, setSelectedPatientId] = useState(undefined); @@ -206,17 +205,16 @@ export default function AppointmentList() { // ---- 新建预约 ---- const openCreate = () => { - form.resetFields(); setSelectedPatientId(undefined); setSelectedDoctorId(undefined); setScheduleHint(null); setSelectedDate(null); - setModalOpen(true); + setDrawerOpen(true); }; // 排班校验:医生 + 日期选定后查询排班 useEffect(() => { - if (!selectedDoctorId || !selectedDate || !modalOpen) { + if (!selectedDoctorId || !selectedDate || !drawerOpen) { setScheduleHint(null); return; } @@ -237,15 +235,9 @@ export default function AppointmentList() { }) .catch(() => { if (!cancelled) setScheduleHint(null); }); return () => { cancelled = true; }; - }, [selectedDoctorId, selectedDate, modalOpen]); + }, [selectedDoctorId, selectedDate, drawerOpen]); - const handleSubmit = async (values: { - appointment_date: Dayjs; - start_time: Dayjs; - end_time: Dayjs; - appointment_type?: string; - notes?: string; - }) => { + const handleSubmit = async (values: Record) => { if (!selectedPatientId) { message.warning('请选择患者'); return; @@ -255,24 +247,26 @@ export default function AppointmentList() { return; } try { + setSubmitting(true); const req: CreateAppointmentReq = { patient_id: selectedPatientId, - doctor_id: selectedDoctorId || undefined, - appointment_date: values.appointment_date.format('YYYY-MM-DD'), - start_time: values.start_time.format('HH:mm'), - end_time: values.end_time.format('HH:mm'), - appointment_type: values.appointment_type || 'outpatient', - notes: values.notes || undefined, + doctor_id: selectedDoctorId, + appointment_date: (values.appointment_date as Dayjs).format('YYYY-MM-DD'), + start_time: (values.start_time as Dayjs).format('HH:mm'), + end_time: (values.end_time as Dayjs).format('HH:mm'), + appointment_type: (values.appointment_type as string) || 'outpatient', + notes: (values.notes as string) || undefined, }; await appointmentApi.create(req); message.success('预约创建成功'); - setModalOpen(false); - form.resetFields(); + setDrawerOpen(false); setSelectedPatientId(undefined); setSelectedDoctorId(undefined); refresh(); } catch { message.error('创建预约失败'); + } finally { + setSubmitting(false); } }; @@ -433,84 +427,91 @@ export default function AppointmentList() { /> {/* 新建预约弹窗 */} - { - setModalOpen(false); - form.resetFields(); + open={drawerOpen} + onClose={() => { + setDrawerOpen(false); setSelectedPatientId(undefined); setSelectedDoctorId(undefined); setScheduleHint(null); }} - onOk={() => form.submit()} - destroyOnHidden - width={560} - > - {scheduleHint && ( - - )} -
- - setSelectedPatientId(val)} - placeholder="搜索选择患者" - /> - - - setSelectedDoctorId(val)} - placeholder="搜索选择医护" - /> - - - - - setSelectedDate(d ? d.format('YYYY-MM-DD') : null)} /> + onSubmit={handleSubmit} + loading={submitting} + width={640} + columns={2} + initialValues={{ appointment_type: 'outpatient' }} + sections={[ + { + title: '患者信息', + fields: ( + <> + + setSelectedPatientId(val)} + placeholder="搜索选择患者" + /> + + + - - - - - - - - - - - - - - - - - - -
-
+ ), + }, + ]} + /> ); } diff --git a/apps/web/src/pages/health/FollowUpTaskList.tsx b/apps/web/src/pages/health/FollowUpTaskList.tsx index dc99f30..5505aec 100644 --- a/apps/web/src/pages/health/FollowUpTaskList.tsx +++ b/apps/web/src/pages/health/FollowUpTaskList.tsx @@ -21,6 +21,7 @@ import { DoctorSelect } from './components/DoctorSelect'; import { AuthButton } from '../../components/AuthButton'; import { PageContainer } from '../../components/PageContainer'; import { EntityName } from '../../components/EntityName'; +import { DrawerForm } from '../../components/DrawerForm'; import { formatDate, formatDateTime } from '../../utils/format'; import { usePaginatedData } from '../../hooks/usePaginatedData'; @@ -55,14 +56,6 @@ interface FollowUpFilters { assigneeId?: string; } -interface RecordFormValues { - executed_date: dayjs.Dayjs; - result: string; - patient_condition: string; - medical_advice: string; - next_follow_up_date?: dayjs.Dayjs; -} - interface AssignFormValues { assigned_to: string; } @@ -105,7 +98,6 @@ export default function FollowUpTaskList() { // Fill record modal const [recordOpen, setRecordOpen] = useState(false); const [recordLoading, setRecordLoading] = useState(false); - const [recordForm] = Form.useForm(); const [activeTask, setActiveTask] = useState(null); // Assign modal @@ -147,28 +139,27 @@ export default function FollowUpTaskList() { // Fill record const openRecordModal = (task: FollowUpTask) => { setActiveTask(task); - recordForm.resetFields(); setRecordOpen(true); }; - const handleRecordSubmit = async () => { + const handleRecordSubmit = async (values: Record) => { if (!activeTask) return; try { - const values = await recordForm.validateFields(); setRecordLoading(true); await followUpApi.createRecord(activeTask.id, { - executed_date: values.executed_date.format('YYYY-MM-DD'), - result: values.result, - patient_condition: values.patient_condition, - medical_advice: values.medical_advice, - next_follow_up_date: values.next_follow_up_date?.format('YYYY-MM-DD'), + executed_date: (values.executed_date as dayjs.Dayjs).format('YYYY-MM-DD'), + result: values.result as string, + patient_condition: values.patient_condition as string, + medical_advice: values.medical_advice as string, + next_follow_up_date: values.next_follow_up_date + ? (values.next_follow_up_date as dayjs.Dayjs).format('YYYY-MM-DD') + : undefined, }); message.success('随访记录填写成功'); setRecordOpen(false); setActiveTask(null); refresh(page); - } catch (err: unknown) { - if (err && typeof err === 'object' && 'errorFields' in err) return; + } catch { message.error('填写随访记录失败'); } finally { setRecordLoading(false); @@ -429,47 +420,58 @@ export default function FollowUpTaskList() { - {/* Fill Record Modal */} - { + onClose={() => { setRecordOpen(false); setActiveTask(null); }} - confirmLoading={recordLoading} - okText="提交" - cancelText="取消" - destroyOnClose + onSubmit={handleRecordSubmit} + loading={recordLoading} width={560} - > -
- - - - - - - - - - - - - - - -
-
+ columns={1} + sections={[ + { + title: '执行信息', + fields: ( + <> + + + + + + + + ), + }, + { + title: '详细记录', + fields: ( + <> + + + + + + + + + + + ), + }, + ]} + /> {/* Assign Modal */} (null); - const [form] = Form.useForm(); + const [editingPatient, setEditingPatient] = useState(null); const [selectedRowKeys, setSelectedRowKeys] = useState([]); // ---- 分页数据 Hook ---- @@ -103,38 +103,37 @@ export default function PatientList() { // ---- CRUD 操作 ---- - const handleCreateOrEdit = async (values: { - name: string; - gender?: string; - birth_date?: unknown; - blood_type?: string; - id_number?: string; - allergy_history?: string; - notes?: string; - }) => { - const formatted = { - ...values, - birth_date: - values.birth_date && - typeof values.birth_date === 'object' && - 'format' in (values.birth_date as object) - ? (values.birth_date as { format: (f: string) => string }).format( - 'YYYY-MM-DD', - ) - : (values.birth_date as string | undefined), + const handleCreateOrEdit = async (values: Record) => { + const birthDate = values.birth_date; + const formattedBirthDate = + birthDate && typeof birthDate === 'object' && 'format' in (birthDate as object) + ? (birthDate as { format: (f: string) => string }).format('YYYY-MM-DD') + : (birthDate as string | undefined); + + const payload = { + name: values.name as string, + gender: values.gender as string | undefined, + birth_date: formattedBirthDate, + blood_type: values.blood_type as string | undefined, + id_number: values.id_number as string | undefined, + allergy_history: values.allergy_history as string | undefined, + medical_history_summary: values.medical_history_summary as string | undefined, + emergency_contact_name: values.emergency_contact_name as string | undefined, + emergency_contact_phone: values.emergency_contact_phone as string | undefined, + source: values.source as string | undefined, + notes: values.notes as string | undefined, }; + try { if (editingPatient) { const req: UpdatePatientReq & { version: number } = { - ...formatted, - version: - (editingPatient as PatientListItem & { version?: number }) - .version ?? 0, + ...payload, + version: editingPatient.version, }; await patientApi.update(editingPatient.id, req); message.success('患者信息更新成功'); } else { - const req: CreatePatientReq = formatted; + const req: CreatePatientReq = payload; await patientApi.create(req); message.success('患者创建成功'); } @@ -151,8 +150,7 @@ export default function PatientList() { const handleDelete = async (id: string) => { try { const patient = patients.find((p) => p.id === id); - const version = - (patient as PatientListItem & { version?: number })?.version ?? 0; + const version = patient?.version ?? 0; await patientApi.delete(id, version); message.success('患者已删除'); refresh(); @@ -163,25 +161,22 @@ export default function PatientList() { const openCreateModal = () => { setEditingPatient(null); - form.resetFields(); setModalOpen(true); }; - const openEditModal = (record: PatientListItem) => { - setEditingPatient(record); - form.setFieldsValue({ - name: record.name, - gender: record.gender, - birth_date: record.birth_date, - blood_type: record.blood_type, - }); - setModalOpen(true); + const openEditModal = async (record: PatientListItem) => { + try { + const detail = await patientApi.get(record.id); + setEditingPatient(detail); + setModalOpen(true); + } catch { + message.error('获取患者详情失败'); + } }; const closeModal = () => { setModalOpen(false); setEditingPatient(null); - form.resetFields(); }; // ---- 列定义 ---- @@ -386,51 +381,103 @@ export default function PatientList() { }} /> - {/* 新建/编辑患者弹窗 */} - form.submit()} - width={520} - > -
- - - - - - - - - - - - - - - -
-
+ onClose={closeModal} + onSubmit={handleCreateOrEdit} + initialValues={ + editingPatient + ? { + name: editingPatient.name, + gender: editingPatient.gender, + birth_date: editingPatient.birth_date, + blood_type: editingPatient.blood_type, + id_number: editingPatient.id_number, + allergy_history: editingPatient.allergy_history, + medical_history_summary: editingPatient.medical_history_summary, + emergency_contact_name: editingPatient.emergency_contact_name, + emergency_contact_phone: editingPatient.emergency_contact_phone, + source: editingPatient.source, + notes: editingPatient.notes, + } + : undefined + } + width={640} + columns={2} + sections={[ + { + title: '基本信息', + fields: ( + <> + + + + + + + + ), + }, + { + title: '联系方式', + fields: ( + <> + + + + + + + + ), + }, + { + title: '医疗信息', + fields: ( + <> + + + + + + + + + + + ), + }, + { + title: '紧急联系人', + fields: ( + <> + + + + + + + + ), + }, + ]} + /> ); } diff --git a/apps/web/src/pages/health/PointsProductList.tsx b/apps/web/src/pages/health/PointsProductList.tsx index 0e6066e..2edda70 100644 --- a/apps/web/src/pages/health/PointsProductList.tsx +++ b/apps/web/src/pages/health/PointsProductList.tsx @@ -24,6 +24,8 @@ import { type CreatePointsProductReq, } from '../../api/health/points'; import { AuthButton } from '../../components/AuthButton'; +import { DrawerForm } from '../../components/DrawerForm'; +import type { FormSection } from '../../components/DrawerForm'; import { PageContainer } from '../../components/PageContainer'; import { usePaginatedData } from '../../hooks/usePaginatedData'; import { formatDateTime } from '../../utils/format'; @@ -57,7 +59,6 @@ interface ProductFilters { export default function PointsProductList() { const [modalOpen, setModalOpen] = useState(false); const [editing, setEditing] = useState(null); - const [form] = Form.useForm(); const fetchProducts = useCallback( async (page: number, pageSize: number, filters: ProductFilters) => { @@ -93,55 +94,49 @@ export default function PointsProductList() { // ---- 新建 / 编辑 ---- const openCreate = () => { setEditing(null); - form.resetFields(); - form.setFieldsValue({ stock: -1, sort_order: 0 }); setModalOpen(true); }; const openEdit = (record: PointsProduct) => { setEditing(record); - form.setFieldsValue({ - name: record.name, - product_type: record.product_type, - points_cost: record.points_cost, - stock: record.stock, - description: record.description, - image_url: record.image_url, - sort_order: record.sort_order, - }); setModalOpen(true); }; - const handleSubmit = async (values: { - name: string; - product_type: string; - points_cost: number; - stock: number; - description?: string; - image_url?: string; - sort_order?: number; - }) => { + const handleCloseDrawer = () => { + setModalOpen(false); + setEditing(null); + }; + + const handleSubmit = async (values: Record) => { try { + const typed = values as { + name: string; + product_type: string; + points_cost: number; + stock: number; + description?: string; + image_url?: string; + sort_order?: number; + }; if (editing) { await pointsApi.updateProduct(editing.id, { - ...values, + ...typed, version: editing.version, }); } else { const req: CreatePointsProductReq = { - name: values.name, - product_type: values.product_type, - points_cost: values.points_cost, - stock: values.stock, - description: values.description, - image_url: values.image_url, - sort_order: values.sort_order, + name: typed.name, + product_type: typed.product_type, + points_cost: typed.points_cost, + stock: typed.stock, + description: typed.description, + image_url: typed.image_url, + sort_order: typed.sort_order, }; await pointsApi.createProduct(req); } message.success(editing ? '更新成功' : '创建成功'); - setModalOpen(false); - form.resetFields(); + handleCloseDrawer(); refresh(page); } catch { message.error(editing ? '更新失败' : '创建失败'); @@ -277,6 +272,57 @@ export default function PointsProductList() { }, ]; + /** 抽屉表单分区 */ + const formSections: FormSection[] = [ + { + title: '基本信息', + fields: ( + <> + + + + + + + + + + + + + + ), + }, + ]; + return ( - {/* 新建 / 编辑弹窗 */} - { - setModalOpen(false); - form.resetFields(); - }} - onOk={() => form.submit()} - destroyOnClose - width={560} - > -
- - - -
- - - - - - - - + onClose={handleCloseDrawer} + onSubmit={handleSubmit} + initialValues={editing + ? { + name: editing.name, + product_type: editing.product_type, + points_cost: editing.points_cost, + stock: editing.stock, + description: editing.description, + image_url: editing.image_url, + sort_order: editing.sort_order, + } + : { stock: -1, sort_order: 0 }} + loading={false} + width={600} + columns={2} + sections={formSections} + /> ); }