feat: Initial CBPOA commit — 武汉儿童呼吸疾病风险评估系统
Context: Build a spatial risk assessment system correlating air quality
data with children's respiratory disease incidence across Wuhan.
Approach: FastAPI backend serving PostGIS spatial queries, React
frontend with Deck.gl maps, and a PyTorch SpatialTemporalGCN pipeline
for multi-day (1d/3d/7d) risk prediction.
Changes:
- backend/ — FastAPI API with auth (JWT), alerts, risk analysis,
geocoded case data, grid statistics, and report endpoints
- frontend/ — React dashboard with interactive risk maps, alert
monitoring, district comparison charts, and timeline player
- models/ — SpatialTemporalGCN model with trained weights and ONNX
export for inference
- scripts/ — ETL pipeline for weather + medical data, grid generation,
feature engineering, training, and daily inference
- deploy/ — Docker Compose configs for backend, frontend, and MLflow
- docs/ — API docs, deployment guide, user guide, and code review
Impact: Enables spatial risk visualization, alert monitoring, and
ML-driven health risk forecasting for environmental health teams.
2026-06-05 02:13:49 +08:00
|
|
|
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 {
|
2026-06-05 02:37:03 +08:00
|
|
|
const data = await insightsApi.getCards();
|
feat: Initial CBPOA commit — 武汉儿童呼吸疾病风险评估系统
Context: Build a spatial risk assessment system correlating air quality
data with children's respiratory disease incidence across Wuhan.
Approach: FastAPI backend serving PostGIS spatial queries, React
frontend with Deck.gl maps, and a PyTorch SpatialTemporalGCN pipeline
for multi-day (1d/3d/7d) risk prediction.
Changes:
- backend/ — FastAPI API with auth (JWT), alerts, risk analysis,
geocoded case data, grid statistics, and report endpoints
- frontend/ — React dashboard with interactive risk maps, alert
monitoring, district comparison charts, and timeline player
- models/ — SpatialTemporalGCN model with trained weights and ONNX
export for inference
- scripts/ — ETL pipeline for weather + medical data, grid generation,
feature engineering, training, and daily inference
- deploy/ — Docker Compose configs for backend, frontend, and MLflow
- docs/ — API docs, deployment guide, user guide, and code review
Impact: Enables spatial risk visualization, alert monitoring, and
ML-driven health risk forecasting for environmental health teams.
2026-06-05 02:13:49 +08:00
|
|
|
set({ insights: data, isLoading: false });
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (isCancelError(e)) return;
|
|
|
|
|
set({ error: (e as Error).message || '加载洞察数据失败', isLoading: false });
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
}));
|