fix(health): 走查止血 — 患者名显示修复 + 枚举补全 + 医护统计 + 设备选择器
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

后端:
- alert_service: list_alerts 批量查询 patient_name 填充 AlertResponse
- consultation_service: list_sessions 批量查询 patient_name/doctor_name
- erp-ai handler: list_analysis 通过 raw SQL 查询 patient_name

前端:
- AlertList/AlertDashboard: 使用后端返回的 patient_name 替代 ID 截断
- ConsultationDetail: 使用 patient_name/doctor_name 替代 ID 截断
- AiAnalysisList: 使用 patient_name 替代 ID 截断
- constants/health: SEVERITY 补 high/medium, STATUS 补 active
- AdminDashboard: 医护人数改为 API 查询(useStatsData 新增 doctorCount)
- DeviceManage: 患者 ID 输入改为 PatientSelect 搜索选择器
This commit is contained in:
iven
2026-05-04 00:03:40 +08:00
parent 20bd9e8cb4
commit 5140552ff6
14 changed files with 155 additions and 29 deletions

View File

@@ -4,6 +4,7 @@ import type { PaginatedResponse } from '../types';
export interface AnalysisItem {
id: string;
patient_id: string;
patient_name?: string;
analysis_type: string;
source_ref: string;
model_used: string;

View File

@@ -5,6 +5,7 @@ import type { PaginatedResponse } from '../types';
export interface Alert {
id: string;
patient_id: string;
patient_name?: string;
rule_id: string;
severity: string;
title: string;

View File

@@ -40,6 +40,8 @@ export const SEVERITY_COLOR: Record<string, string> = {
warning: 'orange',
critical: 'red',
urgent: 'magenta',
high: 'red',
medium: 'orange',
};
export const SEVERITY_LABEL: Record<string, string> = {
@@ -47,18 +49,23 @@ export const SEVERITY_LABEL: Record<string, string> = {
warning: '警告',
critical: '严重',
urgent: '紧急',
high: '严重',
medium: '中等',
};
export const SEVERITY_OPTIONS = [
{ value: 'info', label: '提示' },
{ value: 'warning', label: '警告' },
{ value: 'medium', label: '中等' },
{ value: 'critical', label: '严重' },
{ value: 'high', label: '严重' },
{ value: 'urgent', label: '紧急' },
];
// --- 告警状态(统一 3 处: AlertDashboard, AlertList ---
export const ALERT_STATUS_COLOR: Record<string, string> = {
pending: 'orange',
active: 'gold',
acknowledged: 'blue',
resolved: 'green',
dismissed: 'default',
@@ -66,6 +73,7 @@ export const ALERT_STATUS_COLOR: Record<string, string> = {
export const ALERT_STATUS_LABEL: Record<string, string> = {
pending: '待处理',
active: '活跃',
acknowledged: '已确认',
resolved: '已恢复',
dismissed: '已忽略',
@@ -73,6 +81,7 @@ export const ALERT_STATUS_LABEL: Record<string, string> = {
export const ALERT_STATUS_OPTIONS = [
{ value: '', label: '全部状态' },
{ value: 'active', label: '活跃' },
{ value: 'pending', label: '待处理' },
{ value: 'acknowledged', label: '已确认' },
{ value: 'resolved', label: '已恢复' },

View File

@@ -11,6 +11,7 @@ import {
import { useThemeMode } from '../../hooks/useThemeMode';
import { analysisApi, type AnalysisItem } from '../../api/ai/analysis';
import { suggestionApi, type SuggestionItem } from '../../api/ai/suggestions';
import { EntityName } from '../../components/EntityName';
const { Text } = Typography;
@@ -321,9 +322,9 @@ export default function AiAnalysisList() {
dataIndex: 'patient_id',
key: 'patient_id',
width: 140,
render: (v: string) => (
<Link to={`/health/patients/${v}`} style={{ fontFamily: 'monospace', fontSize: 12 }}>
{v.slice(0, 8)}
render: (_: unknown, record: AnalysisItem) => (
<Link to={`/health/patients/${record.patient_id}`}>
<EntityName name={record.patient_name} id={record.patient_id} />
</Link>
),
},

View File

@@ -245,7 +245,7 @@ export default function AlertDashboard() {
}
description={
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
: <EntityName id={alert.patient_id} />
: <EntityName name={alert.patient_name} id={alert.patient_id} />
{' · '}
{new Date(alert.created_at).toLocaleString('zh-CN')}
</Typography.Text>

View File

@@ -137,12 +137,11 @@ export default function AlertList() {
dataIndex: 'patient_id',
key: 'patient_id',
width: 140,
render: (id: string) => (
<Link to={`/health/patients/${id}`}>
render: (_: unknown, record: Alert) => (
<Link to={`/health/patients/${record.patient_id}`}>
<EntityName
name={undefined}
id={id}
fallbackLabel={id.length > 8 ? id.slice(0, 8) + '...' : id}
name={record.patient_name}
id={record.patient_id}
/>
</Link>
),

View File

@@ -7,6 +7,7 @@ import { StatusTag } from './components/StatusTag';
import { ImagePreview } from './components/ImagePreview';
import { useThemeMode } from '../../hooks/useThemeMode';
import { AuthButton } from '../../components/AuthButton';
import { EntityName } from '../../components/EntityName';
const PAGE_SIZE = 30;
const POLL_INTERVAL = 10_000;
@@ -297,10 +298,10 @@ export default function ConsultationDetail() {
{session && (
<>
<Typography.Text type="secondary" style={{ fontSize: 13 }}>
: {session.patient_id.slice(0, 8)}
: <EntityName name={session.patient_name} id={session.patient_id} />
</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 13 }}>
: {session.doctor_id ? session.doctor_id.slice(0, 8) : '-'}
: <EntityName name={session.doctor_name} id={session.doctor_id} fallbackLabel="未分配" />
</Typography.Text>
<StatusTag status={session.status} />
</>

View File

@@ -5,6 +5,7 @@ import dayjs from 'dayjs';
import { deviceApi, type DeviceItem } from '../../api/health/devices';
import { DEVICE_TYPE_OPTIONS, DEVICE_TYPE_COLOR } from '../../constants/health';
import { PatientSelect } from './components/PatientSelect';
function formatTime(val?: string | null): string {
if (!val) return '-';
@@ -109,13 +110,13 @@ export default function DeviceManage() {
<h2 style={{ marginBottom: 16 }}></h2>
<Space style={{ marginBottom: 16 }} wrap>
<Input
placeholder="患者 ID"
value={filterPatientId}
onChange={(e) => setFilterPatientId(e.target.value)}
style={{ width: 200 }}
allowClear
/>
<div style={{ width: 240 }}>
<PatientSelect
value={filterPatientId || undefined}
onChange={(val) => setFilterPatientId(val || '')}
placeholder="搜索患者"
/>
</div>
<Select
placeholder="设备类型"
value={filterDeviceType}

View File

@@ -11,10 +11,10 @@ import { useCountUp } from '../../../hooks/useCountUp';
import HealthDataCenter from './HealthDataCenter';
export function AdminDashboard() {
const { patientStats, followUpStats, healthDataStats, dialysisStats, loading } = useStatsData();
const { patientStats, followUpStats, healthDataStats, dialysisStats, doctorCount, loading } = useStatsData();
const patientCount = useCountUp(patientStats?.total_patients ?? 0);
const appointmentCount = useCountUp(healthDataStats?.appointments?.this_month ?? 0);
const doctorCount = useCountUp(0);
const doctorCountDisplay = useCountUp(doctorCount);
if (loading && !patientStats) return <Spin size="large" style={{ display: 'block', margin: '80px auto' }} />;
@@ -62,7 +62,7 @@ export function AdminDashboard() {
</Col>
<Col xs={12} sm={8} lg={4}>
<Card size="small">
<Statistic title="医护人数" value={doctorCount} prefix={<UserOutlined />} />
<Statistic title="医护人数" value={doctorCountDisplay} prefix={<UserOutlined />} />
</Card>
</Col>
</Row>

View File

@@ -8,6 +8,7 @@ import {
type HealthDataStats,
type DialysisStatistics,
} from '../../../api/health/points';
import { doctorApi } from '../../../api/health/doctors';
export interface StatsData {
patientStats: PatientStatistics | null;
@@ -16,6 +17,7 @@ export interface StatsData {
pointsStats: PointsStatistics | null;
healthDataStats: HealthDataStats | null;
dialysisStats: DialysisStatistics | null;
doctorCount: number;
loading: boolean;
error: string | null;
refresh: () => void;
@@ -31,6 +33,7 @@ export function useStatsData(): StatsData {
const [pointsStats, setPointsStats] = useState<PointsStatistics | null>(null);
const [healthDataStats, setHealthDataStats] = useState<HealthDataStats | null>(null);
const [dialysisStats, setDialysisStats] = useState<DialysisStatistics | null>(null);
const [doctorCount, setDoctorCount] = useState(0);
const fetchAllStats = useCallback(async () => {
setLoading(true);
@@ -56,9 +59,14 @@ export function useStatsData(): StatsData {
tryFetch(pointsApi.getStatistics, setPointsStats, '积分'),
tryFetch(pointsApi.getHealthDataStats, setHealthDataStats, '健康数据'),
tryFetch(pointsApi.getDialysisStats, setDialysisStats, '透析'),
tryFetch(
async () => { const r = await doctorApi.list({ page: 1, page_size: 1 }); return r.total; },
setDoctorCount,
'医护',
),
]);
if (hasAnyError && errors.length === 6) {
if (hasAnyError && errors.length === 7) {
setError('加载统计数据失败');
}
@@ -70,7 +78,7 @@ export function useStatsData(): StatsData {
}, [fetchAllStats]);
return {
patientStats, consultationStats, followUpStats, pointsStats, healthDataStats, dialysisStats,
patientStats, consultationStats, followUpStats, pointsStats, healthDataStats, dialysisStats, doctorCount,
loading, error, refresh: fetchAllStats,
};
}