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.getOverview();
|
||
|
|
set({ insights: data, isLoading: false });
|
||
|
|
} catch (e) {
|
||
|
|
if (isCancelError(e)) return;
|
||
|
|
set({ error: (e as Error).message || '加载洞察数据失败', isLoading: false });
|
||
|
|
}
|
||
|
|
},
|
||
|
|
}));
|