import { useState, useMemo, useEffect, useCallback } from 'react'; import { useRiskStore } from '@/stores'; import { analysisApi } from '@/services/api'; import type { ExtendedAlert, RiskStats } from './types'; interface UseAlertsDataParams { selectedHorizon: number | 'all'; selectedPriority: 'all' | 'P1' | 'P2'; sortBy: 'risk' | 'time'; debouncedRiskRange: [number, number]; activeTab: 'list' | 'stats'; } interface TrendPoint { date: string; cases: number; risk: number } // 预警仪表盘的数据层:派生 extendedAlerts/filteredAlerts/riskStats、按需拉取风险趋势、 // 以及 CSV/JSON 导出辅助。角色/隐私计算保留在 orchestrator,不在此处。 export function useAlertsData({ selectedHorizon, selectedPriority, sortBy, debouncedRiskRange, activeTab, }: UseAlertsDataParams) { const alerts = useRiskStore((s) => s.alerts); // Risk-trend data for the 风险统计 tab, fetched on demand const [trendData, setTrendData] = useState([]); const [trendLoading, setTrendLoading] = useState(false); const [trendError, setTrendError] = useState(null); const [trendLoaded, setTrendLoaded] = useState(false); // Fetch real risk-trend data when the 风险统计 tab is first opened useEffect(() => { if (activeTab !== 'stats' || trendLoaded) return; let cancelled = false; setTrendLoading(true); setTrendError(null); analysisApi .getTrend(14) .then((res: { dates?: string[]; values?: number[] }) => { if (cancelled) return; const dates = res?.dates ?? []; const values = res?.values ?? []; setTrendData(dates.map((date, i) => ({ date, cases: 0, risk: values[i] ?? 0 }))); setTrendLoaded(true); }) .catch((err: unknown) => { if (cancelled) return; setTrendError(err instanceof Error ? err.message : '加载风险趋势失败'); }) .finally(() => { if (!cancelled) setTrendLoading(false); }); return () => { cancelled = true; }; }, [activeTab, trendLoaded]); const extendedAlerts: ExtendedAlert[] = useMemo(() => { const now = Date.now(); return (alerts || []).map((alert) => { const forecastDate = new Date(alert.forecast_time); const diffDays = Math.ceil((forecastDate.getTime() - now) / (1000 * 60 * 60 * 24)); const horizon = diffDays <= 1 ? 1 : diffDays <= 3 ? 3 : 7; return { ...alert, latitude: alert.latitude || 0, longitude: alert.longitude || 0, forecast_horizon: horizon, }; }); }, [alerts]); const filteredAlerts = useMemo(() => { return extendedAlerts .filter((alert) => { const horizonMatch = selectedHorizon === 'all' || alert.forecast_horizon === selectedHorizon; const priorityMatch = selectedPriority === 'all' || alert.priority === selectedPriority; const riskMatch = alert.risk_value >= debouncedRiskRange[0] && alert.risk_value <= debouncedRiskRange[1]; return horizonMatch && priorityMatch && riskMatch; }) .sort((a, b) => { if (sortBy === 'risk') { return b.risk_value - a.risk_value; } return new Date(b.forecast_time).getTime() - new Date(a.forecast_time).getTime(); }); }, [extendedAlerts, selectedHorizon, selectedPriority, sortBy, debouncedRiskRange]); // Risk distribution stats (includes p1/p2 counts) — single pass over each array const riskStats: RiskStats = useMemo(() => { // p1/p2 reflect the full (unfiltered) alert set let p1 = 0; let p2 = 0; for (const a of extendedAlerts) { if (a.priority === 'P1') p1++; else if (a.priority === 'P2') p2++; } // Single pass over filteredAlerts: counters + sum + district map let high = 0; let mediumHigh = 0; let medium = 0; let sum = 0; const byDistrict: Record = {}; for (const a of filteredAlerts) { const v = a.risk_value; if (v >= 0.8) high++; else if (v >= 0.6) mediumHigh++; else if (v >= 0.4) medium++; sum += v; const d = a.region || '未知'; byDistrict[d] = (byDistrict[d] || 0) + 1; } const avgRisk = filteredAlerts.length > 0 ? sum / filteredAlerts.length : 0; const topDistricts = Object.entries(byDistrict) .sort((a, b) => b[1] - a[1]) .slice(0, 5); return { p1, p2, high, mediumHigh, medium, avgRisk, topDistricts }; }, [extendedAlerts, filteredAlerts]); // Export utilities const exportToCsv = useCallback(() => { const headers = ['alert_id', 'grid_id', 'region', 'street', 'latitude', 'longitude', 'risk_value', 'priority', 'forecast_horizon', 'reason', 'timestamp']; const rows = filteredAlerts.map(a => [ a.alert_id, a.grid_id, a.region, a.street, a.latitude, a.longitude, a.risk_value, a.priority, a.forecast_horizon, `"${a.reason}"`, a.timestamp, ]); const csv = [headers.join(','), ...rows.map(r => r.join(','))].join('\n'); const blob = new Blob(['' + csv], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `alerts_${new Date().toISOString().split('T')[0]}.csv`; a.click(); URL.revokeObjectURL(url); }, [filteredAlerts]); const exportToJson = useCallback(() => { const json = JSON.stringify(filteredAlerts, null, 2); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `alerts_${new Date().toISOString().split('T')[0]}.json`; a.click(); URL.revokeObjectURL(url); }, [filteredAlerts]); return { extendedAlerts, filteredAlerts, riskStats, trendData, trendLoading, trendError, exportToCsv, exportToJson, }; }