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 [vehicleFilters, setVehicleFilters] = useState>(initialRoute.filters ?? {}); const [linkIssueCount, setLinkIssueCount] = useState(null); const [currentVehicleStatus, setCurrentVehicleStatus] = useState(); const [currentVehicleLabel, setCurrentVehicleLabel] = useState(''); const [currentVehicleConsistency, setCurrentVehicleConsistency] = useState(); const loadVehicleContext = useCallback(async (keyword: string) => { const lookupKey = keyword.trim(); if (!lookupKey) { return undefined; } const overview = await api.vehicleServiceOverview(new URLSearchParams({ keyword: lookupKey })); 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).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(route.filters ?? {}); } setActiveProtocol(route.protocol ?? ''); }; 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') { replaceHash(page, analysisVin, activeProtocol); return; } replaceHash(page, undefined, undefined, page === 'vehicles' ? vehicleFilters : undefined); }; const openVehicles = (filters: Record = {}) => { setVehicleFilters(filters); setActivePage('vehicles'); replaceHash('vehicles', undefined, undefined, filters); }; const updateVehicleFilters = (filters: Record = {}) => { setVehicleFilters(filters); replaceHash('vehicles', undefined, undefined, 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); 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); setActivePage('history'); replaceHash('history', nextVin, nextProtocol); }; const openMileageForVehicle = (vin: string, protocol?: string) => { const nextVin = vin.trim(); const nextProtocol = protocol?.trim() ?? activeProtocol; if (!nextVin) { return; } setAnalysisVin(nextVin); setActiveProtocol(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: navigatePage('quality')} onOpenVehicles={openVehicles} />, vehicles: , realtime: , detail: , history: , mileage: , quality: setLinkIssueCount(health.linkHealth.filter((item) => item.status !== 'ok').length)} /> }; return ( {pages[activePage]} ); } function serviceStatusFromOverview(overview: VehicleServiceOverview): VehicleServiceStatus { const sourceCount = overview.sourceCount; const onlineSourceCount = overview.onlineSourceCount; 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 (onlineSourceCount < sourceCount) { return { status: 'degraded', severity: 'warning', title: '部分来源离线', detail: `${onlineSourceCount}/${sourceCount} 个来源在线,车辆服务可用但需要关注离线来源。`, sourceCount, onlineSourceCount }; } return { status: 'healthy', severity: 'ok', title: '服务正常', detail: `${onlineSourceCount}/${sourceCount} 个来源在线,全部已知来源在线。`, sourceCount, onlineSourceCount }; }