diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 7262bba..d7574bd 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -57,6 +57,7 @@ const CarePlanList = lazy(() => import('./pages/health/CarePlanList')); const CarePlanDetail = lazy(() => import('./pages/health/CarePlanDetail')); const ShiftList = lazy(() => import('./pages/health/ShiftList')); const ShiftDetail = lazy(() => import('./pages/health/ShiftDetail')); +const MedicationRecordList = lazy(() => import('./pages/health/MedicationRecordList')); // 内容管理 const ArticleManageList = lazy(() => import('./pages/health/ArticleManageList')); @@ -279,6 +280,7 @@ export default function App() { } /> } /> } /> + } /> {/* 内容管理 */} } /> } /> diff --git a/apps/web/src/api/health/medicationRecords.ts b/apps/web/src/api/health/medicationRecords.ts new file mode 100644 index 0000000..6731cef --- /dev/null +++ b/apps/web/src/api/health/medicationRecords.ts @@ -0,0 +1,111 @@ +import client from '../client'; +import type { PaginatedResponse } from '../types'; + +// --- Types --- + +export interface MedicationRecord { + id: string; + patient_id: string; + medication_name: string; + generic_name?: string; + dosage?: string; + unit?: string; + frequency?: string; + route?: string; + start_date?: string; + end_date?: string; + is_current: boolean; + prescribed_by?: string; + notes?: string; + created_at: string; + updated_at: string; + version: number; +} + +export interface CreateMedicationRecordReq { + patient_id: string; + medication_name: string; + generic_name?: string; + dosage?: string; + unit?: string; + frequency?: string; + route?: string; + start_date?: string; + end_date?: string; + is_current?: boolean; + prescribed_by?: string; + notes?: string; +} + +export interface UpdateMedicationRecordReq { + medication_name?: string; + generic_name?: string; + dosage?: string; + unit?: string; + frequency?: string; + route?: string; + start_date?: string; + end_date?: string; + is_current?: boolean; + prescribed_by?: string; + notes?: string; +} + +// --- Constants --- + +export const FREQUENCY_OPTIONS = [ + { label: '每日一次', value: 'QD' }, + { label: '每日两次', value: 'BID' }, + { label: '每日三次', value: 'TID' }, + { label: '每晚一次', value: 'QN' }, + { label: '每周一次', value: 'QW' }, + { label: '必要时', value: 'PRN' }, +]; + +export const ROUTE_OPTIONS = [ + { label: '口服', value: 'oral' }, + { label: '静脉注射', value: 'iv' }, + { label: '皮下注射', value: 'sc' }, + { label: '外用', value: 'topical' }, + { label: '吸入', value: 'inhalation' }, +]; + +// --- API --- + +export const medicationRecordApi = { + list: async (patientId: string, params?: { page?: number; page_size?: number }) => { + const { data } = await client.get<{ + success: boolean; + data: PaginatedResponse; + }>(`/health/patients/${patientId}/medications`, { params }); + return data.data; + }, + + get: async (id: string) => { + const { data } = await client.get<{ + success: boolean; + data: MedicationRecord; + }>(`/health/medications/${id}`); + return data.data; + }, + + create: async (req: CreateMedicationRecordReq) => { + const { data } = await client.post<{ + success: boolean; + data: MedicationRecord; + }>('/health/medications', req); + return data.data; + }, + + update: async (id: string, req: UpdateMedicationRecordReq & { version: number }) => { + const { data } = await client.put<{ + success: boolean; + data: MedicationRecord; + }>(`/health/medications/${id}`, req); + return data.data; + }, + + delete: async (id: string, version: number) => { + await client.delete(`/health/medications/${id}`, { data: { version } }); + }, +}; diff --git a/apps/web/src/layouts/MainLayout.tsx b/apps/web/src/layouts/MainLayout.tsx index e51ef04..d69ae25 100644 --- a/apps/web/src/layouts/MainLayout.tsx +++ b/apps/web/src/layouts/MainLayout.tsx @@ -115,6 +115,7 @@ const routeTitleFallback: Record = { '/health/care-plans/:id': '护理计划详情', '/health/shifts': '班次管理', '/health/shifts/:id': '班次详情', + '/health/medications': '药物记录', }; function getTitleFromMenus(path: string, menus: MenuInfo[]): string | undefined { diff --git a/apps/web/src/pages/health/MedicationRecordList.tsx b/apps/web/src/pages/health/MedicationRecordList.tsx new file mode 100644 index 0000000..c6175a7 --- /dev/null +++ b/apps/web/src/pages/health/MedicationRecordList.tsx @@ -0,0 +1,304 @@ +import { useState, useCallback, useEffect, useMemo } from 'react'; +import { + Button, DatePicker, Form, Input, message, Modal, Popconfirm, + Result, Select, Space, Switch, Table, Tag, +} from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import dayjs from 'dayjs'; + +import { + medicationRecordApi, + type MedicationRecord, + type CreateMedicationRecordReq, + type UpdateMedicationRecordReq, + FREQUENCY_OPTIONS, + ROUTE_OPTIONS, +} from '../../api/health/medicationRecords'; +import { PageContainer } from '../../components/PageContainer'; +import { usePermission } from '../../hooks/usePermission'; + +export default function MedicationRecordList() { + const { hasPermission } = usePermission('health.medication-records.manage'); + + const [data, setData] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [loading, setLoading] = useState(false); + + const [patientId, setPatientId] = useState(''); + const [searchInput, setSearchInput] = useState(''); + + const [modalOpen, setModalOpen] = useState(false); + const [editRecord, setEditRecord] = useState(null); + const [submitting, setSubmitting] = useState(false); + const [form] = Form.useForm(); + + const pageSize = 20; + + const fetchData = useCallback(async (pid: string, p: number) => { + if (!pid) return; + setLoading(true); + try { + const resp = await medicationRecordApi.list(pid, { page: p, page_size: pageSize }); + setData(resp.data); + setTotal(resp.total); + setPage(p); + } catch { + message.error('加载药物记录失败'); + } finally { + setLoading(false); + } + }, []); + + const handleSearch = () => { + const pid = searchInput.trim(); + if (!pid) { + message.warning('请输入患者 ID'); + return; + } + setPatientId(pid); + fetchData(pid, 1); + }; + + const handleCreate = () => { + setEditRecord(null); + form.resetFields(); + form.setFieldsValue({ patient_id: patientId, is_current: true }); + setModalOpen(true); + }; + + const handleEdit = (record: MedicationRecord) => { + setEditRecord(record); + form.setFieldsValue({ + medication_name: record.medication_name, + generic_name: record.generic_name, + dosage: record.dosage, + unit: record.unit, + frequency: record.frequency, + route: record.route, + start_date: record.start_date ? dayjs(record.start_date) : undefined, + end_date: record.end_date ? dayjs(record.end_date) : undefined, + is_current: record.is_current, + prescribed_by: record.prescribed_by, + notes: record.notes, + }); + setModalOpen(true); + }; + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + const req = { + ...values, + start_date: values.start_date?.format('YYYY-MM-DD'), + end_date: values.end_date?.format('YYYY-MM-DD'), + }; + + setSubmitting(true); + if (editRecord) { + await medicationRecordApi.update(editRecord.id, { + ...req, + version: editRecord.version, + } as UpdateMedicationRecordReq & { version: number }); + message.success('药物记录已更新'); + } else { + await medicationRecordApi.create(req as CreateMedicationRecordReq); + message.success('药物记录已创建'); + } + setModalOpen(false); + fetchData(patientId, page); + } catch { + // validation + } finally { + setSubmitting(false); + } + }; + + const handleDelete = async (record: MedicationRecord) => { + try { + await medicationRecordApi.delete(record.id, record.version); + message.success('药物记录已删除'); + fetchData(patientId, page); + } catch { + message.error('删除失败'); + } + }; + + const columns: ColumnsType = useMemo(() => [ + { + title: '药品名称', + dataIndex: 'medication_name', + width: 160, + render: (v: string, record) => ( + + {v} + {record.generic_name && ({record.generic_name})} + + ), + }, + { + title: '剂量', + width: 120, + render: (_, record) => { + if (!record.dosage) return '-'; + return `${record.dosage}${record.unit ? ' ' + record.unit : ''}`; + }, + }, + { + title: '频次', + dataIndex: 'frequency', + width: 100, + render: (v: string) => { + const label = FREQUENCY_OPTIONS.find((o) => o.value === v)?.label; + return label ?? v ?? '-'; + }, + }, + { + title: '给药途径', + dataIndex: 'route', + width: 100, + render: (v: string) => { + const label = ROUTE_OPTIONS.find((o) => o.value === v)?.label; + return label ?? v ?? '-'; + }, + }, + { + title: '开始日期', + dataIndex: 'start_date', + width: 110, + render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD') : '-', + }, + { + title: '结束日期', + dataIndex: 'end_date', + width: 110, + render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD') : '-', + }, + { + title: '状态', + dataIndex: 'is_current', + width: 80, + render: (v: boolean) => {v ? '在用' : '停用'}, + }, + { + title: '备注', + dataIndex: 'notes', + width: 160, + ellipsis: true, + render: (v: string) => v ?? '-', + }, + { + title: '操作', + width: 140, + render: (_, record) => ( + + + handleDelete(record)}> + + + + ), + }, + ], [patientId, page]); + + if (!hasPermission) { + return ; + } + + return ( + 添加药物 : undefined} + > + + setSearchInput(e.target.value)} + onSearch={handleSearch} + style={{ width: 360 }} + enterButton="查询" + /> + {patientId && ( + 当前患者: {patientId} + )} + + + {!patientId ? ( + + ) : ( + + rowKey="id" + columns={columns} + dataSource={data} + loading={loading} + pagination={{ + current: page, + pageSize, + total, + showTotal: (t) => `共 ${t} 条`, + onChange: (p) => fetchData(patientId, p), + }} + /> + )} + + setModalOpen(false)} + confirmLoading={submitting} + width={600} + > +
+ {!editRecord && ( + + + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ ); +}