fix: distinguish mileage statistics failures

This commit is contained in:
kkfluous
2026-08-07 17:58:54 +08:00
parent d1ed3903fe
commit a344497505
2 changed files with 83 additions and 5 deletions
+82 -5
View File
@@ -12,6 +12,7 @@ import {
import type { TargetSummary, TargetVehicle, TargetYearlyAssessment, TrendPoint } from './types';
import { fetchTargets, fetchTargetVehicles, fetchTrend } from './api';
import Blur from '../../components/Blur';
import { EmptyState, ErrorState, LoadingState } from '../../components/ui/surface';
import { weightedCompletionRate } from '../../shared/analytics/metrics';
import { formatMileageSnapshotDateTime } from './data-freshness';
import { assessVehicleCoverage } from './assessment-coverage';
@@ -64,11 +65,21 @@ function shortTargetName(name: string): string {
return `${count}${desc}`;
}
function getRequestErrorMessage(error: unknown): string {
if (error instanceof Error && error.message && error.message !== 'Failed to fetch') {
return error.message;
}
return '无法获取考核目标数据,请检查服务连接后重试。';
}
export default function StatisticsView() {
const [drillContext, setDrillContext] = useState<MileageDrillContext>(() => (
parseMileageDrillContext(window.location.search)
));
const [targets, setTargets] = useState<TargetSummary[]>([]);
const [targetsLoading, setTargetsLoading] = useState(true);
const [targetsError, setTargetsError] = useState<string | null>(null);
const [targetsRequestVersion, setTargetsRequestVersion] = useState(0);
const [trendData, setTrendData] = useState<TrendPoint[]>([]);
const [targetVehiclesMap, setTargetVehiclesMap] = useState<Record<number, TargetVehicle[]>>({});
const [selectedTargetId, setSelectedTargetId] = useState<number | null>(drillContext.targetId || null);
@@ -124,6 +135,10 @@ export default function StatisticsView() {
setDrillContext(next);
}, []);
const retryTargets = useCallback(() => {
setTargetsRequestVersion(version => version + 1);
}, []);
const loadCurrentTargetVehicles = useCallback((targetId: number) => {
if (targetVehicleRequestsRef.current.has(targetId)) return;
targetVehicleRequestsRef.current.add(targetId);
@@ -215,9 +230,13 @@ export default function StatisticsView() {
window.dispatchEvent(new HashChangeEvent('hashchange'));
}, [selectedAssessmentYear, selectedTargetId, viewAllDate, viewAllTargetId]);
// Load targets on mount
// Load targets on mount and through the shared retry path.
useEffect(() => {
let cancelled = false;
setTargetsLoading(true);
setTargetsError(null);
fetchTargets().then(data => {
if (cancelled) return;
const initialContext = initialDrillContextRef.current;
const focused = data.find(item => item.targetName.includes('羚牛136')) || data[0];
const ordered = focused
@@ -242,9 +261,20 @@ export default function StatisticsView() {
setDrillContext(normalizedContext);
}
}
} else {
setSelectedTargetId(null);
setExpandedTargetId(null);
setAssessmentYearMap({});
}
}).catch(() => {});
}, []);
}).catch(error => {
if (!cancelled) setTargetsError(getRequestErrorMessage(error));
}).finally(() => {
if (!cancelled) setTargetsLoading(false);
});
return () => {
cancelled = true;
};
}, [targetsRequestVersion]);
// Load trend when selectedTargetId changes
useEffect(() => {
@@ -307,6 +337,30 @@ export default function StatisticsView() {
))
), [viewAllSearch, viewAllVehicles]);
if (targetsLoading && targets.length === 0) {
return <LoadingState label="正在加载考核统计" />;
}
if (targetsError && targets.length === 0) {
return (
<ErrorState
title="考核统计加载失败"
message={targetsError}
onRetry={retryTargets}
retrying={targetsLoading}
/>
);
}
if (!targetsLoading && targets.length === 0) {
return (
<EmptyState
title="暂无考核目标"
description="当前权限范围内没有有效考核目标"
/>
);
}
return (
<div className="space-y-2 pb-2 landscape:pb-4 landscape:h-full landscape:overflow-hidden landscape:flex landscape:flex-col flex-none landscape:flex-1 [overflow-anchor:none]" style={{ overflowX: 'clip' }}>
{/* Project Selector */}
@@ -326,6 +380,16 @@ export default function StatisticsView() {
))}
</div>
{targetsError && (
<ErrorState
title="考核统计刷新失败"
message={`${targetsError} 当前仍展示上一次成功加载的数据。`}
onRetry={retryTargets}
retrying={targetsLoading}
variant="inline"
/>
)}
{selectedTarget && (
<motion.div
initial={{ opacity: 0, y: 8 }}
@@ -752,10 +816,14 @@ export default function StatisticsView() {
</div>
<div className="flex items-center gap-2">
<button
onClick={() => { fetchTargets().then(data => { setTargets(data); }).catch(() => {}); }}
type="button"
onClick={retryTargets}
disabled={targetsLoading}
title="刷新考核统计"
aria-label="刷新考核统计"
className="p-1.5 text-slate-500 hover:text-blue-400 transition-colors"
>
<RotateCcw size={13} />
<RotateCcw size={13} className={targetsLoading ? 'animate-spin' : ''} />
</button>
<button
onClick={() => setIsTableFullscreen(false)}
@@ -766,6 +834,15 @@ export default function StatisticsView() {
</div>
</div>
{targetsError && (
<div role="alert" className="flex items-center justify-between gap-3 border-b border-rose-900/50 bg-rose-950/60 px-3 py-2 text-[10px] font-bold text-rose-200">
<span>{targetsError} </span>
<button type="button" onClick={retryTargets} disabled={targetsLoading} className="shrink-0 text-rose-100 underline disabled:opacity-60">
</button>
</div>
)}
{/* Table Area */}
<div className="flex-1 overflow-auto">
<table className="w-full text-left border-collapse">