feat(web): 表单升级 — Modal→DrawerForm + 分组双列布局
4 个表单从 Modal 升级为 DrawerForm: - 患者表单:4 分组(基本信息/联系方式/医疗信息/紧急联系人),编辑时获取完整详情 - 预约表单:3 分组(患者信息/医生与排班/备注),保留排班校验逻辑 - 随访填写:2 分组(执行信息/详细记录),DrawerForm 内部校验 - 积分商品:2 分组(基本信息/展示设置) 统一使用 DrawerForm 组件管理表单实例、校验和提交
This commit is contained in:
@@ -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<string | undefined>(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<string, unknown>) => {
|
||||
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() {
|
||||
/>
|
||||
|
||||
{/* 新建预约弹窗 */}
|
||||
<Modal
|
||||
<DrawerForm
|
||||
title="新建预约"
|
||||
open={modalOpen}
|
||||
onCancel={() => {
|
||||
setModalOpen(false);
|
||||
form.resetFields();
|
||||
open={drawerOpen}
|
||||
onClose={() => {
|
||||
setDrawerOpen(false);
|
||||
setSelectedPatientId(undefined);
|
||||
setSelectedDoctorId(undefined);
|
||||
setScheduleHint(null);
|
||||
}}
|
||||
onOk={() => form.submit()}
|
||||
destroyOnHidden
|
||||
width={560}
|
||||
>
|
||||
{scheduleHint && (
|
||||
<Alert
|
||||
message={scheduleHint}
|
||||
type={scheduleHint.includes('暂无排班') ? 'warning' : 'info'}
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||||
<Form.Item label="患者" required>
|
||||
<PatientSelect
|
||||
value={selectedPatientId}
|
||||
onChange={(val) => setSelectedPatientId(val)}
|
||||
placeholder="搜索选择患者"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="医护" required>
|
||||
<DoctorSelect
|
||||
value={selectedDoctorId}
|
||||
onChange={(val) => setSelectedDoctorId(val)}
|
||||
placeholder="搜索选择医护"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="appointment_date"
|
||||
label="预约日期"
|
||||
rules={[{ required: true, message: '请选择预约日期' }]}
|
||||
>
|
||||
<DatePicker style={{ width: '100%' }} onChange={(d) => setSelectedDate(d ? d.format('YYYY-MM-DD') : null)} />
|
||||
onSubmit={handleSubmit}
|
||||
loading={submitting}
|
||||
width={640}
|
||||
columns={2}
|
||||
initialValues={{ appointment_type: 'outpatient' }}
|
||||
sections={[
|
||||
{
|
||||
title: '患者信息',
|
||||
fields: (
|
||||
<>
|
||||
<Form.Item label="患者" required>
|
||||
<PatientSelect
|
||||
value={selectedPatientId}
|
||||
onChange={(val) => setSelectedPatientId(val)}
|
||||
placeholder="搜索选择患者"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="appointment_type" label="预约类型" initialValue="outpatient">
|
||||
<Select options={APPOINTMENT_TYPE_OPTIONS} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="appointment_date"
|
||||
label="预约日期"
|
||||
rules={[{ required: true, message: '请选择预约日期' }]}
|
||||
>
|
||||
<DatePicker style={{ width: '100%' }} onChange={(d) => setSelectedDate(d ? d.format('YYYY-MM-DD') : null)} />
|
||||
</Form.Item>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '医生与排班',
|
||||
fields: (
|
||||
<>
|
||||
<Form.Item label="医护" required>
|
||||
<DoctorSelect
|
||||
value={selectedDoctorId}
|
||||
onChange={(val) => setSelectedDoctorId(val)}
|
||||
placeholder="搜索选择医护"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="start_time"
|
||||
label="开始时间"
|
||||
rules={[{ required: true, message: '请选择开始时间' }]}
|
||||
>
|
||||
<TimePicker format="HH:mm" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="end_time"
|
||||
label="结束时间"
|
||||
rules={[{ required: true, message: '请选择结束时间' }]}
|
||||
>
|
||||
<TimePicker format="HH:mm" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
{scheduleHint && (
|
||||
<Alert
|
||||
message={scheduleHint}
|
||||
type={scheduleHint.includes('暂无排班') ? 'warning' : 'info'}
|
||||
showIcon
|
||||
style={{ marginBottom: 16, gridColumn: '1 / -1' }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
fields: (
|
||||
<Form.Item name="notes" label="备注" style={{ gridColumn: '1 / -1' }}>
|
||||
<Input.TextArea rows={3} placeholder="预约备注信息" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="appointment_type" label="预约类型" initialValue="outpatient">
|
||||
<Select options={APPOINTMENT_TYPE_OPTIONS} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="start_time"
|
||||
label="开始时间"
|
||||
rules={[{ required: true, message: '请选择开始时间' }]}
|
||||
>
|
||||
<TimePicker format="HH:mm" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="end_time"
|
||||
label="结束时间"
|
||||
rules={[{ required: true, message: '请选择结束时间' }]}
|
||||
>
|
||||
<TimePicker format="HH:mm" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name="notes" label="备注">
|
||||
<Input.TextArea rows={3} placeholder="预约备注信息" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<RecordFormValues>();
|
||||
const [activeTask, setActiveTask] = useState<FollowUpTask | null>(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<string, unknown>) => {
|
||||
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() {
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* Fill Record Modal */}
|
||||
<Modal
|
||||
{/* Fill Record Drawer */}
|
||||
<DrawerForm
|
||||
title={`填写随访记录 — 任务 ${activeTask?.id?.slice(0, 8) ?? ''}`}
|
||||
open={recordOpen}
|
||||
onOk={handleRecordSubmit}
|
||||
onCancel={() => {
|
||||
onClose={() => {
|
||||
setRecordOpen(false);
|
||||
setActiveTask(null);
|
||||
}}
|
||||
confirmLoading={recordLoading}
|
||||
okText="提交"
|
||||
cancelText="取消"
|
||||
destroyOnClose
|
||||
onSubmit={handleRecordSubmit}
|
||||
loading={recordLoading}
|
||||
width={560}
|
||||
>
|
||||
<Form form={recordForm} layout="vertical" autoComplete="off">
|
||||
<Form.Item
|
||||
name="executed_date"
|
||||
label="执行日期"
|
||||
rules={[{ required: true, message: '请选择执行日期' }]}
|
||||
>
|
||||
<DatePicker style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="result"
|
||||
label="随访结果"
|
||||
rules={[{ required: true, message: '请填写随访结果' }]}
|
||||
>
|
||||
<Input.TextArea rows={3} placeholder="描述随访结果" />
|
||||
</Form.Item>
|
||||
<Form.Item name="patient_condition" label="患者状况">
|
||||
<Input.TextArea rows={3} placeholder="描述患者当前状况" />
|
||||
</Form.Item>
|
||||
<Form.Item name="medical_advice" label="医嘱">
|
||||
<Input.TextArea rows={3} placeholder="医嘱内容" />
|
||||
</Form.Item>
|
||||
<Form.Item name="next_follow_up_date" label="下次随访日期">
|
||||
<DatePicker style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
columns={1}
|
||||
sections={[
|
||||
{
|
||||
title: '执行信息',
|
||||
fields: (
|
||||
<>
|
||||
<Form.Item
|
||||
name="executed_date"
|
||||
label="执行日期"
|
||||
rules={[{ required: true, message: '请选择执行日期' }]}
|
||||
>
|
||||
<DatePicker style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="result"
|
||||
label="随访结果"
|
||||
rules={[{ required: true, message: '请填写随访结果' }]}
|
||||
>
|
||||
<Input.TextArea rows={3} placeholder="描述随访结果" />
|
||||
</Form.Item>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '详细记录',
|
||||
fields: (
|
||||
<>
|
||||
<Form.Item name="patient_condition" label="患者状况">
|
||||
<Input.TextArea rows={3} placeholder="描述患者当前状况" />
|
||||
</Form.Item>
|
||||
<Form.Item name="medical_advice" label="医嘱">
|
||||
<Input.TextArea rows={3} placeholder="医嘱内容" />
|
||||
</Form.Item>
|
||||
<Form.Item name="next_follow_up_date" label="下次随访日期">
|
||||
<DatePicker style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Assign Modal */}
|
||||
<Modal
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
Table,
|
||||
Button,
|
||||
Space,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
@@ -20,6 +19,7 @@ import {
|
||||
import { patientApi } from '../../api/health/patients';
|
||||
import type {
|
||||
PatientListItem,
|
||||
PatientDetail,
|
||||
CreatePatientReq,
|
||||
UpdatePatientReq,
|
||||
} from '../../api/health/patients';
|
||||
@@ -27,6 +27,7 @@ import { StatusTag } from './components/StatusTag';
|
||||
import { GENDER_OPTIONS, BLOOD_TYPE_OPTIONS, STATUS_OPTIONS } from '../../constants/health';
|
||||
import { AuthButton } from '../../components/AuthButton';
|
||||
import { PageContainer } from '../../components/PageContainer';
|
||||
import { DrawerForm } from '../../components/DrawerForm';
|
||||
import { usePaginatedData } from '../../hooks/usePaginatedData';
|
||||
import { calcAge, formatDateTime } from '../../utils/format';
|
||||
|
||||
@@ -48,8 +49,7 @@ const DEFAULT_FILTERS: PatientFilters = {
|
||||
export default function PatientList() {
|
||||
const navigate = useNavigate();
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingPatient, setEditingPatient] = useState<PatientListItem | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
const [editingPatient, setEditingPatient] = useState<PatientDetail | null>(null);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
|
||||
|
||||
// ---- 分页数据 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<string, unknown>) => {
|
||||
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() {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 新建/编辑患者弹窗 */}
|
||||
<Modal
|
||||
{/* 新建/编辑患者抽屉 */}
|
||||
<DrawerForm
|
||||
title={editingPatient ? '编辑患者' : '新建患者'}
|
||||
open={modalOpen}
|
||||
onCancel={closeModal}
|
||||
onOk={() => form.submit()}
|
||||
width={520}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleCreateOrEdit}
|
||||
layout="vertical"
|
||||
style={{ marginTop: 16 }}
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="姓名"
|
||||
rules={[{ required: true, message: '请输入患者姓名' }]}
|
||||
>
|
||||
<Input placeholder="请输入姓名" />
|
||||
</Form.Item>
|
||||
<Form.Item name="gender" label="性别">
|
||||
<Select options={GENDER_OPTIONS} placeholder="请选择性别" allowClear />
|
||||
</Form.Item>
|
||||
<Form.Item name="birth_date" label="出生日期">
|
||||
<DatePicker style={{ width: '100%' }} placeholder="请选择出生日期" />
|
||||
</Form.Item>
|
||||
<Form.Item name="blood_type" label="血型">
|
||||
<Select
|
||||
options={BLOOD_TYPE_OPTIONS}
|
||||
placeholder="请选择血型"
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="id_number" label="身份证号">
|
||||
<Input placeholder="请输入身份证号" />
|
||||
</Form.Item>
|
||||
<Form.Item name="allergy_history" label="过敏史">
|
||||
<Input.TextArea rows={2} placeholder="请输入过敏史" />
|
||||
</Form.Item>
|
||||
<Form.Item name="notes" label="备注">
|
||||
<Input.TextArea rows={2} placeholder="请输入备注" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
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: (
|
||||
<>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="姓名"
|
||||
rules={[{ required: true, message: '请输入患者姓名' }]}
|
||||
>
|
||||
<Input placeholder="请输入姓名" />
|
||||
</Form.Item>
|
||||
<Form.Item name="gender" label="性别">
|
||||
<Select options={GENDER_OPTIONS} placeholder="请选择性别" allowClear />
|
||||
</Form.Item>
|
||||
<Form.Item name="birth_date" label="出生日期">
|
||||
<DatePicker style={{ width: '100%' }} placeholder="请选择出生日期" />
|
||||
</Form.Item>
|
||||
<Form.Item name="blood_type" label="血型">
|
||||
<Select
|
||||
options={BLOOD_TYPE_OPTIONS}
|
||||
placeholder="请选择血型"
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '联系方式',
|
||||
fields: (
|
||||
<>
|
||||
<Form.Item name="id_number" label="身份证号">
|
||||
<Input placeholder="请输入身份证号" />
|
||||
</Form.Item>
|
||||
<Form.Item name="source" label="来源">
|
||||
<Input placeholder="请输入患者来源" />
|
||||
</Form.Item>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '医疗信息',
|
||||
fields: (
|
||||
<>
|
||||
<Form.Item name="allergy_history" label="过敏史">
|
||||
<Input.TextArea rows={2} placeholder="请输入过敏史" />
|
||||
</Form.Item>
|
||||
<Form.Item name="medical_history_summary" label="病史摘要">
|
||||
<Input.TextArea rows={2} placeholder="请输入病史摘要" />
|
||||
</Form.Item>
|
||||
<Form.Item name="notes" label="备注">
|
||||
<Input.TextArea rows={2} placeholder="请输入备注" />
|
||||
</Form.Item>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '紧急联系人',
|
||||
fields: (
|
||||
<>
|
||||
<Form.Item name="emergency_contact_name" label="联系人姓名">
|
||||
<Input placeholder="请输入紧急联系人姓名" />
|
||||
</Form.Item>
|
||||
<Form.Item name="emergency_contact_phone" label="联系电话">
|
||||
<Input placeholder="请输入紧急联系人电话" />
|
||||
</Form.Item>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<PointsProduct | null>(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<string, unknown>) => {
|
||||
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: (
|
||||
<>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="商品名称"
|
||||
rules={[{ required: true, message: '请输入商品名称' }]}
|
||||
>
|
||||
<Input placeholder="如:体检套餐兑换券" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="product_type"
|
||||
label="商品类型"
|
||||
rules={[{ required: true, message: '请选择商品类型' }]}
|
||||
>
|
||||
<Select placeholder="选择类型" options={PRODUCT_TYPE_OPTIONS} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="points_cost"
|
||||
label="所需积分"
|
||||
rules={[{ required: true, message: '请输入所需积分' }]}
|
||||
>
|
||||
<InputNumber min={1} max={999999} style={{ width: '100%' }} placeholder="如:100" />
|
||||
</Form.Item>
|
||||
<Form.Item name="stock" label="库存数量">
|
||||
<InputNumber min={-1} max={999999} style={{ width: '100%' }} placeholder="-1 表示无限" />
|
||||
</Form.Item>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '展示设置',
|
||||
fields: (
|
||||
<>
|
||||
<Form.Item name="image_url" label="图片链接">
|
||||
<Input placeholder="商品图片 URL" />
|
||||
</Form.Item>
|
||||
<Form.Item name="sort_order" label="排序">
|
||||
<InputNumber min={0} max={9999} style={{ width: '100%' }} placeholder="0" />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="描述">
|
||||
<Input.TextArea rows={3} placeholder="商品说明" />
|
||||
</Form.Item>
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title="积分商品"
|
||||
@@ -335,60 +381,28 @@ export default function PointsProductList() {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 新建 / 编辑弹窗 */}
|
||||
<Modal
|
||||
{/* 新建 / 编辑抽屉 */}
|
||||
<DrawerForm
|
||||
title={editing ? '编辑商品' : '新建商品'}
|
||||
open={modalOpen}
|
||||
onCancel={() => {
|
||||
setModalOpen(false);
|
||||
form.resetFields();
|
||||
}}
|
||||
onOk={() => form.submit()}
|
||||
destroyOnClose
|
||||
width={560}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="商品名称"
|
||||
rules={[{ required: true, message: '请输入商品名称' }]}
|
||||
>
|
||||
<Input placeholder="如:体检套餐兑换券" />
|
||||
</Form.Item>
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<Form.Item
|
||||
name="product_type"
|
||||
label="商品类型"
|
||||
rules={[{ required: true, message: '请选择商品类型' }]}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Select placeholder="选择类型" options={PRODUCT_TYPE_OPTIONS} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="points_cost"
|
||||
label="所需积分"
|
||||
rules={[{ required: true, message: '请输入所需积分' }]}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<InputNumber min={1} max={999999} style={{ width: '100%' }} placeholder="如:100" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<Form.Item name="stock" label="库存数量" initialValue={-1} style={{ flex: 1 }}>
|
||||
<InputNumber min={-1} max={999999} style={{ width: '100%' }} placeholder="-1 表示无限" />
|
||||
</Form.Item>
|
||||
<Form.Item name="sort_order" label="排序" initialValue={0} style={{ flex: 1 }}>
|
||||
<InputNumber min={0} max={9999} style={{ width: '100%' }} placeholder="0" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item name="image_url" label="图片链接">
|
||||
<Input placeholder="商品图片 URL" />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="描述">
|
||||
<Input.TextArea rows={3} placeholder="商品说明" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
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}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user