feat(web): 表单升级 — Modal→DrawerForm + 分组双列布局
Some checks failed
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled

4 个表单从 Modal 升级为 DrawerForm:
- 患者表单:4 分组(基本信息/联系方式/医疗信息/紧急联系人),编辑时获取完整详情
- 预约表单:3 分组(患者信息/医生与排班/备注),保留排班校验逻辑
- 随访填写:2 分组(执行信息/详细记录),DrawerForm 内部校验
- 积分商品:2 分组(基本信息/展示设置)

统一使用 DrawerForm 组件管理表单实例、校验和提交
This commit is contained in:
iven
2026-04-28 08:40:22 +08:00
parent 3d787adceb
commit 24c7f9451f
4 changed files with 379 additions and 315 deletions

View File

@@ -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>
);
}

View File

@@ -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

View File

@@ -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>
);
}

View File

@@ -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>
);
}