From cb45c44a05a655ba3348c40400063c5748bba9ae Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 09:28:17 +0800 Subject: [PATCH] feat(platform): surface dashboard action queue --- .../apps/web/src/pages/Dashboard.tsx | 48 +++++++++++- .../apps/web/src/test/App.test.tsx | 77 +++++++++++++++++++ 2 files changed, 124 insertions(+), 1 deletion(-) diff --git a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx index 61b2b7c8..1e17f722 100644 --- a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx @@ -1,6 +1,6 @@ import { Button, Card, Col, Form, Row, Select, Space, Spin, Table, Tag, Toast, Typography } from '@douyinfe/semi-ui'; import { IconSearch } from '@douyinfe/semi-icons'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { api } from '../api/client'; import type { DashboardSummary, LinkHealth, ProtocolStat, QualityIssueRow, ServiceStatusStat, VehicleCoverageRow, VehicleRealtimeRow, VehicleServiceSummary } from '../api/types'; import { PageHeader } from '../components/PageHeader'; @@ -163,6 +163,35 @@ export function Dashboard({ onOpenVehicle, onOpenQuality, onOpenVehicles }: { on { label: '身份未绑定', value: formatCount(serviceSummary?.identityRequiredVehicles), filters: { serviceStatus: 'identity_required' } } ]; const missingSourceCounts = new Map((serviceSummary?.missingSources ?? []).map((item) => [item.protocol, item.count])); + const serviceActionQueue = useMemo; detail: string }>>(() => { + const items: Array<{ label: string; count: number; filters: Record; detail: string }> = []; + if ((serviceSummary?.noDataVehicles ?? 0) > 0) { + items.push({ + label: '确认平台转发', + count: serviceSummary?.noDataVehicles ?? 0, + filters: { serviceStatus: 'no_data' }, + detail: '车辆没有形成任何来源证据,优先确认上游平台是否持续转发。' + }); + } + if ((serviceSummary?.identityRequiredVehicles ?? 0) > 0) { + items.push({ + label: '维护身份绑定', + count: serviceSummary?.identityRequiredVehicles ?? 0, + filters: { serviceStatus: 'identity_required' }, + detail: '已有数据但无法稳定归并到 VIN,会影响车辆服务聚合。' + }); + } + for (const source of serviceSummary?.missingSources ?? []) { + if (source.count <= 0) continue; + items.push({ + label: `补齐 ${source.protocol} 来源`, + count: source.count, + filters: { serviceStatus: 'degraded', missingProtocol: source.protocol }, + detail: `${source.protocol} 来源缺失会降低跨来源定位、里程和实时判断可信度。` + }); + } + return items; + }, [serviceSummary]); return (
@@ -191,6 +220,23 @@ export function Dashboard({ onOpenVehicle, onOpenQuality, onOpenVehicles }: { on 0 ? 'orange' : 'green'}>Kafka Lag {formatLag(summary?.kafkaLag)} + {serviceActionQueue.length > 0 ? ( + +
+ {serviceActionQueue.map((item) => ( +
+
+ {item.label} {item.count.toLocaleString()} + {item.detail} +
+ +
+ ))} +
+
+ ) : null} 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 94e6b10e..45423cae 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -207,6 +207,83 @@ test('dashboard presents one vehicle service operating posture', async () => { expect(screen.getByText('7 质量关注')).toBeInTheDocument(); }); +test('dashboard shows vehicle service action queue', async () => { + window.history.replaceState(null, '', '/#/dashboard'); + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const path = String(input); + if (path.includes('/api/ops/health')) { + return { + ok: true, + json: async () => ({ + data: { linkHealth: [], kafkaLag: 0, activeConnections: 0, capacityFindings: [], redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/dashboard/summary')) { + return { + ok: true, + json: async () => ({ + data: { + onlineVehicles: 3, + activeToday: 4, + frameToday: 1286320, + issueVehicles: 7, + kafkaLag: 0, + protocols: [], + serviceStatuses: [], + linkHealth: [] + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/vehicle-service/summary')) { + return { + ok: true, + json: async () => ({ + data: { + totalVehicles: 1033, + boundVehicles: 1024, + onlineVehicles: 208, + singleSourceVehicles: 391, + multiSourceVehicles: 181, + noDataVehicles: 461, + identityRequiredVehicles: 9, + serviceStatuses: [], + protocols: [], + missingSources: [{ protocol: 'YUTONG_MQTT', count: 983 }] + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + return { + ok: true, + json: async () => ({ + data: { items: [], total: 0, limit: 20, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + }); + + render(); + + expect(await screen.findByText('车辆服务处置队列')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '确认平台转发 461' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '维护身份绑定 9' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '补齐 YUTONG_MQTT 来源 983' })).toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button', { name: '确认平台转发 461' })); + await waitFor(() => { + expect(window.location.hash).toBe('#/vehicles?serviceStatus=no_data'); + }); +}); + test.each([ { buttonName: '查看在线车辆', expectedHash: '#/vehicles?online=online' }, { buttonName: '查看多源车辆', expectedHash: '#/vehicles?coverage=multi' },