From 747894ce1db1e3122a7e771fcde66e455262563e Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 17:23:48 +0800 Subject: [PATCH] feat(platform): split trajectory replay and history query --- .../apps/api/internal/platform/service.go | 2 +- vehicle-data-platform/apps/web/src/App.tsx | 42 +++++++++------- .../apps/web/src/api/client.test.ts | 2 +- .../apps/web/src/domain/appRoute.test.ts | 15 ++++++ .../apps/web/src/domain/appRoute.ts | 2 +- .../apps/web/src/layout/AppShell.tsx | 5 +- .../apps/web/src/pages/Dashboard.tsx | 2 +- .../apps/web/src/pages/History.tsx | 15 ++++-- .../apps/web/src/pages/Quality.tsx | 4 +- .../apps/web/src/pages/VehicleDetail.tsx | 4 +- .../apps/web/src/test/App.test.tsx | 48 ++++++++++--------- 11 files changed, 88 insertions(+), 53 deletions(-) diff --git a/vehicle-data-platform/apps/api/internal/platform/service.go b/vehicle-data-platform/apps/api/internal/platform/service.go index 81675c7b..973d0636 100644 --- a/vehicle-data-platform/apps/api/internal/platform/service.go +++ b/vehicle-data-platform/apps/api/internal/platform/service.go @@ -1045,7 +1045,7 @@ func buildQualityPriorityIssue(row QualityIssueRow) QualityPriorityIssue { rawFilters := cloneStringMap(historyFilters) rawFilters["tab"] = "raw" 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) item := QualityPriorityIssue{ QualityIssueRow: row, diff --git a/vehicle-data-platform/apps/web/src/App.tsx b/vehicle-data-platform/apps/web/src/App.tsx index 86391506..c31eb38e 100644 --- a/vehicle-data-platform/apps/web/src/App.tsx +++ b/vehicle-data-platform/apps/web/src/App.tsx @@ -21,7 +21,7 @@ export default function App() { const [activeVin, setActiveVin] = useState(initialVehicleKey); const [analysisVin, setAnalysisVin] = useState(initialVehicleKey); const [activeProtocol, setActiveProtocol] = useState(initialRoute.protocol ?? ''); - const [historyTab, setHistoryTab] = useState(initialRoute.filters?.tab ?? 'location'); + const [historyTab, setHistoryTab] = useState(initialRoute.filters?.tab ?? (initialRoute.page === 'history-query' ? 'raw' : 'location')); const [vehicleFilters, setVehicleFilters] = useState>( initialRoute.page === 'vehicles' ? vehicleFiltersFromRoute(initialRoute) : {} ); @@ -29,7 +29,7 @@ export default function App() { initialRoute.page === 'realtime' || initialRoute.page === 'map' ? realtimeFiltersFromRoute(initialRoute) : {} ); const [historyFilters, setHistoryFilters] = useState>( - initialRoute.page === 'history' ? historyFiltersFromRoute(initialRoute) : {} + initialRoute.page === 'history' || initialRoute.page === 'history-query' ? historyFiltersFromRoute(initialRoute) : {} ); const [mileageFilters, setMileageFilters] = useState>( initialRoute.page === 'mileage' ? mileageFiltersFromRoute(initialRoute) : {} @@ -104,7 +104,7 @@ export default function App() { if (route.page === 'detail') { setActiveVin(route.keyword); } - if (route.page === 'history' || route.page === 'mileage') { + if (route.page === 'history' || route.page === 'history-query' || route.page === 'mileage') { setAnalysisVin(route.keyword); } } @@ -114,7 +114,7 @@ export default function App() { if (route.page === 'realtime' || route.page === 'map') { setRealtimeFilters(realtimeFiltersFromRoute(route)); } - if (route.page === 'history') { + if (route.page === 'history' || route.page === 'history-query') { setHistoryFilters(historyFiltersFromRoute(route)); } if (route.page === 'mileage') { @@ -124,7 +124,7 @@ export default function App() { setQualityFilters(qualityFiltersFromRoute(route)); } setActiveProtocol(route.protocol ?? ''); - setHistoryTab(route.filters?.tab ?? 'location'); + setHistoryTab(route.filters?.tab ?? (route.page === 'history-query' ? 'raw' : 'location')); }; window.addEventListener('hashchange', applyHashRoute); return () => window.removeEventListener('hashchange', applyHashRoute); @@ -143,9 +143,15 @@ export default function App() { replaceHash(page, activeVin, activeProtocol); return; } - if (page === 'history' || page === 'mileage') { + if (page === 'history' || page === 'history-query' || page === 'mileage') { if (page === 'history') { - replaceHistoryHash(historyFilters); + setHistoryTab('location'); + replaceHistoryHash(historyFilters, 'location', 'history'); + return; + } + if (page === 'history-query') { + setHistoryTab('raw'); + replaceHistoryHash(historyFilters, 'raw', 'history-query'); return; } replaceMileageHash(mileageFilters); @@ -226,16 +232,16 @@ export default function App() { if (tab && tab !== 'location') { 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 = {}, tab = historyTab) => { + const replaceHistoryHash = (filters: Record = {}, 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('history', keyword ?? analysisVin, protocol ?? activeProtocol, routeFilters); + replaceHash(page, keyword ?? analysisVin, protocol ?? activeProtocol, routeFilters); }; const updateMileageFilters = (filters: Record = {}) => { @@ -328,18 +334,19 @@ export default function App() { const nextVin = nextFilters.keyword?.trim() || analysisVin; const nextProtocol = nextFilters.protocol?.trim() ?? activeProtocol; const nextTab = nextFilters.tab === 'raw' ? 'raw' : 'location'; + const nextPage = nextTab === 'raw' ? 'history-query' : 'history'; setAnalysisVin(nextVin); setActiveProtocol(nextProtocol); setHistoryTab(nextTab); setHistoryFilters({ ...nextFilters, keyword: nextVin, protocol: nextProtocol }); - setActivePage('history'); + setActivePage(nextPage); const { keyword, protocol, ...restFilters } = nextFilters; if (nextTab === 'raw') { restFilters.tab = nextTab; } else { delete restFilters.tab; } - replaceHash('history', keyword ?? nextVin, protocol ?? nextProtocol, restFilters); + replaceHash(nextPage, keyword ?? nextVin, protocol ?? nextProtocol, restFilters); }; const openRawForVehicle = (vin: string, protocol?: string) => { @@ -352,8 +359,8 @@ export default function App() { setActiveProtocol(nextProtocol); setHistoryTab('raw'); setHistoryFilters({ keyword: nextVin, protocol: nextProtocol }); - setActivePage('history'); - replaceHash('history', nextVin, nextProtocol, { tab: 'raw' }); + setActivePage('history-query'); + replaceHash('history-query', nextVin, nextProtocol, { tab: 'raw' }); }; const openRawWithFilters = (filters: Record = {}) => { @@ -367,9 +374,9 @@ export default function App() { setActiveProtocol(nextProtocol); setHistoryTab('raw'); setHistoryFilters({ ...nextFilters, keyword: nextVin, protocol: nextProtocol }); - setActivePage('history'); + setActivePage('history-query'); 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) => { @@ -431,7 +438,8 @@ export default function App() { map: , realtime: , detail: , - history: , + history: , + 'history-query': , mileage: , quality: navigatePage('notification-rules')} onHealthLoaded={(health) => { setLinkIssueCount(health.linkHealth.filter((item) => item.status !== 'ok').length); diff --git a/vehicle-data-platform/apps/web/src/api/client.test.ts b/vehicle-data-platform/apps/web/src/api/client.test.ts index 88ef428f..8a3aff85 100644 --- a/vehicle-data-platform/apps/web/src/api/client.test.ts +++ b/vehicle-data-platform/apps/web/src/api/client.test.ts @@ -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: [] }, rules: [{ issueType: 'NO_SOURCE', title: '无数据来源', level: 'P0', owner: '平台接入', trigger: '无来源', notify: '立即通知', sla: '30 分钟确认', count: 1 }], 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, p0RuleCount: 1 }, diff --git a/vehicle-data-platform/apps/web/src/domain/appRoute.test.ts b/vehicle-data-platform/apps/web/src/domain/appRoute.test.ts index b6e334fa..90235680 100644 --- a/vehicle-data-platform/apps/web/src/domain/appRoute.test.ts +++ b/vehicle-data-platform/apps/web/src/domain/appRoute.test.ts @@ -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', () => { expect(parseAppHash('#/vehicles?coverage=multi&serviceStatus=degraded&online=online&bindingStatus=bound&archiveStatus=incomplete&archiveMissing=phone&missingProtocol=YUTONG_MQTT')).toEqual({ 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'); }); + 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', () => { 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'); }); diff --git a/vehicle-data-platform/apps/web/src/domain/appRoute.ts b/vehicle-data-platform/apps/web/src/domain/appRoute.ts index f4128cab..13f718d0 100644 --- a/vehicle-data-platform/apps/web/src/domain/appRoute.ts +++ b/vehicle-data-platform/apps/web/src/domain/appRoute.ts @@ -1,6 +1,6 @@ import type { PageKey } from '../layout/AppShell'; -const pageKeys = new Set(['dashboard', 'vehicles', 'map', 'realtime', 'detail', 'history', 'mileage', 'quality', 'notification-rules', 'ops-quality']); +const pageKeys = new Set(['dashboard', 'vehicles', 'map', 'realtime', 'detail', 'history', 'history-query', 'mileage', 'quality', 'notification-rules', 'ops-quality']); export type AppRoute = { page?: PageKey; diff --git a/vehicle-data-platform/apps/web/src/layout/AppShell.tsx b/vehicle-data-platform/apps/web/src/layout/AppShell.tsx index 92aaf977..7212a20c 100644 --- a/vehicle-data-platform/apps/web/src/layout/AppShell.tsx +++ b/vehicle-data-platform/apps/web/src/layout/AppShell.tsx @@ -16,7 +16,7 @@ import { useState } from 'react'; import type { VehicleSourceConsistency, VehicleServiceStatus } from '../api/types'; 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 = [ { itemKey: 'dashboard', text: '运营驾驶舱', icon: }, @@ -25,6 +25,7 @@ const navItems = [ { itemKey: 'realtime', text: '实时监控', icon: }, { itemKey: 'detail', text: '车辆档案', icon: }, { itemKey: 'history', text: '轨迹回放', icon: }, + { itemKey: 'history-query', text: '历史查询', icon: }, { itemKey: 'mileage', text: '统计分析', icon: }, { itemKey: 'quality', text: '告警事件', icon: }, { itemKey: 'notification-rules', text: '通知规则', icon: }, @@ -120,7 +121,7 @@ export function AppShell({ - + diff --git a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx index f70b5276..de6315f4 100644 --- a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx @@ -142,7 +142,7 @@ function priorityIssueNotificationText(row: QualityIssueRow) { `详情:${row.detail || '-'}`, `实时定位:${appURL(buildAppHash({ page: 'realtime', keyword: lookup.key, protocol: row.protocol }))}`, `轨迹证据:${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: 'quality', protocol: row.protocol, filters: { issueType: row.issueType } }))}` ].join('\n'); diff --git a/vehicle-data-platform/apps/web/src/pages/History.tsx b/vehicle-data-platform/apps/web/src/pages/History.tsx index 603a1713..49a2d50b 100644 --- a/vehicle-data-platform/apps/web/src/pages/History.tsx +++ b/vehicle-data-platform/apps/web/src/pages/History.tsx @@ -218,6 +218,7 @@ async function copyText(value: string, label: string) { } export function History({ + mode = 'trajectory', initialVin, initialProtocol, initialTab, @@ -227,6 +228,7 @@ export function History({ onOpenMileage, onOpenRaw }: { + mode?: 'trajectory' | 'query'; initialVin: string; initialProtocol?: string; initialTab?: string; @@ -359,6 +361,13 @@ export function History({ const playbackIntervalMinutes = isFiniteNumber(playbackSpanMinutes) && locationItems.length > 1 ? playbackSpanMinutes / (locationItems.length - 1) : undefined; const currentVehicleKeyword = filters.keyword?.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 selectedFieldCount = splitFields(filters.fields).length; const playbackRows = validLocations.length > 0 ? validLocations : locationItems; @@ -487,8 +496,8 @@ export function History({ return (
onOpenVehicle(currentVehicleKeyword, currentProtocol)}> 当前车辆服务 @@ -498,7 +507,7 @@ export function History({
当前车辆:{currentVehicleKeyword || '-'} 当前来源:{currentProtocol || '全部来源'} - 历史位置和 RAW 帧按当前车辆与来源范围查询。 + {scopeDescription}
submit(values)}> diff --git a/vehicle-data-platform/apps/web/src/pages/Quality.tsx b/vehicle-data-platform/apps/web/src/pages/Quality.tsx index 936d54af..9db4ec2d 100644 --- a/vehicle-data-platform/apps/web/src/pages/Quality.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Quality.tsx @@ -278,7 +278,7 @@ function priorityIssueNotificationText(row: PriorityIssueRow) { `详情:${row.detail || '-'}`, `实时定位:${appURL(buildAppHash({ page: 'realtime', keyword: lookup.key, protocol: row.protocol }))}`, `轨迹证据:${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 }))}`, `告警筛选:${qualityShareURL()}` ].join('\n'); @@ -335,7 +335,7 @@ function priorityIssueTicketText(row: PriorityIssueRow) { `验收标准:${acceptanceCriteria(row)}`, `实时定位:${appURL(buildAppHash({ page: 'realtime', keyword: lookup.key, protocol: row.protocol }))}`, `轨迹证据:${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 }))}`, `告警筛选:${qualityShareURL()}` ].join('\n'); diff --git a/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx b/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx index 3862fccf..66111fd7 100644 --- a/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx +++ b/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx @@ -346,7 +346,7 @@ export function VehicleDetail({ onOpenRaw(filters.keyword, filters.protocol); }; const vehicleServiceURL = (hash: string) => `${window.location.origin}${window.location.pathname}${hash}`; - const vehicleServiceHash = (page: 'detail' | 'realtime' | 'history' | 'mileage', filters?: Record) => buildAppHash({ + const vehicleServiceHash = (page: 'detail' | 'realtime' | 'history' | 'history-query' | 'mileage', filters?: Record) => buildAppHash({ page, keyword: resolvedVIN, protocol: activeProtocol, @@ -380,7 +380,7 @@ export function VehicleDetail({ `车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}`, `实时:${vehicleServiceURL(vehicleServiceHash('realtime'))}`, `轨迹:${vehicleServiceURL(vehicleServiceHash('history'))}`, - `RAW:${vehicleServiceURL(vehicleServiceHash('history', { tab: 'raw', includeFields: 'true' }))}`, + `RAW:${vehicleServiceURL(vehicleServiceHash('history-query', { tab: 'raw', includeFields: 'true' }))}`, `里程:${vehicleServiceURL(vehicleServiceHash('mileage'))}` ]; copyText(lines.join('\n'), '诊断摘要'); diff --git a/vehicle-data-platform/apps/web/src/test/App.test.tsx b/vehicle-data-platform/apps/web/src/test/App.test.tsx index 1e75c368..638a84e3 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -89,7 +89,9 @@ test('exposes AMap operations shortcuts when map key is configured', async () => fireEvent.click(screen.getByRole('button', { name: '顶部轨迹回放' })); expect(window.location.hash.startsWith('#/history')).toBe(true); 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: '顶部统计查询' })); expect(window.location.hash.startsWith('#/mileage')).toBe(true); fireEvent.click(await screen.findByRole('button', { name: '顶部告警事件正常' })); @@ -424,7 +426,7 @@ test('dashboard exposes vehicle data center capability matrix', async () => { await renderDashboard(); 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'); 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 数据缺少 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('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 () => { @@ -3142,7 +3144,7 @@ test('uses backend quality notification plan on quality page', async () => { vehicleLabel: '后端车辆标签 / VIN-PLAN-001', realtimeHash: '#/realtime?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', 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('实时定位: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('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/#/quality')); }); @@ -3975,16 +3977,16 @@ test('opens same-day raw evidence from quality priority queue', async () => { fireEvent.click(screen.getByRole('button', { name: 'RAW证据' })); 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('protocol')).toBe('JT808'); expect(params.get('dateFrom')).toBe('2026-07-03'); expect(params.get('dateTo')).toBe('2026-07-04'); expect(params.get('includeFields')).toBe('true'); 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 () => { @@ -4332,16 +4334,16 @@ test('opens same-day raw evidence from quality issue row', async () => { fireEvent.click(screen.getByRole('button', { name: '核对 RAW' })); 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('protocol')).toBe('GB32960'); expect(params.get('dateFrom')).toBe('2026-07-03'); expect(params.get('dateTo')).toBe('2026-07-04'); expect(params.get('includeFields')).toBe('true'); 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 () => { @@ -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 () => { - 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 path = String(input); if (path.includes('/api/ops/health')) { @@ -5330,7 +5332,7 @@ test('shows and clears current history filters while keeping vehicle scope', asy await waitFor(() => { 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 () => { @@ -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 () => { - 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 revokeObjectURL = vi.spyOn(URL, 'revokeObjectURL').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' })); 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('protocol')).toBe('JT808'); expect(params.get('dateFrom')).toBe('2026-07-03'); expect(params.get('dateTo')).toBe('2026-07-04'); 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 () => { @@ -7015,9 +7017,9 @@ test('opens same-day raw evidence from history location row', async () => { fireEvent.click(screen.getByRole('button', { name: '核对 RAW' })); 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('protocol')).toBe('JT808'); 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 () => { - 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) => { const path = String(input); 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/#/realtime?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'); }); @@ -10207,7 +10209,7 @@ test('opens raw history with the currently selected vehicle detail source', asyn fireEvent.click(screen.getByRole('button', { name: '查看 RAW' })); 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' })); 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'); }); });