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 ? (
+
+ ) : (
+