From 08c275af3425f3700ba38a5516ff72148260d0fa Mon Sep 17 00:00:00 2001 From: lingniu Date: Sun, 5 Jul 2026 19:07:50 +0800 Subject: [PATCH] feat(platform): preserve vehicle context in customer tasks --- vehicle-data-platform/apps/web/src/App.tsx | 42 +++++++++++++++++-- .../apps/web/src/layout/AppShell.tsx | 4 +- .../apps/web/src/pages/Quality.tsx | 3 +- .../apps/web/src/test/App.test.tsx | 9 ++++ 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/App.tsx b/vehicle-data-platform/apps/web/src/App.tsx index 6d36c076..726d832a 100644 --- a/vehicle-data-platform/apps/web/src/App.tsx +++ b/vehicle-data-platform/apps/web/src/App.tsx @@ -329,10 +329,12 @@ export default function App() { const context = await loadVehicleContext(lookupKey, nextProtocol); const resolved = context?.resolved; const nextKey = context?.nextKey ?? lookupKey; + const resolvedProtocol = nextProtocol || context?.overview.primaryProtocol?.trim() || ''; setActiveVin(nextKey); - setActiveProtocol(nextProtocol); + setAnalysisVin(nextKey); + setActiveProtocol(resolvedProtocol); setActivePage('detail'); - replaceHash('detail', nextKey, nextProtocol); + replaceHash('detail', nextKey, resolvedProtocol); if (!resolved) { Toast.warning('未匹配到车辆身份,已打开问题排查视图'); } @@ -443,6 +445,40 @@ export default function App() { 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() ?? ''; @@ -476,7 +512,7 @@ export default function App() { }; return ( - + {pages[activePage]} ); diff --git a/vehicle-data-platform/apps/web/src/layout/AppShell.tsx b/vehicle-data-platform/apps/web/src/layout/AppShell.tsx index be92ceb2..e9b3ac28 100644 --- a/vehicle-data-platform/apps/web/src/layout/AppShell.tsx +++ b/vehicle-data-platform/apps/web/src/layout/AppShell.tsx @@ -131,6 +131,7 @@ export function AppShell({ currentVehicleLabel, currentVehicleConsistency, onChange, + onCustomerTask, onVehicleSearch, children }: { @@ -143,6 +144,7 @@ export function AppShell({ currentVehicleLabel?: string; currentVehicleConsistency?: VehicleSourceConsistency; onChange: (page: PageKey) => void; + onCustomerTask?: (page: PageKey) => void; onVehicleSearch: (keyword: string) => void | Promise; children: ReactNode; }) { @@ -212,7 +214,7 @@ export function AppShell({ key={item.page} type="button" className={selectedPage === item.page ? 'vp-customer-task-dispatch-action vp-customer-task-dispatch-action-active' : 'vp-customer-task-dispatch-action'} - onClick={() => onChange(item.page)} + onClick={() => (onCustomerTask ?? onChange)(item.page)} aria-label={`客户任务 ${item.label} ${item.question}`} > {item.label} diff --git a/vehicle-data-platform/apps/web/src/pages/Quality.tsx b/vehicle-data-platform/apps/web/src/pages/Quality.tsx index 269635e4..dc3b621e 100644 --- a/vehicle-data-platform/apps/web/src/pages/Quality.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Quality.tsx @@ -198,7 +198,8 @@ function qualityActionRecommendation(row: QualityIssueRow) { } function issueCount(summary: QualitySummary, issueType: string) { - return summary.issueTypes.find((item) => item.name === issueType)?.count ?? 0; + const issueTypes = Array.isArray(summary.issueTypes) ? summary.issueTypes : []; + return issueTypes.find((item) => item.name === issueType)?.count ?? 0; } const issuePriorityWeight: Record = { 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 812c2a1f..ed0d9ce1 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -2945,6 +2945,15 @@ test('shows resolved vehicle service status after topbar search', async () => { expect(screen.getByText('一致性:数据通道不完整')).toBeInTheDocument(); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicle-service/overview?keyword=%E7%B2%A4AG18312'), undefined); expect(fetchMock).not.toHaveBeenCalledWith(expect.stringContaining('/api/vehicles/resolve'), undefined); + + fireEvent.click(screen.getByRole('button', { name: '客户任务 轨迹回放 这段时间怎么跑' })); + expect(window.location.hash).toBe('#/history?keyword=LB9A32A24R0LS1426&protocol=JT808'); + fireEvent.click(screen.getByRole('button', { name: '客户任务 统计查询 里程怎么算' })); + expect(window.location.hash).toBe('#/mileage?keyword=LB9A32A24R0LS1426&protocol=JT808'); + fireEvent.click(screen.getByRole('button', { name: '客户任务 数据导出 证据怎么给客户' })); + expect(window.location.hash).toBe('#/history-query?keyword=LB9A32A24R0LS1426&protocol=JT808&tab=raw'); + fireEvent.click(screen.getByRole('button', { name: '客户任务 告警通知 异常谁处理' })); + expect(window.location.hash).toBe('#/alert-events?keyword=LB9A32A24R0LS1426&protocol=JT808'); }); test('shows row service status in dashboard vehicle previews', async () => {