Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/web/src/App.tsx
2026-07-04 04:06:54 +08:00

253 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<PageKey>(initialRoute.page ?? 'dashboard');
const [activeVin, setActiveVin] = useState(initialVehicleKey);
const [analysisVin, setAnalysisVin] = useState(initialVehicleKey);
const [activeProtocol, setActiveProtocol] = useState(initialRoute.protocol ?? '');
const [vehicleFilters, setVehicleFilters] = useState<Record<string, string>>(initialRoute.filters ?? {});
const [linkIssueCount, setLinkIssueCount] = useState<number | null>(null);
const [currentVehicleStatus, setCurrentVehicleStatus] = useState<VehicleServiceStatus | undefined>();
const [currentVehicleLabel, setCurrentVehicleLabel] = useState('');
const [currentVehicleConsistency, setCurrentVehicleConsistency] = useState<VehicleSourceConsistency | undefined>();
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<string, string>) => {
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<string, string> = {}) => {
setVehicleFilters(filters);
setActivePage('vehicles');
replaceHash('vehicles', undefined, undefined, filters);
};
const updateVehicleFilters = (filters: Record<string, string> = {}) => {
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<PageKey, JSX.Element> = {
dashboard: <Dashboard onOpenVehicle={openVehicle} onOpenQuality={() => navigatePage('quality')} onOpenVehicles={openVehicles} />,
vehicles: <Vehicles onOpenVehicle={openVehicle} onFiltersChange={updateVehicleFilters} initialFilters={vehicleFilters} />,
realtime: <Realtime onOpenVehicle={openVehicle} />,
detail: <VehicleDetail vin={activeVin} protocol={activeProtocol} onOpenHistory={openHistoryForVehicle} onOpenMileage={openMileageForVehicle} onQueryChange={updateVehicleDetailQuery} />,
history: <History initialVin={analysisVin} initialProtocol={activeProtocol} onOpenVehicle={openVehicle} />,
mileage: <Mileage initialVin={analysisVin} initialProtocol={activeProtocol} onOpenVehicle={openVehicle} />,
quality: <Quality onOpenVehicle={openVehicle} onHealthLoaded={(health) => setLinkIssueCount(health.linkHealth.filter((item) => item.status !== 'ok').length)} />
};
return (
<AppShell activePage={activePage} linkIssueCount={linkIssueCount} currentVehicleStatus={currentVehicleStatus} currentVehicleLabel={currentVehicleLabel} currentVehicleConsistency={currentVehicleConsistency} onChange={navigatePage} onVehicleSearch={openVehicle}>
{pages[activePage]}
</AppShell>
);
}
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
};
}