feat(platform): split trajectory replay and history query
This commit is contained in:
@@ -1045,7 +1045,7 @@ func buildQualityPriorityIssue(row QualityIssueRow) QualityPriorityIssue {
|
|||||||
rawFilters := cloneStringMap(historyFilters)
|
rawFilters := cloneStringMap(historyFilters)
|
||||||
rawFilters["tab"] = "raw"
|
rawFilters["tab"] = "raw"
|
||||||
rawFilters["includeFields"] = "true"
|
rawFilters["includeFields"] = "true"
|
||||||
rawHash := buildHash("history", lookup, row.Protocol, rawFilters)
|
rawHash := buildHash("history-query", lookup, row.Protocol, rawFilters)
|
||||||
vehicleHash := buildHash("detail", lookup, row.Protocol, nil)
|
vehicleHash := buildHash("detail", lookup, row.Protocol, nil)
|
||||||
item := QualityPriorityIssue{
|
item := QualityPriorityIssue{
|
||||||
QualityIssueRow: row,
|
QualityIssueRow: row,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default function App() {
|
|||||||
const [activeVin, setActiveVin] = useState(initialVehicleKey);
|
const [activeVin, setActiveVin] = useState(initialVehicleKey);
|
||||||
const [analysisVin, setAnalysisVin] = useState(initialVehicleKey);
|
const [analysisVin, setAnalysisVin] = useState(initialVehicleKey);
|
||||||
const [activeProtocol, setActiveProtocol] = useState(initialRoute.protocol ?? '');
|
const [activeProtocol, setActiveProtocol] = useState(initialRoute.protocol ?? '');
|
||||||
const [historyTab, setHistoryTab] = useState(initialRoute.filters?.tab ?? 'location');
|
const [historyTab, setHistoryTab] = useState(initialRoute.filters?.tab ?? (initialRoute.page === 'history-query' ? 'raw' : 'location'));
|
||||||
const [vehicleFilters, setVehicleFilters] = useState<Record<string, string>>(
|
const [vehicleFilters, setVehicleFilters] = useState<Record<string, string>>(
|
||||||
initialRoute.page === 'vehicles' ? vehicleFiltersFromRoute(initialRoute) : {}
|
initialRoute.page === 'vehicles' ? vehicleFiltersFromRoute(initialRoute) : {}
|
||||||
);
|
);
|
||||||
@@ -29,7 +29,7 @@ export default function App() {
|
|||||||
initialRoute.page === 'realtime' || initialRoute.page === 'map' ? realtimeFiltersFromRoute(initialRoute) : {}
|
initialRoute.page === 'realtime' || initialRoute.page === 'map' ? realtimeFiltersFromRoute(initialRoute) : {}
|
||||||
);
|
);
|
||||||
const [historyFilters, setHistoryFilters] = useState<Record<string, string>>(
|
const [historyFilters, setHistoryFilters] = useState<Record<string, string>>(
|
||||||
initialRoute.page === 'history' ? historyFiltersFromRoute(initialRoute) : {}
|
initialRoute.page === 'history' || initialRoute.page === 'history-query' ? historyFiltersFromRoute(initialRoute) : {}
|
||||||
);
|
);
|
||||||
const [mileageFilters, setMileageFilters] = useState<Record<string, string>>(
|
const [mileageFilters, setMileageFilters] = useState<Record<string, string>>(
|
||||||
initialRoute.page === 'mileage' ? mileageFiltersFromRoute(initialRoute) : {}
|
initialRoute.page === 'mileage' ? mileageFiltersFromRoute(initialRoute) : {}
|
||||||
@@ -104,7 +104,7 @@ export default function App() {
|
|||||||
if (route.page === 'detail') {
|
if (route.page === 'detail') {
|
||||||
setActiveVin(route.keyword);
|
setActiveVin(route.keyword);
|
||||||
}
|
}
|
||||||
if (route.page === 'history' || route.page === 'mileage') {
|
if (route.page === 'history' || route.page === 'history-query' || route.page === 'mileage') {
|
||||||
setAnalysisVin(route.keyword);
|
setAnalysisVin(route.keyword);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,7 +114,7 @@ export default function App() {
|
|||||||
if (route.page === 'realtime' || route.page === 'map') {
|
if (route.page === 'realtime' || route.page === 'map') {
|
||||||
setRealtimeFilters(realtimeFiltersFromRoute(route));
|
setRealtimeFilters(realtimeFiltersFromRoute(route));
|
||||||
}
|
}
|
||||||
if (route.page === 'history') {
|
if (route.page === 'history' || route.page === 'history-query') {
|
||||||
setHistoryFilters(historyFiltersFromRoute(route));
|
setHistoryFilters(historyFiltersFromRoute(route));
|
||||||
}
|
}
|
||||||
if (route.page === 'mileage') {
|
if (route.page === 'mileage') {
|
||||||
@@ -124,7 +124,7 @@ export default function App() {
|
|||||||
setQualityFilters(qualityFiltersFromRoute(route));
|
setQualityFilters(qualityFiltersFromRoute(route));
|
||||||
}
|
}
|
||||||
setActiveProtocol(route.protocol ?? '');
|
setActiveProtocol(route.protocol ?? '');
|
||||||
setHistoryTab(route.filters?.tab ?? 'location');
|
setHistoryTab(route.filters?.tab ?? (route.page === 'history-query' ? 'raw' : 'location'));
|
||||||
};
|
};
|
||||||
window.addEventListener('hashchange', applyHashRoute);
|
window.addEventListener('hashchange', applyHashRoute);
|
||||||
return () => window.removeEventListener('hashchange', applyHashRoute);
|
return () => window.removeEventListener('hashchange', applyHashRoute);
|
||||||
@@ -143,9 +143,15 @@ export default function App() {
|
|||||||
replaceHash(page, activeVin, activeProtocol);
|
replaceHash(page, activeVin, activeProtocol);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (page === 'history' || page === 'mileage') {
|
if (page === 'history' || page === 'history-query' || page === 'mileage') {
|
||||||
if (page === 'history') {
|
if (page === 'history') {
|
||||||
replaceHistoryHash(historyFilters);
|
setHistoryTab('location');
|
||||||
|
replaceHistoryHash(historyFilters, 'location', 'history');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (page === 'history-query') {
|
||||||
|
setHistoryTab('raw');
|
||||||
|
replaceHistoryHash(historyFilters, 'raw', 'history-query');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
replaceMileageHash(mileageFilters);
|
replaceMileageHash(mileageFilters);
|
||||||
@@ -226,16 +232,16 @@ export default function App() {
|
|||||||
if (tab && tab !== 'location') {
|
if (tab && tab !== 'location') {
|
||||||
routeFilters.tab = tab;
|
routeFilters.tab = tab;
|
||||||
}
|
}
|
||||||
replaceHash('history', hasKeywordInput ? keyword : (keyword ?? analysisVin), protocol, routeFilters);
|
replaceHash(activePage === 'history-query' ? 'history-query' : 'history', hasKeywordInput ? keyword : (keyword ?? analysisVin), protocol, routeFilters);
|
||||||
};
|
};
|
||||||
|
|
||||||
const replaceHistoryHash = (filters: Record<string, string> = {}, tab = historyTab) => {
|
const replaceHistoryHash = (filters: Record<string, string> = {}, tab = historyTab, page: 'history' | 'history-query' = activePage === 'history-query' ? 'history-query' : 'history') => {
|
||||||
const { keyword, protocol, ...restFilters } = filters;
|
const { keyword, protocol, ...restFilters } = filters;
|
||||||
const routeFilters = { ...restFilters };
|
const routeFilters = { ...restFilters };
|
||||||
if (tab && tab !== 'location') {
|
if (tab && tab !== 'location') {
|
||||||
routeFilters.tab = tab;
|
routeFilters.tab = tab;
|
||||||
}
|
}
|
||||||
replaceHash('history', keyword ?? analysisVin, protocol ?? activeProtocol, routeFilters);
|
replaceHash(page, keyword ?? analysisVin, protocol ?? activeProtocol, routeFilters);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateMileageFilters = (filters: Record<string, unknown> = {}) => {
|
const updateMileageFilters = (filters: Record<string, unknown> = {}) => {
|
||||||
@@ -328,18 +334,19 @@ export default function App() {
|
|||||||
const nextVin = nextFilters.keyword?.trim() || analysisVin;
|
const nextVin = nextFilters.keyword?.trim() || analysisVin;
|
||||||
const nextProtocol = nextFilters.protocol?.trim() ?? activeProtocol;
|
const nextProtocol = nextFilters.protocol?.trim() ?? activeProtocol;
|
||||||
const nextTab = nextFilters.tab === 'raw' ? 'raw' : 'location';
|
const nextTab = nextFilters.tab === 'raw' ? 'raw' : 'location';
|
||||||
|
const nextPage = nextTab === 'raw' ? 'history-query' : 'history';
|
||||||
setAnalysisVin(nextVin);
|
setAnalysisVin(nextVin);
|
||||||
setActiveProtocol(nextProtocol);
|
setActiveProtocol(nextProtocol);
|
||||||
setHistoryTab(nextTab);
|
setHistoryTab(nextTab);
|
||||||
setHistoryFilters({ ...nextFilters, keyword: nextVin, protocol: nextProtocol });
|
setHistoryFilters({ ...nextFilters, keyword: nextVin, protocol: nextProtocol });
|
||||||
setActivePage('history');
|
setActivePage(nextPage);
|
||||||
const { keyword, protocol, ...restFilters } = nextFilters;
|
const { keyword, protocol, ...restFilters } = nextFilters;
|
||||||
if (nextTab === 'raw') {
|
if (nextTab === 'raw') {
|
||||||
restFilters.tab = nextTab;
|
restFilters.tab = nextTab;
|
||||||
} else {
|
} else {
|
||||||
delete restFilters.tab;
|
delete restFilters.tab;
|
||||||
}
|
}
|
||||||
replaceHash('history', keyword ?? nextVin, protocol ?? nextProtocol, restFilters);
|
replaceHash(nextPage, keyword ?? nextVin, protocol ?? nextProtocol, restFilters);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openRawForVehicle = (vin: string, protocol?: string) => {
|
const openRawForVehicle = (vin: string, protocol?: string) => {
|
||||||
@@ -352,8 +359,8 @@ export default function App() {
|
|||||||
setActiveProtocol(nextProtocol);
|
setActiveProtocol(nextProtocol);
|
||||||
setHistoryTab('raw');
|
setHistoryTab('raw');
|
||||||
setHistoryFilters({ keyword: nextVin, protocol: nextProtocol });
|
setHistoryFilters({ keyword: nextVin, protocol: nextProtocol });
|
||||||
setActivePage('history');
|
setActivePage('history-query');
|
||||||
replaceHash('history', nextVin, nextProtocol, { tab: 'raw' });
|
replaceHash('history-query', nextVin, nextProtocol, { tab: 'raw' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const openRawWithFilters = (filters: Record<string, string> = {}) => {
|
const openRawWithFilters = (filters: Record<string, string> = {}) => {
|
||||||
@@ -367,9 +374,9 @@ export default function App() {
|
|||||||
setActiveProtocol(nextProtocol);
|
setActiveProtocol(nextProtocol);
|
||||||
setHistoryTab('raw');
|
setHistoryTab('raw');
|
||||||
setHistoryFilters({ ...nextFilters, keyword: nextVin, protocol: nextProtocol });
|
setHistoryFilters({ ...nextFilters, keyword: nextVin, protocol: nextProtocol });
|
||||||
setActivePage('history');
|
setActivePage('history-query');
|
||||||
const { keyword, protocol, ...restFilters } = nextFilters;
|
const { keyword, protocol, ...restFilters } = nextFilters;
|
||||||
replaceHash('history', keyword ?? nextVin, protocol ?? nextProtocol, { ...restFilters, tab: 'raw' });
|
replaceHash('history-query', keyword ?? nextVin, protocol ?? nextProtocol, { ...restFilters, tab: 'raw' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const openRealtimeForVehicle = (vin: string, protocol?: string) => {
|
const openRealtimeForVehicle = (vin: string, protocol?: string) => {
|
||||||
@@ -431,7 +438,8 @@ export default function App() {
|
|||||||
map: <Realtime title="地图态势" description="以车辆服务为中心查看实时位置、在线状态、来源一致性和高德地图接入状态" onOpenVehicle={openVehicle} onOpenHistory={openHistoryForVehicle} onOpenQuality={openQuality} onFiltersChange={updateMapFilters} initialFilters={realtimeFilters} />,
|
map: <Realtime title="地图态势" description="以车辆服务为中心查看实时位置、在线状态、来源一致性和高德地图接入状态" onOpenVehicle={openVehicle} onOpenHistory={openHistoryForVehicle} onOpenQuality={openQuality} onFiltersChange={updateMapFilters} initialFilters={realtimeFilters} />,
|
||||||
realtime: <Realtime onOpenVehicle={openVehicle} onOpenHistory={openHistoryForVehicle} onOpenQuality={openQuality} onFiltersChange={updateRealtimeFilters} 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} />,
|
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 initialVin={analysisVin} initialProtocol={activeProtocol} initialTab={historyTab} initialFilters={historyFilters} onFiltersChange={updateHistoryFilters} onOpenVehicle={openVehicle} onOpenMileage={openMileageWithFilters} onOpenRaw={openRawWithFilters} />,
|
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} />,
|
mileage: <Mileage initialVin={analysisVin} initialProtocol={activeProtocol} initialFilters={mileageFilters} onFiltersChange={updateMileageFilters} onOpenVehicle={openVehicle} onOpenHistory={openHistoryWithFilters} onOpenRaw={openRawWithFilters} />,
|
||||||
quality: <Quality onOpenVehicle={openVehicle} onOpenRealtime={openRealtime} onOpenHistory={openHistoryWithFilters} onOpenRaw={openRawWithFilters} onOpenMileage={openMileageWithFilters} onOpenNotificationRules={() => navigatePage('notification-rules')} onHealthLoaded={(health) => {
|
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);
|
setLinkIssueCount(health.linkHealth.filter((item) => item.status !== 'ok').length);
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ test('qualityNotificationPlan reads alert rules and priority issues from backend
|
|||||||
summary: { issueVehicleCount: 1, issueRecordCount: 1, errorCount: 1, warningCount: 0, protocols: [], issueTypes: [] },
|
summary: { issueVehicleCount: 1, issueRecordCount: 1, errorCount: 1, warningCount: 0, protocols: [], issueTypes: [] },
|
||||||
rules: [{ issueType: 'NO_SOURCE', title: '无数据来源', level: 'P0', owner: '平台接入', trigger: '无来源', notify: '立即通知', sla: '30 分钟确认', count: 1 }],
|
rules: [{ issueType: 'NO_SOURCE', title: '无数据来源', level: 'P0', owner: '平台接入', trigger: '无来源', notify: '立即通知', sla: '30 分钟确认', count: 1 }],
|
||||||
policies: [{ name: 'P0 实时中断', target: '接入运维', channel: '邮件', condition: '无来源', escalationMinutes: 30, acceptanceCriteria: '来源恢复并持续 10 分钟' }],
|
policies: [{ name: 'P0 实时中断', target: '接入运维', channel: '邮件', condition: '无来源', escalationMinutes: 30, acceptanceCriteria: '来源恢复并持续 10 分钟' }],
|
||||||
priorityIssues: [{ vin: 'VIN001', plate: '粤A001', phone: '', sourceEndpoint: '', protocol: 'VEHICLE_SERVICE', issueType: 'NO_SOURCE', severity: 'error', lastSeen: '', detail: '无来源', priority: 'P0', actionLabel: '确认平台转发', actionDetail: '确认平台转发', sla: '30 分钟确认', vehicleLabel: '粤A001 / VIN001', realtimeHash: '#/realtime?keyword=VIN001', historyHash: '#/history?keyword=VIN001', rawHash: '#/history?keyword=VIN001&tab=raw', vehicleHash: '#/detail?keyword=VIN001', notificationText: '【P0 告警通知】无数据来源' }],
|
priorityIssues: [{ vin: 'VIN001', plate: '粤A001', phone: '', sourceEndpoint: '', protocol: 'VEHICLE_SERVICE', issueType: 'NO_SOURCE', severity: 'error', lastSeen: '', detail: '无来源', priority: 'P0', actionLabel: '确认平台转发', actionDetail: '确认平台转发', sla: '30 分钟确认', vehicleLabel: '粤A001 / VIN001', realtimeHash: '#/realtime?keyword=VIN001', historyHash: '#/history?keyword=VIN001', rawHash: '#/history-query?keyword=VIN001&tab=raw', vehicleHash: '#/detail?keyword=VIN001', notificationText: '【P0 告警通知】无数据来源' }],
|
||||||
activeRuleCount: 1,
|
activeRuleCount: 1,
|
||||||
p0RuleCount: 1
|
p0RuleCount: 1
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,6 +22,17 @@ describe('parseAppHash', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('parses history query page', () => {
|
||||||
|
expect(parseAppHash('#/history-query?keyword=VIN001&protocol=GB32960&tab=raw')).toEqual({
|
||||||
|
page: 'history-query',
|
||||||
|
keyword: 'VIN001',
|
||||||
|
protocol: 'GB32960',
|
||||||
|
filters: {
|
||||||
|
tab: 'raw'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('parses vehicle list filters from hash query', () => {
|
test('parses vehicle list filters from hash query', () => {
|
||||||
expect(parseAppHash('#/vehicles?coverage=multi&serviceStatus=degraded&online=online&bindingStatus=bound&archiveStatus=incomplete&archiveMissing=phone&missingProtocol=YUTONG_MQTT')).toEqual({
|
expect(parseAppHash('#/vehicles?coverage=multi&serviceStatus=degraded&online=online&bindingStatus=bound&archiveStatus=incomplete&archiveMissing=phone&missingProtocol=YUTONG_MQTT')).toEqual({
|
||||||
page: 'vehicles',
|
page: 'vehicles',
|
||||||
@@ -87,6 +98,10 @@ describe('buildAppHash', () => {
|
|||||||
expect(buildAppHash({ page: 'history', keyword: 'VIN001', protocol: 'JT808', filters: { tab: 'raw' } })).toBe('#/history?keyword=VIN001&protocol=JT808&tab=raw');
|
expect(buildAppHash({ page: 'history', keyword: 'VIN001', protocol: 'JT808', filters: { tab: 'raw' } })).toBe('#/history?keyword=VIN001&protocol=JT808&tab=raw');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('builds shareable history query hash', () => {
|
||||||
|
expect(buildAppHash({ page: 'history-query', keyword: 'VIN001', protocol: 'GB32960', filters: { tab: 'raw' } })).toBe('#/history-query?keyword=VIN001&protocol=GB32960&tab=raw');
|
||||||
|
});
|
||||||
|
|
||||||
test('builds shareable vehicle list hash with filters', () => {
|
test('builds shareable vehicle list hash with filters', () => {
|
||||||
expect(buildAppHash({ page: 'vehicles', filters: { coverage: 'multi', serviceStatus: 'degraded', archiveStatus: 'incomplete', archiveMissing: 'phone', missingProtocol: 'YUTONG_MQTT' } })).toBe('#/vehicles?coverage=multi&serviceStatus=degraded&archiveStatus=incomplete&archiveMissing=phone&missingProtocol=YUTONG_MQTT');
|
expect(buildAppHash({ page: 'vehicles', filters: { coverage: 'multi', serviceStatus: 'degraded', archiveStatus: 'incomplete', archiveMissing: 'phone', missingProtocol: 'YUTONG_MQTT' } })).toBe('#/vehicles?coverage=multi&serviceStatus=degraded&archiveStatus=incomplete&archiveMissing=phone&missingProtocol=YUTONG_MQTT');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { PageKey } from '../layout/AppShell';
|
import type { PageKey } from '../layout/AppShell';
|
||||||
|
|
||||||
const pageKeys = new Set<PageKey>(['dashboard', 'vehicles', 'map', 'realtime', 'detail', 'history', 'mileage', 'quality', 'notification-rules', 'ops-quality']);
|
const pageKeys = new Set<PageKey>(['dashboard', 'vehicles', 'map', 'realtime', 'detail', 'history', 'history-query', 'mileage', 'quality', 'notification-rules', 'ops-quality']);
|
||||||
|
|
||||||
export type AppRoute = {
|
export type AppRoute = {
|
||||||
page?: PageKey;
|
page?: PageKey;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { useState } from 'react';
|
|||||||
import type { VehicleSourceConsistency, VehicleServiceStatus } from '../api/types';
|
import type { VehicleSourceConsistency, VehicleServiceStatus } from '../api/types';
|
||||||
import { isAMapConfigured } from '../config/appConfig';
|
import { isAMapConfigured } from '../config/appConfig';
|
||||||
|
|
||||||
export type PageKey = 'dashboard' | 'vehicles' | 'map' | 'realtime' | 'detail' | 'history' | 'mileage' | 'quality' | 'notification-rules' | 'ops-quality';
|
export type PageKey = 'dashboard' | 'vehicles' | 'map' | 'realtime' | 'detail' | 'history' | 'history-query' | 'mileage' | 'quality' | 'notification-rules' | 'ops-quality';
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ itemKey: 'dashboard', text: '运营驾驶舱', icon: <IconHome /> },
|
{ itemKey: 'dashboard', text: '运营驾驶舱', icon: <IconHome /> },
|
||||||
@@ -25,6 +25,7 @@ const navItems = [
|
|||||||
{ itemKey: 'realtime', text: '实时监控', icon: <IconActivity /> },
|
{ itemKey: 'realtime', text: '实时监控', icon: <IconActivity /> },
|
||||||
{ itemKey: 'detail', text: '车辆档案', icon: <IconSetting /> },
|
{ itemKey: 'detail', text: '车辆档案', icon: <IconSetting /> },
|
||||||
{ itemKey: 'history', text: '轨迹回放', icon: <IconMapPin /> },
|
{ itemKey: 'history', text: '轨迹回放', icon: <IconMapPin /> },
|
||||||
|
{ itemKey: 'history-query', text: '历史查询', icon: <IconHistogram /> },
|
||||||
{ itemKey: 'mileage', text: '统计分析', icon: <IconBarChartHStroked /> },
|
{ itemKey: 'mileage', text: '统计分析', icon: <IconBarChartHStroked /> },
|
||||||
{ itemKey: 'quality', text: '告警事件', icon: <IconHistogram /> },
|
{ itemKey: 'quality', text: '告警事件', icon: <IconHistogram /> },
|
||||||
{ itemKey: 'notification-rules', text: '通知规则', icon: <IconBell /> },
|
{ itemKey: 'notification-rules', text: '通知规则', icon: <IconBell /> },
|
||||||
@@ -120,7 +121,7 @@ export function AppShell({
|
|||||||
<Button size="small" aria-label="顶部地图态势" onClick={() => onChange('map')}>地图态势</Button>
|
<Button size="small" aria-label="顶部地图态势" onClick={() => onChange('map')}>地图态势</Button>
|
||||||
<Button size="small" aria-label="顶部实时监控" onClick={() => onChange('realtime')}>实时监控</Button>
|
<Button size="small" aria-label="顶部实时监控" onClick={() => onChange('realtime')}>实时监控</Button>
|
||||||
<Button size="small" aria-label="顶部轨迹回放" onClick={() => onChange('history')}>轨迹回放</Button>
|
<Button size="small" aria-label="顶部轨迹回放" onClick={() => onChange('history')}>轨迹回放</Button>
|
||||||
<Button size="small" aria-label="顶部历史查询" onClick={() => onChange('history')}>历史查询</Button>
|
<Button size="small" aria-label="顶部历史查询" onClick={() => onChange('history-query')}>历史查询</Button>
|
||||||
<Button size="small" aria-label="顶部统计查询" onClick={() => onChange('mileage')}>统计查询</Button>
|
<Button size="small" aria-label="顶部统计查询" onClick={() => onChange('mileage')}>统计查询</Button>
|
||||||
<Button size="small" aria-label="顶部通知规则" onClick={() => onChange('notification-rules')}>通知规则</Button>
|
<Button size="small" aria-label="顶部通知规则" onClick={() => onChange('notification-rules')}>通知规则</Button>
|
||||||
<Button size="small" aria-label="顶部运维质量" onClick={() => onChange('ops-quality')}>运维质量</Button>
|
<Button size="small" aria-label="顶部运维质量" onClick={() => onChange('ops-quality')}>运维质量</Button>
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ function priorityIssueNotificationText(row: QualityIssueRow) {
|
|||||||
`详情:${row.detail || '-'}`,
|
`详情:${row.detail || '-'}`,
|
||||||
`实时定位:${appURL(buildAppHash({ page: 'realtime', keyword: lookup.key, protocol: row.protocol }))}`,
|
`实时定位:${appURL(buildAppHash({ page: 'realtime', keyword: lookup.key, protocol: row.protocol }))}`,
|
||||||
`轨迹证据:${appURL(buildAppHash({ page: 'history', keyword: filters.keyword, protocol: filters.protocol, filters }))}`,
|
`轨迹证据:${appURL(buildAppHash({ page: 'history', keyword: filters.keyword, protocol: filters.protocol, filters }))}`,
|
||||||
`RAW证据:${appURL(buildAppHash({ page: 'history', keyword: rawFilters.keyword, protocol: rawFilters.protocol, filters: rawFilters }))}`,
|
`RAW证据:${appURL(buildAppHash({ page: 'history-query', keyword: rawFilters.keyword, protocol: rawFilters.protocol, filters: rawFilters }))}`,
|
||||||
`车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: lookup.key, protocol: row.protocol }))}`,
|
`车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: lookup.key, protocol: row.protocol }))}`,
|
||||||
`告警筛选:${appURL(buildAppHash({ page: 'quality', protocol: row.protocol, filters: { issueType: row.issueType } }))}`
|
`告警筛选:${appURL(buildAppHash({ page: 'quality', protocol: row.protocol, filters: { issueType: row.issueType } }))}`
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|||||||
@@ -218,6 +218,7 @@ async function copyText(value: string, label: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function History({
|
export function History({
|
||||||
|
mode = 'trajectory',
|
||||||
initialVin,
|
initialVin,
|
||||||
initialProtocol,
|
initialProtocol,
|
||||||
initialTab,
|
initialTab,
|
||||||
@@ -227,6 +228,7 @@ export function History({
|
|||||||
onOpenMileage,
|
onOpenMileage,
|
||||||
onOpenRaw
|
onOpenRaw
|
||||||
}: {
|
}: {
|
||||||
|
mode?: 'trajectory' | 'query';
|
||||||
initialVin: string;
|
initialVin: string;
|
||||||
initialProtocol?: string;
|
initialProtocol?: string;
|
||||||
initialTab?: string;
|
initialTab?: string;
|
||||||
@@ -359,6 +361,13 @@ export function History({
|
|||||||
const playbackIntervalMinutes = isFiniteNumber(playbackSpanMinutes) && locationItems.length > 1 ? playbackSpanMinutes / (locationItems.length - 1) : undefined;
|
const playbackIntervalMinutes = isFiniteNumber(playbackSpanMinutes) && locationItems.length > 1 ? playbackSpanMinutes / (locationItems.length - 1) : undefined;
|
||||||
const currentVehicleKeyword = filters.keyword?.trim() ?? '';
|
const currentVehicleKeyword = filters.keyword?.trim() ?? '';
|
||||||
const currentProtocol = filters.protocol?.trim() ?? '';
|
const currentProtocol = filters.protocol?.trim() ?? '';
|
||||||
|
const pageTitle = mode === 'query' ? '历史查询' : '轨迹回放';
|
||||||
|
const pageDescription = mode === 'query'
|
||||||
|
? '按车辆查询历史位置、RAW 帧和扁平解析字段,支持分页、字段裁剪和证据导出'
|
||||||
|
: '按车辆查询历史位置、轨迹回放和 RAW 帧证据,数据来源只作为过滤和诊断维度';
|
||||||
|
const scopeDescription = mode === 'query'
|
||||||
|
? '历史位置、RAW 帧和解析字段按当前车辆与来源范围分页查询。'
|
||||||
|
: '历史位置和 RAW 帧按当前车辆与来源范围查询。';
|
||||||
const amapConfigured = isAMapConfigured();
|
const amapConfigured = isAMapConfigured();
|
||||||
const selectedFieldCount = splitFields(filters.fields).length;
|
const selectedFieldCount = splitFields(filters.fields).length;
|
||||||
const playbackRows = validLocations.length > 0 ? validLocations : locationItems;
|
const playbackRows = validLocations.length > 0 ? validLocations : locationItems;
|
||||||
@@ -487,8 +496,8 @@ export function History({
|
|||||||
return (
|
return (
|
||||||
<div className="vp-page">
|
<div className="vp-page">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="轨迹回放"
|
title={pageTitle}
|
||||||
description="按车辆查询历史位置、轨迹回放和 RAW 帧证据,数据来源只作为过滤和诊断维度"
|
description={pageDescription}
|
||||||
actions={(
|
actions={(
|
||||||
<Button disabled={!currentVehicleKeyword} onClick={() => onOpenVehicle(currentVehicleKeyword, currentProtocol)}>
|
<Button disabled={!currentVehicleKeyword} onClick={() => onOpenVehicle(currentVehicleKeyword, currentProtocol)}>
|
||||||
当前车辆服务
|
当前车辆服务
|
||||||
@@ -498,7 +507,7 @@ export function History({
|
|||||||
<div className="vp-scope-bar">
|
<div className="vp-scope-bar">
|
||||||
<span className="vp-scope-label">当前车辆:{currentVehicleKeyword || '-'}</span>
|
<span className="vp-scope-label">当前车辆:{currentVehicleKeyword || '-'}</span>
|
||||||
<Tag color={currentProtocol ? 'blue' : 'green'}>当前来源:{currentProtocol || '全部来源'}</Tag>
|
<Tag color={currentProtocol ? 'blue' : 'green'}>当前来源:{currentProtocol || '全部来源'}</Tag>
|
||||||
<Typography.Text type="tertiary">历史位置和 RAW 帧按当前车辆与来源范围查询。</Typography.Text>
|
<Typography.Text type="tertiary">{scopeDescription}</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Card bordered>
|
<Card bordered>
|
||||||
<Form key={JSON.stringify(filters)} initValues={filters} layout="horizontal" onSubmit={(values) => submit(values)}>
|
<Form key={JSON.stringify(filters)} initValues={filters} layout="horizontal" onSubmit={(values) => submit(values)}>
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ function priorityIssueNotificationText(row: PriorityIssueRow) {
|
|||||||
`详情:${row.detail || '-'}`,
|
`详情:${row.detail || '-'}`,
|
||||||
`实时定位:${appURL(buildAppHash({ page: 'realtime', keyword: lookup.key, protocol: row.protocol }))}`,
|
`实时定位:${appURL(buildAppHash({ page: 'realtime', keyword: lookup.key, protocol: row.protocol }))}`,
|
||||||
`轨迹证据:${appURL(buildAppHash({ page: 'history', keyword: lookup.key, protocol: row.protocol, filters: evidenceFilters }))}`,
|
`轨迹证据:${appURL(buildAppHash({ page: 'history', keyword: lookup.key, protocol: row.protocol, filters: evidenceFilters }))}`,
|
||||||
`RAW证据:${appURL(buildAppHash({ page: 'history', keyword: lookup.key, protocol: row.protocol, filters: { tab: 'raw', ...evidenceFilters, includeFields: 'true' } }))}`,
|
`RAW证据:${appURL(buildAppHash({ page: 'history-query', keyword: lookup.key, protocol: row.protocol, filters: { tab: 'raw', ...evidenceFilters, includeFields: 'true' } }))}`,
|
||||||
`车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: lookup.key, protocol: row.protocol }))}`,
|
`车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: lookup.key, protocol: row.protocol }))}`,
|
||||||
`告警筛选:${qualityShareURL()}`
|
`告警筛选:${qualityShareURL()}`
|
||||||
].join('\n');
|
].join('\n');
|
||||||
@@ -335,7 +335,7 @@ function priorityIssueTicketText(row: PriorityIssueRow) {
|
|||||||
`验收标准:${acceptanceCriteria(row)}`,
|
`验收标准:${acceptanceCriteria(row)}`,
|
||||||
`实时定位:${appURL(buildAppHash({ page: 'realtime', keyword: lookup.key, protocol: row.protocol }))}`,
|
`实时定位:${appURL(buildAppHash({ page: 'realtime', keyword: lookup.key, protocol: row.protocol }))}`,
|
||||||
`轨迹证据:${appURL(buildAppHash({ page: 'history', keyword: lookup.key, protocol: row.protocol, filters: evidenceFilters }))}`,
|
`轨迹证据:${appURL(buildAppHash({ page: 'history', keyword: lookup.key, protocol: row.protocol, filters: evidenceFilters }))}`,
|
||||||
`RAW证据:${appURL(buildAppHash({ page: 'history', keyword: lookup.key, protocol: row.protocol, filters: { tab: 'raw', ...evidenceFilters, includeFields: 'true' } }))}`,
|
`RAW证据:${appURL(buildAppHash({ page: 'history-query', keyword: lookup.key, protocol: row.protocol, filters: { tab: 'raw', ...evidenceFilters, includeFields: 'true' } }))}`,
|
||||||
`车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: lookup.key, protocol: row.protocol }))}`,
|
`车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: lookup.key, protocol: row.protocol }))}`,
|
||||||
`告警筛选:${qualityShareURL()}`
|
`告警筛选:${qualityShareURL()}`
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|||||||
@@ -346,7 +346,7 @@ export function VehicleDetail({
|
|||||||
onOpenRaw(filters.keyword, filters.protocol);
|
onOpenRaw(filters.keyword, filters.protocol);
|
||||||
};
|
};
|
||||||
const vehicleServiceURL = (hash: string) => `${window.location.origin}${window.location.pathname}${hash}`;
|
const vehicleServiceURL = (hash: string) => `${window.location.origin}${window.location.pathname}${hash}`;
|
||||||
const vehicleServiceHash = (page: 'detail' | 'realtime' | 'history' | 'mileage', filters?: Record<string, string>) => buildAppHash({
|
const vehicleServiceHash = (page: 'detail' | 'realtime' | 'history' | 'history-query' | 'mileage', filters?: Record<string, string>) => buildAppHash({
|
||||||
page,
|
page,
|
||||||
keyword: resolvedVIN,
|
keyword: resolvedVIN,
|
||||||
protocol: activeProtocol,
|
protocol: activeProtocol,
|
||||||
@@ -380,7 +380,7 @@ export function VehicleDetail({
|
|||||||
`车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}`,
|
`车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}`,
|
||||||
`实时:${vehicleServiceURL(vehicleServiceHash('realtime'))}`,
|
`实时:${vehicleServiceURL(vehicleServiceHash('realtime'))}`,
|
||||||
`轨迹:${vehicleServiceURL(vehicleServiceHash('history'))}`,
|
`轨迹:${vehicleServiceURL(vehicleServiceHash('history'))}`,
|
||||||
`RAW:${vehicleServiceURL(vehicleServiceHash('history', { tab: 'raw', includeFields: 'true' }))}`,
|
`RAW:${vehicleServiceURL(vehicleServiceHash('history-query', { tab: 'raw', includeFields: 'true' }))}`,
|
||||||
`里程:${vehicleServiceURL(vehicleServiceHash('mileage'))}`
|
`里程:${vehicleServiceURL(vehicleServiceHash('mileage'))}`
|
||||||
];
|
];
|
||||||
copyText(lines.join('\n'), '诊断摘要');
|
copyText(lines.join('\n'), '诊断摘要');
|
||||||
|
|||||||
@@ -89,7 +89,9 @@ test('exposes AMap operations shortcuts when map key is configured', async () =>
|
|||||||
fireEvent.click(screen.getByRole('button', { name: '顶部轨迹回放' }));
|
fireEvent.click(screen.getByRole('button', { name: '顶部轨迹回放' }));
|
||||||
expect(window.location.hash.startsWith('#/history')).toBe(true);
|
expect(window.location.hash.startsWith('#/history')).toBe(true);
|
||||||
fireEvent.click(screen.getByRole('button', { name: '顶部历史查询' }));
|
fireEvent.click(screen.getByRole('button', { name: '顶部历史查询' }));
|
||||||
expect(window.location.hash.startsWith('#/history')).toBe(true);
|
expect(window.location.hash.startsWith('#/history-query')).toBe(true);
|
||||||
|
expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('tab')).toBe('raw');
|
||||||
|
expect(await screen.findByRole('heading', { name: '历史查询' })).toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByRole('button', { name: '顶部统计查询' }));
|
fireEvent.click(screen.getByRole('button', { name: '顶部统计查询' }));
|
||||||
expect(window.location.hash.startsWith('#/mileage')).toBe(true);
|
expect(window.location.hash.startsWith('#/mileage')).toBe(true);
|
||||||
fireEvent.click(await screen.findByRole('button', { name: '顶部告警事件正常' }));
|
fireEvent.click(await screen.findByRole('button', { name: '顶部告警事件正常' }));
|
||||||
@@ -424,7 +426,7 @@ test('dashboard exposes vehicle data center capability matrix', async () => {
|
|||||||
|
|
||||||
await renderDashboard();
|
await renderDashboard();
|
||||||
fireEvent.click(screen.getByRole('button', { name: '能力入口 历史数据查询' }));
|
fireEvent.click(screen.getByRole('button', { name: '能力入口 历史数据查询' }));
|
||||||
expect(window.location.hash.startsWith('#/history')).toBe(true);
|
expect(window.location.hash.startsWith('#/history-query')).toBe(true);
|
||||||
expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('tab')).toBe('raw');
|
expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('tab')).toBe('raw');
|
||||||
cleanup();
|
cleanup();
|
||||||
|
|
||||||
@@ -1096,7 +1098,7 @@ test('dashboard copies highest priority quality issue notification text', async
|
|||||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('数据来源:JT808'));
|
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('数据来源:JT808'));
|
||||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('详情:JT808 数据缺少 VIN 映射'));
|
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('详情:JT808 数据缺少 VIN 映射'));
|
||||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹证据:http://localhost:3000/#/history?keyword=%E7%B2%A4A%E5%91%8A%E8%AD%A61&protocol=JT808&dateFrom=2026-07-03&dateTo=2026-07-04'));
|
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹证据:http://localhost:3000/#/history?keyword=%E7%B2%A4A%E5%91%8A%E8%AD%A61&protocol=JT808&dateFrom=2026-07-03&dateTo=2026-07-04'));
|
||||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('RAW证据:http://localhost:3000/#/history?keyword=%E7%B2%A4A%E5%91%8A%E8%AD%A61&protocol=JT808&tab=raw&dateFrom=2026-07-03&dateTo=2026-07-04&includeFields=true'));
|
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('RAW证据:http://localhost:3000/#/history-query?keyword=%E7%B2%A4A%E5%91%8A%E8%AD%A61&protocol=JT808&tab=raw&dateFrom=2026-07-03&dateTo=2026-07-04&includeFields=true'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('dashboard keeps core vehicle service metrics when preview requests fail', async () => {
|
test('dashboard keeps core vehicle service metrics when preview requests fail', async () => {
|
||||||
@@ -3142,7 +3144,7 @@ test('uses backend quality notification plan on quality page', async () => {
|
|||||||
vehicleLabel: '后端车辆标签 / VIN-PLAN-001',
|
vehicleLabel: '后端车辆标签 / VIN-PLAN-001',
|
||||||
realtimeHash: '#/realtime?keyword=VIN-PLAN-001',
|
realtimeHash: '#/realtime?keyword=VIN-PLAN-001',
|
||||||
historyHash: '#/history?keyword=VIN-PLAN-001',
|
historyHash: '#/history?keyword=VIN-PLAN-001',
|
||||||
rawHash: '#/history?keyword=VIN-PLAN-001&tab=raw',
|
rawHash: '#/history-query?keyword=VIN-PLAN-001&tab=raw',
|
||||||
vehicleHash: '#/detail?keyword=VIN-PLAN-001',
|
vehicleHash: '#/detail?keyword=VIN-PLAN-001',
|
||||||
notificationText: '【P0 告警通知】后端计划'
|
notificationText: '【P0 告警通知】后端计划'
|
||||||
}],
|
}],
|
||||||
@@ -3413,7 +3415,7 @@ test('copies notification text from quality priority queue', async () => {
|
|||||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('详情:手机号未映射 VIN,需要维护 binding'));
|
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('详情:手机号未映射 VIN,需要维护 binding'));
|
||||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时定位:http://localhost:3000/#/realtime?keyword=%E7%B2%A4A%E9%80%9A%E7%9F%A51&protocol=JT808'));
|
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时定位:http://localhost:3000/#/realtime?keyword=%E7%B2%A4A%E9%80%9A%E7%9F%A51&protocol=JT808'));
|
||||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹证据:http://localhost:3000/#/history?keyword=%E7%B2%A4A%E9%80%9A%E7%9F%A51&protocol=JT808&dateFrom=2026-07-03&dateTo=2026-07-04'));
|
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹证据:http://localhost:3000/#/history?keyword=%E7%B2%A4A%E9%80%9A%E7%9F%A51&protocol=JT808&dateFrom=2026-07-03&dateTo=2026-07-04'));
|
||||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('RAW证据:http://localhost:3000/#/history?keyword=%E7%B2%A4A%E9%80%9A%E7%9F%A51&protocol=JT808&tab=raw&dateFrom=2026-07-03&dateTo=2026-07-04&includeFields=true'));
|
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('RAW证据:http://localhost:3000/#/history-query?keyword=%E7%B2%A4A%E9%80%9A%E7%9F%A51&protocol=JT808&tab=raw&dateFrom=2026-07-03&dateTo=2026-07-04&includeFields=true'));
|
||||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆服务:http://localhost:3000/#/detail?keyword=%E7%B2%A4A%E9%80%9A%E7%9F%A51&protocol=JT808'));
|
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆服务:http://localhost:3000/#/detail?keyword=%E7%B2%A4A%E9%80%9A%E7%9F%A51&protocol=JT808'));
|
||||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('告警筛选:http://localhost:3000/#/quality'));
|
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('告警筛选:http://localhost:3000/#/quality'));
|
||||||
});
|
});
|
||||||
@@ -3975,16 +3977,16 @@ test('opens same-day raw evidence from quality priority queue', async () => {
|
|||||||
fireEvent.click(screen.getByRole('button', { name: 'RAW证据' }));
|
fireEvent.click(screen.getByRole('button', { name: 'RAW证据' }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(window.location.hash.startsWith('#/history?')).toBe(true);
|
expect(window.location.hash.startsWith('#/history-query?')).toBe(true);
|
||||||
});
|
});
|
||||||
const params = new URLSearchParams(window.location.hash.slice('#/history?'.length));
|
const params = new URLSearchParams(window.location.hash.slice('#/history-query?'.length));
|
||||||
expect(params.get('keyword')).toBe('VIN-ALERT-RAW');
|
expect(params.get('keyword')).toBe('VIN-ALERT-RAW');
|
||||||
expect(params.get('protocol')).toBe('JT808');
|
expect(params.get('protocol')).toBe('JT808');
|
||||||
expect(params.get('dateFrom')).toBe('2026-07-03');
|
expect(params.get('dateFrom')).toBe('2026-07-03');
|
||||||
expect(params.get('dateTo')).toBe('2026-07-04');
|
expect(params.get('dateTo')).toBe('2026-07-04');
|
||||||
expect(params.get('includeFields')).toBe('true');
|
expect(params.get('includeFields')).toBe('true');
|
||||||
expect(params.get('tab')).toBe('raw');
|
expect(params.get('tab')).toBe('raw');
|
||||||
expect(await screen.findByRole('heading', { name: '轨迹回放' })).toBeInTheDocument();
|
expect(await screen.findByRole('heading', { name: '历史查询' })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('opens vehicle service from quality issue with issue source evidence', async () => {
|
test('opens vehicle service from quality issue with issue source evidence', async () => {
|
||||||
@@ -4332,16 +4334,16 @@ test('opens same-day raw evidence from quality issue row', async () => {
|
|||||||
fireEvent.click(screen.getByRole('button', { name: '核对 RAW' }));
|
fireEvent.click(screen.getByRole('button', { name: '核对 RAW' }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(window.location.hash.startsWith('#/history?')).toBe(true);
|
expect(window.location.hash.startsWith('#/history-query?')).toBe(true);
|
||||||
});
|
});
|
||||||
const params = new URLSearchParams(window.location.hash.slice('#/history?'.length));
|
const params = new URLSearchParams(window.location.hash.slice('#/history-query?'.length));
|
||||||
expect(params.get('keyword')).toBe('VIN-QUALITY-RAW');
|
expect(params.get('keyword')).toBe('VIN-QUALITY-RAW');
|
||||||
expect(params.get('protocol')).toBe('GB32960');
|
expect(params.get('protocol')).toBe('GB32960');
|
||||||
expect(params.get('dateFrom')).toBe('2026-07-03');
|
expect(params.get('dateFrom')).toBe('2026-07-03');
|
||||||
expect(params.get('dateTo')).toBe('2026-07-04');
|
expect(params.get('dateTo')).toBe('2026-07-04');
|
||||||
expect(params.get('includeFields')).toBe('true');
|
expect(params.get('includeFields')).toBe('true');
|
||||||
expect(params.get('tab')).toBe('raw');
|
expect(params.get('tab')).toBe('raw');
|
||||||
expect(await screen.findByRole('heading', { name: '轨迹回放' })).toBeInTheDocument();
|
expect(await screen.findByRole('heading', { name: '历史查询' })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('copies trajectory playback summary from history page', async () => {
|
test('copies trajectory playback summary from history page', async () => {
|
||||||
@@ -5273,7 +5275,7 @@ test('applies protocol from shareable history hash to API requests', async () =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('shows and clears current history filters while keeping vehicle scope', async () => {
|
test('shows and clears current history filters while keeping vehicle scope', async () => {
|
||||||
window.history.replaceState(null, '', '/#/history?keyword=VIN-HISTORY-001&protocol=JT808&dateFrom=2026-07-01+00%3A00%3A00&dateTo=2026-07-01+23%3A59%3A59&includeFields=true&fields=jt808.location.longitude%2Cjt808.location.latitude&tab=raw');
|
window.history.replaceState(null, '', '/#/history-query?keyword=VIN-HISTORY-001&protocol=JT808&dateFrom=2026-07-01+00%3A00%3A00&dateTo=2026-07-01+23%3A59%3A59&includeFields=true&fields=jt808.location.longitude%2Cjt808.location.latitude&tab=raw');
|
||||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
|
const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
|
||||||
const path = String(input);
|
const path = String(input);
|
||||||
if (path.includes('/api/ops/health')) {
|
if (path.includes('/api/ops/health')) {
|
||||||
@@ -5330,7 +5332,7 @@ test('shows and clears current history filters while keeping vehicle scope', asy
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/history/locations?limit=10&offset=0&keyword=VIN-HISTORY-001'), undefined);
|
expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/history/locations?limit=10&offset=0&keyword=VIN-HISTORY-001'), undefined);
|
||||||
});
|
});
|
||||||
expect(window.location.hash).toBe('#/history?keyword=VIN-HISTORY-001&tab=raw');
|
expect(window.location.hash).toBe('#/history-query?keyword=VIN-HISTORY-001&tab=raw');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('updates history hash when vehicle history filters are submitted', async () => {
|
test('updates history hash when vehicle history filters are submitted', async () => {
|
||||||
@@ -5833,7 +5835,7 @@ test('exports current history location page as csv', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('exports current raw frame page as csv with parsed fields', async () => {
|
test('exports current raw frame page as csv with parsed fields', async () => {
|
||||||
window.history.replaceState(null, '', '/#/history?keyword=VIN-EXPORT-RAW&protocol=GB32960&includeFields=true&tab=raw');
|
window.history.replaceState(null, '', '/#/history-query?keyword=VIN-EXPORT-RAW&protocol=GB32960&includeFields=true&tab=raw');
|
||||||
const createObjectURL = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:history-raw');
|
const createObjectURL = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:history-raw');
|
||||||
const revokeObjectURL = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => undefined);
|
const revokeObjectURL = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => undefined);
|
||||||
vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => undefined);
|
vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => undefined);
|
||||||
@@ -6383,15 +6385,15 @@ test('opens same-day raw frame evidence from mileage row', async () => {
|
|||||||
fireEvent.click(screen.getByRole('button', { name: '核对 RAW' }));
|
fireEvent.click(screen.getByRole('button', { name: '核对 RAW' }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(window.location.hash.startsWith('#/history?')).toBe(true);
|
expect(window.location.hash.startsWith('#/history-query?')).toBe(true);
|
||||||
});
|
});
|
||||||
const params = new URLSearchParams(window.location.hash.slice('#/history?'.length));
|
const params = new URLSearchParams(window.location.hash.slice('#/history-query?'.length));
|
||||||
expect(params.get('keyword')).toBe('VIN-MILEAGE-RAW');
|
expect(params.get('keyword')).toBe('VIN-MILEAGE-RAW');
|
||||||
expect(params.get('protocol')).toBe('JT808');
|
expect(params.get('protocol')).toBe('JT808');
|
||||||
expect(params.get('dateFrom')).toBe('2026-07-03');
|
expect(params.get('dateFrom')).toBe('2026-07-03');
|
||||||
expect(params.get('dateTo')).toBe('2026-07-04');
|
expect(params.get('dateTo')).toBe('2026-07-04');
|
||||||
expect(params.get('tab')).toBe('raw');
|
expect(params.get('tab')).toBe('raw');
|
||||||
expect(await screen.findByRole('heading', { name: '轨迹回放' })).toBeInTheDocument();
|
expect(await screen.findByRole('heading', { name: '历史查询' })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shows mileage anomaly action guidance', async () => {
|
test('shows mileage anomaly action guidance', async () => {
|
||||||
@@ -7015,9 +7017,9 @@ test('opens same-day raw evidence from history location row', async () => {
|
|||||||
fireEvent.click(screen.getByRole('button', { name: '核对 RAW' }));
|
fireEvent.click(screen.getByRole('button', { name: '核对 RAW' }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(window.location.hash.startsWith('#/history?')).toBe(true);
|
expect(window.location.hash.startsWith('#/history-query?')).toBe(true);
|
||||||
});
|
});
|
||||||
const params = new URLSearchParams(window.location.hash.slice('#/history?'.length));
|
const params = new URLSearchParams(window.location.hash.slice('#/history-query?'.length));
|
||||||
expect(params.get('keyword')).toBe('VIN-HISTORY-RAW');
|
expect(params.get('keyword')).toBe('VIN-HISTORY-RAW');
|
||||||
expect(params.get('protocol')).toBe('JT808');
|
expect(params.get('protocol')).toBe('JT808');
|
||||||
expect(params.get('dateFrom')).toBe('2026-07-03');
|
expect(params.get('dateFrom')).toBe('2026-07-03');
|
||||||
@@ -7119,7 +7121,7 @@ test('opens vehicle service from raw history rows with row source evidence', asy
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('opens same-day mileage statistics from raw history row', async () => {
|
test('opens same-day mileage statistics from raw history row', async () => {
|
||||||
window.history.replaceState(null, '', '/#/history?keyword=VIN-RAW-MILEAGE&protocol=JT808&tab=raw');
|
window.history.replaceState(null, '', '/#/history-query?keyword=VIN-RAW-MILEAGE&protocol=JT808&tab=raw');
|
||||||
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
||||||
const path = String(input);
|
const path = String(input);
|
||||||
if (path.includes('/api/ops/health')) {
|
if (path.includes('/api/ops/health')) {
|
||||||
@@ -9960,7 +9962,7 @@ test('copies vehicle service diagnostic summary', async () => {
|
|||||||
expect(copied).toContain('车辆服务:http://localhost:3000/#/detail?keyword=VIN-SUMMARY-001&protocol=JT808');
|
expect(copied).toContain('车辆服务:http://localhost:3000/#/detail?keyword=VIN-SUMMARY-001&protocol=JT808');
|
||||||
expect(copied).toContain('实时:http://localhost:3000/#/realtime?keyword=VIN-SUMMARY-001&protocol=JT808');
|
expect(copied).toContain('实时:http://localhost:3000/#/realtime?keyword=VIN-SUMMARY-001&protocol=JT808');
|
||||||
expect(copied).toContain('轨迹:http://localhost:3000/#/history?keyword=VIN-SUMMARY-001&protocol=JT808');
|
expect(copied).toContain('轨迹:http://localhost:3000/#/history?keyword=VIN-SUMMARY-001&protocol=JT808');
|
||||||
expect(copied).toContain('RAW:http://localhost:3000/#/history?keyword=VIN-SUMMARY-001&protocol=JT808&tab=raw&includeFields=true');
|
expect(copied).toContain('RAW:http://localhost:3000/#/history-query?keyword=VIN-SUMMARY-001&protocol=JT808&tab=raw&includeFields=true');
|
||||||
expect(copied).toContain('里程:http://localhost:3000/#/mileage?keyword=VIN-SUMMARY-001&protocol=JT808');
|
expect(copied).toContain('里程:http://localhost:3000/#/mileage?keyword=VIN-SUMMARY-001&protocol=JT808');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -10207,7 +10209,7 @@ test('opens raw history with the currently selected vehicle detail source', asyn
|
|||||||
fireEvent.click(screen.getByRole('button', { name: '查看 RAW' }));
|
fireEvent.click(screen.getByRole('button', { name: '查看 RAW' }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(window.location.hash).toBe('#/history?keyword=VIN001&protocol=JT808&tab=raw');
|
expect(window.location.hash).toBe('#/history-query?keyword=VIN001&protocol=JT808&tab=raw');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -10433,7 +10435,7 @@ test('opens source-specific raw history directly from vehicle detail source evid
|
|||||||
fireEvent.click(await screen.findByRole('button', { name: '查看 JT808 RAW' }));
|
fireEvent.click(await screen.findByRole('button', { name: '查看 JT808 RAW' }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(window.location.hash).toBe('#/history?keyword=VIN001&protocol=JT808&tab=raw');
|
expect(window.location.hash).toBe('#/history-query?keyword=VIN001&protocol=JT808&tab=raw');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user