From 2852cdd7b6112b02c3c13b05ef137a43492c7ba1 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sun, 5 Jul 2026 18:49:05 +0800 Subject: [PATCH] feat(platform): add map area monitoring workspace --- .../apps/web/src/pages/Realtime.tsx | 111 ++++++++++++++++++ .../apps/web/src/styles/global.css | 72 ++++++++++++ .../apps/web/src/test/App.test.tsx | 12 ++ 3 files changed, 195 insertions(+) diff --git a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx index 8fccfac5..07e3b064 100644 --- a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx @@ -554,6 +554,39 @@ function mapCustomerDecisionText({ ].filter(Boolean).join('\n'); } +function mapAreaMonitorText({ + filters, + rows, + locatedCount, + attentionCount, + selectedRow, + selectedProtocol, + amapConfigured +}: { + filters: Record; + rows: VehicleRealtimeRow[]; + locatedCount: number; + attentionCount: number; + selectedRow?: VehicleRealtimeRow; + selectedProtocol?: string; + amapConfigured: boolean; +}) { + const selectedVIN = selectedRow?.vin || ''; + return [ + '【区域围栏监控说明】', + `当前筛选:${realtimeFilterSummary(filters).join(';') || '全部车辆'}`, + `区域态势:${locatedCount.toLocaleString()} 辆可定位 / ${attentionCount.toLocaleString()} 辆关注 / ${amapConfigured ? '高德地图可用' : '坐标预览'}`, + `车辆范围:当前页 ${rows.length.toLocaleString()} 辆`, + `围栏建议:先用有效定位车辆作为区域覆盖基础,再对离线、超时、越界和长时间停留车辆触发告警通知。`, + selectedRow ? `选中车辆:${selectedRow.plate || '-'} / ${selectedVIN} / ${selectedProtocol || selectedRow.primaryProtocol || '-'}` : '选中车辆:未选择', + selectedRow ? `选中车辆位置:${isValidCoordinate(selectedRow) ? `${selectedRow.longitude},${selectedRow.latitude}` : '无有效坐标'}` : '', + `实时地图:${appURL(buildAppHash({ page: 'map', protocol: filters.protocol, filters }))}`, + `关注车辆:${appURL(buildAppHash({ page: 'map', protocol: filters.protocol, filters: { ...filters, serviceStatus: 'degraded' } }))}`, + selectedVIN ? `轨迹复盘:${appURL(buildAppHash({ page: 'history', keyword: selectedVIN, protocol: selectedProtocol }))}` : '', + `告警通知:${appURL(buildAppHash({ page: 'alert-events', filters: { serviceStatus: 'degraded' } }))}` + ].filter(Boolean).join('\n'); +} + function buildRealtimeSourceCoverage(rows: VehicleRealtimeRow[]) { const sourceMap = new Map(); rows.forEach((row) => { @@ -1164,6 +1197,53 @@ export function Realtime({ onClick: () => mapAttentionRows.length > 0 && onOpenQuality ? onOpenQuality({ serviceStatus: 'degraded' }) : copyMapCustomerPackage() } ]; + const copyMapAreaMonitor = () => copyText(mapAreaMonitorText({ + filters, + rows, + locatedCount, + attentionCount: mapAttentionRows.length, + selectedRow: selectedMapRow, + selectedProtocol: selectedVehicleProtocol, + amapConfigured + }), '区域围栏监控说明'); + const mapAreaMonitorItems = [ + { + title: '电子围栏', + value: `${locatedCount.toLocaleString()} 辆可定位`, + detail: '先把有效定位车辆作为围栏覆盖基础,后续按区域配置越界和进出围栏规则。', + color: locatedCount > 0 ? 'blue' as const : 'orange' as const, + action: '关注车辆', + disabled: false, + onClick: () => applyFilters({ ...filters, serviceStatus: 'degraded' }) + }, + { + title: '停留超时', + value: selectedVehicleLabel, + detail: selectedMapRow ? '围绕选中车辆回放轨迹,识别停留、低速和异常时间窗。' : '先选择车辆,再进入轨迹复盘判断停留时长。', + color: selectedMapRow ? 'blue' as const : 'grey' as const, + action: '轨迹复盘', + disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin), + onClick: () => selectedMapRow && onOpenHistory?.(selectedMapRow.vin, selectedVehicleProtocol) + }, + { + title: '越界通知', + value: mapAttentionRows.length > 0 ? `${mapAttentionRows.length.toLocaleString()} 辆关注` : '暂无越界', + detail: mapAttentionRows.length > 0 ? '将离线、超时、坐标异常车辆交给告警事件形成通知闭环。' : '当前区域态势稳定,可继续巡检。', + color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const, + action: '告警事件', + disabled: !onOpenQuality, + onClick: () => onOpenQuality?.({ serviceStatus: 'degraded' }) + }, + { + title: '区域报表', + value: `${rows.length.toLocaleString()} 辆当前页`, + detail: '复制区域监控说明,交付当前围栏态势、关注车辆和后续通知链接。', + color: 'blue' as const, + action: '复制说明', + disabled: false, + onClick: copyMapAreaMonitor + } + ]; const mapDispatchActionItems = [ { title: '关注车辆', @@ -1713,6 +1793,37 @@ export function Realtime({ +
+
+ + 区域围栏监控 + 0 ? 'green' : 'orange'}>{locatedCount.toLocaleString()} 辆可定位 + 0 ? 'orange' : 'green'}>{mapAttentionRows.length.toLocaleString()} 辆关注 + + 把地图从“看车在哪里”升级成区域运营工具:围栏、停留、越界和通知都从车辆位置态势进入。 + + 电子围栏先依赖实时定位和关注车辆,后续可接入客户区域配置、进出围栏事件和周期区域报表。 + +
+
+ {mapAreaMonitorItems.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 198edd11..08bc2cb5 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -3708,6 +3708,76 @@ button.vp-realtime-command-item:focus-visible { font-weight: 700; } +.vp-map-area-monitor { + padding: 14px; + border: 1px solid rgba(11, 159, 119, 0.2); + border-radius: var(--vp-radius); + background: #f5fbf9; + display: grid; + grid-template-columns: minmax(300px, 0.72fr) minmax(0, 1.28fr); + gap: 12px; +} + +.vp-map-area-monitor-summary { + display: grid; + gap: 10px; + align-content: start; +} + +.vp-map-area-monitor-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; +} + +.vp-map-area-monitor-item { + min-height: 156px; + padding: 12px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: #fbfefc; + color: inherit; + text-align: left; + font: inherit; + cursor: pointer; + display: grid; + gap: 8px; + align-content: start; +} + +.vp-map-area-monitor-item:hover, +.vp-map-area-monitor-item:focus-visible { + border-color: rgba(11, 159, 119, 0.42); + box-shadow: 0 10px 24px rgba(24, 39, 75, 0.08); + outline: none; +} + +.vp-map-area-monitor-item:disabled { + cursor: not-allowed; + opacity: 0.62; +} + +.vp-map-area-monitor-item strong { + color: var(--vp-text); + font-size: 18px; + line-height: 24px; + font-weight: 700; + word-break: break-word; +} + +.vp-map-area-monitor-item span { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; +} + +.vp-map-area-monitor-item em { + color: #0b9f77; + font-size: 12px; + font-style: normal; + font-weight: 700; +} + .vp-map-dispatch-actions { padding: 14px; border: 1px solid rgba(255, 127, 0, 0.2); @@ -8752,6 +8822,8 @@ button.vp-realtime-command-item:focus-visible { .vp-map-decision-grid, .vp-map-dispatch-actions, .vp-map-dispatch-actions-grid, + .vp-map-area-monitor, + .vp-map-area-monitor-grid, .vp-map-kpi-strip, .vp-map-selected-service-strip, .vp-map-selected-service-actions, 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 338b1451..c295a98f 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -9584,6 +9584,12 @@ test('shows realtime freshness status for recently updated and stale vehicles', expect(screen.getByRole('button', { name: '地图现场指挥 关注车辆 定位关注车' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '地图现场指挥 轨迹复盘 打开轨迹' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '地图现场指挥 通知证据 告警事件' })).toBeInTheDocument(); + expect(screen.getByText('区域围栏监控')).toBeInTheDocument(); + expect(screen.getByText('把地图从“看车在哪里”升级成区域运营工具:围栏、停留、越界和通知都从车辆位置态势进入。')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '区域围栏监控 电子围栏 关注车辆' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '区域围栏监控 停留超时 轨迹复盘' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '区域围栏监控 越界通知 告警事件' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '区域围栏监控 区域报表 复制说明' })).toBeInTheDocument(); expect(screen.getByText('地图调度处置栏')).toBeInTheDocument(); expect(screen.getByText('把地图上的车辆直接转成调度动作:先处理关注车辆,再复盘选中车辆,最后导出交接证据。')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '地图调度处置栏 关注车辆 1 辆 告警事件' })).toBeInTheDocument(); @@ -9631,6 +9637,12 @@ test('shows realtime freshness status for recently updated and stale vehicles', expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【地图客户决策说明】')); }); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('定位判断:2 辆有有效坐标 / 0 辆无坐标')); + fireEvent.click(screen.getByRole('button', { name: '区域围栏监控 区域报表 复制说明' })); + await waitFor(() => { + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【区域围栏监控说明】')); + }); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('区域态势:2 辆可定位 / 1 辆关注 / 坐标预览')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('告警通知:http://localhost:3000/#/alert-events?serviceStatus=degraded')); expect(screen.getByText('自定义时间窗复盘')).toBeInTheDocument(); expect(screen.getByText('按车辆和时间窗复盘发生了什么')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: /复制时间窗包/ }));