Compare commits
3 Commits
3bc4597041
...
23cd62a70f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23cd62a70f | ||
|
|
63ead0c442 | ||
|
|
b6e780e649 |
@@ -1,16 +1,24 @@
|
||||
/**
|
||||
* 健康管理模块共享常量
|
||||
*
|
||||
* 集中定义性别、血型、患者状态等下拉选项,
|
||||
* 供 PatientList / PatientDetail 等页面复用。
|
||||
* 集中定义性别、血型、患者状态、严重度、告警状态、设备类型等映射,
|
||||
* 供各健康模块页面复用。避免在组件中重复定义。
|
||||
*/
|
||||
|
||||
// --- 性别 ---
|
||||
export const GENDER_OPTIONS = [
|
||||
{ value: 'male', label: '男' },
|
||||
{ value: 'female', label: '女' },
|
||||
{ value: 'other', label: '其他' },
|
||||
];
|
||||
|
||||
export const GENDER_LABEL: Record<string, string> = {
|
||||
male: '男',
|
||||
female: '女',
|
||||
other: '其他',
|
||||
};
|
||||
|
||||
// --- 血型 ---
|
||||
export const BLOOD_TYPE_OPTIONS = [
|
||||
{ value: 'A', label: 'A 型' },
|
||||
{ value: 'B', label: 'B 型' },
|
||||
@@ -18,9 +26,106 @@ export const BLOOD_TYPE_OPTIONS = [
|
||||
{ value: 'O', label: 'O 型' },
|
||||
];
|
||||
|
||||
// --- 患者状态 ---
|
||||
export const STATUS_OPTIONS = [
|
||||
{ value: '', label: '全部状态' },
|
||||
{ value: 'active', label: '活跃' },
|
||||
{ value: 'inactive', label: '停用' },
|
||||
{ value: 'deceased', label: '已故' },
|
||||
];
|
||||
|
||||
// --- 严重度(统一 5 处重复定义: AlertDashboard, AlertList, AlertRuleList, DoctorDashboard) ---
|
||||
export const SEVERITY_COLOR: Record<string, string> = {
|
||||
info: 'default',
|
||||
warning: 'orange',
|
||||
critical: 'red',
|
||||
urgent: 'magenta',
|
||||
};
|
||||
|
||||
export const SEVERITY_LABEL: Record<string, string> = {
|
||||
info: '提示',
|
||||
warning: '警告',
|
||||
critical: '严重',
|
||||
urgent: '紧急',
|
||||
};
|
||||
|
||||
export const SEVERITY_OPTIONS = [
|
||||
{ value: 'info', label: '提示' },
|
||||
{ value: 'warning', label: '警告' },
|
||||
{ value: 'critical', label: '严重' },
|
||||
{ value: 'urgent', label: '紧急' },
|
||||
];
|
||||
|
||||
// --- 告警状态(统一 3 处: AlertDashboard, AlertList) ---
|
||||
export const ALERT_STATUS_COLOR: Record<string, string> = {
|
||||
pending: 'orange',
|
||||
acknowledged: 'blue',
|
||||
resolved: 'green',
|
||||
dismissed: 'default',
|
||||
};
|
||||
|
||||
export const ALERT_STATUS_LABEL: Record<string, string> = {
|
||||
pending: '待处理',
|
||||
acknowledged: '已确认',
|
||||
resolved: '已恢复',
|
||||
dismissed: '已忽略',
|
||||
};
|
||||
|
||||
export const ALERT_STATUS_OPTIONS = [
|
||||
{ value: '', label: '全部状态' },
|
||||
{ value: 'pending', label: '待处理' },
|
||||
{ value: 'acknowledged', label: '已确认' },
|
||||
{ value: 'resolved', label: '已恢复' },
|
||||
{ value: 'dismissed', label: '已忽略' },
|
||||
];
|
||||
|
||||
// --- 设备类型(统一 3 处: DeviceManage, DeviceReadingsTab, AlertRuleList) ---
|
||||
export const DEVICE_TYPE_OPTIONS = [
|
||||
{ value: 'blood_pressure', label: '血压' },
|
||||
{ value: 'blood_glucose', label: '血糖' },
|
||||
{ value: 'heart_rate', label: '心率' },
|
||||
{ value: 'blood_oxygen', label: '血氧' },
|
||||
{ value: 'temperature', label: '体温' },
|
||||
{ value: 'steps', label: '步数' },
|
||||
{ value: 'sleep', label: '睡眠' },
|
||||
{ value: 'stress', label: '压力' },
|
||||
];
|
||||
|
||||
export const DEVICE_TYPE_COLOR: Record<string, string> = {
|
||||
blood_pressure: 'red',
|
||||
blood_glucose: 'purple',
|
||||
heart_rate: 'volcano',
|
||||
blood_oxygen: 'blue',
|
||||
temperature: 'orange',
|
||||
steps: 'green',
|
||||
sleep: 'cyan',
|
||||
stress: 'geekblue',
|
||||
};
|
||||
|
||||
// --- 告警规则条件类型 ---
|
||||
export const CONDITION_TYPE_OPTIONS = [
|
||||
{ value: 'single_threshold', label: '单次阈值' },
|
||||
{ value: 'consecutive', label: '连续触发' },
|
||||
{ value: 'trend', label: '趋势变化' },
|
||||
];
|
||||
|
||||
// --- 通用状态标签(StatusTag 组件统一引用) ---
|
||||
export const STATUS_TAG_CONFIG: Record<string, { color: string; label: string }> = {
|
||||
// 预约状态
|
||||
pending: { color: 'gold', label: '待确认' },
|
||||
confirmed: { color: 'blue', label: '已确认' },
|
||||
completed: { color: 'green', label: '已完成' },
|
||||
cancelled: { color: 'default', label: '已取消' },
|
||||
no_show: { color: 'red', label: '未到诊' },
|
||||
// 随访状态
|
||||
overdue: { color: 'red', label: '逾期' },
|
||||
in_progress: { color: 'processing', label: '进行中' },
|
||||
// 咨询状态
|
||||
waiting: { color: 'gold', label: '等待中' },
|
||||
active: { color: 'green', label: '进行中' },
|
||||
closed: { color: 'default', label: '已关闭' },
|
||||
// 患者状态
|
||||
inactive: { color: 'default', label: '停用' },
|
||||
deceased: { color: 'default', label: '已故' },
|
||||
verified: { color: 'green', label: '已认证' },
|
||||
};
|
||||
|
||||
31
apps/web/src/hooks/useDictionary.ts
Normal file
31
apps/web/src/hooks/useDictionary.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
import { listItemsByCode, type DictionaryItemInfo } from '../api/dictionaries';
|
||||
|
||||
export interface DictOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export function useDictionary(code: string, fallback?: DictOption[]) {
|
||||
const [items, setItems] = useState<DictionaryItemInfo[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
listItemsByCode(code)
|
||||
.then((data) => setItems(data))
|
||||
.catch(() => setItems([]))
|
||||
.finally(() => setLoading(false));
|
||||
}, [code]);
|
||||
|
||||
const options = useMemo<DictOption[]>(() => {
|
||||
if (items.length > 0) {
|
||||
return items
|
||||
.sort((a, b) => a.sort_order - b.sort_order)
|
||||
.map((item) => ({ value: item.value, label: item.label }));
|
||||
}
|
||||
return fallback ?? [];
|
||||
}, [items, fallback]);
|
||||
|
||||
return { items, options, loading };
|
||||
}
|
||||
@@ -21,47 +21,12 @@ import {
|
||||
WifiOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { alertApi, type Alert } from '../../api/health/alerts';
|
||||
import { SEVERITY_COLOR, SEVERITY_LABEL, ALERT_STATUS_COLOR, ALERT_STATUS_LABEL, ALERT_STATUS_OPTIONS } from '../../constants/health';
|
||||
import { useAlertSSE, type AlertSSEEvent } from '../../hooks/useAlertSSE';
|
||||
import { AlertDetailPanel } from './components/AlertDetailPanel';
|
||||
import { PageContainer } from '../../components/PageContainer';
|
||||
import { EntityName } from '../../components/EntityName';
|
||||
|
||||
const SEVERITY_COLOR: Record<string, string> = {
|
||||
info: 'default',
|
||||
warning: 'orange',
|
||||
critical: 'red',
|
||||
urgent: 'magenta',
|
||||
};
|
||||
|
||||
const SEVERITY_LABEL: Record<string, string> = {
|
||||
info: '提示',
|
||||
warning: '警告',
|
||||
critical: '严重',
|
||||
urgent: '紧急',
|
||||
};
|
||||
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
pending: 'orange',
|
||||
acknowledged: 'blue',
|
||||
resolved: 'green',
|
||||
dismissed: 'default',
|
||||
};
|
||||
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
pending: '待处理',
|
||||
acknowledged: '已确认',
|
||||
resolved: '已恢复',
|
||||
dismissed: '已忽略',
|
||||
};
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: '', label: '全部状态' },
|
||||
{ value: 'pending', label: '待处理' },
|
||||
{ value: 'acknowledged', label: '已确认' },
|
||||
{ value: 'resolved', label: '已恢复' },
|
||||
{ value: 'dismissed', label: '已忽略' },
|
||||
];
|
||||
|
||||
/**
|
||||
* 实时告警仪表盘 — 医生端。
|
||||
*
|
||||
@@ -183,7 +148,7 @@ export default function AlertDashboard() {
|
||||
<Select
|
||||
value={statusFilter}
|
||||
onChange={setStatusFilter}
|
||||
options={STATUS_OPTIONS}
|
||||
options={ALERT_STATUS_OPTIONS}
|
||||
style={{ width: 120 }}
|
||||
placeholder="按状态筛选"
|
||||
/>
|
||||
@@ -264,7 +229,7 @@ export default function AlertDashboard() {
|
||||
<List.Item.Meta
|
||||
avatar={
|
||||
<Tag
|
||||
color={SEVERITY_COLOR[alert.severity]}
|
||||
color={SEVERITY_COLOR[alert.severity] || 'default'}
|
||||
style={{ margin: 0, minWidth: 48, textAlign: 'center' }}
|
||||
>
|
||||
{SEVERITY_LABEL[alert.severity] ?? alert.severity}
|
||||
@@ -273,8 +238,8 @@ export default function AlertDashboard() {
|
||||
title={
|
||||
<Flex justify="space-between" align="center">
|
||||
<span>{alert.title}</span>
|
||||
<Tag color={STATUS_COLOR[alert.status]} style={{ fontSize: 11 }}>
|
||||
{STATUS_LABEL[alert.status] ?? alert.status}
|
||||
<Tag color={ALERT_STATUS_COLOR[alert.status] || 'default'} style={{ fontSize: 11 }}>
|
||||
{ALERT_STATUS_LABEL[alert.status] ?? alert.status}
|
||||
</Tag>
|
||||
</Flex>
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
alertApi,
|
||||
type Alert,
|
||||
} from '../../api/health/alerts';
|
||||
import { SEVERITY_COLOR, SEVERITY_LABEL, ALERT_STATUS_COLOR, ALERT_STATUS_LABEL, SEVERITY_OPTIONS, ALERT_STATUS_OPTIONS as STATUS_OPTS } from '../../constants/health';
|
||||
import { AuthButton } from '../../components/AuthButton';
|
||||
import { PageContainer } from '../../components/PageContainer';
|
||||
import { EntityName } from '../../components/EntityName';
|
||||
@@ -25,47 +26,8 @@ import { formatRelative, formatDateTime } from '../../utils/format';
|
||||
|
||||
// --- 常量映射 ---
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: 'pending', label: '待处理' },
|
||||
{ value: 'acknowledged', label: '已确认' },
|
||||
{ value: 'resolved', label: '已恢复' },
|
||||
{ value: 'dismissed', label: '已忽略' },
|
||||
];
|
||||
// 状态选项保留给筛选下拉使用(使用常量)
|
||||
|
||||
const SEVERITY_OPTIONS = [
|
||||
{ value: 'info', label: '提示' },
|
||||
{ value: 'warning', label: '警告' },
|
||||
{ value: 'critical', label: '严重' },
|
||||
{ value: 'urgent', label: '紧急' },
|
||||
];
|
||||
|
||||
const SEVERITY_COLOR: Record<string, string> = {
|
||||
info: 'default',
|
||||
warning: 'orange',
|
||||
critical: 'red',
|
||||
urgent: 'magenta',
|
||||
};
|
||||
|
||||
const SEVERITY_LABEL: Record<string, string> = {
|
||||
info: '提示',
|
||||
warning: '警告',
|
||||
critical: '严重',
|
||||
urgent: '紧急',
|
||||
};
|
||||
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
pending: 'orange',
|
||||
acknowledged: 'blue',
|
||||
resolved: 'green',
|
||||
dismissed: 'default',
|
||||
};
|
||||
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
pending: '待处理',
|
||||
acknowledged: '已确认',
|
||||
resolved: '已恢复',
|
||||
dismissed: '已忽略',
|
||||
};
|
||||
|
||||
// --- 筛选器结构 ---
|
||||
|
||||
@@ -216,8 +178,8 @@ export default function AlertList() {
|
||||
key: 'status',
|
||||
width: 100,
|
||||
render: (val: string) => (
|
||||
<Tag color={STATUS_COLOR[val] || 'default'}>
|
||||
{STATUS_LABEL[val] || val}
|
||||
<Tag color={ALERT_STATUS_COLOR[val] || 'default'}>
|
||||
{ALERT_STATUS_LABEL[val] || val}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
@@ -237,7 +199,7 @@ export default function AlertList() {
|
||||
key: 'actions',
|
||||
width: 160,
|
||||
render: (_: unknown, record: Alert) => (
|
||||
<AuthButton code="health.alert.manage">
|
||||
<AuthButton code="health.alerts.manage">
|
||||
<Space size={4}>
|
||||
{record.status === 'pending' && (
|
||||
<Popconfirm
|
||||
@@ -298,7 +260,7 @@ export default function AlertList() {
|
||||
allowClear
|
||||
placeholder="状态筛选"
|
||||
style={{ width: 140 }}
|
||||
options={STATUS_OPTIONS}
|
||||
options={STATUS_OPTS}
|
||||
value={filters.status || undefined}
|
||||
onChange={(v) => handleFilterChange('status', v ?? '')}
|
||||
/>
|
||||
|
||||
@@ -8,35 +8,7 @@ import {
|
||||
type CreateAlertRuleReq,
|
||||
type UpdateAlertRuleReq,
|
||||
} from '../../api/health/alerts';
|
||||
|
||||
const DEVICE_TYPES = [
|
||||
{ label: '心率', value: 'heart_rate' },
|
||||
{ label: '血氧', value: 'blood_oxygen' },
|
||||
{ label: '体温', value: 'temperature' },
|
||||
{ label: '步数', value: 'steps' },
|
||||
{ label: '睡眠', value: 'sleep' },
|
||||
{ label: '压力', value: 'stress' },
|
||||
];
|
||||
|
||||
const CONDITION_TYPES = [
|
||||
{ label: '单次阈值', value: 'single_threshold' },
|
||||
{ label: '连续触发', value: 'consecutive' },
|
||||
{ label: '趋势变化', value: 'trend' },
|
||||
];
|
||||
|
||||
const SEVERITY_OPTIONS = [
|
||||
{ label: '提示', value: 'info' },
|
||||
{ label: '警告', value: 'warning' },
|
||||
{ label: '严重', value: 'critical' },
|
||||
{ label: '紧急', value: 'urgent' },
|
||||
];
|
||||
|
||||
const SEVERITY_COLOR: Record<string, string> = {
|
||||
info: 'default',
|
||||
warning: 'orange',
|
||||
critical: 'red',
|
||||
urgent: 'magenta',
|
||||
};
|
||||
import { SEVERITY_COLOR, SEVERITY_OPTIONS, DEVICE_TYPE_OPTIONS, CONDITION_TYPE_OPTIONS } from '../../constants/health';
|
||||
|
||||
export default function AlertRuleList() {
|
||||
const [data, setData] = useState<AlertRule[]>([]);
|
||||
@@ -144,13 +116,13 @@ export default function AlertRuleList() {
|
||||
title: '指标类型',
|
||||
dataIndex: 'device_type',
|
||||
width: 100,
|
||||
render: (v: string) => DEVICE_TYPES.find((d) => d.value === v)?.label || v,
|
||||
render: (v: string) => DEVICE_TYPE_OPTIONS.find((d) => d.value === v)?.label || v,
|
||||
},
|
||||
{
|
||||
title: '条件类型',
|
||||
dataIndex: 'condition_type',
|
||||
width: 120,
|
||||
render: (v: string) => CONDITION_TYPES.find((c) => c.value === v)?.label || v,
|
||||
render: (v: string) => CONDITION_TYPE_OPTIONS.find((c) => c.value === v)?.label || v,
|
||||
},
|
||||
{
|
||||
title: '严重程度',
|
||||
@@ -219,10 +191,10 @@ export default function AlertRuleList() {
|
||||
</Form.Item>
|
||||
<Space style={{ width: '100%' }} size="large">
|
||||
<Form.Item name="device_type" label="指标类型" rules={[{ required: true }]}>
|
||||
<Select style={{ width: 160 }} options={DEVICE_TYPES} disabled={!!editingRule} />
|
||||
<Select style={{ width: 160 }} options={DEVICE_TYPE_OPTIONS} disabled={!!editingRule} />
|
||||
</Form.Item>
|
||||
<Form.Item name="condition_type" label="条件类型" rules={[{ required: true }]}>
|
||||
<Select style={{ width: 160 }} options={CONDITION_TYPES} disabled={!!editingRule} />
|
||||
<Select style={{ width: 160 }} options={CONDITION_TYPE_OPTIONS} disabled={!!editingRule} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Form.Item
|
||||
|
||||
@@ -23,6 +23,7 @@ import { PageContainer } from '../../components/PageContainer';
|
||||
import { EntityName } from '../../components/EntityName';
|
||||
import { formatDateTime } from '../../utils/format';
|
||||
import { usePaginatedData } from '../../hooks/usePaginatedData';
|
||||
import { useDictionary } from '../../hooks/useDictionary';
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: 'waiting', label: '等待中' },
|
||||
@@ -30,7 +31,7 @@ const STATUS_OPTIONS = [
|
||||
{ value: 'closed', label: '已关闭' },
|
||||
];
|
||||
|
||||
const CONSULTATION_TYPE_OPTIONS = [
|
||||
const CONSULTATION_TYPE_FALLBACK = [
|
||||
{ value: 'customer_service', label: '客服咨询' },
|
||||
{ value: 'medical', label: '医疗咨询' },
|
||||
{ value: 'health_consultation', label: '健康咨询' },
|
||||
@@ -48,6 +49,7 @@ interface ConsultationFilters {
|
||||
}
|
||||
|
||||
export default function ConsultationList() {
|
||||
const { options: CONSULTATION_TYPE_OPTIONS } = useDictionary('health_consultation_type', CONSULTATION_TYPE_FALLBACK);
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const urlPatientId = searchParams.get('patient_id');
|
||||
|
||||
@@ -4,24 +4,7 @@ import type { ColumnsType } from 'antd/es/table';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { deviceApi, type DeviceItem } from '../../api/health/devices';
|
||||
|
||||
const DEVICE_TYPE_OPTIONS = [
|
||||
{ label: '血压', value: 'blood_pressure' },
|
||||
{ label: '血糖', value: 'blood_glucose' },
|
||||
{ label: '心率', value: 'heart_rate' },
|
||||
{ label: '血氧', value: 'blood_oxygen' },
|
||||
{ label: '步数', value: 'steps' },
|
||||
{ label: '体温', value: 'temperature' },
|
||||
];
|
||||
|
||||
const DEVICE_TYPE_COLOR: Record<string, string> = {
|
||||
blood_pressure: 'red',
|
||||
blood_glucose: 'purple',
|
||||
heart_rate: 'volcano',
|
||||
blood_oxygen: 'blue',
|
||||
steps: 'green',
|
||||
temperature: 'orange',
|
||||
};
|
||||
import { DEVICE_TYPE_OPTIONS, DEVICE_TYPE_COLOR } from '../../constants/health';
|
||||
|
||||
function formatTime(val?: string | null): string {
|
||||
if (!val) return '-';
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
DeleteOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { doctorApi, type Doctor, type CreateDoctorReq, type UpdateDoctorReq } from '../../api/health/doctors';
|
||||
import { useDictionary } from '../../hooks/useDictionary';
|
||||
import { AuthButton } from '../../components/AuthButton';
|
||||
import { PageContainer } from '../../components/PageContainer';
|
||||
import { EntityName } from '../../components/EntityName';
|
||||
@@ -27,7 +28,7 @@ import { formatDateTime } from '../../utils/format';
|
||||
import { usePaginatedData } from '../../hooks/usePaginatedData';
|
||||
|
||||
/** 科室选项 — 可后续改为从字典接口获取 */
|
||||
const DEPARTMENT_OPTIONS = [
|
||||
const DEPARTMENT_FALLBACK = [
|
||||
{ value: '全科', label: '全科' },
|
||||
{ value: '内科', label: '内科' },
|
||||
{ value: '外科', label: '外科' },
|
||||
@@ -41,7 +42,7 @@ const DEPARTMENT_OPTIONS = [
|
||||
{ value: '体检中心', label: '体检中心' },
|
||||
];
|
||||
|
||||
const TITLE_OPTIONS = [
|
||||
const TITLE_FALLBACK = [
|
||||
{ value: '住院医师', label: '住院医师' },
|
||||
{ value: '主治医师', label: '主治医师' },
|
||||
{ value: '副主任医师', label: '副主任医师' },
|
||||
@@ -74,6 +75,8 @@ interface DoctorFilters {
|
||||
}
|
||||
|
||||
export default function DoctorList() {
|
||||
const { options: DEPARTMENT_OPTIONS } = useDictionary('health_department', DEPARTMENT_FALLBACK);
|
||||
const { options: TITLE_OPTIONS } = useDictionary('health_title', TITLE_FALLBACK);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<Doctor | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
@@ -25,6 +25,7 @@ import { DrawerForm } from '../../components/DrawerForm';
|
||||
import { formatDate, formatDateTime } from '../../utils/format';
|
||||
import { usePaginatedData } from '../../hooks/usePaginatedData';
|
||||
import { useApiRequest } from '../../hooks/useApiRequest';
|
||||
import { useDictionary } from '../../hooks/useDictionary';
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: 'pending', label: '待处理' },
|
||||
@@ -34,7 +35,7 @@ const STATUS_OPTIONS = [
|
||||
{ value: 'cancelled', label: '已取消' },
|
||||
];
|
||||
|
||||
const FOLLOW_UP_TYPE_OPTIONS = [
|
||||
const FOLLOW_UP_TYPE_FALLBACK = [
|
||||
{ value: 'phone', label: '电话' },
|
||||
{ value: 'outpatient', label: '门诊' },
|
||||
{ value: 'home_visit', label: '家访' },
|
||||
@@ -62,6 +63,7 @@ interface AssignFormValues {
|
||||
}
|
||||
|
||||
export default function FollowUpTaskList() {
|
||||
const { options: FOLLOW_UP_TYPE_OPTIONS } = useDictionary('health_follow_up_type', FOLLOW_UP_TYPE_FALLBACK);
|
||||
const [searchParams] = useSearchParams();
|
||||
const urlPatientId = searchParams.get('patient_id');
|
||||
|
||||
|
||||
@@ -32,15 +32,9 @@ import { PointsAccountTab } from './components/PointsAccountTab';
|
||||
import { AiSuggestionTab } from './components/AiSuggestionTab';
|
||||
import { FamilyMembersTab } from './components/FamilyMembersTab';
|
||||
import { DailyMonitoringTab } from './components/DailyMonitoringTab';
|
||||
import { GENDER_OPTIONS, BLOOD_TYPE_OPTIONS } from '../../constants/health';
|
||||
import { GENDER_OPTIONS, BLOOD_TYPE_OPTIONS, GENDER_LABEL } from '../../constants/health';
|
||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||
|
||||
const GENDER_LABEL: Record<string, string> = {
|
||||
male: '男',
|
||||
female: '女',
|
||||
other: '其他',
|
||||
};
|
||||
|
||||
export default function PatientDetail() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { TagsOutlined, AppstoreOutlined } from '@ant-design/icons';
|
||||
import { patientApi, type TagItem } from '../../api/health/patients';
|
||||
import type { PatientListItem } from '../../api/health/patients';
|
||||
import { GENDER_LABEL, STATUS_TAG_CONFIG } from '../../constants/health';
|
||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||
import { AuthButton } from '../../components/AuthButton';
|
||||
|
||||
@@ -120,8 +121,7 @@ export default function PatientTagManage() {
|
||||
width: 80,
|
||||
render: (v?: string) => {
|
||||
if (!v) return '-';
|
||||
const map: Record<string, string> = { male: '男', female: '女', other: '其他' };
|
||||
return map[v] || v;
|
||||
return GENDER_LABEL[v] || v;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -161,17 +161,8 @@ export default function PatientTagManage() {
|
||||
key: 'status',
|
||||
width: 100,
|
||||
render: (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
active: 'green',
|
||||
inactive: 'default',
|
||||
deceased: 'default',
|
||||
};
|
||||
const labelMap: Record<string, string> = {
|
||||
active: '活跃',
|
||||
inactive: '停用',
|
||||
deceased: '已故',
|
||||
};
|
||||
return <Tag color={colorMap[status] || 'default'}>{labelMap[status] || status}</Tag>;
|
||||
const cfg = STATUS_TAG_CONFIG[status] || { color: 'default', label: status };
|
||||
return <Tag color={cfg.color}>{cfg.label}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -14,20 +14,7 @@ import { pointsApi, type PersonalStats } from '../../../api/health/points';
|
||||
import { alertApi, type Alert } from '../../../api/health/alerts';
|
||||
import { useStatsData } from './useStatsData';
|
||||
import { useCountUp } from '../../../hooks/useCountUp';
|
||||
|
||||
const SEVERITY_COLOR: Record<string, string> = {
|
||||
info: 'default',
|
||||
warning: 'orange',
|
||||
critical: 'red',
|
||||
urgent: 'magenta',
|
||||
};
|
||||
|
||||
const SEVERITY_LABEL: Record<string, string> = {
|
||||
info: '提示',
|
||||
warning: '警告',
|
||||
critical: '严重',
|
||||
urgent: '紧急',
|
||||
};
|
||||
import { SEVERITY_COLOR, SEVERITY_LABEL } from '../../../constants/health';
|
||||
|
||||
export function DoctorDashboard() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -3,20 +3,12 @@ import { Table, Select, Tabs, Card, Typography } from 'antd';
|
||||
import { deviceReadingApi } from '../../../api/health/deviceReadings';
|
||||
import type { DeviceReading, HourlyReading } from '../../../api/health/deviceReadings';
|
||||
import { usePaginatedData } from '../../../hooks/usePaginatedData';
|
||||
import { DEVICE_TYPE_OPTIONS } from '../../../constants/health';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
/* ---------- 常量 ---------- */
|
||||
|
||||
const DEVICE_TYPE_OPTIONS = [
|
||||
{ value: 'heart_rate', label: '心率' },
|
||||
{ value: 'blood_oxygen', label: '血氧' },
|
||||
{ value: 'blood_pressure', label: '血压' },
|
||||
{ value: 'blood_glucose', label: '血糖' },
|
||||
{ value: 'steps', label: '步数' },
|
||||
{ value: 'temperature', label: '体温' },
|
||||
] as const;
|
||||
|
||||
const TIME_RANGE_OPTIONS = [
|
||||
{ value: 1, label: '最近 1 小时' },
|
||||
{ value: 6, label: '最近 6 小时' },
|
||||
|
||||
@@ -3,8 +3,9 @@ import { Table, Button, Form, Input, Select, Drawer, message, Popconfirm, Space
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { patientApi, type FamilyMember, type CreateFamilyMemberReq } from '../../../api/health/patients';
|
||||
import { AuthButton } from '../../../components/AuthButton';
|
||||
import { useDictionary } from '../../../hooks/useDictionary';
|
||||
|
||||
const RELATIONSHIP_OPTIONS = [
|
||||
const RELATIONSHIP_FALLBACK = [
|
||||
{ label: '父母', value: 'parent' },
|
||||
{ label: '配偶', value: 'spouse' },
|
||||
{ label: '子女', value: 'child' },
|
||||
@@ -17,6 +18,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export function FamilyMembersTab({ patientId }: Props) {
|
||||
const { options: RELATIONSHIP_OPTIONS } = useDictionary('health_relationship', RELATIONSHIP_FALLBACK);
|
||||
const [members, setMembers] = useState<FamilyMember[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
|
||||
@@ -1,30 +1,11 @@
|
||||
import { Tag } from 'antd';
|
||||
|
||||
const STATUS_CONFIG: Record<string, { color: string; label: string }> = {
|
||||
// 预约状态
|
||||
pending: { color: 'gold', label: '待确认' },
|
||||
confirmed: { color: 'blue', label: '已确认' },
|
||||
completed: { color: 'green', label: '已完成' },
|
||||
cancelled: { color: 'default', label: '已取消' },
|
||||
no_show: { color: 'red', label: '未到诊' },
|
||||
// 随访状态
|
||||
overdue: { color: 'red', label: '逾期' },
|
||||
in_progress: { color: 'processing', label: '进行中' },
|
||||
// 咨询状态
|
||||
waiting: { color: 'gold', label: '等待中' },
|
||||
active: { color: 'green', label: '进行中' },
|
||||
closed: { color: 'default', label: '已关闭' },
|
||||
// 患者状态
|
||||
inactive: { color: 'default', label: '停用' },
|
||||
deceased: { color: 'default', label: '已故' },
|
||||
verified: { color: 'green', label: '已认证' },
|
||||
};
|
||||
import { STATUS_TAG_CONFIG } from '../../../constants/health';
|
||||
|
||||
interface Props {
|
||||
status: string;
|
||||
}
|
||||
|
||||
export function StatusTag({ status }: Props) {
|
||||
const cfg = STATUS_CONFIG[status] || { color: 'default' as const, label: status };
|
||||
const cfg = STATUS_TAG_CONFIG[status] || { color: 'default' as const, label: status };
|
||||
return <Tag color={cfg.color}>{cfg.label}</Tag>;
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@ mod m20260501_000097_seed_menu_permissions;
|
||||
mod m20260501_000098_create_ai_suggestion;
|
||||
mod m20260501_000099_create_ai_risk_threshold;
|
||||
mod m20260501_000100_seed_action_inbox_menu;
|
||||
mod m20260502_000101_seed_health_dictionaries;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
@@ -207,6 +208,7 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20260501_000098_create_ai_suggestion::Migration),
|
||||
Box::new(m20260501_000099_create_ai_risk_threshold::Migration),
|
||||
Box::new(m20260501_000100_seed_action_inbox_menu::Migration),
|
||||
Box::new(m20260502_000101_seed_health_dictionaries::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
//! 健康模块字典种子数据 — 6 个字典 + 对应条目
|
||||
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let db = manager.get_connection();
|
||||
|
||||
let result = db
|
||||
.query_one(sea_orm::Statement::from_string(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
"SELECT id::text FROM tenant LIMIT 1".to_string(),
|
||||
))
|
||||
.await?;
|
||||
|
||||
let tid = match result {
|
||||
Some(row) => row.try_get_by_index::<String>(0).unwrap_or_default(),
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let sys = "00000000-0000-0000-0000-000000000000";
|
||||
|
||||
// ── 1. 科室 health_department ──
|
||||
let dict_dept = "d1000001-0000-0000-0000-000000000001";
|
||||
insert_dict(db, &tid, dict_dept, "科室", "health_department", "医护科室分类", sys).await?;
|
||||
let dept_items = [
|
||||
("全科", "全科", 1),
|
||||
("内科", "内科", 2),
|
||||
("外科", "外科", 3),
|
||||
("儿科", "儿科", 4),
|
||||
("妇产科", "妇产科", 5),
|
||||
("骨科", "骨科", 6),
|
||||
("眼科", "眼科", 7),
|
||||
("口腔科", "口腔科", 8),
|
||||
("皮肤科", "皮肤科", 9),
|
||||
("中医科", "中医科", 10),
|
||||
("体检中心", "体检中心", 11),
|
||||
];
|
||||
for (i, (label, value, sort)) in dept_items.iter().enumerate() {
|
||||
insert_item(db, &tid, dict_dept, label, value, *sort, None, &(i + 1), sys).await?;
|
||||
}
|
||||
|
||||
// ── 2. 职称 health_title ──
|
||||
let dict_title = "d1000001-0000-0000-0000-000000000002";
|
||||
insert_dict(db, &tid, dict_title, "职称", "health_title", "医护职称分类", sys).await?;
|
||||
let title_items = [
|
||||
("住院医师", "住院医师", 1),
|
||||
("主治医师", "主治医师", 2),
|
||||
("副主任医师", "副主任医师", 3),
|
||||
("主任医师", "主任医师", 4),
|
||||
("护士", "护士", 5),
|
||||
("护师", "护师", 6),
|
||||
("主管护师", "主管护师", 7),
|
||||
("副主任护师", "副主任护师", 8),
|
||||
("主任护师", "主任护师", 9),
|
||||
];
|
||||
for (i, (label, value, sort)) in title_items.iter().enumerate() {
|
||||
insert_item(db, &tid, dict_title, label, value, *sort, None, &(i + 1), sys).await?;
|
||||
}
|
||||
|
||||
// ── 3. 设备类型 health_device_type ──
|
||||
let dict_dev = "d1000001-0000-0000-0000-000000000003";
|
||||
insert_dict(db, &tid, dict_dev, "设备类型", "health_device_type", "健康监测设备类型", sys).await?;
|
||||
let dev_items = [
|
||||
("血压计", "blood_pressure", 1),
|
||||
("血糖仪", "blood_glucose", 2),
|
||||
("心率监测", "heart_rate", 3),
|
||||
("血氧仪", "blood_oxygen", 4),
|
||||
("计步器", "steps", 5),
|
||||
("体温计", "temperature", 6),
|
||||
("睡眠监测", "sleep", 7),
|
||||
("压力监测", "stress", 8),
|
||||
];
|
||||
for (i, (label, value, sort)) in dev_items.iter().enumerate() {
|
||||
insert_item(db, &tid, dict_dev, label, value, *sort, None, &(i + 1), sys).await?;
|
||||
}
|
||||
|
||||
// ── 4. 随访类型 health_follow_up_type ──
|
||||
let dict_fu = "d1000001-0000-0000-0000-000000000004";
|
||||
insert_dict(db, &tid, dict_fu, "随访类型", "health_follow_up_type", "随访方式分类", sys).await?;
|
||||
let fu_items = [
|
||||
("电话", "phone", 1),
|
||||
("门诊", "outpatient", 2),
|
||||
("家访", "home_visit", 3),
|
||||
("线上", "online", 4),
|
||||
("微信", "wechat", 5),
|
||||
];
|
||||
for (i, (label, value, sort)) in fu_items.iter().enumerate() {
|
||||
insert_item(db, &tid, dict_fu, label, value, *sort, None, &(i + 1), sys).await?;
|
||||
}
|
||||
|
||||
// ── 5. 咨询类型 health_consultation_type ──
|
||||
let dict_consult = "d1000001-0000-0000-0000-000000000005";
|
||||
insert_dict(
|
||||
db, &tid, dict_consult, "咨询类型", "health_consultation_type", "咨询会话类型", sys,
|
||||
)
|
||||
.await?;
|
||||
let consult_items = [
|
||||
("客服咨询", "customer_service", 1),
|
||||
("医疗咨询", "medical", 2),
|
||||
("健康咨询", "health_consultation", 3),
|
||||
];
|
||||
for (i, (label, value, sort)) in consult_items.iter().enumerate() {
|
||||
insert_item(db, &tid, dict_consult, label, value, *sort, None, &(i + 1), sys).await?;
|
||||
}
|
||||
|
||||
// ── 6. 关系 health_relationship ──
|
||||
let dict_rel = "d1000001-0000-0000-0000-000000000006";
|
||||
insert_dict(db, &tid, dict_rel, "关系", "health_relationship", "家属与患者关系", sys).await?;
|
||||
let rel_items = [
|
||||
("父母", "parent", 1),
|
||||
("配偶", "spouse", 2),
|
||||
("子女", "child", 3),
|
||||
("兄弟姐妹", "sibling", 4),
|
||||
("其他", "other", 5),
|
||||
];
|
||||
for (i, (label, value, sort)) in rel_items.iter().enumerate() {
|
||||
insert_item(db, &tid, dict_rel, label, value, *sort, None, &(i + 1), sys).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let db = manager.get_connection();
|
||||
|
||||
let dict_ids = [
|
||||
"d1000001-0000-0000-0000-000000000001",
|
||||
"d1000001-0000-0000-0000-000000000002",
|
||||
"d1000001-0000-0000-0000-000000000003",
|
||||
"d1000001-0000-0000-0000-000000000004",
|
||||
"d1000001-0000-0000-0000-000000000005",
|
||||
"d1000001-0000-0000-0000-000000000006",
|
||||
];
|
||||
|
||||
// 先删条目(级联应该也会处理,但显式清理更安全)
|
||||
for dict_id in &dict_ids {
|
||||
db.execute(sea_orm::Statement::from_string(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
format!("DELETE FROM dictionary_items WHERE dictionary_id = '{dict_id}'"),
|
||||
))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
// 再删字典
|
||||
for dict_id in &dict_ids {
|
||||
db.execute(sea_orm::Statement::from_string(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
format!("DELETE FROM dictionaries WHERE id = '{dict_id}'"),
|
||||
))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn insert_dict(
|
||||
db: &sea_orm_migration::SchemaManagerConnection<'_>,
|
||||
tenant_id: &str,
|
||||
id: &str,
|
||||
name: &str,
|
||||
code: &str,
|
||||
description: &str,
|
||||
sys: &str,
|
||||
) -> Result<(), DbErr> {
|
||||
db.execute(sea_orm::Statement::from_string(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
format!(
|
||||
"INSERT INTO dictionaries (id, tenant_id, name, code, description, created_at, updated_at, created_by, updated_by, deleted_at, version) \
|
||||
VALUES ('{id}', '{tenant_id}', '{name}', '{code}', '{description}', NOW(), NOW(), '{sys}', '{sys}', NULL, 1) \
|
||||
ON CONFLICT (id) DO NOTHING"
|
||||
),
|
||||
))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn insert_item(
|
||||
db: &sea_orm_migration::SchemaManagerConnection<'_>,
|
||||
tenant_id: &str,
|
||||
dict_id: &str,
|
||||
label: &str,
|
||||
value: &str,
|
||||
sort_order: i32,
|
||||
color: Option<&str>,
|
||||
idx: &usize,
|
||||
sys: &str,
|
||||
) -> Result<(), DbErr> {
|
||||
// 生成条目 ID:基于字典 ID 后缀 + 序号
|
||||
let suffix = &dict_id[24..];
|
||||
let item_id = format!("d200{idx:04x}-0000-0000-{suffix}");
|
||||
let color_sql = match color {
|
||||
Some(c) => format!("'{c}'"),
|
||||
None => "NULL".to_string(),
|
||||
};
|
||||
db.execute(sea_orm::Statement::from_string(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
format!(
|
||||
"INSERT INTO dictionary_items (id, tenant_id, dictionary_id, label, value, sort_order, color, created_at, updated_at, created_by, updated_by, deleted_at, version) \
|
||||
VALUES ('{item_id}', '{tenant_id}', '{dict_id}', '{label}', '{value}', {sort_order}, {color_sql}, NOW(), NOW(), '{sys}', '{sys}', NULL, 1) \
|
||||
ON CONFLICT (id) DO NOTHING"
|
||||
),
|
||||
))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user