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.
This commit is contained in:
203
frontend/src/services/api.ts
Normal file
203
frontend/src/services/api.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import axios from 'axios';
|
||||
import type {
|
||||
RiskMapResponse,
|
||||
GridDetailResponse,
|
||||
AlertResponse,
|
||||
Stats,
|
||||
ForecastDay,
|
||||
CaseTrendResponse,
|
||||
DistrictCaseResponse,
|
||||
CaseStatsResponse,
|
||||
CaseGridResponse,
|
||||
GeocodedCasesResponse,
|
||||
} from '@/types';
|
||||
|
||||
interface CacheEntry<T> {
|
||||
data: T;
|
||||
timestamp: number;
|
||||
promise?: Promise<T>;
|
||||
}
|
||||
|
||||
const CACHE_TTL = 30000;
|
||||
const cache = new Map<string, CacheEntry<any>>();
|
||||
const pendingControllers = new Map<string, AbortController>();
|
||||
|
||||
function getCacheKey(url: string, params?: Record<string, any>): string {
|
||||
if (!params) return url;
|
||||
const sorted = Object.entries(params)
|
||||
.filter(([, v]) => v !== undefined)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join('&');
|
||||
return sorted ? `${url}?${sorted}` : url;
|
||||
}
|
||||
|
||||
function getCached<T>(key: string): T | undefined {
|
||||
const entry = cache.get(key);
|
||||
if (!entry) return undefined;
|
||||
if (Date.now() - entry.timestamp > CACHE_TTL) {
|
||||
cache.delete(key);
|
||||
return undefined;
|
||||
}
|
||||
return entry.data;
|
||||
}
|
||||
|
||||
function setCache<T>(key: string, data: T): void {
|
||||
cache.set(key, { data, timestamp: Date.now() });
|
||||
}
|
||||
|
||||
function clearPending(key: string): void {
|
||||
const controller = pendingControllers.get(key);
|
||||
if (controller) {
|
||||
controller.abort();
|
||||
pendingControllers.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_URL || '/api',
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('cbpoa_token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
const key = getCacheKey(config.url || '', config.params);
|
||||
const controller = new AbortController();
|
||||
config.signal = controller.signal;
|
||||
clearPending(key);
|
||||
pendingControllers.set(key, controller);
|
||||
return config;
|
||||
});
|
||||
|
||||
api.interceptors.response.use(
|
||||
(response) => {
|
||||
const key = getCacheKey(response.config.url || '', response.config.params);
|
||||
pendingControllers.delete(key);
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
if (error.config) {
|
||||
const key = getCacheKey(error.config.url || '', error.config.params);
|
||||
pendingControllers.delete(key);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
async function cachedGet<T>(url: string, params?: Record<string, any>): Promise<T> {
|
||||
const key = getCacheKey(url, params);
|
||||
const cached = getCached<T>(key);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const entry = cache.get(key);
|
||||
if (entry?.promise) return entry.promise;
|
||||
|
||||
const promise = api.get<T>(url, { params }).then((res) => {
|
||||
setCache(key, res.data);
|
||||
const updated = cache.get(key);
|
||||
if (updated) updated.promise = undefined;
|
||||
return res.data;
|
||||
});
|
||||
|
||||
cache.set(key, { data: undefined as T, timestamp: Date.now(), promise });
|
||||
return promise;
|
||||
}
|
||||
|
||||
export const riskApi = {
|
||||
getCurrentRiskMap: (): Promise<RiskMapResponse> => cachedGet('/risk/current'),
|
||||
|
||||
getForecast: (days: ForecastDay): Promise<RiskMapResponse> => {
|
||||
const d = days === 0 ? '' : `/${days}`;
|
||||
return cachedGet(`/risk/forecast${d}`);
|
||||
},
|
||||
|
||||
getGridDetail: (gridId: string): Promise<GridDetailResponse> =>
|
||||
cachedGet(`/risk/grid/${encodeURIComponent(gridId)}`),
|
||||
|
||||
getStats: (): Promise<Stats> => cachedGet('/risk/stats'),
|
||||
};
|
||||
|
||||
export const alertApi = {
|
||||
getAlerts: (params?: {
|
||||
min_risk?: number;
|
||||
priority?: string;
|
||||
region?: string;
|
||||
}): Promise<AlertResponse> => cachedGet('/alerts', params),
|
||||
|
||||
getAlertRules: (): Promise<any> => cachedGet('/alerts/rules'),
|
||||
};
|
||||
|
||||
export const historyApi = {
|
||||
getHistory: (params: {
|
||||
grid_id?: string;
|
||||
region?: string;
|
||||
days?: number;
|
||||
}): Promise<any> => cachedGet('/history', params),
|
||||
|
||||
getTrend: (gridId: string, days: number = 7): Promise<any> =>
|
||||
cachedGet('/history/trend', { grid_id: gridId, days }),
|
||||
};
|
||||
|
||||
export const caseApi = {
|
||||
getTrend: (days: number = 7): Promise<CaseTrendResponse> =>
|
||||
cachedGet('/cases/trend', { days }),
|
||||
|
||||
getDistricts: (): Promise<DistrictCaseResponse> => cachedGet('/cases/districts'),
|
||||
|
||||
getStats: (): Promise<CaseStatsResponse> => cachedGet('/cases/stats'),
|
||||
|
||||
getGrid: (): Promise<CaseGridResponse> => cachedGet('/cases/grid'),
|
||||
|
||||
getGeocoded: (limit: number = 5000): Promise<GeocodedCasesResponse> =>
|
||||
cachedGet('/cases/geocoded', { limit }),
|
||||
};
|
||||
|
||||
export function clearApiCache(): void {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
export function cancelPendingRequests(): void {
|
||||
pendingControllers.forEach((controller) => controller.abort());
|
||||
pendingControllers.clear();
|
||||
}
|
||||
|
||||
export const gridApi = {
|
||||
getHistoricalAggregated: (
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
aggregation: 'daily' | 'weekly' | 'monthly' = 'daily',
|
||||
district?: string
|
||||
): Promise<any> => {
|
||||
const params: Record<string, string> = { start_date: startDate, end_date: endDate, aggregation };
|
||||
if (district) params.district = district;
|
||||
return cachedGet('/history/aggregated', params);
|
||||
},
|
||||
|
||||
getGridsGeoJSON: (date: string, district?: string): Promise<any> => {
|
||||
const params: Record<string, string> = { date };
|
||||
if (district) params.district = district;
|
||||
return cachedGet('/grids/geojson', params);
|
||||
},
|
||||
|
||||
getMultiDayPrediction: async (date: string, days: number = 7, district?: string): Promise<any> => {
|
||||
const response = await api.post('/predict/multi-day', { date, days, district });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getGridHistory: (gridId: string, days: number = 30): Promise<any> =>
|
||||
cachedGet(`/grids/${encodeURIComponent(gridId)}/history`, { days }),
|
||||
};
|
||||
|
||||
export const analysisApi = {
|
||||
getTrend: (days: number = 7): Promise<any> => cachedGet('/analysis/trend', { days }),
|
||||
getDistricts: (): Promise<any> => cachedGet('/analysis/districts'),
|
||||
};
|
||||
|
||||
export const insightsApi = {
|
||||
getOverview: (): Promise<any> => cachedGet('/insights/overview'),
|
||||
};
|
||||
|
||||
export default api;
|
||||
Reference in New Issue
Block a user