From a6d2426f044fa29d0a0fdaa01c6b3699d7c61e5e Mon Sep 17 00:00:00 2001 From: iven Date: Sun, 26 Apr 2026 10:24:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E8=B6=8B=E5=8A=BF=E5=9B=BE?= =?UTF-8?q?=E5=A4=9A=E6=8C=87=E6=A0=87=E5=B1=95=E7=A4=BA=20=E2=80=94=205?= =?UTF-8?q?=E9=A1=B9=E4=BD=93=E5=BE=81=E6=8C=87=E6=A0=87=E8=BF=B7=E4=BD=A0?= =?UTF-8?q?=E8=B6=8B=E5=8A=BF=E5=8D=A1=E7=89=87=E7=BD=91=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 收缩压/舒张压/心率/体重/血糖各一个迷你 Line 图, 并行加载、无数据指标显示空状态,录入后自动刷新 --- .../health/components/VitalSignsChart.tsx | 156 ++++++++++++------ 1 file changed, 110 insertions(+), 46 deletions(-) diff --git a/apps/web/src/pages/health/components/VitalSignsChart.tsx b/apps/web/src/pages/health/components/VitalSignsChart.tsx index d6ddc21..575f289 100644 --- a/apps/web/src/pages/health/components/VitalSignsChart.tsx +++ b/apps/web/src/pages/health/components/VitalSignsChart.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { Line } from '@ant-design/charts'; -import { Spin, Alert, Typography } from 'antd'; +import { Spin, Typography } from 'antd'; import { LineChartOutlined } from '@ant-design/icons'; import { healthDataApi } from '../../../api/health/healthData'; @@ -11,28 +11,111 @@ interface Props { refreshKey?: number; } -const DEFAULT_INDICATOR = 'systolic_bp_morning'; -const UNIT = 'mmHg'; -const LABEL = '收缩压(晨)'; +interface MetricConfig { + key: string; + label: string; + unit: string; + color: string; +} + +const METRICS: MetricConfig[] = [ + { key: 'systolic_bp_morning', label: '收缩压(晨)', unit: 'mmHg', color: '#ef4444' }, + { key: 'diastolic_bp_morning', label: '舒张压(晨)', unit: 'mmHg', color: '#f97316' }, + { key: 'heart_rate', label: '心率', unit: 'bpm', color: '#3b82f6' }, + { key: 'weight', label: '体重', unit: 'kg', color: '#10b981' }, + { key: 'blood_sugar', label: '血糖', unit: 'mmol/L', color: '#8b5cf6' }, +]; + +interface MetricData { + points: { date: string; value: number }[]; + latest: number | null; +} + +function extractData(res: unknown): { date: string; value: number }[] { + const raw = (res as { data?: { date: string; value: number }[] })?.data; + const arr = Array.isArray(res) ? res : Array.isArray(raw) ? raw : []; + return arr.filter((d) => d?.value != null); +} + +/** 单个指标迷你卡片 */ +function MetricCard({ metric, metricData }: { metric: MetricConfig; metricData: MetricData }) { + if (metricData.points.length === 0) { + return ( +
+ + 暂无{metric.label}数据 +
+ ); + } + + return ( +
+
+ {metric.label} + {metricData.latest != null && ( + + {metricData.latest} {metric.unit} + + )} +
+ +
+ ); +} export function VitalSignsChart({ patientId, refreshKey }: Props) { - const [data, setData] = useState<{ date: string; value: number }[]>([]); + const [metricsData, setMetricsData] = useState>({}); const [loading, setLoading] = useState(true); - const [error, setError] = useState(false); useEffect(() => { if (!patientId) return; setLoading(true); - setError(false); - healthDataApi - .getIndicatorTimeseries(patientId, DEFAULT_INDICATOR) - .then((res) => { - const raw = (res as { data?: { date: string; value: number }[] })?.data; - const points = Array.isArray(res) ? res : Array.isArray(raw) ? raw : []; - setData(points.filter((d) => d?.value != null)); - }) - .catch(() => setError(true)) - .finally(() => setLoading(false)); + + Promise.allSettled( + METRICS.map((m) => + healthDataApi + .getIndicatorTimeseries(patientId, m.key) + .then(extractData) + .then((points) => ({ + key: m.key, + data: { points, latest: points.length > 0 ? points[points.length - 1].value : null }, + })) + ) + ).then((results) => { + const map: Record = {}; + results.forEach((r, i) => { + if (r.status === 'fulfilled') { + map[r.value.key] = r.value.data; + } else { + map[METRICS[i].key] = { points: [], latest: null }; + } + }); + setMetricsData(map); + setLoading(false); + }); }, [patientId, refreshKey]); if (loading) { @@ -43,11 +126,8 @@ export function VitalSignsChart({ patientId, refreshKey }: Props) { ); } - if (error) { - return ; - } - - if (data.length === 0) { + const hasAnyData = Object.values(metricsData).some((d) => d.points.length > 0); + if (!hasAnyData) { return (
- 暂无{LABEL}趋势数据 + 暂无趋势数据
); } - const latestValue = data[data.length - 1].value; - return ( -
-
- {LABEL}趋势 - {latestValue != null && ( - - 最新:{latestValue} {UNIT} - - )} -
- +
+ {METRICS.map((m) => ( + + ))}
); }