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
2026-06-08 18:40:08 +08:00
|
|
|
|
import { useEffect, useState, useCallback } from 'react';
|
2026-06-21 20:12:13 +08:00
|
|
|
|
import { LoadingState } from '@/components/ui';
|
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
2026-06-08 18:40:08 +08:00
|
|
|
|
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
|
|
|
|
|
import { FileText, Download, Activity, TrendingUp, TrendingDown, AlertTriangle, ChevronLeft, RefreshCw } from 'lucide-react';
|
|
|
|
|
|
import { useReportsStore } from '@/stores/reportsStore';
|
|
|
|
|
|
import { useDiseaseStore } from '@/stores/diseaseStore';
|
|
|
|
|
|
import { ErrorBanner } from '@/components/ErrorBanner';
|
|
|
|
|
|
import { DiseaseFilter } from '@/components/DiseaseFilter';
|
|
|
|
|
|
import type { ReportResponse } from '@/types';
|
|
|
|
|
|
|
|
|
|
|
|
const TYPE_LABELS: Record<string, string> = {
|
|
|
|
|
|
daily: '日报', weekly: '周报', monthly: '月报', custom: '自定义',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const TYPE_COLORS: Record<string, string> = {
|
|
|
|
|
|
daily: 'bg-blue-100 text-blue-700', weekly: 'bg-purple-100 text-purple-700',
|
|
|
|
|
|
monthly: 'bg-green-100 text-green-700', custom: 'bg-gray-100 text-gray-700',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const PRIORITY_COLORS: Record<string, string> = {
|
|
|
|
|
|
high: 'bg-red-100 text-red-700 border-red-300',
|
|
|
|
|
|
medium: 'bg-yellow-100 text-yellow-700 border-yellow-300',
|
|
|
|
|
|
low: 'bg-gray-100 text-gray-600 border-gray-300',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function downloadCSV(report: ReportResponse) {
|
|
|
|
|
|
const BOM = '';
|
|
|
|
|
|
const headers = ['报告ID', '标题', '类型', '报告日期', '周期开始', '周期结束', '总病例数', '平均风险',
|
|
|
|
|
|
'峰值风险日期', '峰值风险值', '高风险区域数', '趋势方向', '诊断名称', '门诊病例', '住院病例', '诊断总病例'];
|
|
|
|
|
|
|
|
|
|
|
|
const { metadata, summary } = report;
|
|
|
|
|
|
const breakdowns = report.diagnosis_breakdown || [];
|
|
|
|
|
|
const typeLabel = TYPE_LABELS[metadata.type] || metadata.type;
|
|
|
|
|
|
|
|
|
|
|
|
let csv = BOM + headers.join(',') + '\n';
|
|
|
|
|
|
|
|
|
|
|
|
if (breakdowns.length === 0) {
|
|
|
|
|
|
csv += [
|
|
|
|
|
|
metadata.report_id, `"${metadata.title}"`, typeLabel, metadata.generated_at,
|
|
|
|
|
|
metadata.period_start, metadata.period_end, summary.total_cases, summary.avg_risk,
|
|
|
|
|
|
summary.peak_risk_date, summary.peak_risk_value, summary.high_risk_areas, summary.trend_direction,
|
|
|
|
|
|
'', '', '', ''
|
|
|
|
|
|
].join(',');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
breakdowns.forEach((d) => {
|
|
|
|
|
|
csv += [
|
|
|
|
|
|
metadata.report_id, `"${metadata.title}"`, typeLabel, metadata.generated_at,
|
|
|
|
|
|
metadata.period_start, metadata.period_end, summary.total_cases, summary.avg_risk,
|
|
|
|
|
|
summary.peak_risk_date, summary.peak_risk_value, summary.high_risk_areas, summary.trend_direction,
|
|
|
|
|
|
`"${d.diagnosis}"`, d.outpatient, d.inpatient, d.total
|
|
|
|
|
|
].join(',') + '\n';
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
|
const link = document.createElement('a');
|
|
|
|
|
|
link.href = url;
|
|
|
|
|
|
link.download = `${report.metadata.report_id}.csv`;
|
|
|
|
|
|
link.click();
|
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function ReportList({ onSelect }: { onSelect: (id: string) => void }) {
|
|
|
|
|
|
const reports = useReportsStore((s) => s.reports);
|
|
|
|
|
|
const isLoading = useReportsStore((s) => s.isLoading);
|
|
|
|
|
|
const error = useReportsStore((s) => s.error);
|
|
|
|
|
|
const clearError = useReportsStore((s) => s.clearError);
|
|
|
|
|
|
const fetchReportsList = useReportsStore((s) => s.fetchReportsList);
|
|
|
|
|
|
const [filter, setFilter] = useState<string>('all');
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { fetchReportsList(filter === 'all' ? undefined : filter); }, [filter, fetchReportsList]);
|
|
|
|
|
|
|
|
|
|
|
|
const filters = [
|
|
|
|
|
|
{ key: 'all', label: '全部' },
|
|
|
|
|
|
{ key: 'daily', label: '日报' },
|
|
|
|
|
|
{ key: 'weekly', label: '周报' },
|
|
|
|
|
|
{ key: 'monthly', label: '月报' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
if (error) return <ErrorBanner error={error} onRetry={() => { clearError(); fetchReportsList(filter === 'all' ? undefined : filter); }} onDismiss={clearError} />;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="flex items-center gap-2 mb-4">
|
|
|
|
|
|
{filters.map((f) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={f.key}
|
|
|
|
|
|
onClick={() => setFilter(f.key)}
|
|
|
|
|
|
className={`px-3 py-1 text-[12px] rounded transition-colors ${
|
|
|
|
|
|
filter === f.key ? 'bg-primary text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>{f.label}</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{isLoading ? (
|
2026-06-21 20:12:13 +08:00
|
|
|
|
<LoadingState />
|
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
2026-06-08 18:40:08 +08:00
|
|
|
|
) : reports.length === 0 ? (
|
|
|
|
|
|
<div className="text-center py-12">
|
|
|
|
|
|
<FileText className="w-12 h-12 text-gray-300 mx-auto mb-3" />
|
|
|
|
|
|
<p className="text-gray-500 text-sm">暂无报告</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
feat: Phase 4 — responsive analysis pages + perf harness + god-component splits
Final phase of the UX modernization. Four conflict-free lanes.
Responsive (D4 — desktop+mobile 并重):
- 7 analysis pages made usable at 375px: grid-cols-4/5 → grid-cols-2 sm:*
responsive variants; raw tables wrapped in overflow-x-auto; page overflow guards
- new e2e/responsive.spec.ts loops all 7 analysis routes at 375px asserting no
horizontal scroll
Perf harness:
- playwright.config.ts gains an isolated `perf` project (testMatch /perf/), default
chromium project excludes it (testIgnore)
- new e2e/perf.spec.ts: CDP Network.emulateNetworkConditions (Fast 3G) +
PerformanceObserver LCP on /overview kpi-row + route-transition timing; numbers
reported as a relative regression signal (dev-server, not a prod SLA), not gated
God-component splits (pure refactors, behavior-preserving):
- MonitoringDashboard 686 → 239 lines: extracted components/monitoring/* (StatsBar,
OverviewTab, CaseStatsTab, DistrictStatsTab) + useMonitoringData hook; URL-granularity
source-of-truth + drilldown reconcile kept in the orchestrator (no desync regression)
- AlertsDashboard 816 → 301 lines: extracted components/alerts/* (Toolbar, List,
RiskPanel, MapPanel, DetailModal, …); role/privacy/grid-hide logic kept in the
orchestrator — doctor-view privacy invariant (zero patient-point) still holds
Gates: tsc 0 · vitest 75 · functional e2e 37/37 (incl doctor-view privacy +
granularity + responsive) · build ok · perf project runs + reports
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 21:00:15 +08:00
|
|
|
|
<div className="overflow-x-auto">
|
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
2026-06-08 18:40:08 +08:00
|
|
|
|
<table className="w-full text-sm">
|
|
|
|
|
|
<thead className="bg-gray-50 border-b border-gray-200">
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th className="text-left px-4 py-3 text-xs font-medium text-gray-500 uppercase">报告ID</th>
|
|
|
|
|
|
<th className="text-left px-4 py-3 text-xs font-medium text-gray-500 uppercase">标题</th>
|
|
|
|
|
|
<th className="text-left px-4 py-3 text-xs font-medium text-gray-500 uppercase">类型</th>
|
|
|
|
|
|
<th className="text-left px-4 py-3 text-xs font-medium text-gray-500 uppercase">报告周期</th>
|
|
|
|
|
|
<th className="text-left px-4 py-3 text-xs font-medium text-gray-500 uppercase">生成时间</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
{reports.map((r) => (
|
|
|
|
|
|
<tr
|
|
|
|
|
|
key={r.report_id}
|
|
|
|
|
|
onClick={() => onSelect(r.report_id)}
|
|
|
|
|
|
className="border-b border-gray-100 hover:bg-blue-50 cursor-pointer transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
<td className="px-4 py-3 font-mono text-xs text-gray-900">{r.report_id}</td>
|
|
|
|
|
|
<td className="px-4 py-3 text-gray-900">{r.title}</td>
|
|
|
|
|
|
<td className="px-4 py-3">
|
|
|
|
|
|
<span className={`px-2 py-0.5 rounded text-[11px] font-medium ${TYPE_COLORS[r.type] || 'bg-gray-100 text-gray-600'}`}>
|
|
|
|
|
|
{TYPE_LABELS[r.type] || r.type}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td className="px-4 py-3 text-gray-500 text-xs">
|
|
|
|
|
|
{r.period_start} ~ {r.period_end}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td className="px-4 py-3 text-gray-500 text-xs">{r.generated_at?.slice(0, 10)}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
feat: Phase 4 — responsive analysis pages + perf harness + god-component splits
Final phase of the UX modernization. Four conflict-free lanes.
Responsive (D4 — desktop+mobile 并重):
- 7 analysis pages made usable at 375px: grid-cols-4/5 → grid-cols-2 sm:*
responsive variants; raw tables wrapped in overflow-x-auto; page overflow guards
- new e2e/responsive.spec.ts loops all 7 analysis routes at 375px asserting no
horizontal scroll
Perf harness:
- playwright.config.ts gains an isolated `perf` project (testMatch /perf/), default
chromium project excludes it (testIgnore)
- new e2e/perf.spec.ts: CDP Network.emulateNetworkConditions (Fast 3G) +
PerformanceObserver LCP on /overview kpi-row + route-transition timing; numbers
reported as a relative regression signal (dev-server, not a prod SLA), not gated
God-component splits (pure refactors, behavior-preserving):
- MonitoringDashboard 686 → 239 lines: extracted components/monitoring/* (StatsBar,
OverviewTab, CaseStatsTab, DistrictStatsTab) + useMonitoringData hook; URL-granularity
source-of-truth + drilldown reconcile kept in the orchestrator (no desync regression)
- AlertsDashboard 816 → 301 lines: extracted components/alerts/* (Toolbar, List,
RiskPanel, MapPanel, DetailModal, …); role/privacy/grid-hide logic kept in the
orchestrator — doctor-view privacy invariant (zero patient-point) still holds
Gates: tsc 0 · vitest 75 · functional e2e 37/37 (incl doctor-view privacy +
granularity + responsive) · build ok · perf project runs + reports
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 21:00:15 +08:00
|
|
|
|
</div>
|
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
2026-06-08 18:40:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function ReportDetail({ reportId, onBack }: { reportId: string; onBack: () => void }) {
|
|
|
|
|
|
const currentReport = useReportsStore((s) => s.currentReport);
|
|
|
|
|
|
const isLoading = useReportsStore((s) => s.isLoading);
|
|
|
|
|
|
const error = useReportsStore((s) => s.error);
|
|
|
|
|
|
const clearError = useReportsStore((s) => s.clearError);
|
|
|
|
|
|
const fetchReport = useReportsStore((s) => s.fetchReport);
|
|
|
|
|
|
const { selectedDiagnoses } = useDiseaseStore();
|
|
|
|
|
|
|
feat: add analysis pages and raster risk map
Ship a new app version with broader analytics, restructured
dashboards, and a server-rendered risk map.
Frontend:
- Add Overview, Demographic, Disease, and Environmental Health
analysis pages
- Add AnomalyMarkers, CalendarHeatmap, and MetricHeatmapTable
components
- Rebuild Alerts map onto server-rendered raster risk tiles;
expand Monitoring, Trend, and District Comparison views
- Extend API client, stores, and TypeScript types
Backend:
- Add environment router (pollutants, lag correlations)
- Add risk_raster util serving XYZ 100m risk tiles
- Expand cases endpoints (demographics, seasonality, diagnoses)
and insights; harden auth and file-based loaders
Data & tooling:
- Add processed outpatient/inpatient/combined case parquet (LFS)
- Add nested CLAUDE.md guides, pyrightconfig, and test updates
2026-06-21 17:35:03 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
// Skip refetch if the report is already loaded (e.g. just generated) to avoid clobbering it.
|
|
|
|
|
|
if (useReportsStore.getState().currentReport?.metadata?.report_id !== reportId) {
|
|
|
|
|
|
fetchReport(reportId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [reportId]);
|
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
2026-06-08 18:40:08 +08:00
|
|
|
|
|
|
|
|
|
|
if (error) return <ErrorBanner error={error} onRetry={() => { clearError(); fetchReport(reportId); }} onDismiss={clearError} />;
|
|
|
|
|
|
if (isLoading || !currentReport) return <div className="text-center py-8 text-sm text-gray-500">加载报告...</div>;
|
|
|
|
|
|
|
|
|
|
|
|
const { metadata, summary, sections, recommendations, diagnosis_breakdown } = currentReport;
|
|
|
|
|
|
|
|
|
|
|
|
const filteredBreakdown = diagnosis_breakdown?.filter(
|
|
|
|
|
|
d => selectedDiagnoses.length === 0 || selectedDiagnoses.includes(d.diagnosis)
|
|
|
|
|
|
) || [];
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<button onClick={onBack} className="flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700 mb-4">
|
|
|
|
|
|
<ChevronLeft className="w-4 h-4" /> 返回列表
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
feat: Phase 4 — responsive analysis pages + perf harness + god-component splits
Final phase of the UX modernization. Four conflict-free lanes.
Responsive (D4 — desktop+mobile 并重):
- 7 analysis pages made usable at 375px: grid-cols-4/5 → grid-cols-2 sm:*
responsive variants; raw tables wrapped in overflow-x-auto; page overflow guards
- new e2e/responsive.spec.ts loops all 7 analysis routes at 375px asserting no
horizontal scroll
Perf harness:
- playwright.config.ts gains an isolated `perf` project (testMatch /perf/), default
chromium project excludes it (testIgnore)
- new e2e/perf.spec.ts: CDP Network.emulateNetworkConditions (Fast 3G) +
PerformanceObserver LCP on /overview kpi-row + route-transition timing; numbers
reported as a relative regression signal (dev-server, not a prod SLA), not gated
God-component splits (pure refactors, behavior-preserving):
- MonitoringDashboard 686 → 239 lines: extracted components/monitoring/* (StatsBar,
OverviewTab, CaseStatsTab, DistrictStatsTab) + useMonitoringData hook; URL-granularity
source-of-truth + drilldown reconcile kept in the orchestrator (no desync regression)
- AlertsDashboard 816 → 301 lines: extracted components/alerts/* (Toolbar, List,
RiskPanel, MapPanel, DetailModal, …); role/privacy/grid-hide logic kept in the
orchestrator — doctor-view privacy invariant (zero patient-point) still holds
Gates: tsc 0 · vitest 75 · functional e2e 37/37 (incl doctor-view privacy +
granularity + responsive) · build ok · perf project runs + reports
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 21:00:15 +08:00
|
|
|
|
<div className="flex flex-wrap items-center justify-between gap-3 mb-4">
|
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
2026-06-08 18:40:08 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
<h2 className="text-lg font-semibold text-gray-900">{metadata.title}</h2>
|
|
|
|
|
|
<div className="flex items-center gap-2 mt-1 text-xs text-gray-500">
|
|
|
|
|
|
<span className={`px-2 py-0.5 rounded font-medium ${TYPE_COLORS[metadata.type] || ''}`}>
|
|
|
|
|
|
{TYPE_LABELS[metadata.type] || metadata.type}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span>{metadata.period_start} ~ {metadata.period_end}</span>
|
|
|
|
|
|
<span>生成: {metadata.generated_at?.slice(0, 10)}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
feat: add analysis pages and raster risk map
Ship a new app version with broader analytics, restructured
dashboards, and a server-rendered risk map.
Frontend:
- Add Overview, Demographic, Disease, and Environmental Health
analysis pages
- Add AnomalyMarkers, CalendarHeatmap, and MetricHeatmapTable
components
- Rebuild Alerts map onto server-rendered raster risk tiles;
expand Monitoring, Trend, and District Comparison views
- Extend API client, stores, and TypeScript types
Backend:
- Add environment router (pollutants, lag correlations)
- Add risk_raster util serving XYZ 100m risk tiles
- Expand cases endpoints (demographics, seasonality, diagnoses)
and insights; harden auth and file-based loaders
Data & tooling:
- Add processed outpatient/inpatient/combined case parquet (LFS)
- Add nested CLAUDE.md guides, pyrightconfig, and test updates
2026-06-21 17:35:03 +08:00
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => downloadCSV(currentReport)}
|
|
|
|
|
|
className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-green-600 text-white rounded hover:bg-green-700 transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Download className="w-3.5 h-3.5" /> 导出CSV
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
const json = JSON.stringify(currentReport, null, 2);
|
|
|
|
|
|
const blob = new Blob([json], { type: 'application/json' });
|
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
|
const link = document.createElement('a');
|
|
|
|
|
|
link.href = url;
|
|
|
|
|
|
link.download = `${reportId}.json`;
|
|
|
|
|
|
link.click();
|
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-gray-600 text-white rounded hover:bg-gray-700 transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Download className="w-3.5 h-3.5" /> 导出JSON
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
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
2026-06-08 18:40:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Summary cards */}
|
feat: Phase 4 — responsive analysis pages + perf harness + god-component splits
Final phase of the UX modernization. Four conflict-free lanes.
Responsive (D4 — desktop+mobile 并重):
- 7 analysis pages made usable at 375px: grid-cols-4/5 → grid-cols-2 sm:*
responsive variants; raw tables wrapped in overflow-x-auto; page overflow guards
- new e2e/responsive.spec.ts loops all 7 analysis routes at 375px asserting no
horizontal scroll
Perf harness:
- playwright.config.ts gains an isolated `perf` project (testMatch /perf/), default
chromium project excludes it (testIgnore)
- new e2e/perf.spec.ts: CDP Network.emulateNetworkConditions (Fast 3G) +
PerformanceObserver LCP on /overview kpi-row + route-transition timing; numbers
reported as a relative regression signal (dev-server, not a prod SLA), not gated
God-component splits (pure refactors, behavior-preserving):
- MonitoringDashboard 686 → 239 lines: extracted components/monitoring/* (StatsBar,
OverviewTab, CaseStatsTab, DistrictStatsTab) + useMonitoringData hook; URL-granularity
source-of-truth + drilldown reconcile kept in the orchestrator (no desync regression)
- AlertsDashboard 816 → 301 lines: extracted components/alerts/* (Toolbar, List,
RiskPanel, MapPanel, DetailModal, …); role/privacy/grid-hide logic kept in the
orchestrator — doctor-view privacy invariant (zero patient-point) still holds
Gates: tsc 0 · vitest 75 · functional e2e 37/37 (incl doctor-view privacy +
granularity + responsive) · build ok · perf project runs + reports
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 21:00:15 +08:00
|
|
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 mb-6">
|
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
2026-06-08 18:40:08 +08:00
|
|
|
|
{[
|
|
|
|
|
|
{ label: '总病例数', value: summary.total_cases.toLocaleString(), icon: Activity, color: 'text-blue-600', bg: 'bg-blue-50' },
|
|
|
|
|
|
{ label: '平均风险', value: (summary.avg_risk * 100).toFixed(1) + '%', icon: AlertTriangle, color: 'text-orange-600', bg: 'bg-orange-50' },
|
|
|
|
|
|
{ label: '峰值风险', value: (summary.peak_risk_value * 100).toFixed(1) + '%', icon: TrendingUp, color: 'text-red-600', bg: 'bg-red-50' },
|
|
|
|
|
|
{ label: '高风险区域', value: `${summary.high_risk_areas}个`, icon: TrendingUp, color: 'text-red-600', bg: 'bg-red-50' },
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '趋势', value: summary.trend_direction === 'improving' ? '好转' : summary.trend_direction === 'worsening' ? '恶化' : '平稳',
|
|
|
|
|
|
icon: summary.trend_direction === 'improving' ? TrendingDown : summary.trend_direction === 'worsening' ? TrendingUp : Activity,
|
|
|
|
|
|
color: summary.trend_direction === 'improving' ? 'text-green-600' : summary.trend_direction === 'worsening' ? 'text-red-600' : 'text-gray-600',
|
|
|
|
|
|
bg: summary.trend_direction === 'improving' ? 'bg-green-50' : summary.trend_direction === 'worsening' ? 'bg-red-50' : 'bg-gray-50',
|
|
|
|
|
|
},
|
|
|
|
|
|
].map((stat) => (
|
|
|
|
|
|
<div key={stat.label} className="bg-white rounded-lg border border-gray-200 p-3">
|
|
|
|
|
|
<div className="flex items-center gap-2 mb-1">
|
|
|
|
|
|
<div className={`w-7 h-7 rounded ${stat.bg} flex items-center justify-center`}>
|
|
|
|
|
|
<stat.icon className={`w-3.5 h-3.5 ${stat.color}`} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span className="text-[11px] text-gray-500">{stat.label}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="text-xl font-bold text-gray-900">{stat.value}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Sections and charts in 2-column layout */}
|
feat: Phase 4 — responsive analysis pages + perf harness + god-component splits
Final phase of the UX modernization. Four conflict-free lanes.
Responsive (D4 — desktop+mobile 并重):
- 7 analysis pages made usable at 375px: grid-cols-4/5 → grid-cols-2 sm:*
responsive variants; raw tables wrapped in overflow-x-auto; page overflow guards
- new e2e/responsive.spec.ts loops all 7 analysis routes at 375px asserting no
horizontal scroll
Perf harness:
- playwright.config.ts gains an isolated `perf` project (testMatch /perf/), default
chromium project excludes it (testIgnore)
- new e2e/perf.spec.ts: CDP Network.emulateNetworkConditions (Fast 3G) +
PerformanceObserver LCP on /overview kpi-row + route-transition timing; numbers
reported as a relative regression signal (dev-server, not a prod SLA), not gated
God-component splits (pure refactors, behavior-preserving):
- MonitoringDashboard 686 → 239 lines: extracted components/monitoring/* (StatsBar,
OverviewTab, CaseStatsTab, DistrictStatsTab) + useMonitoringData hook; URL-granularity
source-of-truth + drilldown reconcile kept in the orchestrator (no desync regression)
- AlertsDashboard 816 → 301 lines: extracted components/alerts/* (Toolbar, List,
RiskPanel, MapPanel, DetailModal, …); role/privacy/grid-hide logic kept in the
orchestrator — doctor-view privacy invariant (zero patient-point) still holds
Gates: tsc 0 · vitest 75 · functional e2e 37/37 (incl doctor-view privacy +
granularity + responsive) · build ok · perf project runs + reports
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 21:00:15 +08:00
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6">
|
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
2026-06-08 18:40:08 +08:00
|
|
|
|
{sections.map((section, idx) => (
|
|
|
|
|
|
<div key={idx} className="bg-white rounded-lg border border-gray-200 p-4">
|
|
|
|
|
|
<h3 className="text-sm font-semibold text-gray-900 mb-2">{section.title}</h3>
|
|
|
|
|
|
<p className="text-xs text-gray-600 leading-relaxed">{section.content}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Diagnosis breakdown chart */}
|
|
|
|
|
|
{filteredBreakdown.length > 0 ? (
|
|
|
|
|
|
<div className="bg-white rounded-lg border border-gray-200 p-4 mb-6">
|
|
|
|
|
|
<div className="flex items-center justify-between mb-4">
|
|
|
|
|
|
<h3 className="text-sm font-semibold text-gray-900">诊断分类统计</h3>
|
|
|
|
|
|
<DiseaseFilter />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
|
|
|
|
<BarChart data={filteredBreakdown} margin={{ top: 5, right: 20, left: 10, bottom: 5 }}>
|
|
|
|
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
|
|
|
|
|
|
<XAxis dataKey="diagnosis" tick={{ fontSize: 11, fill: '#6b7280' }} />
|
|
|
|
|
|
<YAxis tick={{ fontSize: 11, fill: '#6b7280' }} />
|
|
|
|
|
|
<Tooltip contentStyle={{ fontSize: 12, borderRadius: 8, border: '1px solid #e5e7eb' }} />
|
|
|
|
|
|
<Bar dataKey="outpatient" name="门诊" stackId="a" fill="#3b82f6" radius={[0, 0, 0, 0]} />
|
|
|
|
|
|
<Bar dataKey="inpatient" name="住院" stackId="a" fill="#ef4444" radius={[4, 4, 0, 0]} />
|
|
|
|
|
|
</BarChart>
|
|
|
|
|
|
</ResponsiveContainer>
|
|
|
|
|
|
<div className="flex items-center gap-4 mt-2 pt-2 border-t border-gray-100">
|
|
|
|
|
|
<div className="flex items-center gap-1.5 text-xs text-gray-500"><span className="w-3 h-3 bg-blue-500 rounded-sm" />门诊</div>
|
|
|
|
|
|
<div className="flex items-center gap-1.5 text-xs text-gray-500"><span className="w-3 h-3 bg-red-500 rounded-sm" />住院</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="bg-white rounded-lg border border-gray-200 p-4 mb-6 text-center">
|
|
|
|
|
|
<p className="text-sm text-gray-400">暂无诊断分类数据</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Recommendations */}
|
|
|
|
|
|
{recommendations.length > 0 && (
|
|
|
|
|
|
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
|
|
|
|
|
<h3 className="text-sm font-semibold text-gray-900 mb-3">建议措施</h3>
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
{recommendations.map((rec, idx) => (
|
|
|
|
|
|
<div key={idx} className={`flex items-start gap-3 p-3 rounded border ${PRIORITY_COLORS[rec.priority] || ''}`}>
|
|
|
|
|
|
<span className={`px-1.5 py-0.5 rounded text-[10px] font-bold shrink-0 ${
|
|
|
|
|
|
rec.priority === 'high' ? 'bg-red-500 text-white' :
|
|
|
|
|
|
rec.priority === 'medium' ? 'bg-yellow-500 text-white' : 'bg-gray-400 text-white'
|
|
|
|
|
|
}`}>
|
|
|
|
|
|
{rec.priority === 'high' ? '高' : rec.priority === 'medium' ? '中' : '低'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="text-sm font-medium text-gray-900">{rec.title}</div>
|
|
|
|
|
|
<div className="text-xs text-gray-600 mt-0.5">{rec.description}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function ReportsCenter() {
|
|
|
|
|
|
const [view, setView] = useState<'list' | 'detail'>('list');
|
|
|
|
|
|
const [selectedReportId, setSelectedReportId] = useState<string | null>(null);
|
|
|
|
|
|
const { error, clearError, fetchReportsList, generateReport } = useReportsStore();
|
|
|
|
|
|
const [genType, setGenType] = useState<string>('daily');
|
|
|
|
|
|
const [isGenerating, setIsGenerating] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
const handleGenerate = useCallback(async () => {
|
|
|
|
|
|
setIsGenerating(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
await generateReport(genType);
|
feat: add analysis pages and raster risk map
Ship a new app version with broader analytics, restructured
dashboards, and a server-rendered risk map.
Frontend:
- Add Overview, Demographic, Disease, and Environmental Health
analysis pages
- Add AnomalyMarkers, CalendarHeatmap, and MetricHeatmapTable
components
- Rebuild Alerts map onto server-rendered raster risk tiles;
expand Monitoring, Trend, and District Comparison views
- Extend API client, stores, and TypeScript types
Backend:
- Add environment router (pollutants, lag correlations)
- Add risk_raster util serving XYZ 100m risk tiles
- Expand cases endpoints (demographics, seasonality, diagnoses)
and insights; harden auth and file-based loaders
Data & tooling:
- Add processed outpatient/inpatient/combined case parquet (LFS)
- Add nested CLAUDE.md guides, pyrightconfig, and test updates
2026-06-21 17:35:03 +08:00
|
|
|
|
const generated = useReportsStore.getState().currentReport;
|
|
|
|
|
|
if (generated) {
|
|
|
|
|
|
setSelectedReportId(generated.metadata.report_id);
|
|
|
|
|
|
setView('detail');
|
|
|
|
|
|
}
|
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
2026-06-08 18:40:08 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
setIsGenerating(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [genType, generateReport]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleSelect = useCallback((id: string) => {
|
|
|
|
|
|
setSelectedReportId(id);
|
|
|
|
|
|
setView('detail');
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const handleBack = useCallback(() => {
|
|
|
|
|
|
setView('list');
|
|
|
|
|
|
setSelectedReportId(null);
|
|
|
|
|
|
fetchReportsList();
|
|
|
|
|
|
}, [fetchReportsList]);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
feat: Phase 4 — responsive analysis pages + perf harness + god-component splits
Final phase of the UX modernization. Four conflict-free lanes.
Responsive (D4 — desktop+mobile 并重):
- 7 analysis pages made usable at 375px: grid-cols-4/5 → grid-cols-2 sm:*
responsive variants; raw tables wrapped in overflow-x-auto; page overflow guards
- new e2e/responsive.spec.ts loops all 7 analysis routes at 375px asserting no
horizontal scroll
Perf harness:
- playwright.config.ts gains an isolated `perf` project (testMatch /perf/), default
chromium project excludes it (testIgnore)
- new e2e/perf.spec.ts: CDP Network.emulateNetworkConditions (Fast 3G) +
PerformanceObserver LCP on /overview kpi-row + route-transition timing; numbers
reported as a relative regression signal (dev-server, not a prod SLA), not gated
God-component splits (pure refactors, behavior-preserving):
- MonitoringDashboard 686 → 239 lines: extracted components/monitoring/* (StatsBar,
OverviewTab, CaseStatsTab, DistrictStatsTab) + useMonitoringData hook; URL-granularity
source-of-truth + drilldown reconcile kept in the orchestrator (no desync regression)
- AlertsDashboard 816 → 301 lines: extracted components/alerts/* (Toolbar, List,
RiskPanel, MapPanel, DetailModal, …); role/privacy/grid-hide logic kept in the
orchestrator — doctor-view privacy invariant (zero patient-point) still holds
Gates: tsc 0 · vitest 75 · functional e2e 37/37 (incl doctor-view privacy +
granularity + responsive) · build ok · perf project runs + reports
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 21:00:15 +08:00
|
|
|
|
<div data-testid="page-reports" className="overflow-x-hidden">
|
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
2026-06-08 18:40:08 +08:00
|
|
|
|
{error && view === 'list' && (
|
|
|
|
|
|
<ErrorBanner error={error} onRetry={() => { clearError(); fetchReportsList(); }} onDismiss={clearError} />
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center justify-between mb-5">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h1 className="font-display text-[18px] font-semibold mb-1 flex items-center gap-2">
|
|
|
|
|
|
<FileText className="w-5 h-5 text-primary" />
|
|
|
|
|
|
报表中心
|
|
|
|
|
|
</h1>
|
|
|
|
|
|
<p className="text-[12px] text-text-muted">健康风险评估报告管理与导出</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{view === 'list' && (
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={genType}
|
|
|
|
|
|
onChange={(e) => setGenType(e.target.value)}
|
|
|
|
|
|
className="px-3 py-1.5 border border-gray-300 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="daily">日报</option>
|
|
|
|
|
|
<option value="weekly">周报</option>
|
|
|
|
|
|
<option value="monthly">月报</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={handleGenerate}
|
|
|
|
|
|
disabled={isGenerating}
|
|
|
|
|
|
className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-primary text-white rounded hover:bg-primary-dark transition-colors disabled:opacity-50"
|
|
|
|
|
|
>
|
|
|
|
|
|
<RefreshCw className={`w-3.5 h-3.5 ${isGenerating ? 'animate-spin' : ''}`} />
|
|
|
|
|
|
生成新报告
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{view === 'list' && <ReportList onSelect={handleSelect} />}
|
|
|
|
|
|
{view === 'detail' && selectedReportId && (
|
|
|
|
|
|
<ReportDetail reportId={selectedReportId} onBack={handleBack} />
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|