diff --git a/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx b/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx index ecb7fa84..b9466839 100644 --- a/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx @@ -599,6 +599,53 @@ export function Vehicles({ onClick: exportVehicles } ], [exportVehicles, filters.keyword, onOpenHistory, onOpenMap, onOpenMileage, onOpenRealtime, rows.length, serviceScopeFilters, summary?.onlineVehicles]); + const primaryAction = actionQueue[0]; + const vehicleDecisionItems = [ + { + title: '在线车辆', + value: `${(summary?.onlineVehicles ?? 0).toLocaleString()} 辆`, + detail: '先看当前仍在上报的车辆,直接进入地图和实时状态。', + color: 'green' as const, + primaryAction: '实时监控', + secondaryAction: '车辆地图', + onPrimary: () => onOpenRealtime?.({ ...serviceScopeFilters, online: 'online' }), + onSecondary: () => onOpenMap?.({ ...serviceScopeFilters, online: 'online' }), + disabled: !onOpenRealtime + }, + { + title: '异常车辆', + value: `${(summary?.noDataVehicles ?? 0).toLocaleString()} 无来源`, + detail: primaryAction ? primaryAction.detail : '当前没有必须立即处理的车辆服务事项。', + color: (summary?.noDataVehicles ?? 0) > 0 || primaryAction ? 'orange' as const : 'green' as const, + primaryAction: primaryAction?.label ?? '查看结果', + secondaryAction: '告警事件', + onPrimary: () => primaryAction ? applyFilters({ ...filters, ...primaryAction.filters }) : applyFilters(filters), + onSecondary: () => onOpenQuality?.(serviceScopeFilters), + disabled: !primaryAction + }, + { + title: '单车服务', + value: filters.keyword?.trim() || '输入车辆', + detail: filters.keyword?.trim() ? '当前已进入单车范围,可查看车辆详情、轨迹和里程。' : '输入 VIN、车牌或手机号后进入完整车辆服务。', + color: filters.keyword?.trim() ? 'blue' as const : 'grey' as const, + primaryAction: '车辆详情', + secondaryAction: '轨迹回放', + onPrimary: () => filters.keyword?.trim() && onOpenVehicle(filters.keyword.trim(), filters.protocol), + onSecondary: () => onOpenHistory?.({ ...serviceScopeFilters, tab: 'location' }), + disabled: !filters.keyword?.trim() + }, + { + title: '数据交付', + value: `${rows.length.toLocaleString()} 当前页`, + detail: '导出当前车辆清单,继续进入历史页交付轨迹、RAW 和字段证据。', + color: rows.length > 0 ? 'blue' as const : 'grey' as const, + primaryAction: '导出清单', + secondaryAction: '历史导出', + onPrimary: exportVehicles, + onSecondary: () => onOpenHistory?.({ ...serviceScopeFilters, tab: 'raw', includeFields: 'true' }), + disabled: rows.length === 0 + } + ]; useEffect(() => { setFilters(initialFilters); @@ -742,6 +789,41 @@ export function Vehicles({ + + + {vehicleDecisionItems.map((item) => ( + + + {item.title} + {item.value} + + {item.detail} + + + {item.primaryAction} + + + {item.secondaryAction} + + + + ))} + + { const nextFilters = values as Record; diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index 7bb6996f..0b246ec6 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -874,6 +874,43 @@ button.vp-realtime-command-item:focus-visible { line-height: 20px; } +.vp-vehicle-decision-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.vp-vehicle-decision-item { + min-height: 172px; + padding: 14px; + border: 1px solid var(--vp-border); + border-radius: 8px; + background: #fbfcff; + display: grid; + gap: 10px; + align-content: start; +} + +.vp-vehicle-decision-head { + display: grid; + gap: 8px; + min-width: 0; +} + +.vp-vehicle-decision-head strong { + color: var(--vp-text); + font-size: 20px; + line-height: 26px; + font-weight: 700; + word-break: break-word; +} + +.vp-vehicle-decision-item span { + color: var(--vp-text-muted); + font-size: 13px; + line-height: 20px; +} + .vp-result-summary-item { min-height: 72px; padding: 12px; @@ -3456,6 +3493,7 @@ button.vp-realtime-command-item:focus-visible { .vp-current-service-grid, .vp-vehicle-service-desk, .vp-vehicle-service-grid, + .vp-vehicle-decision-grid, .vp-realtime-command-board, .vp-realtime-command-grid, .vp-realtime-impact-board, 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 bda789b4..cc9774cb 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -1979,7 +1979,15 @@ test('shows vehicle service result summary on vehicle list filters', async () => expect(await screen.findByText('当前车辆结果')).toBeInTheDocument(); expect(screen.getByText('车辆服务台')).toBeInTheDocument(); expect(screen.getByText('客户视角')).toBeInTheDocument(); - expect(screen.getByText('车辆地图')).toBeInTheDocument(); + expect(screen.getByText('车辆服务决策板')).toBeInTheDocument(); + expect(screen.getAllByText('异常车辆').length).toBeGreaterThan(0); + expect(screen.getByText('4 无来源')).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.getAllByText('车辆地图').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('里程统计').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('数据导出').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('当前页服务判读')).toBeInTheDocument(); @@ -1990,7 +1998,7 @@ test('shows vehicle service result summary on vehicle list filters', async () => expect(screen.getAllByText('181').length).toBeGreaterThanOrEqual(2); expect(screen.getByText('过滤车辆')).toBeInTheDocument(); expect(screen.getByText('73')).toBeInTheDocument(); - expect(screen.getByText('在线车辆')).toBeInTheDocument(); + expect(screen.getAllByText('在线车辆').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('单源车辆')).toBeInTheDocument(); expect(screen.getByText('暂无来源车辆')).toBeInTheDocument(); expect(screen.getByText('待绑定')).toBeInTheDocument(); @@ -1999,7 +2007,7 @@ test('shows vehicle service result summary on vehicle list filters', async () => expect(screen.getByText('处置队列')).toBeInTheDocument(); expect(screen.getAllByText('P0').length).toBeGreaterThanOrEqual(2); expect(screen.getAllByText('P1').length).toBeGreaterThanOrEqual(2); - expect(screen.getByText('已有数据但无法归并到 VIN,先补绑定再看跨来源服务。')).toBeInTheDocument(); + expect(screen.getAllByText('已有数据但无法归并到 VIN,先补绑定再看跨来源服务。').length).toBeGreaterThan(0); expect(screen.getByText('车辆没有任何来源证据,优先确认平台转发、端口和订阅配置。')).toBeInTheDocument(); expect(screen.getByText('缺手机号会影响车辆档案检索、绑定确认和运营侧筛选。')).toBeInTheDocument(); expect(screen.getByText('YUTONG_MQTT 来源缺失会降低跨来源定位、里程和实时判断可信度。')).toBeInTheDocument();