From f8e926876cb320425a45b5b1e67e34b2a2f0deff Mon Sep 17 00:00:00 2001 From: kkfluous Date: Fri, 7 Aug 2026 18:23:50 +0800 Subject: [PATCH] fix: make mileage fullscreen failures visible --- docs/bi-refactor-roadmap.md | 1 + src/modules/mileage/MonitoringView.tsx | 236 +++++++++++++++++-------- 2 files changed, 162 insertions(+), 75 deletions(-) diff --git a/docs/bi-refactor-roadmap.md b/docs/bi-refactor-roadmap.md index 8bca863..a5e754f 100644 --- a/docs/bi-refactor-roadmap.md +++ b/docs/bi-refactor-roadmap.md @@ -176,6 +176,7 @@ SUM(fee) / NULLIF(SUM(kwh), 0) - 里程考核统计主数据已区分加载、真实空数据、首次请求故障和保留旧数据的刷新故障,并提供统一重试入口,接口失败不再显示为零值或空页面。 - 里程考核归因车辆与 7 天趋势已提供独立加载、故障、空数据和重试状态;子请求失败时停止展示“0 台未达标”和“最新日变化 +0”等伪业务结果。 - 里程考核车辆日期侧栏仅展示带目标和日期标识的成功快照;切换日期失败时保留并标注上一成功日期,首次失败不再回退为当前日数据或伪造“0 辆”。 +- 里程实时监控的首页、分页、全屏和导出已共用同一组筛选参数;全屏首次故障不再显示零值,刷新故障会保留并标注上一成功快照,导出成功或失败均提供可见结果和重试入口。 - 已完成电能数据截至时间和实际趋势月展示。 - 电能日期下钻自动核对日汇总与订单全量合计的日期、车辆范围、电量和费用,并显式展示通过或差异状态。 - 已完成氢能结构化故障状态与重试体验。 diff --git a/src/modules/mileage/MonitoringView.tsx b/src/modules/mileage/MonitoringView.tsx index ea27104..814c8df 100644 --- a/src/modules/mileage/MonitoringView.tsx +++ b/src/modules/mileage/MonitoringView.tsx @@ -31,6 +31,7 @@ const HIGH_MILEAGE_ALERT_TARGETS = new Set([ ]); const HIGH_MILEAGE_ALERT_KM = 800; const ALL_MILEAGE_SOURCE_GROUPS: MileageSourceGroup[] = ['instrument', 'gps']; +type MonitoringRequestParams = NonNullable[0]>; const MILEAGE_SOURCE_META = { instrument: { label: '仪表数据', protocol: null }, @@ -463,6 +464,9 @@ export default function MonitoringView() { const [fullscreenStats, setFullscreenStats] = useState({ totalToday: 0, totalAll: 0, vehicleCount: 0, yesterdayTotal: 0 }); const [fullscreenRefresh, setFullscreenRefresh] = useState(0); const [fullscreenLoading, setFullscreenLoading] = useState(false); + const [fullscreenError, setFullscreenError] = useState(null); + const [fullscreenSnapshotKey, setFullscreenSnapshotKey] = useState(null); + const [fullscreenUpdatedAt, setFullscreenUpdatedAt] = useState(null); // New filters from image const [filterPlates, setFilterPlates] = useState(() => ( @@ -479,6 +483,7 @@ export default function MonitoringView() { const [filterMileageRange, setFilterMileageRange] = useState({ min: '', max: '' }); const [appliedMileageRange, setAppliedMileageRange] = useState({ min: '', max: '' }); const [exporting, setExporting] = useState(false); + const [exportStatus, setExportStatus] = useState<{ type: 'success' | 'error'; message: string } | null>(null); const [rangeStart, setRangeStart] = useState(() => drillContext.startDate || defaultMileageDate()); const [rangeEnd, setRangeEnd] = useState(() => drillContext.endDate || defaultMileageDate()); const [sourcePriority, setSourcePriority] = useState(drillContext.sourcePriority); @@ -565,31 +570,54 @@ export default function MonitoringView() { return inAlertTarget && Math.max(0, v.dailyKm || 0) >= HIGH_MILEAGE_ALERT_KM; }, [filterTargetNames]); + const monitoringFilters = useMemo(() => ({ + search: searchTerm || undefined, + dept: filterDept !== 'All' ? filterDept : undefined, + customer: filterCustomer !== 'All' ? filterCustomer : undefined, + project: filterProject !== 'All' ? filterProject : undefined, + entity: filterEntity !== 'All' ? filterEntity : undefined, + rentStatus: filterRentStatus !== 'All' ? filterRentStatus : undefined, + platePrefix: filterPlatePrefix !== 'All' ? filterPlatePrefix : undefined, + targetNames: filterTargetNames.length > 0 ? filterTargetNames : undefined, + region: filterRegion !== 'All' ? filterRegion : undefined, + plate: filterPlates.length > 0 ? filterPlates.join(',') : undefined, + mileageMin: appliedMileageRange.min || undefined, + mileageMax: appliedMileageRange.max || undefined, + startDate: rangeStart || undefined, + endDate: rangeEnd || undefined, + brands: filterBrands.length > 0 ? filterBrands : undefined, + sourcePriority, + }), [ + appliedMileageRange.max, + appliedMileageRange.min, + filterBrands, + filterCustomer, + filterDept, + filterEntity, + filterPlatePrefix, + filterPlates, + filterProject, + filterRegion, + filterRentStatus, + filterTargetNames, + rangeEnd, + rangeStart, + searchTerm, + sourcePriority, + ]); + const monitoringFilterKey = useMemo(() => JSON.stringify(monitoringFilters), [monitoringFilters]); + const fullscreenHasSnapshot = fullscreenSnapshotKey === monitoringFilterKey; + // 加载首页数据 const loadFirstPage = useCallback((showPageLoading = true, force = false) => { if (showPageLoading) setPageLoading(true); setPageError(null); return fetchMonitoring({ + ...monitoringFilters, sortBy, sortOrder, limit: PAGE_SIZE, page: 1, - search: searchTerm || undefined, - dept: filterDept !== 'All' ? filterDept : undefined, - customer: filterCustomer !== 'All' ? filterCustomer : undefined, - project: filterProject !== 'All' ? filterProject : undefined, - entity: filterEntity !== 'All' ? filterEntity : undefined, - rentStatus: filterRentStatus !== 'All' ? filterRentStatus : undefined, - platePrefix: filterPlatePrefix !== 'All' ? filterPlatePrefix : undefined, - targetNames: filterTargetNames.length > 0 ? filterTargetNames : undefined, - region: filterRegion !== 'All' ? filterRegion : undefined, - plate: filterPlates.length > 0 ? filterPlates.join(',') : undefined, - mileageMin: appliedMileageRange.min || undefined, - mileageMax: appliedMileageRange.max || undefined, - startDate: rangeStart || undefined, - endDate: rangeEnd || undefined, - brands: filterBrands.length > 0 ? filterBrands : undefined, - sourcePriority, force, }).then(d => { setVehicles(d.vehicles); @@ -609,7 +637,7 @@ export default function MonitoringView() { }).finally(() => { if (showPageLoading) setPageLoading(false); }); - }, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterRentStatus, filterPlatePrefix, filterTargetNames, filterRegion, filterPlates, appliedMileageRange, rangeStart, rangeEnd, filterBrands, sourcePriority]); + }, [monitoringFilters, rangeEnd, rangeStart, sortBy, sortOrder]); const handleManualRefresh = useCallback(async () => { if (manualRefreshing) return; @@ -628,26 +656,11 @@ export default function MonitoringView() { setLoadingMore(true); setPageError(null); fetchMonitoring({ + ...monitoringFilters, sortBy, sortOrder, limit: PAGE_SIZE, page: nextPage, - search: searchTerm || undefined, - dept: filterDept !== 'All' ? filterDept : undefined, - customer: filterCustomer !== 'All' ? filterCustomer : undefined, - project: filterProject !== 'All' ? filterProject : undefined, - entity: filterEntity !== 'All' ? filterEntity : undefined, - rentStatus: filterRentStatus !== 'All' ? filterRentStatus : undefined, - platePrefix: filterPlatePrefix !== 'All' ? filterPlatePrefix : undefined, - targetNames: filterTargetNames.length > 0 ? filterTargetNames : undefined, - region: filterRegion !== 'All' ? filterRegion : undefined, - plate: filterPlates.length > 0 ? filterPlates.join(',') : undefined, - mileageMin: appliedMileageRange.min || undefined, - mileageMax: appliedMileageRange.max || undefined, - startDate: rangeStart || undefined, - endDate: rangeEnd || undefined, - brands: filterBrands.length > 0 ? filterBrands : undefined, - sourcePriority, }).then(d => { setVehicles(prev => [...prev, ...d.vehicles]); setPage(nextPage); @@ -659,7 +672,7 @@ export default function MonitoringView() { scope: 'more', }); }).finally(() => setLoadingMore(false)); - }, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterRentStatus, filterPlatePrefix, filterTargetNames, filterRegion, filterPlates, appliedMileageRange, rangeStart, rangeEnd, page, loadingMore, hasMore, filterBrands, sourcePriority]); + }, [hasMore, loadingMore, monitoringFilters, page, sortBy, sortOrder]); // 筛选/排序变化时重新加载 useEffect(() => { @@ -678,36 +691,27 @@ export default function MonitoringView() { const handleDownload = useCallback(async () => { if (exporting) return; setExporting(true); + setExportStatus(null); try { const d = await fetchMonitoring({ + ...monitoringFilters, sortBy, sortOrder, limit: 9999, page: 1, - search: searchTerm || undefined, - dept: filterDept !== 'All' ? filterDept : undefined, - customer: filterCustomer !== 'All' ? filterCustomer : undefined, - project: filterProject !== 'All' ? filterProject : undefined, - entity: filterEntity !== 'All' ? filterEntity : undefined, - rentStatus: filterRentStatus !== 'All' ? filterRentStatus : undefined, - platePrefix: filterPlatePrefix !== 'All' ? filterPlatePrefix : undefined, - targetNames: filterTargetNames.length > 0 ? filterTargetNames : undefined, - region: filterRegion !== 'All' ? filterRegion : undefined, - plate: filterPlates.length > 0 ? filterPlates.join(',') : undefined, - mileageMin: appliedMileageRange.min || undefined, - mileageMax: appliedMileageRange.max || undefined, - startDate: rangeStart || undefined, - endDate: rangeEnd || undefined, - brands: filterBrands.length > 0 ? filterBrands : undefined, - sourcePriority, }); exportMileageXlsx(d.vehicles, { startDate: d.dateRange?.start || rangeStart, endDate: d.dateRange?.end || rangeEnd, sortBy }); + setExportStatus({ type: 'success', message: `已导出 ${d.vehicles.length} 辆车辆` }); } catch (err) { console.error('export failed', err); + setExportStatus({ + type: 'error', + message: err instanceof Error ? err.message : '导出失败,请稍后重试', + }); } finally { setExporting(false); } - }, [exporting, sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterRentStatus, filterPlatePrefix, filterTargetNames, filterRegion, filterPlates, appliedMileageRange, rangeStart, rangeEnd, filterBrands, sourcePriority]); + }, [exporting, monitoringFilters, rangeEnd, rangeStart, sortBy, sortOrder]); // 每分钟自动刷新 useEffect(() => { @@ -742,31 +746,31 @@ export default function MonitoringView() { // 全屏时加载全部数据(无分页),筛选变化时重新加载 useEffect(() => { if (!isFullscreen) return; + let cancelled = false; setFullscreenLoading(true); + setFullscreenError(null); fetchMonitoring({ + ...monitoringFilters, sortBy, sortOrder, limit: 9999, page: 1, - search: searchTerm || undefined, - dept: filterDept !== 'All' ? filterDept : undefined, - customer: filterCustomer !== 'All' ? filterCustomer : undefined, - rentStatus: filterRentStatus !== 'All' ? filterRentStatus : undefined, - platePrefix: filterPlatePrefix !== 'All' ? filterPlatePrefix : undefined, - targetNames: filterTargetNames.length > 0 ? filterTargetNames : undefined, - region: filterRegion !== 'All' ? filterRegion : undefined, - plate: filterPlates.length > 0 ? filterPlates.join(',') : undefined, - startDate: rangeStart || undefined, - endDate: rangeEnd || undefined, - brands: filterBrands.length > 0 ? filterBrands : undefined, - sourcePriority, }).then(d => { + if (cancelled) return; setFullscreenVehicles(d.vehicles); setFullscreenStats(d.stats); setFilterOptions(d.filters); setDataUpdatedAt(d.updatedAt); - }).catch(() => {}).finally(() => setFullscreenLoading(false)); - }, [isFullscreen, sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterRentStatus, filterPlatePrefix, filterTargetNames, filterRegion, filterPlates, rangeStart, rangeEnd, fullscreenRefresh, filterBrands, sourcePriority]); + setFullscreenUpdatedAt(d.updatedAt); + setFullscreenSnapshotKey(monitoringFilterKey); + }).catch(error => { + if (cancelled) return; + setFullscreenError(error instanceof Error ? error.message : '全屏监控加载失败,请稍后重试'); + }).finally(() => { + if (!cancelled) setFullscreenLoading(false); + }); + return () => { cancelled = true; }; + }, [fullscreenRefresh, isFullscreen, monitoringFilterKey, monitoringFilters, sortBy, sortOrder]); // 全屏时禁止背景滚动 useEffect(() => { @@ -804,6 +808,9 @@ export default function MonitoringView() { initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} + role="dialog" + aria-modal="true" + aria-label="里程全屏监控" className="fixed z-[100] bg-slate-950 flex flex-col overflow-hidden" style={ forceLandscape @@ -827,13 +834,13 @@ export default function MonitoringView() {

全屏监控

- 区间 {Math.round(fullscreenStats.totalToday).toLocaleString()} km + 区间 {fullscreenHasSnapshot ? Math.round(fullscreenStats.totalToday).toLocaleString() : '—'} km | - 累计 {Math.round(fullscreenStats.totalAll).toLocaleString()} km + 累计 {fullscreenHasSnapshot ? Math.round(fullscreenStats.totalAll).toLocaleString() : '—'} km | - 车辆 {fullscreenStats.vehicleCount} + 车辆 {fullscreenHasSnapshot ? fullscreenStats.vehicleCount : '—'} | - {(fullscreenStats.vehicleCount > 0 ? (sortBy === 'total' ? fullscreenStats.totalAll : fullscreenStats.totalToday) / fullscreenStats.vehicleCount : 0).toFixed(0)} km + {fullscreenHasSnapshot ? (fullscreenStats.vehicleCount > 0 ? (sortBy === 'total' ? fullscreenStats.totalAll : fullscreenStats.totalToday) / fullscreenStats.vehicleCount : 0).toFixed(0) : '—'} km | 来源 @@ -845,11 +852,14 @@ export default function MonitoringView() {
-
@@ -879,9 +889,24 @@ export default function MonitoringView() { {/* Table Area */}
+ {fullscreenError && fullscreenHasSnapshot && ( +
+ + 刷新失败,仍展示截至 {formatMileageSnapshotTime(fullscreenUpdatedAt)} 的数据 + + +
+ )}
- {fullscreenLoading && ( + {fullscreenLoading && fullscreenHasSnapshot && (
@@ -889,7 +914,29 @@ export default function MonitoringView() {
)} - + {!fullscreenHasSnapshot && ( +
+ {fullscreenLoading ? ( +
+ + 加载全屏监控数据... +
+ ) : ( +
+

全屏监控暂不可用

+

{fullscreenError || '未取得当前筛选条件下的数据'}

+ +
+ )} +
+ )} + {fullscreenHasSnapshot &&
@@ -1017,6 +1064,13 @@ export default function MonitoringView() { + {fullscreenVehicles.length === 0 && ( + + + + )} {fullscreenVehicles.map((v) => { const highMileageAlert = isHighMileageAlert(v); const statisticTime = vehicleStatisticTime(v, relativeNow); @@ -1058,7 +1112,7 @@ export default function MonitoringView() { ); })} -
状态
+ 当前筛选条件下暂无车辆数据 +
+ }
@@ -1072,10 +1126,16 @@ export default function MonitoringView() {

里程看板

- -
@@ -1120,6 +1180,32 @@ export default function MonitoringView() {
+ {exportStatus && ( +
+ + {exportStatus.type === 'success' && } + {exportStatus.message} + + {exportStatus.type === 'error' && ( + + )} +
+ )} +