diff --git a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx index 89280ef3..f3eea265 100644 --- a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx @@ -1717,6 +1717,67 @@ export function Realtime({ onClick: exportRealtime } ]; + const mapLayerControlItems = [ + { + label: '在线车辆', + value: `${onlineCount.toLocaleString()} 在线`, + detail: '只看当前还能实时服务的车辆,适合客户查看在线态势。', + color: onlineCount > 0 ? 'green' as const : 'orange' as const, + onClick: () => applyFilters({ ...filters, online: 'online' }) + }, + { + label: '关注车辆', + value: `${mapAttentionRows.length.toLocaleString()} 关注`, + detail: mapAttentionRows[0] ? `优先车辆 ${mapAttentionRows[0].plate || mapAttentionRows[0].vin}` : '当前没有离线、超时或通道异常车辆。', + color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const, + onClick: () => mapAttentionRows[0] ? selectRealtimeRow(mapAttentionRows[0]) : applyFilters({ ...filters, serviceStatus: 'degraded' }) + }, + { + label: '定位车辆', + value: `${locatedCount.toLocaleString()} 定位`, + detail: `有效定位率 ${formatPercent(locatedRate)},决定地图、轨迹和围栏是否可用。`, + color: locatedCount > 0 ? 'blue' as const : 'orange' as const, + onClick: () => load(filters, pagination.currentPage, pagination.pageSize) + }, + { + label: '选中车辆', + value: selectedVehicleLabel, + detail: selectedMapRow ? '进入单车服务、轨迹、统计、历史和告警。' : '从地图或车辆列表选择一辆车。', + color: selectedMapRow ? vehicleServiceStatus(selectedMapRow).color : 'grey' as const, + disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin), + onClick: () => selectedMapRow && onOpenVehicle(selectedMapRow.vin, selectedVehicleProtocol) + } + ]; + const mapLayerControlActions = [ + { + label: '只看在线', + action: '在线层', + color: 'green' as const, + disabled: false, + onClick: () => applyFilters({ ...filters, online: 'online' }) + }, + { + label: '优先关注', + action: '关注层', + color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const, + disabled: mapAttentionRows.length === 0, + onClick: () => mapAttentionRows[0] && selectRealtimeRow(mapAttentionRows[0]) + }, + { + label: '选车服务', + action: '服务层', + color: selectedMapRow ? 'blue' as const : 'grey' as const, + disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin), + onClick: () => selectedMapRow && onOpenVehicle(selectedMapRow.vin, selectedVehicleProtocol) + }, + { + label: '导出态势', + action: '交付层', + color: rows.length > 0 ? 'blue' as const : 'grey' as const, + disabled: rows.length === 0, + onClick: exportRealtime + } + ]; const copyMapCustomerPackage = () => copyText(mapCustomerPackageText({ filters, rows, @@ -2341,6 +2402,55 @@ export function Realtime({ +
+
+ + 地图图层控制台 + {amapConfigured ? '高德底图' : '坐标预览'} + 0 ? 'orange' : 'green'}> + {mapAttentionRows.length > 0 ? '关注优先' : '稳定图层'} + + + + 把实时地图拆成客户能理解的图层:在线、关注、定位、选车服务和交付导出。 + + + 图层控制不改变底层数据,只改变客户当下的查看重点:先看在线与定位,再处理关注车辆,最后进入单车服务或导出。 + +
+
+ {mapLayerControlItems.map((item) => ( + + ))} +
+
+ {mapLayerControlActions.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 7f6e1886..094a5f7c 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -4939,6 +4939,108 @@ body { word-break: break-word; } +.vp-map-layer-control { + border: 1px solid rgba(22, 100, 255, 0.18); + border-radius: var(--vp-radius); + background: #fff; + display: grid; + grid-template-columns: minmax(260px, 0.72fr) minmax(0, 1.28fr) minmax(180px, 0.52fr); + overflow: hidden; +} + +.vp-map-layer-control-copy { + padding: 16px; + border-right: 1px solid var(--vp-border); + background: #f7fbff; + display: grid; + gap: 10px; + align-content: start; +} + +.vp-map-layer-control-grid { + padding: 12px; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; +} + +.vp-map-layer-control-item { + min-height: 112px; + padding: 12px; + border: 1px solid rgba(22, 100, 255, 0.13); + border-radius: 8px; + background: #fbfcff; + color: inherit; + cursor: pointer; + font: inherit; + text-align: left; + display: grid; + gap: 8px; + align-content: start; +} + +.vp-map-layer-control-item:hover, +.vp-map-layer-control-item:focus-visible, +.vp-map-layer-control-action:hover, +.vp-map-layer-control-action:focus-visible { + border-color: rgba(22, 100, 255, 0.42); + background: #f5f9ff; + box-shadow: var(--vp-shadow-sm); + outline: none; +} + +.vp-map-layer-control-item:disabled, +.vp-map-layer-control-action:disabled { + cursor: not-allowed; + opacity: 0.62; +} + +.vp-map-layer-control-item strong { + color: var(--vp-text); + font-size: 18px; + line-height: 24px; + word-break: break-word; +} + +.vp-map-layer-control-item span { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; + word-break: break-word; +} + +.vp-map-layer-control-actions { + padding: 12px; + border-left: 1px solid var(--vp-border); + background: #fafcff; + display: grid; + gap: 8px; +} + +.vp-map-layer-control-action { + min-height: 44px; + padding: 8px 10px; + border: 1px solid rgba(22, 100, 255, 0.13); + border-radius: 8px; + background: #fff; + color: inherit; + cursor: pointer; + font: inherit; + text-align: left; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.vp-map-layer-control-action span { + color: var(--vp-primary); + font-size: 12px; + font-weight: 700; + line-height: 18px; + white-space: nowrap; +} + .vp-realtime-customer-steps { padding: 16px; display: grid; @@ -14062,6 +14164,8 @@ button.vp-realtime-command-item:focus-visible { .vp-map-ops-work, .vp-map-status-filter-strip, .vp-map-status-filter-grid, + .vp-map-layer-control, + .vp-map-layer-control-grid, .vp-customer-service-command-main, .vp-customer-service-command-grid, .vp-customer-fleet-groups, @@ -14393,6 +14497,8 @@ button.vp-realtime-command-item:focus-visible { .vp-history-next-export-actions, .vp-time-next-action-copy, .vp-time-next-action-actions, + .vp-map-layer-control-copy, + .vp-map-layer-control-actions, .vp-notification-orchestration-copy, .vp-notification-orchestration-actions { border-left: 0; 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 6b311076..6120cedc 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -10917,6 +10917,16 @@ test('shows realtime freshness status for recently updated and stale vehicles', expect(screen.getByRole('button', { name: '地图状态筛选条 离线车辆 1 离线' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '地图状态筛选条 无坐标车辆 0 无坐标' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '地图状态筛选条 需关注车辆 1 关注' })).toBeInTheDocument(); + expect(screen.getByText('地图图层控制台')).toBeInTheDocument(); + expect(screen.getByText('把实时地图拆成客户能理解的图层:在线、关注、定位、选车服务和交付导出。')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '地图图层控制台 在线车辆 1 在线' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '地图图层控制台 关注车辆 1 关注' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '地图图层控制台 定位车辆 2 定位' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '地图图层控制台 选中车辆 粤A超时1' })).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.getAllByText('数据新鲜').length).toBeGreaterThan(0); expect(screen.getAllByText('更新超时').length).toBeGreaterThan(0); expect(screen.getByText('车辆服务闭环')).toBeInTheDocument();