diff --git a/docs/bi-refactor-roadmap.md b/docs/bi-refactor-roadmap.md index a5e754f..a1facf7 100644 --- a/docs/bi-refactor-roadmap.md +++ b/docs/bi-refactor-roadmap.md @@ -177,6 +177,7 @@ SUM(fee) / NULLIF(SUM(kwh), 0) - 里程考核归因车辆与 7 天趋势已提供独立加载、故障、空数据和重试状态;子请求失败时停止展示“0 台未达标”和“最新日变化 +0”等伪业务结果。 - 里程考核车辆日期侧栏仅展示带目标和日期标识的成功快照;切换日期失败时保留并标注上一成功日期,首次失败不再回退为当前日数据或伪造“0 辆”。 - 里程实时监控的首页、分页、全屏和导出已共用同一组筛选参数;全屏首次故障不再显示零值,刷新故障会保留并标注上一成功快照,导出成功或失败均提供可见结果和重试入口。 +- 里程实时监控主列表快照已绑定完整筛选与排序口径;新口径请求失败时隐藏旧 KPI、趋势和车辆,原口径刷新失败时保留并标注旧快照,快速筛选与分页的过期响应不会覆盖当前结果。 - 已完成电能数据截至时间和实际趋势月展示。 - 电能日期下钻自动核对日汇总与订单全量合计的日期、车辆范围、电量和费用,并显式展示通过或差异状态。 - 已完成氢能结构化故障状态与重试体验。 diff --git a/src/modules/mileage/MonitoringView.tsx b/src/modules/mileage/MonitoringView.tsx index 814c8df..cc17978 100644 --- a/src/modules/mileage/MonitoringView.tsx +++ b/src/modules/mileage/MonitoringView.tsx @@ -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(null); const [manualRefreshing, setManualRefreshing] = useState(false); const [dataUpdatedAt, setDataUpdatedAt] = useState(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() { - OneOS 分钟级快照 · 截至 {formatMileageSnapshotTime(dataUpdatedAt)} + OneOS 分钟级快照 · 截至 {formatMileageSnapshotTime(pageUpdatedAt)} @@ -1153,7 +1175,7 @@ export default function MonitoringView() {
- 数据截至 {formatMileageSnapshotTime(dataUpdatedAt)} + 数据截至 {formatMileageSnapshotTime(pageUpdatedAt)} +
+ )} + {/* Sticky header: KPI + 清单标题 */}
-
+
{sortBy === 'total' ? '累计' : (isRangeMode ? '区间' : '当日')}总里程
- {pageLoading ?
: <>{Math.round(sortBy === 'total' ? stats.totalAll : stats.totalToday).toLocaleString()} km} + {pageLoading && !pageHasSnapshot + ?
+ : <>{pageHasSnapshot ? Math.round(sortBy === 'total' ? stats.totalAll : stats.totalToday).toLocaleString() : '—'} km}
{rangeLabel}
平均单车
-
{pageLoading ?
: (stats.vehicleCount > 0 ? (sortBy === 'total' ? stats.totalAll : stats.totalToday) / stats.vehicleCount : 0).toFixed(0)}
+
+ {pageLoading && !pageHasSnapshot + ?
+ : pageHasSnapshot ? (stats.vehicleCount > 0 ? (sortBy === 'total' ? stats.totalAll : stats.totalToday) / stats.vehicleCount : 0).toFixed(0) : '—'} +
km/台
监控台数
-
{pageLoading ?
: stats.vehicleCount}
+
+ {pageLoading && !pageHasSnapshot + ?
+ : pageHasSnapshot ? stats.vehicleCount : '—'} +
- {pageLoading ? ( + {pageLoading && !pageHasSnapshot ? (
+ ) : !pageHasSnapshot ? ( +
+ 当前筛选尚无可用快照 +
) : isRangeMode ? (
@@ -1589,7 +1654,7 @@ export default function MonitoringView() {
车辆详情清单 - 已加载 {filteredVehicles.length} / 共 {total} 条 + {pageHasSnapshot ? `已加载 ${filteredVehicles.length} / 共 ${total} 条` : '数据不可用'}
@@ -1603,7 +1668,7 @@ export default function MonitoringView() { {/* Vehicle List */}
- {pageLoading && ( + {pageLoading && !pageHasSnapshot && (
{Array.from({ length: 6 }).map((_, i) => (
@@ -1705,18 +1770,18 @@ export default function MonitoringView() { })}
- {filteredVehicles.length === 0 && !loadingMore && !pageError && ( + {pageHasSnapshot && filteredVehicles.length === 0 && !loadingMore && !pageError && (

未找到匹配数据

)} - {pageError && ( + {pageError?.scope === 'more' && pageHasSnapshot && (

{pageError.message}

)} - {hasMore && !pageError && filteredVehicles.length > 0 && ( + {pageHasSnapshot && hasMore && !pageError && filteredVehicles.length > 0 && (
)} - {!hasMore && filteredVehicles.length > 0 && ( + {pageHasSnapshot && !hasMore && filteredVehicles.length > 0 && (
已加载全部 {total} 条