From 2c881dae7ccb3391b8bebe9da991ea603df4ca8d Mon Sep 17 00:00:00 2001 From: lingniu Date: Sun, 5 Jul 2026 01:55:54 +0800 Subject: [PATCH] feat(platform): add vehicle service task board --- .../apps/web/src/pages/Dashboard.tsx | 96 +++++++++++++++++++ .../apps/web/src/styles/global.css | 36 +++++++ .../apps/web/src/test/App.test.tsx | 28 +++++- 3 files changed, 159 insertions(+), 1 deletion(-) diff --git a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx index 285bc3b8..85d61e9b 100644 --- a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx @@ -445,6 +445,67 @@ export function Dashboard({ onClick: () => onOpenHistory() } ]; + const taskTimeMonitorSummary = (() => { + const vehicle = String(timeMonitorFilters.keyword ?? '').trim() || '全部车辆'; + const source = String(timeMonitorFilters.protocol ?? '').trim() || '全部数据通道'; + const dateFrom = String(timeMonitorFilters.dateFrom ?? '').trim(); + const dateTo = String(timeMonitorFilters.dateTo ?? '').trim(); + const range = dateFrom || dateTo ? `${dateFrom || '-'} 至 ${dateTo || '-'}` : '全部时间'; + return `${vehicle} / ${source} / ${range}`; + })(); + const customerTaskBoard = [ + { + title: '先看全域在线', + value: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线`, + detail: `地图有效定位 ${commandLocatedCount.toLocaleString()} 辆,优先确认车辆是否仍在上报。`, + color: 'green' as const, + primaryAction: '打开实时地图', + secondaryAction: '查看在线车辆', + onPrimary: () => onOpenMap({ online: 'online' }), + onSecondary: () => onOpenRealtime({ online: 'online' }) + }, + { + title: '再看重点车辆', + value: focusVehicle?.label ?? '暂无重点车辆', + detail: focusVehicle ? `${focusVehicle.reason},${focusVehicle.realtimeEvidence}` : '当前没有高优先级车辆,保持日常巡检。', + color: focusVehicle?.statusColor ?? 'green' as const, + primaryAction: '车辆服务', + secondaryAction: '复制处置卡', + onPrimary: () => focusVehicle?.lookupKey && onOpenVehicle(focusVehicle.lookupKey, focusVehicle.protocol), + onSecondary: () => copyFocusVehicleService(), + disabled: !focusVehicle?.lookupKey + }, + { + title: '按时间窗复盘', + value: taskTimeMonitorSummary, + detail: '同一时间窗贯穿轨迹、里程、RAW 和告警,不需要跨页面重复输入。', + color: 'blue' as const, + primaryAction: '轨迹回放', + secondaryAction: '里程统计', + onPrimary: () => openTimeMonitorHistory(), + onSecondary: () => openTimeMonitorMileage() + }, + { + title: '导出证据数据', + value: `${formatCount(summary?.frameToday)} 今日数据`, + detail: '面向 BI、客户复盘和异常定位,按车辆、时间、字段裁剪导出。', + color: 'blue' as const, + primaryAction: '历史导出', + secondaryAction: '导出概览', + onPrimary: () => openTimeMonitorRaw(), + onSecondary: () => exportDashboardSnapshot() + }, + { + title: '闭环告警通知', + value: `${formatCount(summary?.issueVehicles)} 告警车辆`, + detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '暂无高优先级告警。', + color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const, + primaryAction: '告警事件', + secondaryAction: '复制交接', + onPrimary: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {}), + onSecondary: () => copyOperationsHandoff() + } + ]; const capabilities = [ { title: '实时地图', @@ -1039,6 +1100,41 @@ export function Dashboard({ ))} + +
+ {customerTaskBoard.map((item) => ( +
+
+ {item.title} + {item.value} +
+ {item.detail} + + + + +
+ ))} +
+
{kpis.map((item) => ( diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index 2d1d6835..a7c3e816 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -410,6 +410,41 @@ body { line-height: 26px; } +.vp-customer-task-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 12px; +} + +.vp-customer-task-item { + min-height: 172px; + padding: 14px; + border: 1px solid var(--vp-border); + border-radius: 8px; + background: #fbfcff; + display: grid; + gap: 12px; + align-content: space-between; +} + +.vp-customer-task-head { + min-width: 0; + display: grid; + gap: 8px; +} + +.vp-customer-task-head strong { + color: var(--vp-text); + font-size: 18px; + line-height: 24px; + word-break: break-word; +} + +.vp-customer-task-item .semi-typography { + font-size: 13px; + line-height: 20px; +} + .vp-time-monitor-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); @@ -3105,6 +3140,7 @@ button.vp-realtime-command-item:focus-visible { .vp-map-ops-readiness, .vp-map-ops-work, .vp-time-monitor-grid, + .vp-customer-task-grid, .vp-current-service-board, .vp-current-service-grid, .vp-vehicle-service-desk, 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 51c1bafb..780f1f22 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -791,6 +791,12 @@ test('dashboard exposes vehicle data center capability matrix', async () => { expect(screen.getByText('客户常用工作流')).toBeInTheDocument(); expect(screen.getByText('实时车辆地图')).toBeInTheDocument(); expect(screen.getByText('历史数据导出')).toBeInTheDocument(); + expect(screen.getByText('今日车辆服务任务板')).toBeInTheDocument(); + expect(screen.getByText('先看全域在线')).toBeInTheDocument(); + expect(screen.getByText('再看重点车辆')).toBeInTheDocument(); + expect(screen.getByText('按时间窗复盘')).toBeInTheDocument(); + expect(screen.getByText('导出证据数据')).toBeInTheDocument(); + expect(screen.getByText('闭环告警通知')).toBeInTheDocument(); expect(screen.getByText('车辆服务入口')).toBeInTheDocument(); expect(screen.getByText('自定义时间监控')).toBeInTheDocument(); expect(screen.getByText('应用时间窗')).toBeInTheDocument(); @@ -844,6 +850,23 @@ test('dashboard exposes vehicle data center capability matrix', async () => { expect(window.location.hash).toBe('#/map?online=online'); cleanup(); + await renderDashboard(); + fireEvent.click(screen.getByRole('button', { name: '今日任务 先看全域在线 打开实时地图' })); + expect(window.location.hash).toBe('#/map?online=online'); + cleanup(); + + await renderDashboard(); + fireEvent.click(screen.getByRole('button', { name: '今日任务 按时间窗复盘 里程统计' })); + expect(window.location.hash.startsWith('#/mileage')).toBe(true); + expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('dateFrom')).toBeTruthy(); + cleanup(); + + await renderDashboard(); + fireEvent.click(screen.getByRole('button', { name: '今日任务 导出证据数据 历史导出' })); + expect(window.location.hash.startsWith('#/history-query')).toBe(true); + expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('includeFields')).toBe('true'); + cleanup(); + await renderDashboard(); fireEvent.click(screen.getByRole('button', { name: '客户工作流 历史数据导出' })); expect(window.location.hash.startsWith('#/history-query')).toBe(true); @@ -5528,7 +5551,10 @@ test('drills into quality issues by issue type', async () => { render(); - fireEvent.click(await screen.findByRole('button', { name: /主要问题 暂无数据来源 1/ })); + const issueButtons = await screen.findAllByRole('button', { name: /主要问题 暂无数据来源 1/ }); + const issueSummaryButton = issueButtons[issueButtons.length - 1]; + expect(issueButtons.length).toBeGreaterThanOrEqual(1); + fireEvent.click(issueSummaryButton as HTMLElement); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/alert-events?issueType=NO_SOURCE&limit=20&offset=0'), undefined);