From 378706cc5c913a71ba638a3b82f856a9474e2f15 Mon Sep 17 00:00:00 2001 From: lingniu Date: Mon, 6 Jul 2026 05:09:50 +0800 Subject: [PATCH] feat(platform): add customer map command center --- .../apps/web/src/pages/Realtime.tsx | 126 ++++++++++++++++++ .../apps/web/src/test/App.test.tsx | 15 +++ 2 files changed, 141 insertions(+) diff --git a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx index 299e2d41..8aae261c 100644 --- a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx @@ -513,6 +513,45 @@ function mapCustomerPackageText({ ].filter(Boolean).join('\n'); } +function mapExecutiveControlPackageText({ + filters, + rows, + total, + onlineCount, + locatedCount, + attentionCount, + selectedRow, + selectedProtocol, + amapConfigured +}: { + filters: Record; + rows: VehicleRealtimeRow[]; + total: number; + onlineCount: number; + locatedCount: number; + attentionCount: number; + selectedRow?: VehicleRealtimeRow; + selectedProtocol?: string; + amapConfigured: boolean; +}) { + const selectedVIN = selectedRow?.vin || ''; + return [ + '【客户车辆地图总控包】', + `当前筛选:${realtimeFilterSummary(filters).join(';') || '全部车辆'}`, + `地图总控:当前页 ${rows.length.toLocaleString()} / 总计 ${total.toLocaleString()}`, + `服务链路:实时找车 -> 轨迹回放 -> 围栏告警 -> 报表导出`, + `在线定位:${onlineCount.toLocaleString()} 在线;${locatedCount.toLocaleString()} 辆有坐标;${attentionCount.toLocaleString()} 辆关注`, + `地图能力:${amapConfigured ? '高德地图可用' : '坐标预览'}`, + `选中车辆:${selectedRow ? `${selectedRow.plate || '-'} / ${selectedVIN} / ${selectedProtocol || selectedRow.primaryProtocol || '-'}` : '未选择'}`, + selectedRow ? `选中车辆状态:${vehicleServiceStatus(selectedRow).label};${dataFreshness(selectedRow).detail}` : '', + `实时地图:${appURL(buildAppHash({ page: 'map', protocol: filters.protocol, filters: { ...filters, online: filters.online || 'online' } }))}`, + selectedVIN ? `轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: selectedVIN, protocol: selectedProtocol }))}` : '', + `围栏告警:${appURL(buildAppHash({ page: 'alert-events', filters: { serviceStatus: 'degraded', ...(filters.protocol ? { protocol: filters.protocol } : {}) } }))}`, + selectedVIN ? `统计查询:${appURL(buildAppHash({ page: 'mileage', keyword: selectedVIN, protocol: selectedProtocol }))}` : '', + selectedVIN ? `历史导出:${appURL(buildAppHash({ page: 'history-query', keyword: selectedVIN, protocol: selectedProtocol, filters: { tab: 'raw', includeFields: 'true' } }))}` : '' + ].filter(Boolean).join('\n'); +} + function selectedMapVehicleHandoffText(row: VehicleRealtimeRow, protocol: string) { const vin = row.vin || ''; const vehicle = `${row.plate || '-'} / ${vin || '-'} / ${protocol || row.primaryProtocol || '-'}`; @@ -1790,6 +1829,17 @@ export function Realtime({ selectedProtocol: selectedVehicleProtocol, amapConfigured }), '客户地图监控包'); + const copyMapExecutiveControlPackage = () => copyText(mapExecutiveControlPackageText({ + filters, + rows, + total: pagination.total, + onlineCount, + locatedCount, + attentionCount: mapAttentionRows.length, + selectedRow: selectedMapRow, + selectedProtocol: selectedVehicleProtocol, + amapConfigured + }), '客户车辆地图总控包'); const copyMapCustomerDecision = () => copyText(mapCustomerDecisionText({ filters, rows, @@ -1801,6 +1851,44 @@ export function Realtime({ selectedProtocol: selectedVehicleProtocol, amapConfigured }), '地图客户决策说明'); + const mapExecutiveControlItems = [ + { + title: '实时找车', + value: `${onlineCount.toLocaleString()} 在线`, + detail: `${locatedCount.toLocaleString()} 辆有坐标,先确认客户当前能看见哪些车。`, + action: '打开地图', + color: onlineCount > 0 ? 'green' as const : 'orange' as const, + disabled: false, + onClick: () => applyFilters({ ...filters, online: 'online' }) + }, + { + title: '轨迹回放', + value: selectedMapRow?.plate || selectedMapRow?.vin || '先选车', + detail: selectedMapRow ? '围绕选中车辆回放位置、速度、里程和断点。' : '从地图或列表选择一辆车后进入轨迹回放。', + action: '回放轨迹', + color: selectedMapRow ? 'blue' as const : 'grey' as const, + disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin), + onClick: () => selectedMapRow && onOpenHistory?.(selectedMapRow.vin, selectedVehicleProtocol) + }, + { + title: '围栏告警', + value: `${mapAttentionRows.length.toLocaleString()} 关注`, + detail: mapAttentionRows[0] ? `${mapAttentionRows[0].plate || mapAttentionRows[0].vin}:${realtimeIssueLabels(mapAttentionRows[0]).join(';')}` : '当前没有明显离线、超时或定位异常车辆。', + action: '告警通知', + color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const, + disabled: !onOpenQuality, + onClick: () => onOpenQuality?.({ serviceStatus: 'degraded', protocol: filters.protocol }) + }, + { + title: '报表导出', + value: `${rows.length.toLocaleString()} 当前页`, + detail: '导出当前地图车辆范围、在线状态、位置和服务状态。', + action: '导出报表', + color: rows.length > 0 ? 'blue' as const : 'grey' as const, + disabled: rows.length === 0, + onClick: exportRealtime + } + ]; const realtimeCustomerJourneyItems = [ { step: '01', @@ -2324,6 +2412,44 @@ export function Realtime({ +
+
+ + 客户车辆地图总控 + {amapConfigured ? '高德地图可用' : '坐标预览'} + 0 ? 'orange' : 'green'}> + {mapAttentionRows.length > 0 ? `${mapAttentionRows.length.toLocaleString()} 辆关注` : '态势稳定'} + + + 把实时找车、轨迹回放、围栏告警和报表导出收敛成一条客户能直接使用的地图服务链路。 + + 参考现代车队 Live Map 的使用方式,客户先看车辆是否在线和可定位,再围绕选中车辆完成复盘、通知和交付。 + + + + + + +
+
+ {mapExecutiveControlItems.map((item) => ( + + ))} +
+
+
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 d6f91161..9d751cf4 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -11446,6 +11446,13 @@ test('shows realtime freshness status for recently updated and stale vehicles', expect(screen.getByText('把 32960、808、MQTT 收敛成同一张车辆地图:看位置、判在线、选车辆、回放轨迹、处理告警和导出证据。')).toBeInTheDocument(); expect(screen.getByText('高德地图底座')).toBeInTheDocument(); expect(screen.getByText('来源证据仅用于筛选和追溯,不作为客户主路径。')).toBeInTheDocument(); + expect(screen.getByText('客户车辆地图总控')).toBeInTheDocument(); + expect(screen.getByText('把实时找车、轨迹回放、围栏告警和报表导出收敛成一条客户能直接使用的地图服务链路。')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户车辆地图总控 实时找车 1 在线 打开地图' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户车辆地图总控 轨迹回放 粤A超时1 回放轨迹' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户车辆地图总控 围栏告警 1 关注 告警通知' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户车辆地图总控 报表导出 2 当前页 导出报表' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /复制地图总控包/ })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '车辆服务地图任务 实时监控 1 在线' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '车辆服务地图任务 轨迹回放 粤A超时1' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '车辆服务地图任务 历史查询 JT808' })).toBeInTheDocument(); @@ -11504,6 +11511,14 @@ test('shows realtime freshness status for recently updated and stale vehicles', expect(writeText).toHaveBeenCalledWith(expect.stringContaining('问题:')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('天未更新;JT808 离线')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆服务:http://localhost:3000/#/detail?keyword=VIN-STALE-001&protocol=JT808')); + fireEvent.click(screen.getByRole('button', { name: /复制地图总控包/ })); + await waitFor(() => { + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户车辆地图总控包】')); + }); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('地图总控:当前页 2 / 总计 2')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('服务链路:实时找车 -> 轨迹回放 -> 围栏告警 -> 报表导出')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时地图:http://localhost:3000/#/map?')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('选中车辆:粤A超时1 / VIN-STALE-001 / JT808')); fireEvent.click(screen.getByRole('button', { name: /复制地图监控包/ })); await waitFor(() => { expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户地图监控包】'));