diff --git a/apps/web/src/pages/health/components/VitalSignsChart.tsx b/apps/web/src/pages/health/components/VitalSignsChart.tsx index 575f289..b92ff6b 100644 --- a/apps/web/src/pages/health/components/VitalSignsChart.tsx +++ b/apps/web/src/pages/health/components/VitalSignsChart.tsx @@ -1,8 +1,9 @@ import { useEffect, useState } from 'react'; import { Line } from '@ant-design/charts'; import { Spin, Typography } from 'antd'; -import { LineChartOutlined } from '@ant-design/icons'; +import { LineChartOutlined, CloseOutlined } from '@ant-design/icons'; import { healthDataApi } from '../../../api/health/healthData'; +import { useThemeMode } from '../../../hooks/useThemeMode'; const { Text } = Typography; @@ -16,14 +17,15 @@ interface MetricConfig { label: string; unit: string; color: string; + normalRange?: [number, number]; } 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: 'systolic_bp_morning', label: '收缩压(晨)', unit: 'mmHg', color: '#ef4444', normalRange: [90, 140] }, + { key: 'diastolic_bp_morning', label: '舒张压(晨)', unit: 'mmHg', color: '#f97316', normalRange: [60, 90] }, + { key: 'heart_rate', label: '心率', unit: 'bpm', color: '#3b82f6', normalRange: [60, 100] }, { key: 'weight', label: '体重', unit: 'kg', color: '#10b981' }, - { key: 'blood_sugar', label: '血糖', unit: 'mmol/L', color: '#8b5cf6' }, + { key: 'blood_sugar', label: '血糖', unit: 'mmol/L', color: '#8b5cf6', normalRange: [3.9, 6.1] }, ]; interface MetricData { @@ -37,51 +39,63 @@ function extractData(res: unknown): { date: string; value: number }[] { return arr.filter((d) => d?.value != null); } -/** 单个指标迷你卡片 */ -function MetricCard({ metric, metricData }: { metric: MetricConfig; metricData: MetricData }) { - if (metricData.points.length === 0) { - return ( -
- - 暂无{metric.label}数据 -
- ); - } +const emptyData: MetricData = { points: [], latest: null }; + +/** 概览卡片 — 指标名 + 最新值 + 微型趋势线 */ +function MetricCard({ + metric, + metricData, + selected, + onClick, +}: { + metric: MetricConfig; + metricData: MetricData; + selected: boolean; + onClick: () => void; +}) { + const hasData = metricData.points.length > 0; + const cardStyle: React.CSSProperties = { + minWidth: 130, + padding: '8px 12px', + background: 'var(--ant-color-bg-container, #fafafa)', + borderRadius: 8, + border: selected + ? `2px solid ${metric.color}` + : '1px solid var(--ant-color-border-secondary, #f0f0f0)', + cursor: hasData ? 'pointer' : 'default', + transition: 'border-color 0.2s, box-shadow 0.2s', + flex: '0 0 auto', + ...(selected && hasData ? { boxShadow: `0 0 0 1px ${metric.color}22` } : {}), + }; return ( -
+
{metric.label} {metricData.latest != null && ( - {metricData.latest} {metric.unit} + {metricData.latest} + {metric.unit} )}
- + {hasData ? ( + + ) : ( +
+ 暂无数据 +
+ )}
); } @@ -89,6 +103,8 @@ function MetricCard({ metric, metricData }: { metric: MetricConfig; metricData: export function VitalSignsChart({ patientId, refreshKey }: Props) { const [metricsData, setMetricsData] = useState>({}); const [loading, setLoading] = useState(true); + const [selectedKey, setSelectedKey] = useState(null); + const isDark = useThemeMode(); useEffect(() => { if (!patientId) return; @@ -110,7 +126,7 @@ export function VitalSignsChart({ patientId, refreshKey }: Props) { if (r.status === 'fulfilled') { map[r.value.key] = r.value.data; } else { - map[METRICS[i].key] = { points: [], latest: null }; + map[METRICS[i].key] = emptyData; } }); setMetricsData(map); @@ -120,7 +136,7 @@ export function VitalSignsChart({ patientId, refreshKey }: Props) { if (loading) { return ( -
+
); @@ -144,15 +160,69 @@ export function VitalSignsChart({ patientId, refreshKey }: Props) { ); } + const selectedMetric = METRICS.find((m) => m.key === selectedKey); + const selectedData = selectedKey ? metricsData[selectedKey] : null; + const axisLabelStyle = { fill: isDark ? '#94a3b8' : '#475569' }; + return ( -
- {METRICS.map((m) => ( - - ))} +
+ {/* 概览卡片条 */} +
+ {METRICS.map((m) => ( + setSelectedKey((prev) => (prev === m.key ? null : m.key))} + /> + ))} +
+ + {/* 详情图区域 */} + {selectedMetric && selectedData && selectedData.points.length > 0 && ( +
+
+
+ {selectedMetric.label} + + {selectedData.latest} + + {selectedMetric.unit} +
+
setSelectedKey(null)} + style={{ cursor: 'pointer', padding: '2px 6px', borderRadius: 4, color: 'var(--ant-color-text-secondary)' }} + > + +
+
+ + d.date, + items: [{ channel: 'y', valueFormatter: (v: number) => `${v} ${selectedMetric.unit}` }], + }} + /> +
+ )}
); }