fix: preserve dated vehicle panel snapshots

This commit is contained in:
kkfluous
2026-08-07 18:09:52 +08:00
parent b11187aba7
commit 017001e889
2 changed files with 118 additions and 47 deletions
+1
View File
@@ -174,6 +174,7 @@ SUM(fee) / NULLIF(SUM(kwh), 0)
- 里程考核接口区分目标声明台数与当前有效车辆数;两者不一致时页面明确标注缺少或超配数量,并说明完成率、缺口和归因的实际计算范围。
- 里程考核统计主数据已区分加载、真实空数据、首次请求故障和保留旧数据的刷新故障,并提供统一重试入口,接口失败不再显示为零值或空页面。
- 里程考核归因车辆与 7 天趋势已提供独立加载、故障、空数据和重试状态;子请求失败时停止展示“0 台未达标”和“最新日变化 +0”等伪业务结果。
- 里程考核车辆日期侧栏仅展示带目标和日期标识的成功快照;切换日期失败时保留并标注上一成功日期,首次失败不再回退为当前日数据或伪造“0 辆”。
- 已完成电能数据截至时间和实际趋势月展示。
- 电能日期下钻自动核对日汇总与订单全量合计的日期、车辆范围、电量和费用,并显式展示通过或差异状态。
- 已完成氢能结构化故障状态与重试体验。
+81 -11
View File
@@ -104,7 +104,11 @@ export default function StatisticsView() {
const [viewAllSort, setViewAllSort] = useState<'asc' | 'desc'>('desc');
const [viewAllDate, setViewAllDate] = useState(getDefaultDate);
const [viewAllLoading, setViewAllLoading] = useState(false);
const [viewAllError, setViewAllError] = useState<string | null>(null);
const [viewAllRequestVersion, setViewAllRequestVersion] = useState(0);
const [viewAllHistoricalVehicles, setViewAllHistoricalVehicles] = useState<TargetVehicle[] | null>(null);
const [viewAllLoadedDate, setViewAllLoadedDate] = useState<string | null>(null);
const [viewAllLoadedTargetId, setViewAllLoadedTargetId] = useState<number | null>(null);
const targetVehicleRequestsRef = useRef(new Set<number>());
const initialDrillContextRef = useRef(drillContext);
@@ -148,6 +152,10 @@ export default function StatisticsView() {
setTrendRequestVersion(version => version + 1);
}, []);
const retryViewAllVehicles = useCallback(() => {
setViewAllRequestVersion(version => version + 1);
}, []);
const loadCurrentTargetVehicles = useCallback((targetId: number) => {
if (targetVehicleRequestsRef.current.has(targetId)) return;
targetVehicleRequestsRef.current.add(targetId);
@@ -194,6 +202,9 @@ export default function StatisticsView() {
setViewAllSearch('');
setViewAllDate(getDefaultDate());
setViewAllHistoricalVehicles(null);
setViewAllLoadedDate(null);
setViewAllLoadedTargetId(null);
setViewAllError(null);
commitDrillContext({
level: 'breakdown',
targetId: selectedTargetId,
@@ -211,6 +222,9 @@ export default function StatisticsView() {
setViewAllSearch('');
setViewAllDate(getDefaultDate());
setViewAllHistoricalVehicles(null);
setViewAllLoadedDate(null);
setViewAllLoadedTargetId(null);
setViewAllError(null);
commitDrillContext({
level: 'breakdown',
targetId: target.id,
@@ -332,11 +346,25 @@ export default function StatisticsView() {
// Re-fetch target vehicles when viewAllDate changes
useEffect(() => {
if (viewAllTargetId === null) return;
let cancelled = false;
setViewAllLoading(true);
setViewAllError(null);
fetchTargetVehicles(viewAllTargetId, viewAllDate).then(data => {
if (cancelled) return;
setViewAllHistoricalVehicles(data);
}).catch(() => {}).finally(() => setViewAllLoading(false));
}, [viewAllTargetId, viewAllDate]);
setViewAllLoadedDate(viewAllDate);
setViewAllLoadedTargetId(viewAllTargetId);
}).catch(error => {
if (!cancelled) {
setViewAllError(getRequestErrorMessage(error, '无法获取所选日期的车辆明细,请稍后重试。'));
}
}).finally(() => {
if (!cancelled) setViewAllLoading(false);
});
return () => {
cancelled = true;
};
}, [viewAllTargetId, viewAllDate, viewAllRequestVersion]);
useEffect(() => {
const handlePopState = () => {
@@ -361,9 +389,10 @@ export default function StatisticsView() {
return () => window.removeEventListener('popstate', handlePopState);
}, [targets]);
const hasViewAllSnapshot = viewAllHistoricalVehicles !== null
&& viewAllLoadedTargetId === viewAllTargetId;
const viewAllVehicles = useMemo(() => {
const vehicles = viewAllHistoricalVehicles
|| (viewAllTargetId === null ? [] : targetVehiclesMap[viewAllTargetId] || []);
const vehicles = hasViewAllSnapshot ? viewAllHistoricalVehicles : [];
if (
drillContext.level === 'breakdown'
&& drillContext.dimensionValue
@@ -372,7 +401,7 @@ export default function StatisticsView() {
return filterAttributionVehicles(vehicles, drillContext.dimension, drillContext.dimensionValue);
}
return vehicles;
}, [drillContext, targetVehiclesMap, viewAllHistoricalVehicles, viewAllTargetId]);
}, [drillContext, hasViewAllSnapshot, viewAllHistoricalVehicles]);
const searchedViewAllVehicles = useMemo(() => (
viewAllVehicles.filter(vehicle => (
@@ -996,7 +1025,7 @@ export default function StatisticsView() {
{viewAllTargetName || selectedTarget?.targetName || '考核车辆'}
</h3>
<p className="mt-1 text-xs font-bold uppercase tracking-widest text-slate-400">
{selectedAssessment ? `${selectedAssessment.label} · ` : ''}
{selectedAssessment ? `${selectedAssessment.label} · ` : ''}
</p>
{selectedBreakdownValue && drillContext.dimension !== 'assessment-target' && (
<p className="mt-1 text-[10px] font-bold text-blue-600">{selectedBreakdownValue}</p>
@@ -1010,7 +1039,7 @@ export default function StatisticsView() {
</button>
</div>
<div className="px-6 py-3 border-b border-slate-50 space-y-2">
<div className="px-4 py-3 border-b border-slate-50 space-y-2 md:px-6">
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={14} />
@@ -1028,15 +1057,26 @@ export default function StatisticsView() {
type="date"
value={viewAllDate}
onChange={(e) => setViewAllDate(e.target.value)}
className="pl-8 pr-2 py-2 bg-slate-50 border border-slate-100 rounded-xl text-xs font-bold text-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all w-[130px]"
className="w-[145px] bg-slate-50 py-2 pl-8 pr-2 text-xs font-bold text-slate-700 border border-slate-100 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
/>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">
{viewAllLoading ? '加载中...' : (() => {
const totalKm = searchedViewAllVehicles.reduce((sum, vehicle) => sum + (vehicle.todayMileage || 0), 0);
return `${searchedViewAllVehicles.length} · 合计 ${fmtKm(totalKm)} km`;
{viewAllLoading
? hasViewAllSnapshot
? `正在加载 ${viewAllDate} · 当前显示 ${viewAllLoadedDate}`
: `正在加载 ${viewAllDate}`
: viewAllError
? hasViewAllSnapshot
? `当前显示 ${viewAllLoadedDate} 已成功数据`
: `${viewAllDate} 暂不可用`
: (() => {
const totalKm = searchedViewAllVehicles.reduce(
(sum, vehicle) => sum + (vehicle.todayMileage || 0),
0,
);
return `${viewAllLoadedDate || viewAllDate} · ${searchedViewAllVehicles.length} 辆 · 合计 ${fmtKm(totalKm)} km`;
})()}
</span>
<button
@@ -1050,6 +1090,34 @@ export default function StatisticsView() {
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-2 no-scrollbar">
{viewAllLoading && !hasViewAllSnapshot ? (
<LoadingState label={`正在加载 ${viewAllDate} 车辆明细`} variant="inline" />
) : viewAllError && !hasViewAllSnapshot ? (
<ErrorState
title="车辆明细加载失败"
message={viewAllError}
onRetry={retryViewAllVehicles}
retrying={viewAllLoading}
variant="inline"
/>
) : (
<>
{viewAllError && (
<ErrorState
title={`${viewAllDate} 车辆明细刷新失败`}
message={`${viewAllError} 当前保留 ${viewAllLoadedDate} 已成功加载的数据。`}
onRetry={retryViewAllVehicles}
retrying={viewAllLoading}
variant="inline"
/>
)}
{!viewAllLoading && !viewAllError && searchedViewAllVehicles.length === 0 ? (
<EmptyState
title={viewAllHistoricalVehicles?.length ? '没有匹配车辆' : '该日期暂无车辆里程'}
description={viewAllHistoricalVehicles?.length ? '请调整车牌搜索条件' : `${viewAllLoadedDate || viewAllDate} 未返回可展示的车辆数据`}
variant="inline"
/>
) : null}
{[...searchedViewAllVehicles].sort((a, b) => {
const valA = a.todayMileage || 0;
const valB = b.todayMileage || 0;
@@ -1087,6 +1155,8 @@ export default function StatisticsView() {
</div>
</button>
))}
</>
)}
</div>
<div className="p-4 bg-slate-50 border-t border-slate-100">