fix: preserve dated vehicle panel snapshots
This commit is contained in:
@@ -174,6 +174,7 @@ SUM(fee) / NULLIF(SUM(kwh), 0)
|
||||
- 里程考核接口区分目标声明台数与当前有效车辆数;两者不一致时页面明确标注缺少或超配数量,并说明完成率、缺口和归因的实际计算范围。
|
||||
- 里程考核统计主数据已区分加载、真实空数据、首次请求故障和保留旧数据的刷新故障,并提供统一重试入口,接口失败不再显示为零值或空页面。
|
||||
- 里程考核归因车辆与 7 天趋势已提供独立加载、故障、空数据和重试状态;子请求失败时停止展示“0 台未达标”和“最新日变化 +0”等伪业务结果。
|
||||
- 里程考核车辆日期侧栏仅展示带目标和日期标识的成功快照;切换日期失败时保留并标注上一成功日期,首次失败不再回退为当前日数据或伪造“0 辆”。
|
||||
- 已完成电能数据截至时间和实际趋势月展示。
|
||||
- 电能日期下钻自动核对日汇总与订单全量合计的日期、车辆范围、电量和费用,并显式展示通过或差异状态。
|
||||
- 已完成氢能结构化故障状态与重试体验。
|
||||
|
||||
@@ -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,16 +1057,27 @@ 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
|
||||
onClick={() => setViewAllSort(prev => prev === 'desc' ? 'asc' : 'desc')}
|
||||
@@ -1050,43 +1090,73 @@ export default function StatisticsView() {
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-2 no-scrollbar">
|
||||
{[...searchedViewAllVehicles].sort((a, b) => {
|
||||
const valA = a.todayMileage || 0;
|
||||
const valB = b.todayMileage || 0;
|
||||
return viewAllSort === 'desc' ? valB - valA : valA - valB;
|
||||
}).map(tv => (
|
||||
<button
|
||||
type="button"
|
||||
key={tv.plateNumber}
|
||||
onClick={() => openVehicleDiagnostic(tv)}
|
||||
className="flex w-full items-center justify-between rounded-xl border border-slate-100 bg-white px-3 py-2 text-left shadow-sm transition-all hover:border-blue-200 hover:shadow-md"
|
||||
>
|
||||
<div className="flex items-center gap-3 overflow-hidden flex-1">
|
||||
<div className="relative flex-shrink-0">
|
||||
<div className="w-8 h-8 rounded-lg bg-slate-50 flex items-center justify-center">
|
||||
<Truck size={14} className="text-slate-400" />
|
||||
{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;
|
||||
return viewAllSort === 'desc' ? valB - valA : valA - valB;
|
||||
}).map(tv => (
|
||||
<button
|
||||
type="button"
|
||||
key={tv.plateNumber}
|
||||
onClick={() => openVehicleDiagnostic(tv)}
|
||||
className="flex w-full items-center justify-between rounded-xl border border-slate-100 bg-white px-3 py-2 text-left shadow-sm transition-all hover:border-blue-200 hover:shadow-md"
|
||||
>
|
||||
<div className="flex items-center gap-3 overflow-hidden flex-1">
|
||||
<div className="relative flex-shrink-0">
|
||||
<div className="w-8 h-8 rounded-lg bg-slate-50 flex items-center justify-center">
|
||||
<Truck size={14} className="text-slate-400" />
|
||||
</div>
|
||||
<div className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-white ${tv.isOnline ? 'bg-green-500' : 'bg-slate-300'}`} />
|
||||
</div>
|
||||
<div className="overflow-hidden flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs font-black text-slate-900 font-mono"><Blur>{tv.plateNumber}</Blur></span>
|
||||
<span className={`text-[8px] px-1 rounded ${tv.isOnline ? 'bg-green-50 text-green-600' : 'bg-slate-100 text-slate-400'} font-bold`}>
|
||||
{tv.isOnline ? '在线' : '离线'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[8px] text-slate-300 font-bold">{tv.rentStatus || ''}{tv.department ? ` · ${tv.department.replace('业务', '')}` : ''}</span>
|
||||
<span className="text-[9px] font-bold text-slate-600 truncate">{tv.customer || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-white ${tv.isOnline ? 'bg-green-500' : 'bg-slate-300'}`} />
|
||||
</div>
|
||||
<div className="overflow-hidden flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs font-black text-slate-900 font-mono"><Blur>{tv.plateNumber}</Blur></span>
|
||||
<span className={`text-[8px] px-1 rounded ${tv.isOnline ? 'bg-green-50 text-green-600' : 'bg-slate-100 text-slate-400'} font-bold`}>
|
||||
{tv.isOnline ? '在线' : '离线'}
|
||||
</span>
|
||||
<div className="text-right flex-shrink-0 ml-2">
|
||||
<div className="text-sm font-black text-blue-600">{tv.todayMileage.toLocaleString()} <span className="text-[8px] text-slate-400">KM</span></div>
|
||||
<div className="text-[9px] font-bold text-slate-300 mt-0.5">累计: {fmtKm(tv.totalMileage || 0)} km</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[8px] text-slate-300 font-bold">{tv.rentStatus || ''}{tv.department ? ` · ${tv.department.replace('业务', '')}` : ''}</span>
|
||||
<span className="text-[9px] font-bold text-slate-600 truncate">{tv.customer || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right flex-shrink-0 ml-2">
|
||||
<div className="text-sm font-black text-blue-600">{tv.todayMileage.toLocaleString()} <span className="text-[8px] text-slate-400">KM</span></div>
|
||||
<div className="text-[9px] font-bold text-slate-300 mt-0.5">累计: {fmtKm(tv.totalMileage || 0)} km</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-slate-50 border-t border-slate-100">
|
||||
|
||||
Reference in New Issue
Block a user