diff --git a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx index 2b932efe..d8e0ad4d 100644 --- a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx @@ -1701,6 +1701,71 @@ export function Realtime({ }); return protocols.length > 0 ? protocols.join(' / ') : '暂无来源证据'; })(); + const realtimeServiceOverviewItems = [ + { + label: '在线车辆', + value: `${onlineCount.toLocaleString()} 在线`, + detail: `${(rows.length - onlineCount).toLocaleString()} 辆离线,在线率 ${formatPercent(onlineRate)}`, + color: onlineCount > 0 ? 'green' as const : 'orange' as const, + onClick: () => applyFilters({ ...filters, online: 'online' }) + }, + { + label: '有效定位', + value: `${locatedCount.toLocaleString()} 辆`, + detail: `${missingCoordinateCount.toLocaleString()} 辆无坐标,定位率 ${formatPercent(locatedRate)}`, + color: locatedCount > 0 ? 'blue' as const : 'orange' as const, + onClick: () => { + window.location.hash = buildAppHash({ page: 'map', protocol: filters.protocol, filters }); + } + }, + { + label: '需关注', + value: `${mapAttentionRows.length.toLocaleString()} 辆`, + detail: mapAttentionRows[0] ? `${mapAttentionRows[0].plate || mapAttentionRows[0].vin}:${realtimeIssueLabels(mapAttentionRows[0]).join(';')}` : '当前没有离线、超时或定位异常车辆', + color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const, + onClick: () => applyFilters({ ...filters, serviceStatus: 'degraded' }) + }, + { + label: '地图能力', + displayLabel: '高德能力', + value: amapConfigured ? '高德可用' : '坐标预览', + detail: amapConfigured ? '高德 Web JS 与安全代理就绪' : '地图降级为坐标预览', + color: amapConfigured ? 'green' as const : 'orange' as const, + onClick: () => load(filters, pagination.currentPage, pagination.pageSize) + } + ]; + const realtimeServiceActions = [ + { + label: '打开地图', + action: '地图态势', + color: 'blue' as const, + disabled: false, + onClick: () => { + window.location.hash = buildAppHash({ page: 'map', protocol: filters.protocol, filters }); + } + }, + { + label: '轨迹复盘', + action: '轨迹回放', + color: selectedMapRow ? 'blue' as const : 'grey' as const, + disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin), + onClick: () => selectedMapRow && onOpenHistory?.(selectedMapRow.vin, selectedVehicleProtocol) + }, + { + label: '导出清单', + action: '导出CSV', + color: rows.length > 0 ? 'blue' as const : 'grey' as const, + disabled: rows.length === 0, + onClick: exportRealtime + }, + { + label: '告警通知', + action: '关注车辆', + color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const, + disabled: !onOpenQuality, + onClick: () => onOpenQuality?.({ serviceStatus: 'degraded' }) + } + ]; const realtimeCustomerCommandItems = [ { label: '实时地图', @@ -2628,6 +2693,51 @@ export function Realtime({ return (
+
+
+ + 实时车辆服务总览 + {amapConfigured ? '高德可用' : '坐标预览'} + 0 ? 'orange' : 'green'}>{mapAttentionRows.length.toLocaleString()} 辆需关注 + + + 客户先看车辆是否在线、地图是否可见、哪些车辆需要处置,再进入轨迹、统计、导出和告警。 + + + 当前范围:{filterSummary.length > 0 ? filterSummary.join(' / ') : '全部车辆'},来源证据:{realtimeSourceEvidenceText} + +
+
+ {realtimeServiceOverviewItems.map((item) => ( + + ))} +
+
+ {realtimeServiceActions.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 bb13541d..e858f40c 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -4112,6 +4112,109 @@ body { gap: 8px; } +.vp-realtime-service-overview { + margin-bottom: 16px; + border: 1px solid rgba(22, 100, 255, 0.18); + border-radius: var(--vp-radius); + background: #ffffff; + display: grid; + grid-template-columns: minmax(260px, 0.86fr) minmax(0, 1.36fr) minmax(190px, 0.58fr); + overflow: hidden; +} + +.vp-realtime-service-overview-copy { + padding: 18px; + border-right: 1px solid var(--vp-border); + background: #f7fbff; + display: grid; + gap: 10px; + align-content: start; +} + +.vp-realtime-service-overview-metrics { + padding: 14px; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; +} + +.vp-realtime-service-overview-metric { + min-height: 116px; + 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-realtime-service-overview-metric:hover, +.vp-realtime-service-overview-metric:focus-visible, +.vp-realtime-service-overview-action:hover, +.vp-realtime-service-overview-action:focus-visible { + border-color: rgba(22, 100, 255, 0.42); + background: #f5f9ff; + box-shadow: var(--vp-shadow-sm); + outline: none; +} + +.vp-realtime-service-overview-metric strong { + color: var(--vp-text); + font-size: 19px; + line-height: 24px; + word-break: break-word; +} + +.vp-realtime-service-overview-metric span { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; + word-break: break-word; +} + +.vp-realtime-service-overview-actions { + padding: 14px; + border-left: 1px solid var(--vp-border); + background: #fafcff; + display: grid; + grid-template-columns: 1fr; + gap: 8px; +} + +.vp-realtime-service-overview-action { + min-height: 46px; + 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-realtime-service-overview-action:disabled { + cursor: not-allowed; + opacity: 0.62; +} + +.vp-realtime-service-overview-action span { + color: var(--vp-primary); + font-size: 12px; + font-weight: 700; + line-height: 18px; + white-space: nowrap; +} + .vp-realtime-command-center { margin-bottom: 16px; border: 1px solid rgba(22, 100, 255, 0.16); @@ -13321,6 +13424,8 @@ button.vp-realtime-command-item:focus-visible { .vp-time-audit-grid, .vp-time-delivery-strip, .vp-time-delivery-grid, + .vp-realtime-service-overview, + .vp-realtime-service-overview-metrics, .vp-realtime-command-center, .vp-realtime-command-center-grid, .vp-realtime-priority-board, @@ -13560,6 +13665,13 @@ button.vp-realtime-command-item:focus-visible { border-bottom: 1px solid var(--vp-border); } + .vp-realtime-service-overview-copy, + .vp-realtime-service-overview-actions { + border-left: 0; + border-right: 0; + border-bottom: 1px solid var(--vp-border); + } + .vp-customer-cockpit-workflow { grid-template-columns: 1fr; } 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 ffda5412..2910b633 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -10445,6 +10445,16 @@ test('realtime page prioritizes customer vehicle monitoring command center', asy render(); + expect(await screen.findByText('实时车辆服务总览')).toBeInTheDocument(); + expect(screen.getByText('客户先看车辆是否在线、地图是否可见、哪些车辆需要处置,再进入轨迹、统计、导出和告警。')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '实时车辆服务总览 在线车辆 2 在线' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '实时车辆服务总览 有效定位 1 辆' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '实时车辆服务总览 需关注 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: '实时服务动作 导出清单 导出CSV' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '实时服务动作 告警通知 关注车辆' })).toBeInTheDocument(); expect(await screen.findByText('客户实时监控总控台')).toBeInTheDocument(); expect(screen.getByText('实时页先回答客户能不能看到车、车辆在哪里、哪些车辆异常,再进入轨迹、统计、导出和告警。')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户实时监控总控台 实时地图 1 有定位 打开地图' })).toBeInTheDocument();