diff --git a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx index 3264e2da..8cf82cbf 100644 --- a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx @@ -504,6 +504,58 @@ export function Dashboard({ ] } ]; + const scenarioNavigation = [ + { + title: '实时监控', + objective: '确认车辆是否在线、位置是否可信、三类来源是否齐全。', + evidence: `Redis 在线态 / 最新定位 / 有效坐标 ${commandLocatedCount.toLocaleString()}`, + sla: '目标 0-1 秒内进入实时视图', + primaryAction: '实时监控', + secondaryAction: '地图态势', + onPrimary: () => onOpenRealtime({ online: 'online' }), + onSecondary: () => onOpenMap({ online: 'online' }) + }, + { + title: '轨迹回放', + objective: '按车辆复盘位置、速度、里程和断点,定位平台转发或车辆异常。', + evidence: `高德轨迹底座 / 今日活跃 ${formatCount(summary?.activeToday)}`, + sla: '支持按车辆和时间窗快速回放', + primaryAction: '轨迹回放', + secondaryAction: 'RAW证据', + onPrimary: () => onOpenHistory(), + onSecondary: () => onOpenHistory({ tab: 'raw', includeFields: 'true' }) + }, + { + title: '历史数据查询', + objective: '围绕车辆查询位置、RAW、解析字段,给 BI、运维和业务复盘提供证据。', + evidence: `TDengine / 今日帧 ${formatCount(summary?.frameToday)} / Kafka Lag ${formatLag(summary?.kafkaLag)}`, + sla: '优先返回必要字段,避免大 JSON 拖慢查询', + primaryAction: '历史查询', + secondaryAction: '字段证据', + onPrimary: () => onOpenHistory({ tab: 'location' }), + onSecondary: () => onOpenHistory({ tab: 'raw', includeFields: 'true' }) + }, + { + title: '告警事件触发和通知', + objective: '把断链、无来源、VIN 缺失、字段缺失转换为可通知、可闭环事件。', + evidence: `告警车辆 ${formatCount(summary?.issueVehicles)} / 最高优先级 ${highPriorityIssue ? qualityIssueLabel(highPriorityIssue.issueType) : '暂无'}`, + sla: 'P0 进入通知队列,超时升级', + primaryAction: '告警事件', + secondaryAction: '通知规则', + onPrimary: () => onOpenQuality(), + onSecondary: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {}) + }, + { + title: '统计查询', + objective: '按车辆口径查询里程等指标,保证区间统计和日统计可闭合。', + evidence: `车辆口径 ${formatCount(serviceSummary?.totalVehicles)} / 多源覆盖 ${formatCount(serviceSummary?.multiSourceVehicles)}`, + sla: '统计值必须能追溯轨迹和 RAW 证据', + primaryAction: '统计查询', + secondaryAction: '车辆中心', + onPrimary: () => onOpenMileage(), + onSecondary: () => onOpenVehicles({}) + } + ]; const copyOperationsHandoff = () => { const unhealthyLinks = (summary?.linkHealth ?? []).filter((item) => item.status !== 'ok'); const priorityAction = serviceActionQueue[0]; @@ -532,6 +584,24 @@ export function Dashboard({ ].filter(Boolean); copyText(lines.join('\n'), '运营交接摘要'); }; + const copyScenarioBlueprint = () => { + const lines = [ + '【车辆数据中台功能蓝图】', + '定位:32960 / 808 / 宇通 MQTT 都只是车辆服务证据源,最终围绕一辆车提供实时、轨迹、历史、告警、统计能力。', + `高德地图:${amapConfigured ? '已配置,前端只使用运行时配置和安全代理' : '待配置'}`, + `车辆规模:${formatCount(serviceSummary?.totalVehicles)};在线:${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)};告警:${formatCount(summary?.issueVehicles)}`, + '', + ...scenarioNavigation.map((item, index) => [ + `${index + 1}. ${item.title}`, + ` 目标:${item.objective}`, + ` 证据:${item.evidence}`, + ` SLA:${item.sla}` + ].join('\n')), + '', + `入口:${appURL(buildAppHash({ page: 'dashboard' }))}` + ]; + copyText(lines.join('\n'), '功能蓝图'); + }; const buildDashboardSnapshotRows = () => { const unhealthyLinks = (summary?.linkHealth ?? []).filter((item) => item.status !== 'ok'); const priorityAction = serviceActionQueue[0]; @@ -707,6 +777,32 @@ export function Dashboard({ ))} + 车联网场景导航} + style={{ marginBottom: 16 }} + > +
+ {scenarioNavigation.map((item) => ( +
+
+ {item.title} + {item.objective} +
+
{item.evidence}
+ {item.sla} + + + + +
+ ))} +
+
{workflowSteps.map((item, index) => ( diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index c3fb4651..e977a2f7 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -1160,6 +1160,42 @@ button.vp-realtime-command-item:focus-visible { line-height: 32px; } +.vp-scenario-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 12px; +} + +.vp-scenario-item { + min-height: 220px; + padding: 14px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: #fbfcff; + display: grid; + gap: 12px; + align-content: space-between; +} + +.vp-scenario-main { + display: grid; + gap: 8px; +} + +.vp-scenario-main .semi-typography { + line-height: 20px; +} + +.vp-scenario-evidence { + min-height: 42px; + padding: 10px; + border-radius: 6px; + background: rgba(22, 100, 255, 0.06); + color: var(--vp-text); + font-size: 12px; + line-height: 18px; +} + .vp-operation-step { min-height: 176px; padding: 14px; @@ -1317,6 +1353,7 @@ button.vp-realtime-command-item:focus-visible { .vp-realtime-command-board, .vp-realtime-command-grid, .vp-workbench-grid, + .vp-scenario-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 da22d2b3..08ee8470 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -408,6 +408,11 @@ test('dashboard presents one vehicle service operating posture', async () => { test('dashboard exposes vehicle data center capability matrix', async () => { window.history.replaceState(null, '', '/#/dashboard'); + const writeText = vi.fn(() => Promise.resolve()); + Object.defineProperty(navigator, 'clipboard', { + configurable: true, + value: { writeText } + }); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { @@ -508,6 +513,7 @@ test('dashboard exposes vehicle data center capability matrix', async () => { await renderDashboard(); expect(screen.getByText('车辆服务作业台')).toBeInTheDocument(); + expect(screen.getByText('车联网场景导航')).toBeInTheDocument(); expect(screen.getAllByText('实时地图').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('实时监控').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('轨迹回放').length).toBeGreaterThanOrEqual(1); @@ -525,11 +531,31 @@ test('dashboard exposes vehicle data center capability matrix', async () => { expect(screen.getAllByText('今日帧 1,286,320').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('告警 7').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('统计车辆 1,033').length).toBeGreaterThanOrEqual(1); + expect(screen.getByText('确认车辆是否在线、位置是否可信、三类来源是否齐全。')).toBeInTheDocument(); + expect(screen.getByText('按车辆复盘位置、速度、里程和断点,定位平台转发或车辆异常。')).toBeInTheDocument(); + expect(screen.getByText('优先返回必要字段,避免大 JSON 拖慢查询')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '复制功能蓝图' })); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【车辆数据中台功能蓝图】')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('32960 / 808 / 宇通 MQTT 都只是车辆服务证据源')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆规模:1,033;在线:208;告警:7')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. 实时监控')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('SLA:目标 0-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(); + + await renderDashboard(); + fireEvent.click(screen.getByRole('button', { name: '场景导航 轨迹回放 RAW证据' })); + 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).toBe('#/realtime?online=online');