import { useEffect, useState, useMemo } from 'react'; import { Activity } from 'lucide-react'; import { caseApi, riskApi, alertApi, envApi } from '@/services/api'; import { ErrorBanner } from '@/components/ErrorBanner'; import { LoadingState, Segmented } from '@/components/ui'; import { TESTIDS } from '@/utils/testids'; import type { CaseTrendPoint, DistrictCaseData, PollutantPoint, DiagnosisBreakdown, Alert, } from '@/types'; import { KpiRow, type KpiData } from '@/components/overview/KpiRow'; import { CaseAqiTrend, type MergedTrendItem } from '@/components/overview/CaseAqiTrend'; import { DistrictChoropleth } from '@/components/overview/DistrictChoropleth'; import { TopDistrictsBar } from '@/components/overview/TopDistrictsBar'; import { TopDiagnosesBar } from '@/components/overview/TopDiagnosesBar'; import { AlertSeverityDonut, type AlertSlice } from '@/components/overview/AlertSeverityDonut'; import { CHART_COLORS } from '@/components/overview/chartColors'; import { joinDistrictCases, buildMetricLookup, type MetricKey, } from '@/components/overview/districtNormalize'; const METRIC_OPTIONS: { value: MetricKey; label: string }[] = [ { value: 'all', label: '全部' }, { value: 'outpatient', label: '门诊' }, { value: 'inpatient', label: '住院' }, ]; const METRIC_LABEL: Record = { all: '病例', outpatient: '门诊', inpatient: '住院', }; function computeChangeRatio(trend: CaseTrendPoint[]): number | null { if (trend.length < 8) return null; const recent7 = trend.slice(-7).reduce((s, p) => s + p.total, 0); const prior7 = trend.slice(-14, -7).reduce((s, p) => s + p.total, 0); if (prior7 === 0) return null; return ((recent7 - prior7) / prior7) * 100; } export function OverviewDashboard() { const [kpi, setKpi] = useState(null); const [mergedTrend, setMergedTrend] = useState([]); const [districts, setDistricts] = useState([]); const [topDiagnoses, setTopDiagnoses] = useState([]); const [alertPie, setAlertPie] = useState([]); const [isLoading, setIsLoading] = useState(true); const [errors, setErrors] = useState([]); // 门诊/住院/全部 — 同时驱动 choropleth 与 Top5 区县条形图。 const [metric, setMetric] = useState('all'); useEffect(() => { let cancelled = false; const fetchAll = async () => { setIsLoading(true); setErrors([]); const now = new Date(); const endStr = now.toISOString().split('T')[0]; const start14 = new Date(now); start14.setDate(start14.getDate() - 14); const start14Str = start14.toISOString().split('T')[0]; const start30 = new Date(now); start30.setDate(start30.getDate() - 30); const start30Str = start30.toISOString().split('T')[0]; // KPI sources — Promise.allSettled to survive individual failures. const [statsR, trend14R, alertsR, riskStatsR, pollutantsR] = await Promise.allSettled([ caseApi.getStats(), caseApi.getTrend({ start_date: start14Str, end_date: endStr, group_by: 'day' }), alertApi.getAlerts(), riskApi.getStats(), envApi.getPollutants(7), ]); // Trend + district sources. const [trend30R, districtsR, diagStatsR] = await Promise.allSettled([ caseApi.getTrend({ start_date: start30Str, end_date: endStr, group_by: 'day' }), caseApi.getDistricts(), caseApi.getStats(), // reuse for top_diagnoses ]); if (cancelled) return; const newErrors: string[] = []; // --- KPI --- let totalCases = 0; if (statsR.status === 'fulfilled') { const s = statsR.value; totalCases = (s.total_outpatient || 0) + (s.total_inpatient || 0); } else { newErrors.push('累计病例数据加载失败'); } let todayCases = 0; let changeRatio: number | null = null; if (trend14R.status === 'fulfilled') { const trend = trend14R.value.trend || []; if (trend.length > 0) todayCases = trend[trend.length - 1].total; changeRatio = computeChangeRatio(trend); } else { newErrors.push('今日病例数据加载失败'); } let activeAlerts = 0; let alertList: Alert[] = []; if (alertsR.status === 'fulfilled') { alertList = alertsR.value.alerts || []; activeAlerts = alertList.length; } else { newErrors.push('预警数据加载失败'); } let highRiskGrids = 0; if (riskStatsR.status === 'fulfilled') { highRiskGrids = riskStatsR.value.high_risk_count || 0; } else { newErrors.push('风险网格数据加载失败'); } let avgAQI = 0; let pollutantData: PollutantPoint[] = []; if (pollutantsR.status === 'fulfilled') { pollutantData = pollutantsR.value.data || []; if (pollutantData.length > 0) { const sumAQI = pollutantData.reduce((s, p) => s + (p.AQI || 0), 0); avgAQI = Math.round(sumAQI / pollutantData.length); } } else { newErrors.push('AQI数据加载失败'); } setKpi({ totalCases, todayCases, changeRatio, activeAlerts, highRiskGrids, avgAQI }); // --- Merge case trend + AQI --- if (trend30R.status === 'fulfilled') { const trend30 = trend30R.value.trend || []; const aqiMap: Record = {}; for (const p of pollutantData) aqiMap[p.date] = p.AQI || 0; setMergedTrend( trend30.map((t) => ({ date: t.date, cases: t.total, aqi: aqiMap[t.date] || 0 })) ); } else if (!newErrors.includes('今日病例数据加载失败')) { newErrors.push('趋势数据加载失败'); } // --- Districts (feeds choropleth + Top5 via normalize/join) --- if (districtsR.status === 'fulfilled') { setDistricts(districtsR.value.districts || []); } else { newErrors.push('区县数据加载失败'); } // --- Top 5 Diagnoses --- if (diagStatsR.status === 'fulfilled') { const topDiag = diagStatsR.value.top_diagnoses || []; setTopDiagnoses( topDiag.slice(0, 5).map((d) => ({ diagnosis: d.diagnosis, outpatient: d.outpatient, inpatient: d.inpatient, total: d.outpatient + d.inpatient, })) ); } // --- Alert severity donut --- const p1 = alertList.filter((a) => a.priority === 'P1').length; const p2 = alertList.filter((a) => a.priority === 'P2').length; setAlertPie([ { name: 'P1 紧急', value: p1, color: CHART_COLORS.alertP1 }, { name: 'P2 关注', value: p2, color: CHART_COLORS.alertP2 }, ]); setErrors(newErrors); setIsLoading(false); }; fetchAll(); return () => { cancelled = true; }; }, []); // 归一并聚合到 13 区一次,供 choropleth 与 Top5 共享。 const joinedDistricts = useMemo(() => joinDistrictCases(districts), [districts]); const metricLookup = useMemo( () => buildMetricLookup(joinedDistricts, metric), [joinedDistricts, metric] ); if (isLoading) { return ; } return (
{errors.length > 0 && (
window.location.reload()} onDismiss={() => setErrors([])} />
)}
{/* Page header + honesty badge + metric toggle */}

综合概览 数据截至2023-12

病例、环境与预警关键指标总览

{/* KPI Row */} {/* Headline: Wuhan 13-district choropleth */}
武汉市13区{METRIC_LABEL[metric]}分布(高风险高亮)
{/* Case + AQI trend */} {/* Top districts (metric-driven) + Top diagnoses */}
{/* Alert severity donut */}
); }