diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx
index a3579fb..b52547e 100644
--- a/apps/web/src/App.tsx
+++ b/apps/web/src/App.tsx
@@ -60,6 +60,8 @@ const ShiftDetail = lazy(() => import('./pages/health/ShiftDetail'));
const MedicationRecordList = lazy(() => import('./pages/health/MedicationRecordList'));
const BleGatewayList = lazy(() => import('./pages/health/BleGatewayList'));
const BleGatewayDetail = lazy(() => import('./pages/health/BleGatewayDetail'));
+const CriticalValueThresholdList = lazy(() => import('./pages/health/CriticalValueThresholdList'));
+const DiagnosisList = lazy(() => import('./pages/health/DiagnosisList'));
// 内容管理
const ArticleManageList = lazy(() => import('./pages/health/ArticleManageList'));
@@ -285,6 +287,8 @@ export default function App() {
} />
} />
} />
+ } />
+ } />
{/* 内容管理 */}
} />
} />
diff --git a/apps/web/src/api/health/criticalValueThresholds.ts b/apps/web/src/api/health/criticalValueThresholds.ts
new file mode 100644
index 0000000..55f75a4
--- /dev/null
+++ b/apps/web/src/api/health/criticalValueThresholds.ts
@@ -0,0 +1,110 @@
+import client from '../client';
+
+// --- Types ---
+
+export interface CriticalValueThreshold {
+ id: string;
+ tenant_id: string;
+ indicator: string;
+ direction: string;
+ threshold_value: number;
+ level: string;
+ department?: string;
+ age_min?: number;
+ age_max?: number;
+ is_active: boolean;
+ created_at: string;
+ updated_at: string;
+ version: number;
+}
+
+export interface CreateThresholdReq {
+ indicator: string;
+ direction: string;
+ threshold_value: number;
+ level?: string;
+ department?: string;
+ age_min?: number;
+ age_max?: number;
+}
+
+export interface UpdateThresholdReq {
+ threshold_value: number;
+ level?: string;
+ department?: string;
+ age_min?: number;
+ age_max?: number;
+ version: number;
+}
+
+// --- Constants ---
+
+export const INDICATOR_OPTIONS = [
+ { label: '收缩压', value: 'systolic_bp' },
+ { label: '舒张压', value: 'diastolic_bp' },
+ { label: '心率', value: 'heart_rate' },
+ { label: '血糖', value: 'blood_sugar' },
+ { label: '空腹血糖', value: 'blood_sugar_fasting' },
+ { label: '餐后血糖', value: 'blood_sugar_postprandial' },
+ { label: '血氧', value: 'blood_oxygen' },
+ { label: '体温', value: 'temperature' },
+];
+
+export const DIRECTION_OPTIONS = [
+ { label: '偏高', value: 'high' },
+ { label: '偏低', value: 'low' },
+];
+
+export const LEVEL_OPTIONS = [
+ { label: '危急', value: 'critical' },
+ { label: '警告', value: 'warning' },
+];
+
+export const LEVEL_COLOR: Record = {
+ critical: 'red',
+ warning: 'orange',
+};
+
+export const INDICATOR_LABEL: Record = Object.fromEntries(
+ INDICATOR_OPTIONS.map((o) => [o.value, o.label]),
+);
+
+export const DIRECTION_LABEL: Record = Object.fromEntries(
+ DIRECTION_OPTIONS.map((o) => [o.value, o.label]),
+);
+
+export const LEVEL_LABEL: Record = Object.fromEntries(
+ LEVEL_OPTIONS.map((o) => [o.value, o.label]),
+);
+
+// --- API ---
+
+export const criticalValueThresholdApi = {
+ list: async () => {
+ const { data } = await client.get<{
+ success: boolean;
+ data: CriticalValueThreshold[];
+ }>('/health/critical-value-thresholds');
+ return data.data;
+ },
+
+ create: async (req: CreateThresholdReq) => {
+ const { data } = await client.post<{
+ success: boolean;
+ data: CriticalValueThreshold;
+ }>('/health/critical-value-thresholds', req);
+ return data.data;
+ },
+
+ update: async (id: string, req: UpdateThresholdReq) => {
+ const { data } = await client.put<{
+ success: boolean;
+ data: CriticalValueThreshold;
+ }>(`/health/critical-value-thresholds/${id}`, req);
+ return data.data;
+ },
+
+ delete: async (id: string) => {
+ await client.delete(`/health/critical-value-thresholds/${id}`);
+ },
+};
diff --git a/apps/web/src/api/health/diagnoses.ts b/apps/web/src/api/health/diagnoses.ts
new file mode 100644
index 0000000..3fe68e7
--- /dev/null
+++ b/apps/web/src/api/health/diagnoses.ts
@@ -0,0 +1,108 @@
+import client from '../client';
+import type { PaginatedResponse } from '../types';
+
+// --- Types ---
+
+export interface Diagnosis {
+ id: string;
+ patient_id: string;
+ health_record_id?: string;
+ icd_code: string;
+ diagnosis_name: string;
+ diagnosis_type: string;
+ diagnosed_date: string;
+ status: string;
+ diagnosed_by?: string;
+ notes?: string;
+ created_at: string;
+ updated_at: string;
+ version: number;
+}
+
+export interface CreateDiagnosisReq {
+ icd_code: string;
+ diagnosis_name: string;
+ diagnosis_type?: string;
+ diagnosed_date: string;
+ status?: string;
+ health_record_id?: string;
+ diagnosed_by?: string;
+ notes?: string;
+}
+
+export interface UpdateDiagnosisReq {
+ icd_code?: string;
+ diagnosis_name?: string;
+ diagnosis_type?: string;
+ diagnosed_date?: string;
+ status?: string;
+ health_record_id?: string;
+ diagnosed_by?: string;
+ notes?: string;
+}
+
+// --- Constants ---
+
+export const DIAGNOSIS_TYPE_OPTIONS = [
+ { label: '主要诊断', value: 'primary' },
+ { label: '次要诊断', value: 'secondary' },
+ { label: '合并症', value: 'comorbid' },
+];
+
+export const DIAGNOSIS_STATUS_OPTIONS = [
+ { label: '活跃', value: 'active' },
+ { label: '已缓解', value: 'resolved' },
+ { label: '慢性', value: 'chronic' },
+];
+
+export const DIAGNOSIS_TYPE_COLOR: Record = {
+ primary: 'red',
+ secondary: 'blue',
+ comorbid: 'orange',
+};
+
+export const DIAGNOSIS_STATUS_COLOR: Record = {
+ active: 'green',
+ resolved: 'default',
+ chronic: 'orange',
+};
+
+export const DIAGNOSIS_TYPE_LABEL: Record = Object.fromEntries(
+ DIAGNOSIS_TYPE_OPTIONS.map((o) => [o.value, o.label]),
+);
+
+export const DIAGNOSIS_STATUS_LABEL: Record = Object.fromEntries(
+ DIAGNOSIS_STATUS_OPTIONS.map((o) => [o.value, o.label]),
+);
+
+// --- API ---
+
+export const diagnosisApi = {
+ list: async (patientId: string, params?: { page?: number; page_size?: number }) => {
+ const { data } = await client.get<{
+ success: boolean;
+ data: PaginatedResponse;
+ }>(`/health/patients/${patientId}/diagnoses`, { params });
+ return data.data;
+ },
+
+ create: async (patientId: string, req: CreateDiagnosisReq) => {
+ const { data } = await client.post<{
+ success: boolean;
+ data: Diagnosis;
+ }>(`/health/patients/${patientId}/diagnoses`, req);
+ return data.data;
+ },
+
+ update: async (diagnosisId: string, req: UpdateDiagnosisReq & { version: number }) => {
+ const { data } = await client.put<{
+ success: boolean;
+ data: Diagnosis;
+ }>(`/health/diagnoses/${diagnosisId}`, req);
+ return data.data;
+ },
+
+ delete: async (diagnosisId: string, version: number) => {
+ await client.delete(`/health/diagnoses/${diagnosisId}`, { data: { version } });
+ },
+};
diff --git a/apps/web/src/layouts/MainLayout.tsx b/apps/web/src/layouts/MainLayout.tsx
index e2fb194..ab5b7f2 100644
--- a/apps/web/src/layouts/MainLayout.tsx
+++ b/apps/web/src/layouts/MainLayout.tsx
@@ -118,6 +118,8 @@ const routeTitleFallback: Record = {
'/health/medications': '药物记录',
'/health/ble-gateways': 'BLE 网关管理',
'/health/ble-gateways/:id': '网关详情',
+ '/health/critical-value-thresholds': '危急值阈值',
+ '/health/diagnoses': '诊断记录',
};
function getTitleFromMenus(path: string, menus: MenuInfo[]): string | undefined {
diff --git a/apps/web/src/pages/health/CriticalValueThresholdList.tsx b/apps/web/src/pages/health/CriticalValueThresholdList.tsx
new file mode 100644
index 0000000..0c07add
--- /dev/null
+++ b/apps/web/src/pages/health/CriticalValueThresholdList.tsx
@@ -0,0 +1,236 @@
+import { useState, useCallback } from 'react';
+import {
+ Button, Form, Input, InputNumber, message, Modal, Popconfirm, Result, Select, Space, Switch, Table, Tag,
+} from 'antd';
+import type { ColumnsType } from 'antd/es/table';
+
+import {
+ criticalValueThresholdApi,
+ type CriticalValueThreshold,
+ type CreateThresholdReq,
+ type UpdateThresholdReq,
+ INDICATOR_OPTIONS,
+ DIRECTION_OPTIONS,
+ LEVEL_OPTIONS,
+ LEVEL_COLOR,
+ INDICATOR_LABEL,
+ DIRECTION_LABEL,
+ LEVEL_LABEL,
+} from '../../api/health/criticalValueThresholds';
+import { PageContainer } from '../../components/PageContainer';
+import { usePermission } from '../../hooks/usePermission';
+
+export default function CriticalValueThresholdList() {
+ const { hasPermission } = usePermission('health.critical-value-thresholds.list');
+
+ const [data, setData] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [loaded, setLoaded] = useState(false);
+
+ const [modalOpen, setModalOpen] = useState(false);
+ const [editRecord, setEditRecord] = useState(null);
+ const [submitting, setSubmitting] = useState(false);
+ const [form] = Form.useForm();
+
+ const fetchData = useCallback(async () => {
+ setLoading(true);
+ try {
+ const list = await criticalValueThresholdApi.list();
+ setData(list);
+ setLoaded(true);
+ } catch {
+ message.error('加载危急值阈值失败');
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ const handleCreate = () => {
+ setEditRecord(null);
+ form.resetFields();
+ form.setFieldsValue({ level: 'critical' });
+ setModalOpen(true);
+ };
+
+ const handleEdit = (record: CriticalValueThreshold) => {
+ setEditRecord(record);
+ form.setFieldsValue({
+ indicator: record.indicator,
+ direction: record.direction,
+ threshold_value: record.threshold_value,
+ level: record.level,
+ department: record.department,
+ age_min: record.age_min,
+ age_max: record.age_max,
+ });
+ setModalOpen(true);
+ };
+
+ const handleSubmit = async () => {
+ try {
+ const values = await form.validateFields();
+ setSubmitting(true);
+
+ if (editRecord) {
+ const req: UpdateThresholdReq = {
+ threshold_value: values.threshold_value,
+ level: values.level,
+ department: values.department,
+ age_min: values.age_min,
+ age_max: values.age_max,
+ version: editRecord.version,
+ };
+ await criticalValueThresholdApi.update(editRecord.id, req);
+ message.success('阈值已更新');
+ } else {
+ const req: CreateThresholdReq = values;
+ await criticalValueThresholdApi.create(req);
+ message.success('阈值已创建');
+ }
+
+ setModalOpen(false);
+ fetchData();
+ } catch {
+ // validation
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const handleDelete = async (record: CriticalValueThreshold) => {
+ try {
+ await criticalValueThresholdApi.delete(record.id);
+ message.success('阈值已删除');
+ fetchData();
+ } catch {
+ message.error('删除失败');
+ }
+ };
+
+ const columns: ColumnsType = [
+ {
+ title: '指标',
+ dataIndex: 'indicator',
+ width: 120,
+ render: (v: string) => INDICATOR_LABEL[v] ?? v,
+ },
+ {
+ title: '方向',
+ dataIndex: 'direction',
+ width: 80,
+ render: (v: string) => DIRECTION_LABEL[v] ?? v,
+ },
+ {
+ title: '阈值',
+ dataIndex: 'threshold_value',
+ width: 100,
+ render: (v: number) => v,
+ },
+ {
+ title: '级别',
+ dataIndex: 'level',
+ width: 80,
+ render: (v: string) => (
+ {LEVEL_LABEL[v] ?? v}
+ ),
+ },
+ {
+ title: '科室',
+ dataIndex: 'department',
+ width: 100,
+ render: (v: string) => v ?? '通用',
+ },
+ {
+ title: '年龄范围',
+ width: 120,
+ render: (_, record) => {
+ if (record.age_min == null && record.age_max == null) return '不限';
+ return `${record.age_min ?? 0} - ${record.age_max ?? '∞'}`;
+ },
+ },
+ {
+ title: '状态',
+ dataIndex: 'is_active',
+ width: 80,
+ render: (v: boolean) => {v ? '启用' : '停用'},
+ },
+ {
+ title: '操作',
+ width: 140,
+ render: (_, record) => (
+
+
+ handleDelete(record)}>
+
+
+
+ ),
+ },
+ ];
+
+ if (!hasPermission) {
+ return ;
+ }
+
+ return (
+ 添加阈值}
+ >
+
+
+
+
+ {loaded && (
+
+ rowKey="id"
+ columns={columns}
+ dataSource={data}
+ loading={loading}
+ pagination={false}
+ />
+ )}
+
+ setModalOpen(false)}
+ confirmLoading={submitting}
+ width={520}
+ >
+
+
+
+
+
+
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/pages/health/DiagnosisList.tsx b/apps/web/src/pages/health/DiagnosisList.tsx
new file mode 100644
index 0000000..0d56f74
--- /dev/null
+++ b/apps/web/src/pages/health/DiagnosisList.tsx
@@ -0,0 +1,271 @@
+import { useState, useCallback, useMemo } from 'react';
+import {
+ Button, DatePicker, Form, Input, message, Modal, Popconfirm,
+ Result, Select, Space, Table, Tag,
+} from 'antd';
+import type { ColumnsType } from 'antd/es/table';
+import dayjs from 'dayjs';
+
+import {
+ diagnosisApi,
+ type Diagnosis,
+ type CreateDiagnosisReq,
+ type UpdateDiagnosisReq,
+ DIAGNOSIS_TYPE_OPTIONS,
+ DIAGNOSIS_STATUS_OPTIONS,
+ DIAGNOSIS_TYPE_COLOR,
+ DIAGNOSIS_STATUS_COLOR,
+ DIAGNOSIS_TYPE_LABEL,
+ DIAGNOSIS_STATUS_LABEL,
+} from '../../api/health/diagnoses';
+import { PageContainer } from '../../components/PageContainer';
+import { usePermission } from '../../hooks/usePermission';
+
+export default function DiagnosisList() {
+ const { hasPermission } = usePermission('health.health-data.list');
+
+ 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 diagnosisApi.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, diagnosis_type: 'primary', status: 'active' });
+ setModalOpen(true);
+ };
+
+ const handleEdit = (record: Diagnosis) => {
+ setEditRecord(record);
+ form.setFieldsValue({
+ icd_code: record.icd_code,
+ diagnosis_name: record.diagnosis_name,
+ diagnosis_type: record.diagnosis_type,
+ diagnosed_date: record.diagnosed_date ? dayjs(record.diagnosed_date) : undefined,
+ status: record.status,
+ diagnosed_by: record.diagnosed_by,
+ notes: record.notes,
+ });
+ setModalOpen(true);
+ };
+
+ const handleSubmit = async () => {
+ try {
+ const values = await form.validateFields();
+ const req = {
+ ...values,
+ diagnosed_date: values.diagnosed_date?.format('YYYY-MM-DD'),
+ };
+
+ setSubmitting(true);
+ if (editRecord) {
+ await diagnosisApi.update(editRecord.id, {
+ ...req,
+ version: editRecord.version,
+ } as UpdateDiagnosisReq & { version: number });
+ message.success('诊断记录已更新');
+ } else {
+ await diagnosisApi.create(req.patient_id, req as CreateDiagnosisReq);
+ message.success('诊断记录已创建');
+ }
+ setModalOpen(false);
+ fetchData(patientId, page);
+ } catch {
+ // validation
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const handleDelete = async (record: Diagnosis) => {
+ try {
+ await diagnosisApi.delete(record.id, record.version);
+ message.success('诊断记录已删除');
+ fetchData(patientId, page);
+ } catch {
+ message.error('删除失败');
+ }
+ };
+
+ const columns: ColumnsType = useMemo(() => [
+ {
+ title: 'ICD 编码',
+ dataIndex: 'icd_code',
+ width: 110,
+ },
+ {
+ title: '诊断名称',
+ dataIndex: 'diagnosis_name',
+ width: 200,
+ ellipsis: true,
+ },
+ {
+ title: '类型',
+ dataIndex: 'diagnosis_type',
+ width: 100,
+ render: (v: string) => (
+
+ {DIAGNOSIS_TYPE_LABEL[v] ?? v}
+
+ ),
+ },
+ {
+ title: '确诊日期',
+ dataIndex: 'diagnosed_date',
+ width: 110,
+ render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD') : '-',
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ width: 80,
+ render: (v: string) => (
+
+ {DIAGNOSIS_STATUS_LABEL[v] ?? 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 ? (
+
+ ) : (
+