import { useCallback, useEffect, useState } from 'react'; import { Toast } from '@douyinfe/semi-ui'; import { api } from './api/client'; import type { VehicleSourceConsistency, VehicleServiceOverview, VehicleServiceStatus } from './api/types'; import { buildAppHash, parseAppHash } from './domain/appRoute'; import { AppShell, type PageKey } from './layout/AppShell'; import { Dashboard } from './pages/Dashboard'; import { History } from './pages/History'; import { Mileage } from './pages/Mileage'; import { Quality } from './pages/Quality'; import { Realtime } from './pages/Realtime'; import { VehicleDetail } from './pages/VehicleDetail'; import { Vehicles } from './pages/Vehicles'; export default function App() { const initialRoute = parseAppHash(window.location.hash); const initialVehicleKey = initialRoute.keyword || 'LB9A32A24R0LS1426'; 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 ?? 'location'); const [vehicleFilters, setVehicleFilters] = useState>( initialRoute.page === 'vehicles' ? vehicleFiltersFromRoute(initialRoute) : {} ); const [realtimeFilters, setRealtimeFilters] = useState>( initialRoute.page === 'realtime' ? realtimeFiltersFromRoute(initialRoute) : {} ); const [historyFilters, setHistoryFilters] = useState>( initialRoute.page === 'history' ? historyFiltersFromRoute(initialRoute) : {} ); const [mileageFilters, setMileageFilters] = useState>( initialRoute.page === 'mileage' ? mileageFiltersFromRoute(initialRoute) : {} ); const [qualityFilters, setQualityFilters] = useState>( initialRoute.page === 'quality' ? qualityFiltersFromRoute(initialRoute) : {} ); const [linkIssueCount, setLinkIssueCount] = useState(null); 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 refreshOpsHealth = useCallback((showError = true) => { return api.opsHealth() .then((health) => { setLinkIssueCount(health.linkHealth.filter((item) => item.status !== 'ok').length); 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 === 'history' || route.page === 'mileage') { setAnalysisVin(route.keyword); } } if (route.page === 'vehicles') { setVehicleFilters(vehicleFiltersFromRoute(route)); } if (route.page === 'realtime') { setRealtimeFilters(realtimeFiltersFromRoute(route)); } if (route.page === 'history') { setHistoryFilters(historyFiltersFromRoute(route)); } if (route.page === 'mileage') { setMileageFilters(mileageFiltersFromRoute(route)); } if (route.page === 'quality') { setQualityFilters(qualityFiltersFromRoute(route)); } setActiveProtocol(route.protocol ?? ''); setHistoryTab(route.filters?.tab ?? '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 === 'mileage') { if (page === 'history') { replaceHistoryHash(historyFilters); return; } replaceMileageHash(mileageFilters); return; } if (page === 'quality') { replaceQualityHash(qualityFilters); return; } if (page === 'vehicles') { replaceVehicleHash(vehicleFilters); return; } if (page === 'realtime') { replaceRealtimeHash(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 replaceRealtimeHash = (filters: Record = {}) => { const { keyword, protocol, ...restFilters } = filters; replaceHash('realtime', keyword, protocol, restFilters); }; const updateHistoryFilters = (filters: Record = {}, tab = historyTab) => { const nextFilters = normalizeHistoryFilterValues(filters); setHistoryFilters(nextFilters); if (nextFilters.keyword) { setAnalysisVin(nextFilters.keyword); } setActiveProtocol(nextFilters.protocol ?? ''); setHistoryTab(tab); const { keyword, protocol, ...restFilters } = nextFilters; const routeFilters = { ...restFilters }; if (tab && tab !== 'location') { routeFilters.tab = tab; } replaceHash('history', keyword ?? analysisVin, protocol, routeFilters); }; const replaceHistoryHash = (filters: Record = {}, tab = historyTab) => { const { keyword, protocol, ...restFilters } = filters; const routeFilters = { ...restFilters }; if (tab && tab !== 'location') { routeFilters.tab = tab; } replaceHash('history', keyword ?? analysisVin, protocol ?? activeProtocol, routeFilters); }; const updateMileageFilters = (filters: Record = {}) => { const nextFilters = normalizeMileageFilterValues(filters); 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; } replaceHash('quality', keyword, protocol, issueFilters); }; const updateQualityFilters = (filters: Record = {}) => { setQualityFilters(filters); replaceQualityHash(filters); }; const openQuality = (filters: Record = {}) => { setQualityFilters(filters); setActivePage('quality'); replaceQualityHash(filters); }; const openRealtime = (filters: Record = {}) => { setRealtimeFilters(filters); setActivePage('realtime'); replaceRealtimeHash(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; setActiveVin(nextKey); setActiveProtocol(nextProtocol); setActivePage('detail'); replaceHash('detail', nextKey, nextProtocol); 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); const nextVin = nextFilters.keyword?.trim() || analysisVin; const nextProtocol = nextFilters.protocol?.trim() ?? activeProtocol; setAnalysisVin(nextVin); setActiveProtocol(nextProtocol); setHistoryTab('location'); setHistoryFilters({ ...nextFilters, keyword: nextVin, protocol: nextProtocol }); setActivePage('history'); const { keyword, protocol, ...restFilters } = nextFilters; replaceHash('history', 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'); replaceHash('history', nextVin, nextProtocol, { 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 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: , vehicles: , realtime: , detail: , history: , mileage: , quality: setLinkIssueCount(health.linkHealth.filter((item) => item.status !== 'ok').length)} onFiltersChange={updateQualityFilters} initialFilters={qualityFilters} /> }; return ( {pages[activePage]} ); } function qualityFiltersFromRoute(route: ReturnType): Record { return { ...(route.keyword ? { keyword: route.keyword } : {}), ...(route.protocol ? { protocol: route.protocol } : {}), ...(route.filters?.issueType ? { issueType: route.filters.issueType } : {}) }; } 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'] 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 }; }