diff --git a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx index b1ebde19..1bff4bb1 100644 --- a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx @@ -436,6 +436,50 @@ function realtimeImpactReportText({ ].join('\n'); } +function mapCustomerPackageText({ + filters, + rows, + total, + onlineCount, + locatedCount, + attentionCount, + staleCount, + selectedRow, + selectedProtocol, + amapConfigured +}: { + filters: Record; + rows: VehicleRealtimeRow[]; + total: number; + onlineCount: number; + locatedCount: number; + attentionCount: number; + staleCount: number; + selectedRow?: VehicleRealtimeRow; + selectedProtocol?: string; + amapConfigured: boolean; +}) { + const selectedVIN = selectedRow?.vin || ''; + return [ + '【客户地图监控包】', + `当前筛选:${realtimeFilterSummary(filters).join(';') || '全部车辆'}`, + `车辆范围:当前页 ${rows.length.toLocaleString()} / 总计 ${total.toLocaleString()}`, + `在线态势:${onlineCount.toLocaleString()} 在线 / ${(rows.length - onlineCount).toLocaleString()} 离线`, + `定位态势:${locatedCount.toLocaleString()} 辆有有效坐标`, + `关注车辆:${attentionCount.toLocaleString()} 辆;更新超时 ${staleCount.toLocaleString()} 辆`, + `地图能力:${amapConfigured ? '高德地图可用' : '坐标预览'}`, + `选中车辆:${selectedRow ? `${selectedRow.plate || '-'} / ${selectedVIN} / ${selectedProtocol || selectedRow.primaryProtocol || '-'}` : '未选择'}`, + selectedRow ? `选中车辆状态:${vehicleServiceStatus(selectedRow).label};${dataFreshness(selectedRow).detail};${realtimeIssueLabels(selectedRow).join(';')}` : '', + selectedRow ? `选中车辆位置:${isValidCoordinate(selectedRow) ? `${selectedRow.longitude},${selectedRow.latitude}` : '无有效坐标'}` : '', + `实时地图:${appURL(buildAppHash({ page: 'map', protocol: filters.protocol, filters }))}`, + selectedVIN ? `车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: selectedVIN, protocol: selectedProtocol }))}` : '', + selectedVIN ? `轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: selectedVIN, protocol: selectedProtocol }))}` : '', + selectedVIN ? `里程统计:${appURL(buildAppHash({ page: 'mileage', keyword: selectedVIN, protocol: selectedProtocol }))}` : '', + selectedVIN ? `历史数据:${appURL(buildAppHash({ page: 'history-query', keyword: selectedVIN, protocol: selectedProtocol, filters: { tab: 'raw', includeFields: 'true' } }))}` : '', + selectedVIN ? `告警通知:${appURL(buildAppHash({ page: 'alert-events', keyword: selectedVIN, protocol: selectedProtocol }))}` : '' + ].filter(Boolean).join('\n'); +} + function buildRealtimeSourceCoverage(rows: VehicleRealtimeRow[]) { const sourceMap = new Map(); rows.forEach((row) => { @@ -842,6 +886,50 @@ export function Realtime({ secondaryDisabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin) } ]; + const mapCustomerPackageItems = [ + { + label: '车辆范围', + value: `${rows.length.toLocaleString()} / ${pagination.total.toLocaleString()}`, + detail: `当前筛选覆盖 ${formatPercent(pageCoverageRate)}`, + color: pageCoverageRate >= 100 ? 'green' as const : 'blue' as const + }, + { + label: '在线态势', + value: `${onlineCount.toLocaleString()} 在线`, + detail: `${(rows.length - onlineCount).toLocaleString()} 辆离线`, + color: rows.length - onlineCount > 0 ? 'orange' as const : 'green' as const + }, + { + label: '定位态势', + value: `${locatedCount.toLocaleString()} 有坐标`, + detail: `定位率 ${formatPercent(locatedRate)}`, + color: locatedCount > 0 ? 'green' as const : 'orange' as const + }, + { + label: '关注车辆', + value: `${mapAttentionRows.length.toLocaleString()} 辆`, + detail: `${staleCount.toLocaleString()} 辆更新超时`, + color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const + }, + { + label: '选中车辆', + value: selectedVehicleLabel, + detail: selectedMapRow ? `${vehicleServiceStatus(selectedMapRow).label} / ${dataFreshness(selectedMapRow).detail}` : '从地图或车辆列表选择', + color: selectedMapRow ? vehicleServiceStatus(selectedMapRow).color : 'grey' as const + } + ]; + const copyMapCustomerPackage = () => copyText(mapCustomerPackageText({ + filters, + rows, + total: pagination.total, + onlineCount, + locatedCount, + attentionCount: mapAttentionRows.length, + staleCount, + selectedRow: selectedMapRow, + selectedProtocol: selectedVehicleProtocol, + amapConfigured + }), '客户地图监控包'); if (mode === 'map') { return ( @@ -926,6 +1014,37 @@ export function Realtime({ ))} +
+
+ 客户地图监控包 + + {amapConfigured ? '高德地图可用' : '坐标预览'} + 0 ? 'orange' : 'green'}> + {mapAttentionRows.length > 0 ? '存在关注车辆' : '地图态势稳定'} + + + {selectedVehicleLabel} + + 面向客户看图时,先确认车辆范围、在线态势、定位态势和选中车辆,再进入轨迹、里程、历史数据或告警通知。 + + + + + + + +
+
+ {mapCustomerPackageItems.map((item) => ( +
+ {item.label} + {item.value} + {item.detail} +
+ ))} +
+
+
diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index 484ad27f..a3d0fcfc 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -1194,6 +1194,58 @@ button.vp-realtime-command-item:focus-visible { line-height: 20px; } +.vp-map-customer-package { + padding: 14px; + border: 1px solid rgba(0, 164, 184, 0.24); + border-radius: var(--vp-radius); + background: #f3fbfc; + display: grid; + grid-template-columns: minmax(300px, 0.82fr) minmax(0, 1.18fr); + gap: 12px; +} + +.vp-map-customer-package-summary { + min-width: 0; + display: grid; + gap: 10px; + align-content: start; +} + +.vp-map-customer-package-summary .semi-typography { + line-height: 21px; +} + +.vp-map-customer-package-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 10px; +} + +.vp-map-customer-package-item { + min-height: 134px; + padding: 12px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: #fbfcff; + display: grid; + gap: 8px; + align-content: start; +} + +.vp-map-customer-package-item strong { + color: var(--vp-text); + font-size: 18px; + line-height: 24px; + font-weight: 700; + word-break: break-word; +} + +.vp-map-customer-package-item span { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; +} + .vp-map-vehicle-workbench { padding: 14px; border: 1px solid rgba(22, 100, 255, 0.18); @@ -3926,6 +3978,8 @@ button.vp-realtime-command-item:focus-visible { .vp-source-coverage-grid, .vp-map-kpi-strip, .vp-map-task-strip, + .vp-map-customer-package, + .vp-map-customer-package-grid, .vp-map-vehicle-work-grid, .vp-map-workspace, .vp-runbook-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 a47a8d11..92d255ea 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -9034,6 +9034,11 @@ test('refreshes realtime vehicle data from the monitoring workspace', async () = test('shows realtime freshness status for recently updated and stale vehicles', async () => { window.history.replaceState(null, '', '/#/map'); + 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/realtime/vehicles')) { @@ -9104,11 +9109,17 @@ test('shows realtime freshness status for recently updated and stale vehicles', expect(screen.getByText('实时盯车')).toBeInTheDocument(); expect(screen.getByText('异常优先')).toBeInTheDocument(); expect(screen.getAllByText('轨迹复盘').length).toBeGreaterThanOrEqual(1); - expect(screen.getByText('数据交付')).toBeInTheDocument(); + expect(screen.getAllByText('数据交付').length).toBeGreaterThanOrEqual(1); 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.getByText('车辆范围')).toBeInTheDocument(); + expect(screen.getByText('在线态势')).toBeInTheDocument(); + expect(screen.getByText('定位态势')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /复制地图监控包/ })).toBeInTheDocument(); expect(screen.getByText('地图车辆作业台')).toBeInTheDocument(); expect(screen.getAllByText('粤A新鲜1').length).toBeGreaterThanOrEqual(1); expect(screen.getByRole('button', { name: '地图车辆作业 车辆服务 车辆档案' })).toBeInTheDocument(); @@ -9116,9 +9127,15 @@ 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.getAllByText('关注车辆').length).toBeGreaterThanOrEqual(1); - expect(screen.getByText('选中车辆')).toBeInTheDocument(); + expect(screen.getAllByText('选中车辆').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('车辆列表')).toBeInTheDocument(); - expect(screen.getByText('1 辆更新超时')).toBeInTheDocument(); + expect(screen.getAllByText('1 辆更新超时').length).toBeGreaterThanOrEqual(1); + fireEvent.click(screen.getByRole('button', { name: /复制地图监控包/ })); + await waitFor(() => { + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户地图监控包】')); + }); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('在线态势:1 在线 / 1 离线')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆服务:http://localhost:3000/#/detail?keyword=VIN-STALE-001&protocol=JT808')); }); test('exports current realtime vehicles as CSV', async () => {