diff --git a/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx b/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx index e0675c1e..56ed4589 100644 --- a/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx @@ -7,6 +7,7 @@ import { DataEmpty } from '../components/DataEmpty'; import { PageHeader } from '../components/PageHeader'; import { SourceStatusTags } from '../components/SourceStatusTags'; import { StatusTag } from '../components/StatusTag'; +import { buildAppHash } from '../domain/appRoute'; import { buildCsv, downloadCsv, type CsvColumn } from '../domain/csvExport'; import { summarizeVehicleService, type VehicleServiceVerdict } from '../domain/vehicleService'; @@ -192,6 +193,48 @@ function vehicleGovernanceSummaryText({ ].join('\n'); } +function appURL(hash: string) { + return `${window.location.origin}${window.location.pathname}${hash}`; +} + +function vehicleDispatchListText({ + filters, + rows, + total +}: { + filters: Record; + rows: VehicleCoverageRow[]; + total: number; +}) { + const lines = [ + '【车辆处置清单】', + `当前筛选:${vehicleFilterSummary(filters).join(';') || '全部车辆'}`, + `当前页:${rows.length.toLocaleString()} 辆 / 总计 ${total.toLocaleString()} 辆`, + '' + ]; + rows.forEach((row, index) => { + const action = vehicleActionRecommendation(row); + const status = vehicleServiceStatus(row); + const protocol = primaryRowProtocol(row, filters.protocol); + const identity = [ + row.plate ? `车牌 ${row.plate}` : '车牌 -', + row.vin ? `VIN ${row.vin}` : 'VIN -', + row.phone ? `手机号 ${row.phone}` : '手机号 -', + row.oem ? `OEM ${row.oem}` : 'OEM -' + ].join(' / '); + lines.push( + `${index + 1}. ${identity}`, + ` 状态:${status.label};在线:${row.online ? '在线' : '离线'};来源:${sourceDiagnosisText(row)};最后时间:${row.lastSeen || '-'}`, + ` 建议动作:${action.label}`, + ` 车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: row.vin, protocol }))}`, + ` 实时监控:${appURL(buildAppHash({ page: 'realtime', keyword: row.vin, protocol }))}`, + ` 轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: row.vin, protocol }))}`, + ` 告警事件:${appURL(buildAppHash({ page: 'alert-events', keyword: row.vin, protocol }))}` + ); + }); + return lines.join('\n'); +} + type VehicleActionRecommendation = { label: string; color: 'green' | 'orange' | 'red'; @@ -451,6 +494,13 @@ export function Vehicles({ const copyGovernanceSummary = () => { copyText(vehicleGovernanceSummaryText({ filters, summary, actionQueue }), '治理摘要'); }; + const copyDispatchList = () => { + if (rows.length === 0) { + Toast.warning('当前没有可复制的车辆处置清单'); + return; + } + copyText(vehicleDispatchListText({ filters, rows, total: pagination.total }), '车辆处置清单'); + }; useEffect(() => { setFilters(initialFilters); @@ -628,6 +678,7 @@ export function Vehicles({ 当前车辆结果 + )} style={{ marginTop: 16 }} 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 2f98254a..eeb5a5d7 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -10,6 +10,15 @@ afterEach(() => { }); test('renders vehicle platform shell', () => { + vi.spyOn(globalThis, 'fetch').mockImplementation(async () => ({ + ok: true, + json: async () => ({ + data: { items: [], total: 0, limit: 20, offset: 0, linkHealth: [], runtime: { requestTimeoutMs: 5000 } }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + }) as Response); + render(); expect(screen.getByText('车辆服务中台')).toBeInTheDocument(); expect(screen.getByText('一车一服务 / 多源归并')).toBeInTheDocument(); @@ -125,7 +134,7 @@ test('exposes AMap operations shortcuts when map key is configured', async () => render(); expect(await screen.findByText('地图就绪')).toBeInTheDocument(); - expect(screen.getByText((content) => content.includes('platform-20260704153357'))).toBeInTheDocument(); + expect(await screen.findByText((content) => content.includes('platform-20260704153357'))).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '顶部地图态势' })); expect(window.location.hash).toBe('#/map'); expect(await screen.findByRole('heading', { name: '实时地图' })).toBeInTheDocument(); @@ -1749,6 +1758,19 @@ test('copies vehicle governance summary from vehicle center', async () => { expect(writeText).toHaveBeenCalledWith(expect.stringContaining('处置队列:')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('[P0] 维护身份绑定 4')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('[P1] 补齐 YUTONG_MQTT 来源 8')); + + fireEvent.click(screen.getByRole('button', { name: '复制当前页处置清单' })); + + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【车辆处置清单】')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前筛选:缺失来源:YUTONG_MQTT')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前页:1 辆 / 总计 12 辆')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. 车牌 粤A治理1 / VIN VIN-GOV-001 / 手机号 - / OEM G7s')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('状态:来源不完整;在线:在线;来源:1/1 来源在线,缺 YUTONG_MQTT;最后时间:2026-07-03 20:12:10')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('建议动作:补齐 YUTONG_MQTT 来源')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆服务:http://localhost:3000/#/detail?keyword=VIN-GOV-001&protocol=JT808')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时监控:http://localhost:3000/#/realtime?keyword=VIN-GOV-001&protocol=JT808')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹回放:http://localhost:3000/#/history?keyword=VIN-GOV-001&protocol=JT808')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('告警事件:http://localhost:3000/#/alert-events?keyword=VIN-GOV-001&protocol=JT808')); }); test('filters vehicle list from recommended action', async () => {