import { useCallback, useEffect, useState } from 'react'; import { Toast } from '@douyinfe/semi-ui'; import { api } from './api/client'; import type { QualityNotificationPlan, VehicleSourceConsistency, VehicleServiceOverview, VehicleServiceStatus } from './api/types'; import { buildAppHash, parseAppHash } from './domain/appRoute'; import { AppShell, type CustomerView, type PageKey } from './layout/AppShell'; import { Dashboard } from './pages/Dashboard'; import { History } from './pages/History'; import { Mileage } from './pages/Mileage'; import { NotificationRules } from './pages/NotificationRules'; import { OpsQuality } from './pages/OpsQuality'; import { Quality } from './pages/Quality'; import { Realtime } from './pages/Realtime'; import { VehicleDetail } from './pages/VehicleDetail'; import { Vehicles } from './pages/Vehicles'; const customerViewsStorageKey = 'lingniu.customerViews.v1'; export default function App() { const initialRoute = parseAppHash(window.location.hash); const initialVehicleKey = initialRoute.keyword || ''; const [activePage, setActivePage] = useState(initialRoute.page ?? 'dashboard'); const [activeVin, setActiveVin] = useState(initialVehicleKey); const [analysisVin, setAnalysisVin] = useState(initialVehicleKey); const [activeProtocol, setActiveProtocol] = useState(initialRoute.protocol ?? ''); const [historyTab, setHistoryTab] = useState(initialRoute.filters?.tab ?? (initialRoute.page === 'history-query' ? 'raw' : 'location')); const [vehicleFilters, setVehicleFilters] = useState>( initialRoute.page === 'vehicles' ? vehicleFiltersFromRoute(initialRoute) : {} ); const [realtimeFilters, setRealtimeFilters] = useState>( initialRoute.page === 'realtime' || initialRoute.page === 'map' ? realtimeFiltersFromRoute(initialRoute) : {} ); const [historyFilters, setHistoryFilters] = useState>( initialRoute.page === 'history' || initialRoute.page === 'history-query' ? historyFiltersFromRoute(initialRoute) : {} ); const [mileageFilters, setMileageFilters] = useState>( initialRoute.page === 'mileage' ? mileageFiltersFromRoute(initialRoute) : {} ); const [qualityFilters, setQualityFilters] = useState>( initialRoute.page === 'quality' || initialRoute.page === 'alert-events' ? qualityFiltersFromRoute(initialRoute) : {} ); const [customerTimeWindow, setCustomerTimeWindow] = useState>(timeWindowFiltersFromRoute(initialRoute)); const [customerViews, setCustomerViews] = useState(readCustomerViews); const [linkIssueCount, setLinkIssueCount] = useState(null); const [activeAlertRuleCount, setActiveAlertRuleCount] = useState(null); const [p0AlertRuleCount, setP0AlertRuleCount] = useState(null); const [platformRelease, setPlatformRelease] = useState(''); const [currentVehicleStatus, setCurrentVehicleStatus] = useState(); const [currentVehicleLabel, setCurrentVehicleLabel] = useState(''); const [currentVehicleConsistency, setCurrentVehicleConsistency] = useState(); const loadVehicleContext = useCallback(async (keyword: string, protocol?: string) => { const lookupKey = keyword.trim(); if (!lookupKey) { return undefined; } const params = new URLSearchParams({ keyword: lookupKey }); const source = protocol?.trim(); if (source) { params.set('protocol', source); } const overview = await api.vehicleServiceOverview(params); const nextKey = overview.vin || lookupKey; const resolved = Boolean(overview.vin); setCurrentVehicleStatus(overview.serviceStatus ?? serviceStatusFromOverview(overview)); setCurrentVehicleLabel(resolved ? [overview.plate, overview.vin || nextKey].filter(Boolean).join(' / ') : lookupKey); setCurrentVehicleConsistency(overview.sourceConsistency); return { resolved, nextKey, overview }; }, []); const applyNotificationPlanSummary = (plan?: QualityNotificationPlan | null) => { if (!plan) { return; } const activeCount = Number(plan.activeRuleCount); const p0Count = Number(plan.p0RuleCount); if (Number.isFinite(activeCount)) { setActiveAlertRuleCount(activeCount); } if (Number.isFinite(p0Count)) { setP0AlertRuleCount(p0Count); } }; const refreshOpsHealth = useCallback((showError = true) => { return Promise.all([ api.opsHealth(), api.alertEventNotificationPlan(new URLSearchParams({ limit: '5' })).catch(() => null) ]) .then(([health, notificationPlan]) => { setLinkIssueCount((health.linkHealth ?? []).filter((item) => item.status !== 'ok').length); setPlatformRelease(health.runtime?.platformRelease ?? ''); applyNotificationPlanSummary(notificationPlan); return health; }) .catch((error: Error) => { if (showError) { Toast.error(error.message); } }); }, [loadVehicleContext]); useEffect(() => { if (initialRoute.page !== 'detail' || !initialRoute.keyword) { return; } loadVehicleContext(initialRoute.keyword, initialRoute.protocol).catch((error) => { Toast.error(error instanceof Error ? error.message : '车辆查询失败'); }); }, [initialRoute.keyword, initialRoute.page, loadVehicleContext]); useEffect(() => { refreshOpsHealth(); const timer = window.setInterval(() => { refreshOpsHealth(false); }, 60000); return () => window.clearInterval(timer); }, [refreshOpsHealth]); useEffect(() => { const applyHashRoute = () => { const route = parseAppHash(window.location.hash); if (!route.page) { return; } setActivePage(route.page); if (route.keyword) { if (route.page === 'detail') { setActiveVin(route.keyword); } if (route.page === 'dashboard' || route.page === 'history' || route.page === 'history-query' || route.page === 'mileage') { setAnalysisVin(route.keyword); } if (route.page === 'time-monitor') { setAnalysisVin(route.keyword); } } if (route.page === 'vehicles') { setVehicleFilters(vehicleFiltersFromRoute(route)); } if (route.page === 'realtime' || route.page === 'map') { setRealtimeFilters(realtimeFiltersFromRoute(route)); } if (route.page === 'history' || route.page === 'history-query') { setHistoryFilters(historyFiltersFromRoute(route)); } if (route.page === 'mileage') { setMileageFilters(mileageFiltersFromRoute(route)); } if (route.page === 'quality' || route.page === 'alert-events') { setQualityFilters(qualityFiltersFromRoute(route)); } const routeTimeWindow = timeWindowFiltersFromRoute(route); if (routeTimeWindow.dateFrom || routeTimeWindow.dateTo) { setCustomerTimeWindow(routeTimeWindow); } setActiveProtocol(route.protocol ?? ''); setHistoryTab(route.filters?.tab ?? (route.page === 'history-query' ? 'raw' : 'location')); }; window.addEventListener('hashchange', applyHashRoute); return () => window.removeEventListener('hashchange', applyHashRoute); }, []); const replaceHash = (page: PageKey, keyword?: string, protocol?: string, filters?: Record) => { const nextHash = buildAppHash({ page, keyword, protocol, filters }); if (window.location.hash !== nextHash) { window.history.replaceState(null, '', nextHash); } }; const navigatePage = (page: PageKey) => { setActivePage(page); if (page === 'detail') { replaceHash(page, activeVin, activeProtocol); return; } if (page === 'history' || page === 'history-query' || page === 'mileage') { if (page === 'history') { setHistoryTab('location'); replaceHistoryHash(historyFilters, 'location', 'history'); return; } if (page === 'history-query') { setHistoryTab('raw'); replaceHistoryHash(historyFilters, 'raw', 'history-query'); return; } replaceMileageHash(mileageFilters); return; } if (page === 'time-monitor') { const keyword = (activeVin || analysisVin).trim(); const protocol = activeProtocol.trim(); replaceHash('time-monitor', keyword, protocol, customerTimeWindow); return; } if (page === 'quality' || page === 'alert-events') { replaceQualityHash(qualityFilters); return; } if (page === 'notification-rules' || page === 'ops-quality') { replaceHash(page); return; } if (page === 'vehicles') { replaceVehicleHash(vehicleFilters); return; } if (page === 'realtime') { replaceRealtimeHash(realtimeFilters); return; } if (page === 'map') { replaceMapHash(realtimeFilters); return; } replaceHash(page); }; const openVehicles = (filters: Record = {}) => { setVehicleFilters(filters); setActivePage('vehicles'); replaceVehicleHash(filters); }; const updateVehicleFilters = (filters: Record = {}) => { setVehicleFilters(filters); replaceVehicleHash(filters); }; const replaceVehicleHash = (filters: Record = {}) => { const { keyword, protocol, ...restFilters } = filters; replaceHash('vehicles', keyword, protocol, restFilters); }; const updateRealtimeFilters = (filters: Record = {}) => { setRealtimeFilters(filters); replaceRealtimeHash(filters); }; const updateMapFilters = (filters: Record = {}) => { setRealtimeFilters(filters); replaceMapHash(filters); }; const replaceRealtimeHash = (filters: Record = {}) => { const { keyword, protocol, ...restFilters } = filters; replaceHash('realtime', keyword, protocol, restFilters); }; const replaceMapHash = (filters: Record = {}) => { const { keyword, protocol, ...restFilters } = filters; replaceHash('map', keyword, protocol, restFilters); }; const updateHistoryFilters = (filters: Record = {}, tab = historyTab) => { const hasKeywordInput = Object.prototype.hasOwnProperty.call(filters, 'keyword'); const nextFilters = normalizeHistoryFilterValues(filters); setCustomerTimeWindow(timeWindowFiltersFromValues(nextFilters)); setHistoryFilters(nextFilters); if (nextFilters.keyword) { setAnalysisVin(nextFilters.keyword); } else if (hasKeywordInput) { setAnalysisVin(''); } setActiveProtocol(nextFilters.protocol ?? ''); setHistoryTab(tab); const { keyword, protocol, ...restFilters } = nextFilters; const routeFilters = { ...restFilters }; if (tab && tab !== 'location') { routeFilters.tab = tab; } replaceHash(activePage === 'history-query' ? 'history-query' : 'history', hasKeywordInput ? keyword : (keyword ?? analysisVin), protocol, routeFilters); }; const replaceHistoryHash = (filters: Record = {}, tab = historyTab, page: 'history' | 'history-query' = activePage === 'history-query' ? 'history-query' : 'history') => { const { keyword, protocol, ...restFilters } = filters; const routeFilters = { ...restFilters }; if (tab && tab !== 'location') { routeFilters.tab = tab; } replaceHash(page, keyword ?? analysisVin, protocol ?? activeProtocol, routeFilters); }; const updateMileageFilters = (filters: Record = {}) => { const nextFilters = normalizeMileageFilterValues(filters); setCustomerTimeWindow(timeWindowFiltersFromValues(nextFilters)); setMileageFilters(nextFilters); if (nextFilters.keyword) { setAnalysisVin(nextFilters.keyword); } setActiveProtocol(nextFilters.protocol ?? ''); const { keyword, protocol, ...restFilters } = nextFilters; replaceHash('mileage', keyword ?? analysisVin, protocol, restFilters); }; const replaceMileageHash = (filters: Record = {}) => { const { keyword, protocol, ...restFilters } = filters; replaceHash('mileage', keyword ?? analysisVin, protocol ?? activeProtocol, restFilters); }; const replaceQualityHash = (filters: Record = {}) => { const keyword = filters.keyword; const protocol = filters.protocol; const issueFilters: Record = {}; if (filters.issueType) { issueFilters.issueType = filters.issueType; } if (filters.dateFrom) { issueFilters.dateFrom = filters.dateFrom; } if (filters.dateTo) { issueFilters.dateTo = filters.dateTo; } replaceHash('alert-events', keyword, protocol, issueFilters); }; const updateQualityFilters = (filters: Record = {}) => { setCustomerTimeWindow(timeWindowFiltersFromValues(filters)); setQualityFilters(filters); replaceQualityHash(filters); }; const openQuality = (filters: Record = {}) => { setCustomerTimeWindow(timeWindowFiltersFromValues(filters)); setQualityFilters(filters); setActivePage('alert-events'); replaceQualityHash(filters); }; const openRealtime = (filters: Record = {}) => { setRealtimeFilters(filters); setActivePage('realtime'); replaceRealtimeHash(filters); }; const openMap = (filters: Record = {}) => { setRealtimeFilters(filters); setActivePage('map'); replaceMapHash(filters); }; const openVehicle = async (keyword: string, protocol?: string) => { const lookupKey = keyword.trim(); const nextProtocol = protocol?.trim() ?? ''; if (!lookupKey) { return; } try { const context = await loadVehicleContext(lookupKey, nextProtocol); const resolved = context?.resolved; const nextKey = context?.nextKey ?? lookupKey; const resolvedProtocol = nextProtocol || context?.overview.primaryProtocol?.trim() || ''; setActiveVin(nextKey); setAnalysisVin(nextKey); setActiveProtocol(resolvedProtocol); setActivePage('detail'); replaceHash('detail', nextKey, resolvedProtocol); if (!resolved) { Toast.warning('未匹配到车辆身份,已打开问题排查视图'); } } catch (error) { Toast.error(error instanceof Error ? error.message : '车辆查询失败'); } }; const openHistoryForVehicle = (vin: string, protocol?: string) => { const nextVin = vin.trim(); const nextProtocol = protocol?.trim() ?? activeProtocol; if (!nextVin) { return; } setAnalysisVin(nextVin); setActiveProtocol(nextProtocol); setHistoryTab('location'); setHistoryFilters({ keyword: nextVin, protocol: nextProtocol }); setActivePage('history'); replaceHash('history', nextVin, nextProtocol); }; const openHistoryWithFilters = (filters: Record = {}) => { const nextFilters = normalizeHistoryFilterValues(filters); setCustomerTimeWindow(timeWindowFiltersFromValues(nextFilters)); const hasKeywordInput = Object.prototype.hasOwnProperty.call(nextFilters, 'keyword'); const nextVin = nextFilters.keyword?.trim() || (hasKeywordInput ? '' : analysisVin); const nextProtocol = nextFilters.protocol?.trim() ?? activeProtocol; const nextTab = nextFilters.tab === 'raw' || nextFilters.tab === 'fields' ? nextFilters.tab : 'location'; const nextPage = nextTab === 'raw' || nextTab === 'fields' ? 'history-query' : 'history'; setAnalysisVin(nextVin); setActiveProtocol(nextProtocol); setHistoryTab(nextTab); setHistoryFilters({ ...nextFilters, keyword: nextVin, protocol: nextProtocol }); setActivePage(nextPage); const { keyword, protocol, ...restFilters } = nextFilters; if (nextTab === 'raw') { restFilters.tab = nextTab; } else { delete restFilters.tab; } replaceHash(nextPage, keyword ?? nextVin, protocol ?? nextProtocol, restFilters); }; const openRawForVehicle = (vin: string, protocol?: string) => { const nextVin = vin.trim(); const nextProtocol = protocol?.trim() ?? activeProtocol; if (!nextVin) { return; } setAnalysisVin(nextVin); setActiveProtocol(nextProtocol); setHistoryTab('raw'); setHistoryFilters({ keyword: nextVin, protocol: nextProtocol }); setActivePage('history-query'); replaceHash('history-query', nextVin, nextProtocol, { tab: 'raw' }); }; const openRawWithFilters = (filters: Record = {}) => { const nextFilters = normalizeHistoryFilterValues(filters); setCustomerTimeWindow(timeWindowFiltersFromValues(nextFilters)); const hasKeywordInput = Object.prototype.hasOwnProperty.call(nextFilters, 'keyword'); const nextVin = nextFilters.keyword?.trim() || (hasKeywordInput ? '' : analysisVin); const nextProtocol = nextFilters.protocol?.trim() ?? activeProtocol; setAnalysisVin(nextVin); setActiveProtocol(nextProtocol); setHistoryTab('raw'); setHistoryFilters({ ...nextFilters, keyword: nextVin, protocol: nextProtocol }); setActivePage('history-query'); const { keyword, protocol, ...restFilters } = nextFilters; replaceHash('history-query', keyword ?? nextVin, protocol ?? nextProtocol, { ...restFilters, tab: 'raw' }); }; const openRealtimeForVehicle = (vin: string, protocol?: string) => { const nextVin = vin.trim(); const nextProtocol = protocol?.trim() ?? activeProtocol; if (!nextVin) { return; } const nextFilters = { keyword: nextVin, protocol: nextProtocol }; setAnalysisVin(nextVin); setActiveProtocol(nextProtocol); setRealtimeFilters(nextFilters); setActivePage('realtime'); replaceRealtimeHash(nextFilters); }; const openMileageForVehicle = (vin: string, protocol?: string) => { const nextVin = vin.trim(); const nextProtocol = protocol?.trim() ?? activeProtocol; if (!nextVin) { return; } setAnalysisVin(nextVin); setActiveProtocol(nextProtocol); setMileageFilters({ keyword: nextVin, protocol: nextProtocol }); setActivePage('mileage'); replaceHash('mileage', nextVin, nextProtocol); }; const openMileageWithFilters = (filters: Record = {}) => { const nextFilters = normalizeMileageFilterValues(filters); setCustomerTimeWindow(timeWindowFiltersFromValues(nextFilters)); const nextVin = nextFilters.keyword?.trim() || analysisVin; const nextProtocol = nextFilters.protocol?.trim() ?? activeProtocol; setAnalysisVin(nextVin); setActiveProtocol(nextProtocol); setMileageFilters({ ...nextFilters, keyword: nextVin, protocol: nextProtocol }); setActivePage('mileage'); const { keyword, protocol, ...restFilters } = nextFilters; replaceHash('mileage', keyword ?? nextVin, protocol ?? nextProtocol, restFilters); }; const currentCustomerTaskTimeWindow = () => { const globalDateFrom = customerTimeWindow.dateFrom?.trim(); const globalDateTo = customerTimeWindow.dateTo?.trim(); return { ...(globalDateFrom ? { dateFrom: globalDateFrom } : {}), ...(globalDateTo ? { dateTo: globalDateTo } : {}) }; }; const openCustomerTask = (page: PageKey) => { const keyword = (activeVin || analysisVin).trim(); const protocol = activeProtocol.trim(); const timeWindow = currentCustomerTaskTimeWindow(); if (!keyword) { navigatePage(page); return; } const scopedFilters = { keyword, protocol, ...timeWindow }; if (page === 'map') { openMap({ keyword, protocol }); return; } if (page === 'realtime') { openRealtime({ keyword, protocol }); return; } if (page === 'history') { openHistoryWithFilters(scopedFilters); return; } if (page === 'history-query') { openRawWithFilters({ ...scopedFilters, tab: 'raw' }); return; } if (page === 'mileage') { openMileageWithFilters(scopedFilters); return; } if (page === 'time-monitor') { setActivePage('time-monitor'); replaceHash('time-monitor', keyword, protocol, timeWindow); return; } if (page === 'alert-events' || page === 'quality') { openQuality(scopedFilters); return; } navigatePage(page); }; const customerScopeURL = (page: PageKey, filters: Record = {}) => { return `${window.location.origin}${window.location.pathname}${buildAppHash({ page, keyword: filters.keyword, protocol: filters.protocol, filters })}`; }; const copyCustomerScope = async () => { const keyword = (activeVin || analysisVin).trim(); if (!keyword) { Toast.warning('请先查询一辆车'); return; } const protocol = activeProtocol.trim(); const timeWindow = currentCustomerTaskTimeWindow(); const scopedFilters = { keyword, protocol, ...timeWindow }; const rangeText = timeWindow.dateFrom || timeWindow.dateTo ? `${timeWindow.dateFrom || '未指定'} 至 ${timeWindow.dateTo || '未指定'}` : '实时范围'; const lines = [ '【客户车辆服务范围】', `车辆:${currentVehicleLabel || keyword}`, `协议:${protocol || '全部'}`, `时间窗:${rangeText}`, `实时地图:${customerScopeURL('map', { keyword, protocol })}`, `实时监控:${customerScopeURL('realtime', { keyword, protocol })}`, `轨迹回放:${customerScopeURL('history', scopedFilters)}`, `里程统计:${customerScopeURL('mileage', scopedFilters)}`, `数据导出:${customerScopeURL('history-query', { ...scopedFilters, tab: 'raw' })}`, `告警通知:${customerScopeURL('alert-events', scopedFilters)}` ]; try { await navigator.clipboard.writeText(lines.join('\n')); Toast.success('已复制客户服务范围'); } catch { Toast.error('复制客户服务范围失败'); } }; const saveCustomerView = () => { const keyword = (activeVin || analysisVin).trim(); if (!keyword) { Toast.warning('请先查询一辆车'); return; } const protocol = activeProtocol.trim(); const timeWindow = currentCustomerTaskTimeWindow(); const view: CustomerView = { id: [keyword, protocol, timeWindow.dateFrom ?? '', timeWindow.dateTo ?? ''].join('|'), label: currentVehicleLabel || keyword, keyword, protocol, ...timeWindow }; setCustomerViews((prev) => { const next = [view, ...prev.filter((item) => item.id !== view.id)].slice(0, 5); writeCustomerViews(next); return next; }); Toast.success('已保存客户视图'); }; const openCustomerView = (view: CustomerView) => { const keyword = view.keyword.trim(); if (!keyword) { return; } const protocol = view.protocol?.trim() ?? ''; const timeWindow = { ...(view.dateFrom ? { dateFrom: view.dateFrom } : {}), ...(view.dateTo ? { dateTo: view.dateTo } : {}) }; setActiveVin(keyword); setAnalysisVin(keyword); setActiveProtocol(protocol); setCurrentVehicleLabel(view.label || keyword); setCustomerTimeWindow(timeWindow); setActivePage('detail'); replaceHash('detail', keyword, protocol); }; const updateVehicleDetailQuery = (keyword: string, protocol?: string) => { const nextKeyword = keyword.trim(); const nextProtocol = protocol?.trim() ?? ''; if (!nextKeyword) { return; } setActiveVin(nextKeyword); setActiveProtocol(nextProtocol); replaceHash('detail', nextKeyword, nextProtocol); }; const pages: Record = { dashboard: , 'time-monitor': , vehicles: , map: , realtime: , detail: , history: , 'history-query': , mileage: , 'alert-events': navigatePage('notification-rules')} onHealthLoaded={(health) => { setLinkIssueCount((health.linkHealth ?? []).filter((item) => item.status !== 'ok').length); setPlatformRelease(health.runtime?.platformRelease ?? ''); }} onNotificationPlanLoaded={applyNotificationPlanSummary} onFiltersChange={updateQualityFilters} initialFilters={qualityFilters} />, quality: navigatePage('notification-rules')} onHealthLoaded={(health) => { setLinkIssueCount((health.linkHealth ?? []).filter((item) => item.status !== 'ok').length); setPlatformRelease(health.runtime?.platformRelease ?? ''); }} onNotificationPlanLoaded={applyNotificationPlanSummary} onFiltersChange={updateQualityFilters} initialFilters={qualityFilters} />, 'notification-rules': , 'ops-quality': }; return ( {pages[activePage]} ); } function readCustomerViews(): CustomerView[] { try { const parsed = JSON.parse(window.localStorage.getItem(customerViewsStorageKey) || '[]'); return Array.isArray(parsed) ? parsed.filter((item): item is CustomerView => Boolean(item?.id && item?.label && item?.keyword)).slice(0, 5) : []; } catch { return []; } } function writeCustomerViews(views: CustomerView[]) { window.localStorage.setItem(customerViewsStorageKey, JSON.stringify(views.slice(0, 5))); } function timeWindowFiltersFromRoute(route: ReturnType): Record { return timeWindowFiltersFromValues(route.filters ?? {}); } function timeWindowFiltersFromValues(filters: Record): Record { const dateFrom = String(filters.dateFrom ?? '').trim(); const dateTo = String(filters.dateTo ?? '').trim(); return { ...(dateFrom ? { dateFrom } : {}), ...(dateTo ? { dateTo } : {}) }; } function qualityFiltersFromRoute(route: ReturnType): Record { return { ...(route.keyword ? { keyword: route.keyword } : {}), ...(route.protocol ? { protocol: route.protocol } : {}), ...(route.filters?.issueType ? { issueType: route.filters.issueType } : {}), ...(route.filters?.dateFrom ? { dateFrom: route.filters.dateFrom } : {}), ...(route.filters?.dateTo ? { dateTo: route.filters.dateTo } : {}) }; } function vehicleFiltersFromRoute(route: ReturnType): Record { return { ...(route.keyword ? { keyword: route.keyword } : {}), ...(route.protocol ? { protocol: route.protocol } : {}), ...(route.filters ?? {}) }; } function realtimeFiltersFromRoute(route: ReturnType): Record { return { ...(route.keyword ? { keyword: route.keyword } : {}), ...(route.protocol ? { protocol: route.protocol } : {}), ...(route.filters?.online ? { online: route.filters.online } : {}), ...(route.filters?.serviceStatus ? { serviceStatus: route.filters.serviceStatus } : {}) }; } function historyFiltersFromRoute(route: ReturnType): Record { return normalizeHistoryFilterValues({ ...(route.keyword ? { keyword: route.keyword } : {}), ...(route.protocol ? { protocol: route.protocol } : {}), ...(route.filters?.dateFrom ? { dateFrom: route.filters.dateFrom } : {}), ...(route.filters?.dateTo ? { dateTo: route.filters.dateTo } : {}), ...(route.filters?.includeFields ? { includeFields: route.filters.includeFields } : {}), ...(route.filters?.fields ? { fields: route.filters.fields } : {}) }); } function mileageFiltersFromRoute(route: ReturnType): Record { return normalizeMileageFilterValues({ ...(route.keyword ? { keyword: route.keyword } : {}), ...(route.protocol ? { protocol: route.protocol } : {}), ...(route.filters?.dateFrom ? { dateFrom: route.filters.dateFrom } : {}), ...(route.filters?.dateTo ? { dateTo: route.filters.dateTo } : {}) }); } function normalizeMileageFilterValues(filters: Record = {}): Record { const normalized: Record = {}; for (const key of ['keyword', 'protocol', 'dateFrom', 'dateTo'] as const) { const value = String(filters[key] ?? '').trim(); if (value) { normalized[key] = value; } } return normalized; } function normalizeHistoryFilterValues(filters: Record = {}): Record { const normalized: Record = {}; for (const key of ['keyword', 'protocol', 'dateFrom', 'dateTo', 'fields', 'tab'] as const) { const value = String(filters[key] ?? '').trim(); if (value) { normalized[key] = value; } } const includeFields = filters.includeFields; if (includeFields === true || includeFields === 'true') { normalized.includeFields = 'true'; } return normalized; } function serviceStatusFromOverview(overview: VehicleServiceOverview): VehicleServiceStatus { const sourceCount = overview.sourceCount; const onlineSourceCount = overview.onlineSourceCount; const missingProtocols = overview.sourceConsistency?.missingProtocols ?? []; if (!overview.vin) { return { status: 'identity_required', severity: 'warning', title: '身份未绑定', detail: '车辆关键词暂未解析到 VIN,需先维护身份绑定后才能形成完整车辆服务。', sourceCount, onlineSourceCount }; } if (sourceCount <= 0) { return { status: 'no_data', severity: 'warning', title: '暂无数据来源', detail: '车辆已解析,但暂未查询到 32960、808 或 MQTT 数据来源。', sourceCount, onlineSourceCount }; } if (onlineSourceCount <= 0) { return { status: 'offline', severity: 'error', title: '车辆离线', detail: '所有已知数据来源均未在线,需要检查平台转发、终端上报或链路状态。', sourceCount, onlineSourceCount }; } if (missingProtocols.length > 0) { return { status: 'degraded', severity: 'warning', title: '来源不完整', detail: `${onlineSourceCount}/${sourceCount} 个来源在线,车辆服务可用但缺少 ${missingProtocols.join('、')} 来源。`, sourceCount, onlineSourceCount }; } if (onlineSourceCount < sourceCount) { return { status: 'degraded', severity: 'warning', title: '来源不完整', detail: `${onlineSourceCount}/${sourceCount} 个来源在线,车辆服务可用但需要关注离线来源。`, sourceCount, onlineSourceCount }; } return { status: 'healthy', severity: 'ok', title: '服务正常', detail: `${onlineSourceCount}/${sourceCount} 个来源在线,全部已知来源在线。`, sourceCount, onlineSourceCount }; }