From 2aa7b394a6e27245af0c591d3046cac786b474f4 Mon Sep 17 00:00:00 2001 From: lingniu Date: Mon, 6 Jul 2026 04:30:47 +0800 Subject: [PATCH] feat(platform): expose customer service scope actions --- vehicle-data-platform/apps/web/src/App.tsx | 10 ++- .../apps/web/src/pages/Dashboard.tsx | 21 +++++ .../apps/web/src/styles/global.css | 30 +++++++ .../apps/web/src/test/App.test.tsx | 82 +++++++++++++++++++ 4 files changed, 140 insertions(+), 3 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/App.tsx b/vehicle-data-platform/apps/web/src/App.tsx index 790af1f2..8e4a8ced 100644 --- a/vehicle-data-platform/apps/web/src/App.tsx +++ b/vehicle-data-platform/apps/web/src/App.tsx @@ -128,7 +128,7 @@ export default function App() { if (route.page === 'detail') { setActiveVin(route.keyword); } - if (route.page === 'history' || route.page === 'history-query' || route.page === 'mileage') { + if (route.page === 'dashboard' || route.page === 'history' || route.page === 'history-query' || route.page === 'mileage') { setAnalysisVin(route.keyword); } if (route.page === 'time-monitor') { @@ -614,8 +614,12 @@ export default function App() { }; const pages: Record = { - dashboard: , - 'time-monitor': , + 'time-monitor': ) => void; onOpenMileage: (filters?: Record) => void; customerTimeWindow?: Record; + onCopyCustomerScope?: () => void; + onSaveCustomerView?: () => void; focusMode?: 'overview' | 'time-window'; initialTimeMonitorFilters?: Record; }) { @@ -913,6 +917,7 @@ export function Dashboard({ } ]; const timeMonitorScopeValue = timeMonitorScope(); + const customerServiceScopeLabel = `${timeMonitorScopeValue.keyword || '全部车辆'} / ${timeMonitorScopeValue.protocol || '全部数据通道'} / ${timeMonitorScopeValue.dateFrom || '-'} 至 ${timeMonitorScopeValue.dateTo || '-'}`; const usesCustomerTimeWindow = Boolean( (customerTimeWindow?.dateFrom || customerTimeWindow?.dateTo) && customerTimeWindow?.dateFrom === timeMonitorScopeValue.dateFrom @@ -2753,6 +2758,22 @@ export function Dashboard({ ))} +
+
+ 当前服务范围 + + {customerServiceScopeLabel} + +
+
+ + +
+
) : null} {focusMode === 'time-window' ? ( diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index e73c9a73..8568988e 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -1390,6 +1390,36 @@ body { line-height: 18px; } +.vp-customer-service-scope { + grid-column: 1 / -1; + padding: 12px 14px; + border-top: 1px solid var(--vp-border); + background: #f8fafc; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.vp-customer-service-scope-copy { + min-width: 0; + display: grid; + gap: 2px; +} + +.vp-customer-service-scope-copy .semi-typography-secondary { + font-size: 12px; + line-height: 18px; + word-break: break-word; +} + +.vp-customer-service-scope-actions { + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 8px; +} + .vp-vehicle-priority-queue { padding: 12px; border: 1px solid rgba(255, 127, 0, 0.2); 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 f6bfb9af..d856e7d3 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -321,6 +321,88 @@ test('dashboard gives a customer next best action directly on the first screen', expect(within(nextActionRegion).getByRole('button', { name: '建议下一步 时间窗 按同一范围交付数据 复制复盘包' })).toBeInTheDocument(); }); +test('dashboard exposes customer service scope delivery actions on the first screen', async () => { + window.history.replaceState(null, '', '/#/dashboard?keyword=VIN-SCOPE-001&protocol=JT808&dateFrom=2026-07-01&dateTo=2026-07-03'); + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const path = String(input); + if (path.includes('/api/ops/health')) { + return { + ok: true, + json: async () => ({ + data: { + linkHealth: [], + kafkaLag: 0, + activeConnections: 15, + redisOnlineKeys: 92, + tdengineWritable: true, + mysqlWritable: true, + runtime: { requestTimeoutMs: 5000, amapWebJsConfigured: true, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false } + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/dashboard/summary')) { + return { + ok: true, + json: async () => ({ + data: { + onlineVehicles: 92, + activeToday: 120, + frameToday: 21000, + issueVehicles: 7, + kafkaLag: 0, + protocols: [], + serviceStatuses: [], + linkHealth: [] + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/vehicle-service/summary')) { + return { + ok: true, + json: async () => ({ + data: { + totalVehicles: 1033, + boundVehicles: 988, + onlineVehicles: 92, + singleSourceVehicles: 170, + multiSourceVehicles: 220, + noDataVehicles: 15, + identityRequiredVehicles: 9, + serviceStatuses: [], + protocols: [], + missingSources: [] + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + return { + ok: true, + json: async () => ({ + data: { items: [], total: 0, limit: 8, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + }); + + render(); + + expect(await screen.findByRole('heading', { name: '客户车辆服务路径' })).toBeInTheDocument(); + const scopeRegion = screen.getByLabelText('客户服务范围'); + expect(within(scopeRegion).getByText('当前服务范围')).toBeInTheDocument(); + expect(within(scopeRegion).getByText('VIN-SCOPE-001 / JT808 / 2026-07-01 至 2026-07-03')).toBeInTheDocument(); + expect(within(scopeRegion).getByRole('button', { name: '客户服务范围 复制范围' })).toBeInTheDocument(); + expect(within(scopeRegion).getByRole('button', { name: '客户服务范围 保存视图' })).toBeInTheDocument(); +}); + test('topbar frames customer work as a vehicle service delivery strip', () => { vi.spyOn(globalThis, 'fetch').mockImplementation(async () => ({ ok: true,