fix: bind mileage list to request context
This commit is contained in:
@@ -177,6 +177,7 @@ SUM(fee) / NULLIF(SUM(kwh), 0)
|
||||
- 里程考核归因车辆与 7 天趋势已提供独立加载、故障、空数据和重试状态;子请求失败时停止展示“0 台未达标”和“最新日变化 +0”等伪业务结果。
|
||||
- 里程考核车辆日期侧栏仅展示带目标和日期标识的成功快照;切换日期失败时保留并标注上一成功日期,首次失败不再回退为当前日数据或伪造“0 辆”。
|
||||
- 里程实时监控的首页、分页、全屏和导出已共用同一组筛选参数;全屏首次故障不再显示零值,刷新故障会保留并标注上一成功快照,导出成功或失败均提供可见结果和重试入口。
|
||||
- 里程实时监控主列表快照已绑定完整筛选与排序口径;新口径请求失败时隐藏旧 KPI、趋势和车辆,原口径刷新失败时保留并标注旧快照,快速筛选与分页的过期响应不会覆盖当前结果。
|
||||
- 已完成电能数据截至时间和实际趋势月展示。
|
||||
- 电能日期下钻自动核对日汇总与订单全量合计的日期、车辆范围、电量和费用,并显式展示通过或差异状态。
|
||||
- 已完成氢能结构化故障状态与重试体验。
|
||||
|
||||
@@ -499,14 +499,14 @@ export default function MonitoringView() {
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [pageLoading, setPageLoading] = useState(true);
|
||||
const [pageError, setPageError] = useState<{ message: string; scope: 'first' | 'more' } | null>(null);
|
||||
const [pageSnapshotKey, setPageSnapshotKey] = useState<string | null>(null);
|
||||
const [manualRefreshing, setManualRefreshing] = useState(false);
|
||||
const [dataUpdatedAt, setDataUpdatedAt] = useState<string | null>(null);
|
||||
const [showBackToTop, setShowBackToTop] = useState(false);
|
||||
const [relativeNow, setRelativeNow] = useState(() => Date.now());
|
||||
const firstPageRequestRef = useRef(0);
|
||||
const loadMoreRequestRef = useRef(0);
|
||||
const PAGE_SIZE = 50;
|
||||
const detailVehicle = drillContext.level === 'vehicle' && drillContext.plate
|
||||
? vehicles.find(vehicle => vehicle.plate === drillContext.plate) || null
|
||||
: null;
|
||||
|
||||
const commitDrillContext = useCallback((next: MileageDrillContext, mode: 'push' | 'replace') => {
|
||||
const url = buildMileageDrillUrl(window.location, next);
|
||||
@@ -553,11 +553,6 @@ export default function MonitoringView() {
|
||||
|
||||
const departments = filterOptions.departments;
|
||||
const plateNumbers = filterOptions.plates;
|
||||
const rangeLabel = normalizeRangeLabel(effectiveRange.start, effectiveRange.end);
|
||||
const isRangeMode = !!effectiveRange.start && !!effectiveRange.end && effectiveRange.start !== effectiveRange.end;
|
||||
const averageDailyKm = rangeDailyTotals.length > 0
|
||||
? rangeDailyTotals.reduce((sum, item) => sum + item.totalKm, 0) / rangeDailyTotals.length
|
||||
: 0;
|
||||
const applyRangePreset = useCallback((preset: RangePreset) => {
|
||||
const range = getRangePreset(preset);
|
||||
setRangeStart(range.start);
|
||||
@@ -605,11 +600,31 @@ export default function MonitoringView() {
|
||||
searchTerm,
|
||||
sourcePriority,
|
||||
]);
|
||||
const monitoringFilterKey = useMemo(() => JSON.stringify(monitoringFilters), [monitoringFilters]);
|
||||
const fullscreenHasSnapshot = fullscreenSnapshotKey === monitoringFilterKey;
|
||||
const monitoringRequestKey = useMemo(
|
||||
() => JSON.stringify({ ...monitoringFilters, sortBy, sortOrder }),
|
||||
[monitoringFilters, sortBy, sortOrder],
|
||||
);
|
||||
const pageHasSnapshot = pageSnapshotKey === monitoringRequestKey;
|
||||
const fullscreenHasSnapshot = fullscreenSnapshotKey === monitoringRequestKey;
|
||||
const pageUpdatedAt = pageHasSnapshot ? dataUpdatedAt : null;
|
||||
const detailVehicle = pageHasSnapshot && drillContext.level === 'vehicle' && drillContext.plate
|
||||
? vehicles.find(vehicle => vehicle.plate === drillContext.plate) || null
|
||||
: null;
|
||||
const displayedRange = pageHasSnapshot
|
||||
? effectiveRange
|
||||
: { start: rangeStart, end: rangeEnd };
|
||||
const rangeLabel = normalizeRangeLabel(displayedRange.start, displayedRange.end);
|
||||
const isRangeMode = !!displayedRange.start && !!displayedRange.end && displayedRange.start !== displayedRange.end;
|
||||
const averageDailyKm = pageHasSnapshot && rangeDailyTotals.length > 0
|
||||
? rangeDailyTotals.reduce((sum, item) => sum + item.totalKm, 0) / rangeDailyTotals.length
|
||||
: 0;
|
||||
|
||||
// 加载首页数据
|
||||
const loadFirstPage = useCallback((showPageLoading = true, force = false) => {
|
||||
const requestId = ++firstPageRequestRef.current;
|
||||
const requestKey = monitoringRequestKey;
|
||||
++loadMoreRequestRef.current;
|
||||
setLoadingMore(false);
|
||||
if (showPageLoading) setPageLoading(true);
|
||||
setPageError(null);
|
||||
return fetchMonitoring({
|
||||
@@ -620,6 +635,7 @@ export default function MonitoringView() {
|
||||
page: 1,
|
||||
force,
|
||||
}).then(d => {
|
||||
if (requestId !== firstPageRequestRef.current) return;
|
||||
setVehicles(d.vehicles);
|
||||
setStats(d.stats);
|
||||
setFilterOptions(d.filters);
|
||||
@@ -629,15 +645,17 @@ export default function MonitoringView() {
|
||||
setPage(1);
|
||||
setHasMore(d.page < d.totalPages);
|
||||
setDataUpdatedAt(d.updatedAt);
|
||||
setPageSnapshotKey(requestKey);
|
||||
}).catch(error => {
|
||||
if (requestId !== firstPageRequestRef.current) return;
|
||||
setPageError({
|
||||
message: error instanceof Error ? error.message : '里程数据加载失败,请稍后重试',
|
||||
scope: 'first',
|
||||
});
|
||||
}).finally(() => {
|
||||
if (showPageLoading) setPageLoading(false);
|
||||
if (requestId === firstPageRequestRef.current) setPageLoading(false);
|
||||
});
|
||||
}, [monitoringFilters, rangeEnd, rangeStart, sortBy, sortOrder]);
|
||||
}, [monitoringFilters, monitoringRequestKey, rangeEnd, rangeStart, sortBy, sortOrder]);
|
||||
|
||||
const handleManualRefresh = useCallback(async () => {
|
||||
if (manualRefreshing) return;
|
||||
@@ -653,6 +671,7 @@ export default function MonitoringView() {
|
||||
const loadMore = useCallback(() => {
|
||||
if (loadingMore || !hasMore) return;
|
||||
const nextPage = page + 1;
|
||||
const requestId = ++loadMoreRequestRef.current;
|
||||
setLoadingMore(true);
|
||||
setPageError(null);
|
||||
fetchMonitoring({
|
||||
@@ -662,16 +681,20 @@ export default function MonitoringView() {
|
||||
limit: PAGE_SIZE,
|
||||
page: nextPage,
|
||||
}).then(d => {
|
||||
if (requestId !== loadMoreRequestRef.current) return;
|
||||
setVehicles(prev => [...prev, ...d.vehicles]);
|
||||
setPage(nextPage);
|
||||
setHasMore(nextPage < d.totalPages);
|
||||
setDataUpdatedAt(d.updatedAt);
|
||||
}).catch(error => {
|
||||
if (requestId !== loadMoreRequestRef.current) return;
|
||||
setPageError({
|
||||
message: error instanceof Error ? error.message : '更多车辆加载失败,请稍后重试',
|
||||
scope: 'more',
|
||||
});
|
||||
}).finally(() => setLoadingMore(false));
|
||||
}).finally(() => {
|
||||
if (requestId === loadMoreRequestRef.current) setLoadingMore(false);
|
||||
});
|
||||
}, [hasMore, loadingMore, monitoringFilters, page, sortBy, sortOrder]);
|
||||
|
||||
// 筛选/排序变化时重新加载
|
||||
@@ -737,7 +760,7 @@ export default function MonitoringView() {
|
||||
document.body.scrollTop = 0;
|
||||
};
|
||||
|
||||
const filteredVehicles = vehicles;
|
||||
const filteredVehicles = pageHasSnapshot ? vehicles : [];
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
setIsFullscreen(!isFullscreen);
|
||||
@@ -760,9 +783,8 @@ export default function MonitoringView() {
|
||||
setFullscreenVehicles(d.vehicles);
|
||||
setFullscreenStats(d.stats);
|
||||
setFilterOptions(d.filters);
|
||||
setDataUpdatedAt(d.updatedAt);
|
||||
setFullscreenUpdatedAt(d.updatedAt);
|
||||
setFullscreenSnapshotKey(monitoringFilterKey);
|
||||
setFullscreenSnapshotKey(monitoringRequestKey);
|
||||
}).catch(error => {
|
||||
if (cancelled) return;
|
||||
setFullscreenError(error instanceof Error ? error.message : '全屏监控加载失败,请稍后重试');
|
||||
@@ -770,7 +792,7 @@ export default function MonitoringView() {
|
||||
if (!cancelled) setFullscreenLoading(false);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [fullscreenRefresh, isFullscreen, monitoringFilterKey, monitoringFilters, sortBy, sortOrder]);
|
||||
}, [fullscreenRefresh, isFullscreen, monitoringFilters, monitoringRequestKey, sortBy, sortOrder]);
|
||||
|
||||
// 全屏时禁止背景滚动
|
||||
useEffect(() => {
|
||||
@@ -1143,9 +1165,9 @@ export default function MonitoringView() {
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-blue-500" />
|
||||
<span
|
||||
className="truncate text-[9px] font-bold tracking-tight text-slate-400"
|
||||
title={`OneOS 分钟级快照 · 数据截至 ${formatMileageSnapshotTime(dataUpdatedAt)}`}
|
||||
title={`OneOS 分钟级快照 · 数据截至 ${formatMileageSnapshotTime(pageUpdatedAt)}`}
|
||||
>
|
||||
OneOS 分钟级快照 · 截至 {formatMileageSnapshotTime(dataUpdatedAt)}
|
||||
OneOS 分钟级快照 · 截至 {formatMileageSnapshotTime(pageUpdatedAt)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1153,7 +1175,7 @@ export default function MonitoringView() {
|
||||
|
||||
<div className="flex flex-shrink-0 items-center gap-1.5">
|
||||
<span className="hidden text-[9px] font-bold tabular-nums text-slate-400 sm:inline">
|
||||
数据截至 {formatMileageSnapshotTime(dataUpdatedAt)}
|
||||
数据截至 {formatMileageSnapshotTime(pageUpdatedAt)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
@@ -1508,30 +1530,73 @@ export default function MonitoringView() {
|
||||
);
|
||||
})()}
|
||||
|
||||
{pageError?.scope === 'first' && (
|
||||
<div
|
||||
role="alert"
|
||||
className={`flex items-center justify-between gap-3 rounded-xl border px-3 py-2 ${
|
||||
pageHasSnapshot
|
||||
? 'border-amber-200 bg-amber-50 text-amber-700'
|
||||
: 'border-rose-200 bg-rose-50 text-rose-700'
|
||||
}`}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-[10px] font-black" title={pageError.message}>
|
||||
{pageHasSnapshot
|
||||
? `刷新失败,仍展示截至 ${formatMileageSnapshotTime(pageUpdatedAt)} 的数据`
|
||||
: '当前筛选数据暂不可用'}
|
||||
</p>
|
||||
{!pageHasSnapshot && <p className="mt-0.5 truncate text-[9px] opacity-70">{pageError.message}</p>}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => pageHasSnapshot ? handleManualRefresh() : loadFirstPage()}
|
||||
disabled={pageLoading || manualRefreshing}
|
||||
className="inline-flex h-7 shrink-0 items-center gap-1 rounded-lg border border-current/20 bg-white/70 px-2 text-[9px] font-black disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw size={11} className={pageLoading || manualRefreshing ? 'animate-spin' : ''} />
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sticky header: KPI + 清单标题 */}
|
||||
<div className="sticky top-[44px] z-20 bg-[var(--app-bg)] pt-1 pb-1 space-y-2">
|
||||
<div className={`grid grid-cols-4 gap-2 transition-opacity ${pageLoading ? 'opacity-60' : ''}`}>
|
||||
<div className={`grid grid-cols-4 gap-2 transition-opacity ${pageLoading && pageHasSnapshot ? 'opacity-60' : ''}`}>
|
||||
<div className="relative col-span-2 flex min-h-[68px] flex-col justify-center overflow-hidden rounded-xl bg-slate-900 p-2.5 text-white">
|
||||
<div className="text-[7px] font-bold text-slate-500 uppercase tracking-wider">{sortBy === 'total' ? '累计' : (isRangeMode ? '区间' : '当日')}总里程</div>
|
||||
<div className="text-lg font-black tracking-tighter leading-tight flex items-baseline gap-1">
|
||||
{pageLoading ? <div className="h-5 w-20 bg-slate-700 rounded animate-pulse"></div> : <>{Math.round(sortBy === 'total' ? stats.totalAll : stats.totalToday).toLocaleString()} <span className="text-[8px] text-slate-400">km</span></>}
|
||||
{pageLoading && !pageHasSnapshot
|
||||
? <div className="h-5 w-20 bg-slate-700 rounded animate-pulse"></div>
|
||||
: <>{pageHasSnapshot ? Math.round(sortBy === 'total' ? stats.totalAll : stats.totalToday).toLocaleString() : '—'} <span className="text-[8px] text-slate-400">km</span></>}
|
||||
</div>
|
||||
<div className="mt-0.5 truncate text-[8px] font-bold text-slate-500">{rangeLabel}</div>
|
||||
</div>
|
||||
<div className="flex min-h-[68px] flex-col justify-center rounded-xl border border-gray-100 bg-white p-2.5 shadow-sm">
|
||||
<div className="text-[7px] font-bold text-slate-400 uppercase">平均单车</div>
|
||||
<div className="text-sm font-black text-slate-800 leading-tight">{pageLoading ? <div className="h-4 w-8 bg-slate-100 rounded animate-pulse"></div> : (stats.vehicleCount > 0 ? (sortBy === 'total' ? stats.totalAll : stats.totalToday) / stats.vehicleCount : 0).toFixed(0)}</div>
|
||||
<div className="text-sm font-black text-slate-800 leading-tight">
|
||||
{pageLoading && !pageHasSnapshot
|
||||
? <div className="h-4 w-8 bg-slate-100 rounded animate-pulse"></div>
|
||||
: pageHasSnapshot ? (stats.vehicleCount > 0 ? (sortBy === 'total' ? stats.totalAll : stats.totalToday) / stats.vehicleCount : 0).toFixed(0) : '—'}
|
||||
</div>
|
||||
<div className="text-[7px] text-slate-400">km/台</div>
|
||||
</div>
|
||||
<div className="flex min-h-[68px] flex-col justify-center rounded-xl border border-gray-100 bg-white p-2.5 shadow-sm">
|
||||
<div className="text-[7px] font-bold text-slate-400 uppercase">监控台数</div>
|
||||
<div className="text-sm font-black text-slate-800 leading-tight">{pageLoading ? <div className="h-4 w-8 bg-slate-100 rounded animate-pulse"></div> : stats.vehicleCount}</div>
|
||||
<div className="text-sm font-black text-slate-800 leading-tight">
|
||||
{pageLoading && !pageHasSnapshot
|
||||
? <div className="h-4 w-8 bg-slate-100 rounded animate-pulse"></div>
|
||||
: pageHasSnapshot ? stats.vehicleCount : '—'}
|
||||
</div>
|
||||
<div className="text-[7px] text-slate-400">台</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-xl border border-slate-100 bg-white shadow-sm overflow-hidden">
|
||||
{pageLoading ? (
|
||||
{pageLoading && !pageHasSnapshot ? (
|
||||
<div className="h-[74px] bg-slate-50 animate-pulse" />
|
||||
) : !pageHasSnapshot ? (
|
||||
<div className="flex h-[74px] items-center justify-center px-3 text-[10px] font-bold text-slate-400">
|
||||
当前筛选尚无可用快照
|
||||
</div>
|
||||
) : isRangeMode ? (
|
||||
<div className="grid grid-cols-[92px_minmax(0,1fr)_62px] items-center gap-2 px-2 py-2">
|
||||
<div className="min-w-0">
|
||||
@@ -1589,7 +1654,7 @@ export default function MonitoringView() {
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<span className="text-[9px] font-black text-slate-400 uppercase tracking-widest">车辆详情清单</span>
|
||||
<span className="text-[9px] font-bold text-slate-400">
|
||||
已加载 {filteredVehicles.length} / 共 {total} 条
|
||||
{pageHasSnapshot ? `已加载 ${filteredVehicles.length} / 共 ${total} 条` : '数据不可用'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="hidden grid-cols-[minmax(190px,1.1fr)_minmax(160px,1fr)_130px_130px] items-center gap-3 rounded-lg border border-slate-200 bg-white px-4 py-2 text-[10px] font-bold text-slate-400 md:grid">
|
||||
@@ -1603,7 +1668,7 @@ export default function MonitoringView() {
|
||||
{/* Vehicle List */}
|
||||
<div className="space-y-1.5">
|
||||
|
||||
{pageLoading && (
|
||||
{pageLoading && !pageHasSnapshot && (
|
||||
<div className="space-y-1.5">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="bg-white px-3 py-3 rounded-xl border border-slate-50 shadow-sm flex items-center justify-between animate-pulse">
|
||||
@@ -1705,18 +1770,18 @@ export default function MonitoringView() {
|
||||
})}
|
||||
</div>
|
||||
|
||||
{filteredVehicles.length === 0 && !loadingMore && !pageError && (
|
||||
{pageHasSnapshot && filteredVehicles.length === 0 && !loadingMore && !pageError && (
|
||||
<div className="py-10 text-center bg-white rounded-2xl border border-dashed border-slate-100">
|
||||
<p className="text-xs font-bold text-slate-300">未找到匹配数据</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pageError && (
|
||||
{pageError?.scope === 'more' && pageHasSnapshot && (
|
||||
<div className="rounded-xl border border-rose-100 bg-rose-50 px-3 py-3 text-center">
|
||||
<p className="text-[10px] font-bold text-rose-600">{pageError.message}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => pageError.scope === 'more' ? loadMore() : loadFirstPage()}
|
||||
onClick={loadMore}
|
||||
disabled={pageLoading || loadingMore}
|
||||
className="mt-2 inline-flex h-8 items-center gap-1.5 rounded-lg border border-rose-200 bg-white px-3 text-[10px] font-black text-rose-700 transition-colors hover:bg-rose-100 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
@@ -1725,7 +1790,7 @@ export default function MonitoringView() {
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{hasMore && !pageError && filteredVehicles.length > 0 && (
|
||||
{pageHasSnapshot && hasMore && !pageError && filteredVehicles.length > 0 && (
|
||||
<div className="flex flex-col items-center gap-1.5 py-4">
|
||||
<button
|
||||
type="button"
|
||||
@@ -1741,7 +1806,7 @@ export default function MonitoringView() {
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{!hasMore && filteredVehicles.length > 0 && (
|
||||
{pageHasSnapshot && !hasMore && filteredVehicles.length > 0 && (
|
||||
<div className="py-4 text-center">
|
||||
<span className="text-[10px] font-bold text-slate-300">已加载全部 {total} 条</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user