fix: distinguish mileage statistics failures
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user