diff --git a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx index 9ed1409d..823279e1 100644 --- a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx @@ -618,6 +618,62 @@ export function Dashboard({ ] } ]; + const dataFlowStages = [ + { + stage: '01', + title: '协议接入', + owner: '网关接收', + evidence: `32960 / 808 / MQTT,连接 ${formatCount(opsHealth?.activeConnections)}`, + detail: '只接收真实实时数据帧和必要鉴权注册帧,协议不是最终产品。', + action: '运维质量', + onClick: () => onOpenQuality() + }, + { + stage: '02', + title: '统一解析', + owner: '字段映射', + evidence: `今日帧 ${formatCount(summary?.frameToday)},Kafka Lag ${formatLag(summary?.kafkaLag)}`, + detail: '一次解析生成扁平字段、原始证据和车辆身份关联,避免三处重复解析。', + action: '历史字段', + onClick: () => onOpenHistory({ tab: 'raw', includeFields: 'true' }) + }, + { + stage: '03', + title: '实时投影', + owner: 'Redis KV', + evidence: `在线 Key ${formatCount(opsHealth?.redisOnlineKeys)},在线车辆 ${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)}`, + detail: '快速写入车辆最新状态、在线 TTL 和全量协议字段,支撑 0-1 秒监控。', + action: '实时监控', + onClick: () => onOpenRealtime({ online: 'online' }) + }, + { + stage: '04', + title: '车辆归并', + owner: 'Vehicle Service', + evidence: `多源 ${formatCount(serviceSummary?.multiSourceVehicles)},单源 ${formatCount(serviceSummary?.singleSourceVehicles)}`, + detail: '以 VIN 合并来源证据,车牌、手机号和 OEM 只是车辆服务的检索键。', + action: '车辆中心', + onClick: () => onOpenVehicles({}) + }, + { + stage: '05', + title: '历史证据', + owner: 'TDengine / MySQL', + evidence: `轨迹 ${formatCount(summary?.activeToday)} 活跃,RAW ${formatCount(summary?.frameToday)} 帧`, + detail: '位置、RAW 和解析字段可分页查询,并能回跳轨迹、车辆服务和告警。', + action: '轨迹回放', + onClick: () => onOpenHistory() + }, + { + stage: '06', + title: '运营闭环', + owner: '告警 / 统计', + evidence: `告警 ${formatCount(summary?.issueVehicles)},统计车辆 ${formatCount(serviceSummary?.totalVehicles)}`, + detail: '断链、缺字段、里程异常和离线时长进入通知、统计和复核工作流。', + action: '统计查询', + onClick: () => onOpenMileage() + } + ]; const scenarioNavigation = [ { title: '实时监控', @@ -716,6 +772,28 @@ export function Dashboard({ ]; copyText(lines.join('\n'), '功能蓝图'); }; + const copyDataFlowBlueprint = () => { + const lines = [ + '【车辆数据中台数据流转图】', + '原则:32960 / 808 / 宇通 MQTT 是接入证据,最终都归并为同一辆车的车辆服务。', + `运行态:连接 ${formatCount(opsHealth?.activeConnections)};在线 Key ${formatCount(opsHealth?.redisOnlineKeys)};Kafka Lag ${formatLag(summary?.kafkaLag)};今日帧 ${formatCount(summary?.frameToday)}`, + '', + ...dataFlowStages.map((item) => [ + `${item.stage}. ${item.title} / ${item.owner}`, + ` 证据:${item.evidence}`, + ` 说明:${item.detail}` + ].join('\n')), + '', + `实时监控:${appURL(buildAppHash({ page: 'realtime', filters: { online: 'online' } }))}`, + `车辆中心:${appURL(buildAppHash({ page: 'vehicles' }))}`, + `轨迹回放:${appURL(buildAppHash({ page: 'history' }))}`, + `历史查询:${appURL(buildAppHash({ page: 'history-query', filters: { tab: 'raw', includeFields: 'true' } }))}`, + `告警事件:${appURL(buildAppHash({ page: 'alert-events' }))}`, + `统计查询:${appURL(buildAppHash({ page: 'mileage' }))}`, + `运维质量:${appURL(buildAppHash({ page: 'ops-quality' }))}` + ]; + copyText(lines.join('\n'), '数据流转图'); + }; const copyFocusVehicleService = () => { if (!focusVehicle) { Toast.warning('当前没有重点车辆服务可复制'); @@ -976,6 +1054,26 @@ export function Dashboard({ ))} + 数据流转作业链复制流转图} + style={{ marginBottom: 16 }} + > + + {dataFlowStages.map((item) => ( + + + {item.stage} + {item.owner} + + {item.title} + {item.evidence} + {item.detail} + {item.action} + + ))} + + 车联网场景导航复制功能蓝图} diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index 5928e120..d8521657 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -1980,6 +1980,64 @@ button.vp-realtime-command-item:focus-visible { line-height: 32px; } +.vp-data-flow-grid { + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 12px; +} + +.vp-data-flow-item { + min-height: 216px; + padding: 14px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: #fbfcff; + color: inherit; + display: grid; + gap: 10px; + align-content: start; + text-align: left; + cursor: pointer; + transition: border-color 0.16s ease, box-shadow 0.16s ease, background 0.16s ease; +} + +.vp-data-flow-item:hover, +.vp-data-flow-item:focus-visible { + border-color: rgba(22, 100, 255, 0.42); + box-shadow: 0 0 0 3px rgba(22, 100, 255, 0.08); + outline: none; +} + +.vp-data-flow-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.vp-data-flow-head > span { + width: 30px; + height: 30px; + border-radius: 7px; + background: rgba(22, 100, 255, 0.09); + color: #1664ff; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 700; +} + +.vp-data-flow-evidence { + min-height: 54px; + padding: 10px; + border-radius: 6px; + background: rgba(10, 168, 107, 0.08); + color: var(--vp-text); + font-size: 12px; + line-height: 18px; +} + .vp-scenario-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); @@ -2252,6 +2310,7 @@ button.vp-realtime-command-item:focus-visible { .vp-scenario-grid, .vp-operation-flow, .vp-service-model-grid, + .vp-data-flow-grid, .vp-capability-grid, .vp-conclusion-grid, .vp-monitor-layout, 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 8ebf3422..5ba50c12 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -546,11 +546,18 @@ test('dashboard exposes vehicle data center capability matrix', async () => { await renderDashboard(); expect(screen.getByText('车辆服务作业台')).toBeInTheDocument(); expect(screen.getByText('地图运营能力')).toBeInTheDocument(); + expect(screen.getByText('数据流转作业链')).toBeInTheDocument(); expect(screen.getByText('车联网场景导航')).toBeInTheDocument(); expect(screen.getByText('高德 Web JS')).toBeInTheDocument(); expect(screen.getByText('服务端 API')).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.getAllByText('历史证据').length).toBeGreaterThanOrEqual(1); + expect(screen.getByText('运营闭环')).toBeInTheDocument(); expect(screen.getAllByText('已配置').length).toBeGreaterThanOrEqual(2); expect(screen.getByText('已启用')).toBeInTheDocument(); expect(screen.getByText('未暴露')).toBeInTheDocument(); @@ -581,6 +588,9 @@ test('dashboard exposes vehicle data center capability matrix', async () => { expect(writeText).toHaveBeenCalledWith(expect.stringContaining('高德地图:Web JS 已配置;服务端 API 已配置;安全代理 已启用;安全码未暴露')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. 实时监控')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('SLA:目标 0-1 秒内进入实时视图')); + fireEvent.click(screen.getByRole('button', { name: '复制流转图' })); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【车辆数据中台数据流转图】')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('03. 实时投影 / Redis KV')); fireEvent.click(screen.getByRole('button', { name: '地图运营 实时监控' })); expect(window.location.hash).toBe('#/map?online=online'); @@ -592,6 +602,17 @@ test('dashboard exposes vehicle data center capability matrix', async () => { 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'); + 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).toBe('#/map?online=online');