feat: add reports center, admin drill-down, disease filter + bug fixes + perf optimization

Frontend features:
- 报表中心 (ReportsCenter): list/detail views, diagnosis breakdown chart, CSV export
- 多级行政下钻 (AdminBreadcrumb): 湖北省→武汉市→区→街道 hierarchical drill-down
- 按病种筛选 (DiseaseFilter): multi-select diagnosis filter on monitoring + reports pages

Backend:
- Add /forecast/{days} endpoint, diagnosis filter params on cases endpoints
- Add /streets aggregation endpoint, enrich reports with real case data
- Extract shared case_loader module

Bug fixes (14):
- Fix missing /risk/forecast route (404), historyApi pointing to non-existent router
- Fix min_risk filter silently ignored in insights/hotspots
- Fix type mismatches: CaseTrendResponse, CaseStatsResponse shapes
- Fix silent .catch(() => {}) swallowing errors, fetchAlerts not clearing stale state
- Fix lru_cache caching exceptions, generateReport used cachedGet for write op
- Fix missing useEffect deps in Insights, DistrictComparison, ReportsCenter

Performance (9):
- Zustand selectors across 9 components (eliminate re-render cascades)
- Fix districtCases.sort() mutating store state, inline IIFE → memo'd component
- CaseLocationMap: React.memo, race protection, correct deps
- AlertCard: stable callbacks, TimelinePlayer: useMemo, TopNav: clock isolation
- SideNav: modules array to module scope, DiseaseFilter: memoized filter
This commit is contained in:
2026-06-08 18:40:08 +08:00
parent 47f4bb4ab2
commit 8ddd8e87bb
30 changed files with 1368 additions and 302 deletions

View File

@@ -1,7 +1,7 @@
"""
Router for geocoded case data and grid aggregated data
"""
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel
from typing import List, Optional
import logging
@@ -53,6 +53,18 @@ class GeocodedResponse(BaseModel):
cases: List[GeocodedCaseData]
total_count: int
class StreetData(BaseModel):
"""Street-level aggregated case data"""
name: str
total_cases: int
outpatient: int
inpatient: int
class StreetsResponse(BaseModel):
streets: List[StreetData]
@router.get("/grid", response_model=GridCaseResponse, summary="Get aggregated grid case data")
async def get_grid_cases():
"""
@@ -176,3 +188,38 @@ async def get_geocoded_count():
except Exception as e:
logger.exception("Error counting geocoded cases")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/streets", response_model=StreetsResponse, summary="Get street-level aggregation")
async def get_streets(district: str = Query(..., description="District name")):
"""Get street-level aggregated case data for a district."""
cases_file = DATA_DIR / "geocoded_all_cases.csv"
if not cases_file.exists():
raise HTTPException(status_code=404, detail="Geocoded data not found")
try:
df = _load_csv(cases_file)
df = df.dropna(subset=['latitude', 'longitude'])
df = df[df['district'] == district]
# Group by street
streets = []
if 'street' in df.columns:
street_groups = df.groupby('street')
for street, group in street_groups:
if pd.isna(street) or str(street).strip() == '':
continue
out_count = len(group[group['case_type'] == 'outpatient'])
in_count = len(group[group['case_type'] == 'inpatient'])
streets.append(StreetData(
name=str(street),
total_cases=len(group),
outpatient=out_count,
inpatient=in_count
))
streets.sort(key=lambda s: s.total_cases, reverse=True)
return StreetsResponse(streets=streets)
except Exception as e:
logger.exception("Error loading street data")
raise HTTPException(status_code=500, detail="Internal server error")