Compare commits

...

3 Commits

Author SHA1 Message Date
iven
23cd62a70f feat(db): 健康模块字典种子数据 — 6 个字典 + 43 个条目
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
- health_department (11 科室)
- health_title (9 职称)
- health_device_type (8 设备类型)
- health_follow_up_type (5 随访类型)
- health_consultation_type (3 咨询类型)
- health_relationship (5 关系类型)
2026-05-02 11:34:35 +08:00
iven
63ead0c442 refactor(web): 新增 useDictionary hook + 4 个页面下拉选项改用字典 API
- 新增 useDictionary hook 支持字典 API 获取 + fallback 降级
- DoctorList 科室/职称改用 useDictionary (health_department/health_title)
- FollowUpTaskList 随访类型改用 useDictionary (health_follow_up_type)
- ConsultationList 咨询类型改用 useDictionary (health_consultation_type)
- FamilyMembersTab 家庭关系改用 useDictionary (health_relationship)
2026-05-02 11:27:11 +08:00
iven
b6e780e649 refactor(web): 统一健康模块静态映射常量到 constants/health.ts
- 收敛 SEVERITY_COLOR/LABEL (5处→1处)
- 收敛 ALERT_STATUS_COLOR/LABEL (3处→1处)
- 收敛 DEVICE_TYPE_OPTIONS/COLOR (3处→1处)
- 收敛 GENDER_LABEL (4处→1处)
- StatusTag 组件改引用 STATUS_TAG_CONFIG
- DoctorDashboard 严重度映射改引用常量
2026-05-02 11:24:34 +08:00
17 changed files with 393 additions and 206 deletions

View File

@@ -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: '已认证' },
};

View 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 };
}

View File

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

View File

@@ -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 ?? '')}
/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>;
},
},
{

View File

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

View File

@@ -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 小时' },

View File

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

View File

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

View File

@@ -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),
]
}
}

View File

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