Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/web/src/App.tsx

655 lines
27 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 { QualityNotificationPlan, 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 { 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';
export default function App() {
const initialRoute = parseAppHash(window.location.hash);
const initialVehicleKey = initialRoute.keyword || '';
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 [historyTab, setHistoryTab] = useState(initialRoute.filters?.tab ?? (initialRoute.page === 'history-query' ? 'raw' : 'location'));
const [vehicleFilters, setVehicleFilters] = useState<Record<string, string>>(
initialRoute.page === 'vehicles' ? vehicleFiltersFromRoute(initialRoute) : {}
);
const [realtimeFilters, setRealtimeFilters] = useState<Record<string, string>>(
initialRoute.page === 'realtime' || initialRoute.page === 'map' ? realtimeFiltersFromRoute(initialRoute) : {}
);
const [historyFilters, setHistoryFilters] = useState<Record<string, string>>(
initialRoute.page === 'history' || initialRoute.page === 'history-query' ? historyFiltersFromRoute(initialRoute) : {}
);
const [mileageFilters, setMileageFilters] = useState<Record<string, string>>(
initialRoute.page === 'mileage' ? mileageFiltersFromRoute(initialRoute) : {}
);
const [qualityFilters, setQualityFilters] = useState<Record<string, string>>(
initialRoute.page === 'quality' || initialRoute.page === 'alert-events' ? qualityFiltersFromRoute(initialRoute) : {}
);
const [linkIssueCount, setLinkIssueCount] = useState<number | null>(null);
const [activeAlertRuleCount, setActiveAlertRuleCount] = useState<number | null>(null);
const [p0AlertRuleCount, setP0AlertRuleCount] = useState<number | null>(null);
const [platformRelease, setPlatformRelease] = useState('');
const [currentVehicleStatus, setCurrentVehicleStatus] = useState<VehicleServiceStatus | undefined>();
const [currentVehicleLabel, setCurrentVehicleLabel] = useState('');
const [currentVehicleConsistency, setCurrentVehicleConsistency] = useState<VehicleSourceConsistency | undefined>();
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 === 'history' || route.page === 'history-query' || route.page === 'mileage') {
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));
}
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<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 === '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 === '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<string, string> = {}) => {
setVehicleFilters(filters);
setActivePage('vehicles');
replaceVehicleHash(filters);
};
const updateVehicleFilters = (filters: Record<string, string> = {}) => {
setVehicleFilters(filters);
replaceVehicleHash(filters);
};
const replaceVehicleHash = (filters: Record<string, string> = {}) => {
const { keyword, protocol, ...restFilters } = filters;
replaceHash('vehicles', keyword, protocol, restFilters);
};
const updateRealtimeFilters = (filters: Record<string, string> = {}) => {
setRealtimeFilters(filters);
replaceRealtimeHash(filters);
};
const updateMapFilters = (filters: Record<string, string> = {}) => {
setRealtimeFilters(filters);
replaceMapHash(filters);
};
const replaceRealtimeHash = (filters: Record<string, string> = {}) => {
const { keyword, protocol, ...restFilters } = filters;
replaceHash('realtime', keyword, protocol, restFilters);
};
const replaceMapHash = (filters: Record<string, string> = {}) => {
const { keyword, protocol, ...restFilters } = filters;
replaceHash('map', keyword, protocol, restFilters);
};
const updateHistoryFilters = (filters: Record<string, unknown> = {}, tab = historyTab) => {
const hasKeywordInput = Object.prototype.hasOwnProperty.call(filters, 'keyword');
const nextFilters = normalizeHistoryFilterValues(filters);
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<string, string> = {}, 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<string, unknown> = {}) => {
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<string, string> = {}) => {
const { keyword, protocol, ...restFilters } = filters;
replaceHash('mileage', keyword ?? analysisVin, protocol ?? activeProtocol, restFilters);
};
const replaceQualityHash = (filters: Record<string, string> = {}) => {
const keyword = filters.keyword;
const protocol = filters.protocol;
const issueFilters: Record<string, string> = {};
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<string, string> = {}) => {
setQualityFilters(filters);
replaceQualityHash(filters);
};
const openQuality = (filters: Record<string, string> = {}) => {
setQualityFilters(filters);
setActivePage('alert-events');
replaceQualityHash(filters);
};
const openRealtime = (filters: Record<string, string> = {}) => {
setRealtimeFilters(filters);
setActivePage('realtime');
replaceRealtimeHash(filters);
};
const openMap = (filters: Record<string, string> = {}) => {
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<string, string> = {}) => {
const nextFilters = normalizeHistoryFilterValues(filters);
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<string, string> = {}) => {
const nextFilters = normalizeHistoryFilterValues(filters);
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<string, string> = {}) => {
const nextFilters = normalizeMileageFilterValues(filters);
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 openCustomerTask = (page: PageKey) => {
const keyword = (activeVin || analysisVin).trim();
const protocol = activeProtocol.trim();
if (!keyword) {
navigatePage(page);
return;
}
if (page === 'map') {
openMap({ keyword, protocol });
return;
}
if (page === 'realtime') {
openRealtime({ keyword, protocol });
return;
}
if (page === 'history') {
openHistoryWithFilters({ keyword, protocol });
return;
}
if (page === 'history-query') {
openRawWithFilters({ keyword, protocol, tab: 'raw' });
return;
}
if (page === 'mileage') {
openMileageWithFilters({ keyword, protocol });
return;
}
if (page === 'alert-events' || page === 'quality') {
openQuality({ keyword, protocol });
return;
}
navigatePage(page);
};
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={openQuality} onOpenMap={openMap} onOpenRealtime={openRealtime} onOpenVehicles={openVehicles} onOpenHistory={openHistoryWithFilters} onOpenMileage={openMileageWithFilters} />,
vehicles: <Vehicles onOpenVehicle={openVehicle} onOpenQuality={openQuality} onOpenMap={openMap} onOpenRealtime={openRealtime} onOpenHistory={openHistoryWithFilters} onOpenMileage={openMileageWithFilters} onFiltersChange={updateVehicleFilters} initialFilters={vehicleFilters} />,
map: <Realtime mode="map" title="车辆实时地图" description="以车辆为中心查看位置分布、在线状态、关注车辆和轨迹入口" onOpenVehicle={openVehicle} onOpenHistory={openHistoryForVehicle} onOpenQuality={openQuality} onFiltersChange={updateMapFilters} initialFilters={realtimeFilters} />,
realtime: <Realtime onOpenVehicle={openVehicle} onOpenHistory={openHistoryForVehicle} onOpenQuality={openQuality} onFiltersChange={updateRealtimeFilters} initialFilters={realtimeFilters} />,
detail: <VehicleDetail vin={activeVin} protocol={activeProtocol} platformRelease={platformRelease} onOpenRealtime={openRealtimeForVehicle} onOpenHistory={openHistoryForVehicle} onOpenRaw={openRawForVehicle} onOpenMileage={openMileageForVehicle} onOpenVehicles={openVehicles} onOpenQuality={openQuality} onOpenHistoryEvidence={openHistoryWithFilters} onOpenRawEvidence={openRawWithFilters} onQueryChange={updateVehicleDetailQuery} />,
history: <History mode="trajectory" initialVin={analysisVin} initialProtocol={activeProtocol} initialTab={historyTab} initialFilters={historyFilters} onFiltersChange={updateHistoryFilters} onOpenVehicle={openVehicle} onOpenMileage={openMileageWithFilters} onOpenRaw={openRawWithFilters} />,
'history-query': <History mode="query" initialVin={analysisVin} initialProtocol={activeProtocol} initialTab={historyTab} initialFilters={historyFilters} onFiltersChange={updateHistoryFilters} onOpenVehicle={openVehicle} onOpenMileage={openMileageWithFilters} onOpenRaw={openRawWithFilters} />,
mileage: <Mileage initialVin={analysisVin} initialProtocol={activeProtocol} initialFilters={mileageFilters} onFiltersChange={updateMileageFilters} onOpenVehicle={openVehicle} onOpenHistory={openHistoryWithFilters} onOpenRaw={openRawWithFilters} />,
'alert-events': <Quality onOpenVehicle={openVehicle} onOpenRealtime={openRealtime} onOpenHistory={openHistoryWithFilters} onOpenRaw={openRawWithFilters} onOpenMileage={openMileageWithFilters} onOpenNotificationRules={() => 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: <Quality onOpenVehicle={openVehicle} onOpenRealtime={openRealtime} onOpenHistory={openHistoryWithFilters} onOpenRaw={openRawWithFilters} onOpenMileage={openMileageWithFilters} onOpenNotificationRules={() => 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': <NotificationRules />,
'ops-quality': <OpsQuality />
};
return (
<AppShell activePage={activePage} linkIssueCount={linkIssueCount} activeAlertRuleCount={activeAlertRuleCount} p0AlertRuleCount={p0AlertRuleCount} platformRelease={platformRelease} currentVehicleStatus={currentVehicleStatus} currentVehicleLabel={currentVehicleLabel} currentVehicleConsistency={currentVehicleConsistency} onChange={navigatePage} onCustomerTask={openCustomerTask} onVehicleSearch={openVehicle}>
{pages[activePage]}
</AppShell>
);
}
function qualityFiltersFromRoute(route: ReturnType<typeof parseAppHash>): Record<string, string> {
return {
...(route.keyword ? { keyword: route.keyword } : {}),
...(route.protocol ? { protocol: route.protocol } : {}),
...(route.filters?.issueType ? { issueType: route.filters.issueType } : {})
};
}
function vehicleFiltersFromRoute(route: ReturnType<typeof parseAppHash>): Record<string, string> {
return {
...(route.keyword ? { keyword: route.keyword } : {}),
...(route.protocol ? { protocol: route.protocol } : {}),
...(route.filters ?? {})
};
}
function realtimeFiltersFromRoute(route: ReturnType<typeof parseAppHash>): Record<string, string> {
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<typeof parseAppHash>): Record<string, string> {
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<typeof parseAppHash>): Record<string, string> {
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<string, unknown> = {}): Record<string, string> {
const normalized: Record<string, string> = {};
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<string, unknown> = {}): Record<string, string> {
const normalized: Record<string, string> = {};
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
};
}