From 09c86022b27f24d32c59c86c1a86320f60d81b68 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 21:07:14 +0800 Subject: [PATCH] feat(platform): add realtime source coverage board --- .../apps/web/src/pages/Realtime.tsx | 57 +++++++++++++++++++ .../apps/web/src/styles/global.css | 56 ++++++++++++++++++ .../apps/web/src/test/App.test.tsx | 6 ++ 3 files changed, 119 insertions(+) diff --git a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx index 6a853d4a..fb91b737 100644 --- a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx @@ -168,6 +168,15 @@ const realtimeExportColumns: CsvColumn[] = [ { title: '绑定状态', value: (row) => row.bindingStatus } ]; +type RealtimeSourceCoverage = { + protocol: string; + total: number; + online: number; + located: number; + degraded: number; + stale: number; +}; + function realtimeExportFileName(filters: Record) { const keyword = filters.keyword?.trim() || 'all'; const protocol = filters.protocol?.trim() || 'all-source'; @@ -387,6 +396,26 @@ function realtimeDutyHandoffText({ return lines.join('\n'); } +function buildRealtimeSourceCoverage(rows: VehicleRealtimeRow[]) { + const sourceMap = new Map(); + rows.forEach((row) => { + const protocols = row.sourceStatus?.length + ? row.sourceStatus.map((source) => source.protocol) + : row.primaryProtocol ? [row.primaryProtocol] : []; + protocols.forEach((protocol) => { + const source = row.sourceStatus?.find((item) => item.protocol === protocol); + const current = sourceMap.get(protocol) ?? { protocol, total: 0, online: 0, located: 0, degraded: 0, stale: 0 }; + current.total += 1; + current.online += (source ? source.online : row.online) ? 1 : 0; + current.located += isValidCoordinate(row) ? 1 : 0; + current.degraded += (!source || !source.online || !source.hasRealtime || row.onlineSourceCount < row.sourceCount) ? 1 : 0; + current.stale += dataFreshness(row).stale ? 1 : 0; + sourceMap.set(protocol, current); + }); + }); + return [...sourceMap.values()].sort((left, right) => right.total - left.total || left.protocol.localeCompare(right.protocol)); +} + async function copyText(value: string, label: string) { const text = value.trim(); if (!text) { @@ -499,6 +528,7 @@ export function Realtime({ const degradedRate = rows.length > 0 ? (degradedCount / rows.length) * 100 : 0; const pageCoverageRate = pagination.total > 0 ? (rows.length / pagination.total) * 100 : 0; const primaryProtocols = new Set(rows.map((row) => row.primaryProtocol).filter(Boolean)); + const sourceCoverageRows = buildRealtimeSourceCoverage(rows); const sourceIssueRows = rows .filter((row) => canOpenVehicle(row.vin) && hasSourceIssue(row)) .sort((a, b) => { @@ -705,6 +735,33 @@ export function Realtime({ +
+
协议地图覆盖
+
+ {sourceCoverageRows.length === 0 ? ( + 当前页暂无协议来源。 + ) : sourceCoverageRows.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 ab382bc1..1457791c 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -344,6 +344,61 @@ button.vp-realtime-command-item:focus-visible { line-height: 28px; } +.vp-source-coverage-board { + margin-bottom: 16px; + padding: 14px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: var(--vp-surface); +} + +.vp-source-coverage-grid { + margin-top: 10px; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; +} + +.vp-source-coverage-item { + min-height: 98px; + padding: 12px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: #fbfcff; + text-align: left; + font: inherit; + cursor: pointer; +} + +.vp-source-coverage-item:hover, +.vp-source-coverage-item:focus-visible { + border-color: rgba(22, 100, 255, 0.42); + background: #f5f9ff; + outline: none; +} + +.vp-source-coverage-item-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.vp-source-coverage-item-head span { + color: var(--vp-text); + font-weight: 700; +} + +.vp-source-coverage-bars { + margin-top: 12px; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 8px; + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; +} + .vp-result-summary-item { min-height: 72px; padding: 12px; @@ -1402,6 +1457,7 @@ button.vp-realtime-command-item:focus-visible { .vp-current-service-grid, .vp-realtime-command-board, .vp-realtime-command-grid, + .vp-source-coverage-grid, .vp-runbook-grid, .vp-workbench-grid, .vp-scenario-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 11d41836..b79afcca 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -8511,6 +8511,10 @@ test('copies realtime operations summary from realtime page', async () => { expect(screen.getByRole('button', { name: '实时筛选 降级服务 2' })).toBeInTheDocument(); expect(screen.getByText('建议处置')).toBeInTheDocument(); expect(screen.getByText('排查降级服务 2')).toBeInTheDocument(); + expect(screen.getByText('协议地图覆盖')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '筛选协议 GB32960' })).toHaveTextContent('GB32960'); + expect(screen.getByRole('button', { name: '筛选协议 JT808' })).toHaveTextContent('2 辆'); + expect(screen.getByRole('button', { name: '筛选协议 YUTONG_MQTT' })).toHaveTextContent('超时 1'); expect(screen.getByText('实时覆盖判读')).toBeInTheDocument(); expect(screen.getByText('在线率')).toBeInTheDocument(); expect(screen.getAllByText('66.7%').length).toBeGreaterThanOrEqual(3); @@ -8552,6 +8556,8 @@ test('copies realtime operations summary from realtime page', async () => { expect(writeText).toHaveBeenCalledWith(expect.stringContaining('历史RAW:http://localhost:3000/api/history/raw-frames?protocol=JT808&vin=VIN-RT-SUMMARY-003&limit=20&includeFields=true')); fireEvent.click(screen.getByRole('button', { name: '实时筛选 降级服务 2' })); expect(window.location.hash).toBe('#/realtime?protocol=JT808&serviceStatus=degraded&online=online'); + fireEvent.click(screen.getByRole('button', { name: '筛选协议 GB32960' })); + expect(window.location.hash).toBe('#/realtime?protocol=GB32960&serviceStatus=degraded&online=online'); }); test('shows production AMap integration status on realtime page', async () => {