From 1bde4b44c00b89195e4259a58b4059f58f81c853 Mon Sep 17 00:00:00 2001 From: iven Date: Tue, 28 Apr 2026 22:10:13 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20VitalSignsChart=20hooks=20=E9=A1=BA?= =?UTF-8?q?=E5=BA=8F=E4=BF=AE=E5=A4=8D=20+=20=E8=B6=8B=E5=8A=BF=E7=BA=BF?= =?UTF-8?q?=E9=A2=9C=E8=89=B2=E5=8C=BA=E5=88=86=E5=BA=A6=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复 React hooks 在 early return 之后调用导致的渲染崩溃, 将所有 useMemo 移至条件返回之前。趋势图三系列改用高对比色: 实际值(原色实线)、移动平均(青色短虚线)、趋势线(琥珀色长虚线)。 --- .../health/components/VitalSignsChart.tsx | 183 +++++++++--------- 1 file changed, 91 insertions(+), 92 deletions(-) diff --git a/apps/web/src/pages/health/components/VitalSignsChart.tsx b/apps/web/src/pages/health/components/VitalSignsChart.tsx index 617c319..4a42171 100644 --- a/apps/web/src/pages/health/components/VitalSignsChart.tsx +++ b/apps/web/src/pages/health/components/VitalSignsChart.tsx @@ -214,7 +214,7 @@ export function VitalSignsChart({ patientId, refreshKey }: Props) { }); }, [patientId, refreshKey]); - // 为每个指标预计算趋势统计 + // 趋势统计 — 所有 hooks 必须在 early return 之前调用 const trendStats = useMemo(() => { const stats: Record; @@ -240,6 +240,81 @@ export function VitalSignsChart({ patientId, refreshKey }: Props) { return stats; }, [metricsData]); + const selectedMetric = METRICS.find((m) => m.key === selectedKey); + const selectedData = selectedKey ? metricsData[selectedKey] : null; + const axisLabelStyle = { fill: isDark ? '#94a3b8' : '#475569' }; + + const chartSeries = useMemo(() => { + if (!selectedKey || !selectedData) return []; + const stat = trendStats[selectedKey]; + if (!stat) return []; + + const points = selectedData.points; + const series: { date: string; value: number; series: string; isAnomaly?: boolean }[] = []; + + points.forEach((p, i) => { + series.push({ + date: p.date, + value: p.value, + series: '实际值', + isAnomaly: stat.anomalyIndices.has(i), + }); + }); + + stat.ma.forEach((val, i) => { + if (val != null) { + series.push({ + date: points[i].date, + value: val, + series: '移动平均', + }); + } + }); + + if (stat.regression) { + const { intercept, slope } = stat.regression; + points.forEach((p, i) => { + series.push({ + date: p.date, + value: intercept + slope * i, + series: '趋势线', + }); + }); + } + + return series; + }, [selectedKey, selectedData, trendStats]); + + const getTrendDirection = (key: string): 'rising' | 'falling' | 'stable' | null => { + const stat = trendStats[key]; + if (!stat?.regression) return null; + const { slope } = stat.regression; + if (Math.abs(slope) < 0.01) return 'stable'; + return slope > 0 ? 'rising' : 'falling'; + }; + + const trendDescription = useMemo(() => { + if (!selectedKey || !selectedData || !trendStats[selectedKey]) return null; + const stat = trendStats[selectedKey]; + if (!stat?.regression) return null; + + const { slope, rSquared } = stat.regression; + const direction = getTrendDirection(selectedKey); + const anomalyCount = stat.anomalyIndices.size; + const points = selectedData.points; + const totalChange = slope * (points.length - 1); + + return { + direction: direction === 'rising' ? '上升' : direction === 'falling' ? '下降' : '平稳', + slope, + rSquared, + totalChange, + periodDays: points.length > 1 ? points.length : 0, + anomalyCount, + }; + }, [selectedKey, selectedData, trendStats]); + + // Early returns — 放在所有 hooks 之后 if (loading) { return (
@@ -266,87 +341,6 @@ 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' }; - - // 为详情图构建多系列数据 - const chartSeries = useMemo(() => { - if (!selectedKey || !selectedData) return []; - const stat = trendStats[selectedKey]; - if (!stat) return []; - - const points = selectedData.points; - const series: { date: string; value: number; series: string; isAnomaly?: boolean }[] = []; - - // 原始数据系列 - points.forEach((p, i) => { - series.push({ - date: p.date, - value: p.value, - series: '实际值', - isAnomaly: stat.anomalyIndices.has(i), - }); - }); - - // 移动平均系列 - stat.ma.forEach((val, i) => { - if (val != null) { - series.push({ - date: points[i].date, - value: val, - series: '移动平均', - }); - } - }); - - // 趋势线系列(线性回归) - if (stat.regression) { - const { intercept, slope } = stat.regression; - points.forEach((p, i) => { - series.push({ - date: p.date, - value: intercept + slope * i, - series: '趋势线', - }); - }); - } - - return series; - }, [selectedKey, selectedData, trendStats]); - - // 趋势方向摘要 - const getTrendDirection = (key: string): 'rising' | 'falling' | 'stable' | null => { - const stat = trendStats[key]; - if (!stat?.regression) return null; - const { slope } = stat.regression; - if (Math.abs(slope) < 0.01) return 'stable'; - return slope > 0 ? 'rising' : 'falling'; - }; - - // 趋势描述文本 - const trendDescription = useMemo(() => { - if (!selectedKey || !selectedData || !trendStats[selectedKey]) return null; - const stat = trendStats[selectedKey]; - if (!stat?.regression) return null; - - const { slope, rSquared } = stat.regression; - const direction = getTrendDirection(selectedKey); - const anomalyCount = stat.anomalyIndices.size; - const points = selectedData.points; - const periodDays = points.length > 1 ? points.length : 0; - const totalChange = slope * (points.length - 1); - - return { - direction: direction === 'rising' ? '上升' : direction === 'falling' ? '下降' : '平稳', - slope, - rSquared, - totalChange, - periodDays, - anomalyCount, - }; - }, [selectedKey, selectedData, trendStats]); - return (
{/* 概览卡片条 */} @@ -426,17 +420,22 @@ export function VitalSignsChart({ patientId, refreshKey }: Props) { y: { title: selectedMetric.unit, label: { style: axisLabelStyle } }, }} style={{ - lineWidth: 2, + lineWidth: (datum: Record) => { + const series = datum.series as string; + if (series === '趋势线') return 1.5; + if (series === '移动平均') return 2; + return 2.5; + }, stroke: (datum: Record) => { const series = datum.series as string; - if (series === '趋势线') return '#94a3b8'; - if (series === '移动平均') return selectedMetric.color + '88'; + if (series === '趋势线') return '#f59e0b'; + if (series === '移动平均') return '#14b8a6'; return selectedMetric.color; }, lineDash: (datum: Record) => { const series = datum.series as string; - if (series === '趋势线') return [6, 3]; - if (series === '移动平均') return []; + if (series === '趋势线') return [8, 4]; + if (series === '移动平均') return [4, 2]; return []; }, }} @@ -463,16 +462,16 @@ export function VitalSignsChart({ patientId, refreshKey }: Props) { {/* 图例说明 */}
-
+
实际值
-
- 趋势线 +
+ 移动平均
-
- 移动平均 +
+ 趋势线