From ea76ac22da7eb5b4e6af1d70d66cc1434f5bd12c Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 19:44:51 +0800 Subject: [PATCH] feat(platform): add vehicle service workbench --- .../apps/web/src/pages/Dashboard.tsx | 85 +++++++++++++++++++ .../apps/web/src/styles/global.css | 38 +++++++++ .../apps/web/src/test/App.test.tsx | 11 +++ 3 files changed, 134 insertions(+) diff --git a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx index 6c8511fa..33088ebe 100644 --- a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx @@ -432,6 +432,63 @@ export function Dashboard({ onClick: () => onOpenMileage() } ]; + const operationWorkbench = [ + { + title: '实时监控', + value: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线`, + meta: `有效定位 ${commandLocatedCount.toLocaleString()}`, + color: 'green' as const, + detail: '面向值班人员确认车辆是否在线、坐标是否有效、来源是否完整。', + actions: [ + { label: '打开实时地图', onClick: () => onOpenMap({ online: 'online' }) }, + { label: '查看在线车辆', onClick: () => onOpenRealtime({ online: 'online' }) } + ] + }, + { + title: '轨迹回放', + value: `${formatCount(summary?.activeToday)} 今日活跃`, + meta: '高德轨迹证据', + color: 'blue' as const, + detail: '按车辆回看位置、速度、里程和断点,用于定位异常复盘。', + actions: [ + { label: '打开轨迹回放', onClick: () => onOpenHistory() }, + { label: '查 RAW 证据', onClick: () => onOpenHistory({ tab: 'raw', includeFields: 'true' }) } + ] + }, + { + title: '历史数据查询', + value: `${formatCount(summary?.frameToday)} 今日帧`, + meta: `Kafka Lag ${formatLag(summary?.kafkaLag)}`, + color: (summary?.kafkaLag ?? 0) > 0 ? 'orange' as const : 'blue' as const, + detail: '查询位置历史、RAW 帧和解析字段,为车辆服务提供可追溯证据。', + actions: [ + { label: '查询历史数据', onClick: () => onOpenHistory({ tab: 'location' }) }, + { label: '查询解析字段', onClick: () => onOpenHistory({ tab: 'raw', includeFields: 'true' }) } + ] + }, + { + title: '告警事件与通知', + value: `${formatCount(summary?.issueVehicles)} 告警车辆`, + meta: highPriorityIssue ? `${highPriorityIssue.severity === 'error' ? 'P0' : 'P1'} ${qualityIssueLabel(highPriorityIssue.issueType)}` : '暂无高优先级', + color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const, + detail: '把断链、无来源、VIN 缺失、字段缺失转成可通知、可闭环的事件。', + actions: [ + { label: '查看告警事件', onClick: () => onOpenQuality() }, + { label: '处理最高优先级', onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {}) } + ] + }, + { + title: '统计查询', + value: `${formatCount(serviceSummary?.totalVehicles)} 车辆口径`, + meta: '区间闭合复核', + color: 'green' as const, + detail: '里程等指标先按车辆口径闭合,再回溯轨迹和 RAW 证据。', + actions: [ + { label: '打开统计查询', onClick: () => onOpenMileage() }, + { label: '查看车辆中心', onClick: () => onOpenVehicles({}) } + ] + } + ]; return (
@@ -460,6 +517,34 @@ export function Dashboard({ 0 ? 'orange' : 'green'}>Kafka Lag {formatLag(summary?.kafkaLag)} + +
+ {operationWorkbench.map((item) => ( +
+
+ {item.title} + {item.meta} +
+
{item.value}
+ {item.detail} + + {item.actions.map((action) => ( + + ))} + +
+ ))} +
+
{vehicleServicePrinciples.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 feefcf9d..c3fb4651 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -1093,6 +1093,43 @@ button.vp-realtime-command-item:focus-visible { min-width: 0; } +.vp-workbench-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 12px; +} + +.vp-workbench-item { + min-height: 190px; + padding: 14px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: #fbfcff; + display: grid; + gap: 10px; + align-content: start; +} + +.vp-workbench-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.vp-workbench-head .semi-typography { + font-size: 12px; + line-height: 18px; + text-align: right; +} + +.vp-workbench-value { + color: var(--vp-text); + font-size: 24px; + font-weight: 700; + line-height: 30px; +} + .vp-operation-flow { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); @@ -1279,6 +1316,7 @@ button.vp-realtime-command-item:focus-visible { .vp-current-service-grid, .vp-realtime-command-board, .vp-realtime-command-grid, + .vp-workbench-grid, .vp-operation-flow, .vp-service-model-grid, .vp-capability-grid, 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 eebe72d9..bb55ce89 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -456,12 +456,18 @@ test('dashboard exposes vehicle data center capability matrix', async () => { }; await renderDashboard(); + expect(screen.getByText('车辆服务作业台')).toBeInTheDocument(); expect(screen.getAllByText('实时地图').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('实时监控').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('轨迹回放').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('历史数据查询').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('告警事件触发与通知').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('统计查询').length).toBeGreaterThanOrEqual(1); + expect(screen.getByText('告警事件与通知')).toBeInTheDocument(); + expect(screen.getByText('区间闭合复核')).toBeInTheDocument(); + expect(screen.getByText('高德轨迹证据')).toBeInTheDocument(); + expect(screen.getAllByText('Kafka Lag 0').length).toBeGreaterThanOrEqual(1); + expect(screen.getByText('1,033 车辆口径')).toBeInTheDocument(); expect(screen.getAllByText('在线 208').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('在线态势 208').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('有效定位 0').length).toBeGreaterThanOrEqual(1); @@ -469,6 +475,11 @@ test('dashboard exposes vehicle data center capability matrix', async () => { expect(screen.getAllByText('告警 7').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('统计车辆 1,033').length).toBeGreaterThanOrEqual(1); + 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).toBe('#/realtime?online=online'); cleanup();