Add /api/insights/cards endpoint with proper card format matching frontend expectations. Fixes "Cannot read properties of undefined (reading 'map')" crash. Switch frontend to use new endpoint. Replace alert marker rectangles with circleMarkers so they don't look like a second grid. Default showAlertMarkers to false. Add || [] guards on data.features.map() and alerts.map(). Reduce RiskMap grid count 3000→1500, debounce 150ms→300ms.
118 lines
3.1 KiB
TypeScript
118 lines
3.1 KiB
TypeScript
import { create } from 'zustand';
|
|
import axios from 'axios';
|
|
import { analysisApi, insightsApi } from '@/services/api';
|
|
|
|
function isCancelError(e: unknown): boolean {
|
|
return axios.isCancel(e) || (e as Error)?.message === 'canceled';
|
|
}
|
|
|
|
interface TrendDataPoint {
|
|
date: string;
|
|
aqi: number;
|
|
pm25: number;
|
|
pm10: number;
|
|
so2: number;
|
|
no2: number;
|
|
co: number;
|
|
o3: number;
|
|
}
|
|
|
|
interface DistrictData {
|
|
district: string;
|
|
avg_aqi: number;
|
|
avg_risk: number;
|
|
high_risk_count: number;
|
|
population: number;
|
|
}
|
|
|
|
interface InsightCard {
|
|
id: string;
|
|
title: string;
|
|
description: string;
|
|
type: 'warning' | 'info' | 'success' | 'danger';
|
|
metric?: string;
|
|
metricValue?: string;
|
|
timestamp: string;
|
|
}
|
|
|
|
interface InsightsOverview {
|
|
total_insights: number;
|
|
warning_count: number;
|
|
info_count: number;
|
|
success_count: number;
|
|
cards: InsightCard[];
|
|
}
|
|
|
|
interface AnalysisState {
|
|
trendData: TrendDataPoint[];
|
|
districtData: DistrictData[];
|
|
insights: InsightsOverview | null;
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
selectedDays: number;
|
|
setSelectedDays: (days: number) => void;
|
|
fetchTrend: (days?: number) => Promise<void>;
|
|
fetchDistricts: () => Promise<void>;
|
|
fetchInsights: () => Promise<void>;
|
|
clearError: () => void;
|
|
}
|
|
|
|
export const useAnalysisStore = create<AnalysisState>((set, get) => ({
|
|
trendData: [],
|
|
districtData: [],
|
|
insights: null,
|
|
isLoading: false,
|
|
error: null,
|
|
selectedDays: 7,
|
|
|
|
setSelectedDays: (days) => {
|
|
set({ selectedDays: days });
|
|
get().fetchTrend(days);
|
|
},
|
|
|
|
clearError: () => set({ error: null }),
|
|
|
|
fetchTrend: async (days = 7) => {
|
|
set({ isLoading: true, error: null });
|
|
try {
|
|
const data = await analysisApi.getTrend(days);
|
|
const trendData: TrendDataPoint[] = (data.dates || []).map((date: string, i: number) => ({
|
|
date,
|
|
aqi: Math.round((data.values?.[i] || 0.5) * 200),
|
|
pm25: Math.round((data.values?.[i] || 0.5) * 100),
|
|
pm10: Math.round((data.values?.[i] || 0.5) * 150),
|
|
so2: Math.round((data.values?.[i] || 0.5) * 30),
|
|
no2: Math.round((data.values?.[i] || 0.5) * 80),
|
|
co: Math.round((data.values?.[i] || 0.5) * 2 * 100) / 100,
|
|
o3: Math.round((data.values?.[i] || 0.5) * 150),
|
|
}));
|
|
set({ trendData, isLoading: false });
|
|
} catch (e) {
|
|
if (isCancelError(e)) return;
|
|
set({ error: (e as Error).message || '加载趋势数据失败', isLoading: false });
|
|
}
|
|
},
|
|
|
|
fetchDistricts: async () => {
|
|
set({ isLoading: true, error: null });
|
|
try {
|
|
const data = await analysisApi.getDistricts();
|
|
set({ districtData: data.districts || [], isLoading: false });
|
|
} catch (e) {
|
|
if (isCancelError(e)) return;
|
|
set({ error: (e as Error).message || '加载区域数据失败', isLoading: false });
|
|
}
|
|
},
|
|
|
|
fetchInsights: async () => {
|
|
set({ isLoading: true, error: null });
|
|
try {
|
|
const data = await insightsApi.getCards();
|
|
set({ insights: data, isLoading: false });
|
|
} catch (e) {
|
|
if (isCancelError(e)) return;
|
|
set({ error: (e as Error).message || '加载洞察数据失败', isLoading: false });
|
|
}
|
|
},
|
|
}));
|