From 11710ec6fa584f7fa610070e43b6f20dd53ea61d Mon Sep 17 00:00:00 2001 From: lingniu Date: Sun, 5 Jul 2026 01:16:45 +0800 Subject: [PATCH] feat(platform): customerize realtime map workspace --- vehicle-data-platform/apps/web/src/App.tsx | 2 +- .../apps/web/src/pages/Realtime.tsx | 152 +++++++++++++ .../apps/web/src/styles/global.css | 201 ++++++++++++++++++ .../apps/web/src/test/App.test.tsx | 13 +- 4 files changed, 362 insertions(+), 6 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/App.tsx b/vehicle-data-platform/apps/web/src/App.tsx index 60bbc22a..5a9b28e1 100644 --- a/vehicle-data-platform/apps/web/src/App.tsx +++ b/vehicle-data-platform/apps/web/src/App.tsx @@ -455,7 +455,7 @@ export default function App() { const pages: Record = { dashboard: , vehicles: , - map: , + map: , realtime: , detail: , history: , diff --git a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx index 3026023b..03300f6f 100644 --- a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx @@ -471,6 +471,7 @@ async function copyText(value: string, label: string) { } export function Realtime({ + mode = 'realtime', title = '实时监控', description = '以车辆为主对象查看最新位置、在线来源、核心实时数据和地图作业状态', onOpenVehicle, @@ -479,6 +480,7 @@ export function Realtime({ onFiltersChange, initialFilters = {} }: { + mode?: 'realtime' | 'map'; title?: string; description?: string; onOpenVehicle: (vin: string, protocol?: string) => void; @@ -718,6 +720,156 @@ export function Realtime({ const selectRealtimeMapPoint = (point: VehicleMapPoint) => { setSelectedMapPointId(point.id); }; + const mapAttentionRows = rows + .filter((row) => canOpenVehicle(row.vin) && (dataFreshness(row).stale || !isValidCoordinate(row) || hasSourceIssue(row))) + .sort((a, b) => serviceStatusWeight(b) - serviceStatusWeight(a) || String(b.lastSeen ?? '').localeCompare(String(a.lastSeen ?? ''))) + .slice(0, 8); + const mapFleetKpis = [ + { label: '车辆总数', value: pagination.total.toLocaleString(), color: 'blue' as const, helper: `当前页 ${rows.length.toLocaleString()} 辆` }, + { label: '在线车辆', value: onlineCount.toLocaleString(), color: onlineCount > 0 ? 'green' as const : 'orange' as const, helper: `在线率 ${formatPercent(onlineRate)}` }, + { label: '有效定位', value: locatedCount.toLocaleString(), color: locatedCount > 0 ? 'green' as const : 'orange' as const, helper: `定位率 ${formatPercent(locatedRate)}` }, + { label: '需关注', value: mapAttentionRows.length.toLocaleString(), color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const, helper: `${staleCount.toLocaleString()} 辆更新超时` } + ]; + + if (mode === 'map') { + return ( +
+ +
+
{ + applyFilters(values as Record); + }}> + + + GB32960 + JT808 + YUTONG_MQTT + + + 在线 + 离线 + + + + + + + + {autoRefresh ? `自动刷新 ${refreshIntervalSeconds}秒` : '自动刷新已暂停'} + 最后刷新:{lastRefreshAt || '等待首次刷新'} + + + +
+ +
+ {mapFleetKpis.map((item) => ( + + ))} +
+ +
+
+
+ + {amapConfigured ? '高德地图可用' : '坐标预览'} + {locatedCount.toLocaleString()} 辆有效定位 + {filterSummary.map((item) => {item})} + + + + + +
+ +
+ + +
+
+ ); + } return (
diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index 6a7de42b..fc972dfb 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -778,6 +778,195 @@ button.vp-realtime-command-item:focus-visible { height: 380px; } +.vp-map-page { + display: grid; + gap: 14px; +} + +.vp-map-commandbar { + padding: 14px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: var(--vp-surface); + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; +} + +.vp-map-refresh-actions { + min-height: 32px; + align-items: center; +} + +.vp-map-kpi-strip { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.vp-map-kpi-item { + min-height: 96px; + padding: 14px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: #fbfcff; + text-align: left; + font: inherit; + cursor: pointer; + display: grid; + align-content: space-between; + gap: 8px; +} + +.vp-map-kpi-item:hover, +.vp-map-kpi-item:focus-visible { + border-color: rgba(22, 100, 255, 0.42); + background: #f5f9ff; + outline: none; +} + +.vp-map-kpi-item strong { + color: var(--vp-text); + font-size: 26px; + line-height: 32px; + font-weight: 700; +} + +.vp-map-kpi-item span { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; +} + +.vp-map-workspace { + min-height: 680px; + display: grid; + grid-template-columns: minmax(0, 1fr) 340px; + gap: 14px; +} + +.vp-map-canvas-panel { + min-width: 0; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: var(--vp-surface); + overflow: hidden; +} + +.vp-map-canvas-toolbar { + min-height: 54px; + padding: 12px 14px; + border-bottom: 1px solid var(--vp-border); + background: #fbfcff; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; +} + +.vp-map-customer-canvas { + height: 626px; +} + +.vp-map-customer-side { + min-width: 0; + display: grid; + gap: 12px; + align-content: start; +} + +.vp-map-customer-card { + padding: 12px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: var(--vp-surface); + display: grid; + gap: 10px; +} + +.vp-map-selected-card { + border-color: rgba(22, 100, 255, 0.28); + background: #f5f9ff; +} + +.vp-map-card-title { + color: var(--vp-text); + font-size: 13px; + line-height: 20px; + font-weight: 700; +} + +.vp-map-selected-metrics { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 7px 10px; + font-size: 12px; + line-height: 18px; +} + +.vp-map-selected-metrics span { + color: var(--vp-text-muted); +} + +.vp-map-selected-metrics strong { + color: var(--vp-text); + font-weight: 600; + word-break: break-word; +} + +.vp-map-attention-row, +.vp-map-vehicle-row { + width: 100%; + padding: 10px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius-sm); + background: #fbfcff; + color: inherit; + text-align: left; + font: inherit; + cursor: pointer; +} + +.vp-map-attention-row { + display: grid; + gap: 6px; +} + +.vp-map-attention-row > div { + display: grid; + min-width: 0; +} + +.vp-map-vehicle-row { + min-height: 42px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.vp-map-vehicle-row span { + min-width: 0; + overflow: hidden; + color: var(--vp-text); + font-weight: 600; + text-overflow: ellipsis; + white-space: nowrap; +} + +.vp-map-attention-row:hover, +.vp-map-attention-row:focus-visible, +.vp-map-vehicle-row:hover, +.vp-map-vehicle-row:focus-visible, +.vp-map-vehicle-row-selected { + border-color: rgba(22, 100, 255, 0.5); + background: #f5f9ff; + outline: none; +} + .vp-map-dot { position: absolute; width: 10px; @@ -2577,6 +2766,8 @@ button.vp-realtime-command-item:focus-visible { .vp-realtime-impact-board, .vp-realtime-impact-grid, .vp-source-coverage-grid, + .vp-map-kpi-strip, + .vp-map-workspace, .vp-runbook-grid, .vp-workbench-grid, .vp-focus-service, @@ -2624,6 +2815,16 @@ button.vp-realtime-command-item:focus-visible { grid-template-columns: 1fr; } + .vp-map-commandbar, + .vp-map-canvas-toolbar { + align-items: flex-start; + flex-direction: column; + } + + .vp-map-customer-canvas { + height: 460px; + } + .vp-stat-closure-action { align-items: flex-start; flex-direction: column; 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 0ca12aa2..9750adce 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -191,8 +191,9 @@ test('exposes AMap operations shortcuts when map key is configured', async () => expect(await screen.findByText((content) => content.includes('platform-20260704153357'))).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '顶部地图态势' })); expect(window.location.hash).toBe('#/map'); - expect(await screen.findByRole('heading', { name: '实时地图' })).toBeInTheDocument(); - expect(screen.getByText('地图车辆队列')).toBeInTheDocument(); + expect(await screen.findByRole('heading', { name: '车辆实时地图' })).toBeInTheDocument(); + expect(screen.getByText('关注车辆')).toBeInTheDocument(); + expect(screen.getByText('车辆列表')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '顶部实时监控' })); expect(window.location.hash).toBe('#/realtime'); fireEvent.click(screen.getByRole('button', { name: '顶部轨迹回放' })); @@ -8968,11 +8969,13 @@ test('shows realtime freshness status for recently updated and stale vehicles', render(); - expect(await screen.findByRole('heading', { name: '实时地图' })).toBeInTheDocument(); + expect(await screen.findByRole('heading', { name: '车辆实时地图' })).toBeInTheDocument(); expect(screen.getAllByText('数据新鲜').length).toBeGreaterThan(0); expect(screen.getAllByText('更新超时').length).toBeGreaterThan(0); - expect(screen.getByText('超时车辆')).toBeInTheDocument(); - expect(screen.getByText('1 辆超过 5 分钟未更新。')).toBeInTheDocument(); + expect(screen.getByText('关注车辆')).toBeInTheDocument(); + expect(screen.getByText('选中车辆')).toBeInTheDocument(); + expect(screen.getByText('车辆列表')).toBeInTheDocument(); + expect(screen.getByText('1 辆更新超时')).toBeInTheDocument(); }); test('exports current realtime vehicles as CSV', async () => {