From 1da75a5d7543a2a9e2ae7b2ea8985e3742ce84a6 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 13:40:48 +0800 Subject: [PATCH] feat(platform): copy realtime operations summary --- .../apps/web/src/pages/Realtime.tsx | 83 +++++++++- .../apps/web/src/test/App.test.tsx | 149 ++++++++++++++++++ 2 files changed, 226 insertions(+), 6 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx index 362e3cb2..bf1e811a 100644 --- a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx @@ -1,4 +1,5 @@ import { Button, Card, Form, Select, Space, Table, Tabs, Tag, Toast, Typography } from '@douyinfe/semi-ui'; +import { IconCopy } from '@douyinfe/semi-icons'; import { useEffect, useState } from 'react'; import { api } from '../api/client'; import type { VehicleRealtimeRow } from '../api/types'; @@ -105,6 +106,69 @@ function realtimeExportFileName(filters: Record) { return `realtime-vehicles-${keyword}-${protocol}-${online}.csv`; } +function realtimeFilterSummary(filters: Record) { + return [ + filters.keyword ? `关键词:${filters.keyword}` : '', + filters.protocol ? `数据来源:${filters.protocol}` : '', + filters.online ? `在线:${onlineLabel[filters.online] ?? filters.online}` : '', + filters.serviceStatus ? `服务状态:${serviceStatusLabel[filters.serviceStatus] ?? filters.serviceStatus}` : '' + ].filter(Boolean); +} + +function realtimeOperationsSummaryText({ + filters, + rows, + total, + onlineCount, + locatedCount, + degradedCount, + sourceTypeCount, + amapConfigured, + sourceIssueRows +}: { + filters: Record; + rows: VehicleRealtimeRow[]; + total: number; + onlineCount: number; + locatedCount: number; + degradedCount: number; + sourceTypeCount: number; + amapConfigured: boolean; + sourceIssueRows: VehicleRealtimeRow[]; +}) { + const issueLines = sourceIssueRows.length > 0 + ? sourceIssueRows.map((row, index) => { + const status = vehicleServiceStatus(row); + return `${index + 1}. ${row.plate || row.vin} / ${row.primaryProtocol || '-'} / ${status.label} / ${row.serviceStatus?.detail || sourceEvidenceText(row)}`; + }).join('\n') + : '暂无重点车辆'; + return [ + '【实时监控摘要】', + `当前筛选:${realtimeFilterSummary(filters).join(';') || '全部实时车辆'}`, + `车辆总数:${total.toLocaleString()},当前页:${rows.length.toLocaleString()}`, + `在线车辆:${onlineCount.toLocaleString()},定位有效:${locatedCount.toLocaleString()}`, + `降级服务:${degradedCount.toLocaleString()},来源类型:${sourceTypeCount.toLocaleString()}`, + `地图配置:${amapConfigured ? '已配置' : '未配置'}`, + '重点车辆:', + issueLines, + `实时页面:${window.location.origin}${window.location.pathname}${window.location.hash}` + ].join('\n'); +} + +async function copyText(value: string, label: string) { + const text = value.trim(); + if (!text) { + Toast.warning(`${label}为空`); + return; + } + try { + await navigator.clipboard.writeText(text); + Toast.success(`已复制${label}`); + } catch { + Toast.error(`复制${label}失败`); + } +} + export function Realtime({ onOpenVehicle, onOpenHistory, @@ -165,12 +229,7 @@ export function Realtime({ downloadCsv(realtimeExportFileName(filters), buildCsv(realtimeExportColumns, rows)); Toast.success(`已导出 ${rows.length} 条实时车辆`); }; - const filterSummary = [ - filters.keyword ? `关键词:${filters.keyword}` : '', - filters.protocol ? `数据来源:${filters.protocol}` : '', - filters.online ? `在线:${onlineLabel[filters.online] ?? filters.online}` : '', - filters.serviceStatus ? `服务状态:${serviceStatusLabel[filters.serviceStatus] ?? filters.serviceStatus}` : '' - ].filter(Boolean); + const filterSummary = realtimeFilterSummary(filters); const onlineCount = rows.filter((row) => row.online).length; const locatedCount = rows.filter(isValidCoordinate).length; const degradedCount = rows.filter((row) => row.onlineSourceCount < row.sourceCount).length; @@ -199,6 +258,17 @@ export function Realtime({ online: row.online, title: `${row.plate || row.vin || '-'} ${vehicleServiceStatus(row).label} ${row.primaryProtocol || ''} ${row.lastSeen || ''}` })); + const copyRealtimeSummary = () => copyText(realtimeOperationsSummaryText({ + filters, + rows, + total: pagination.total, + onlineCount, + locatedCount, + degradedCount, + sourceTypeCount: primaryProtocols.size, + amapConfigured, + sourceIssueRows + }), '实时摘要'); return (
@@ -250,6 +320,7 @@ export function Realtime({ {locatedCount.toLocaleString()} 辆有定位 {onlineCount.toLocaleString()} 辆在线 +
{ expect(revokeObjectURL).toHaveBeenCalledWith('blob:realtime-export'); }); +test('copies realtime operations summary from realtime page', async () => { + window.history.replaceState(null, '', '/#/realtime?protocol=JT808&online=online'); + Object.defineProperty(window, '__LINGNIU_APP_CONFIG__', { + configurable: true, + value: { amapWebJsKey: 'amap-key' } + }); + const writeText = vi.fn(() => Promise.resolve()); + Object.defineProperty(navigator, 'clipboard', { + configurable: true, + value: { writeText } + }); + 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, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/realtime/vehicles')) { + return { + ok: true, + json: async () => ({ + data: { + items: [ + { + vin: 'VIN-RT-SUMMARY-001', + plate: '粤A摘要1', + phone: '13300000001', + oem: 'G7s', + protocols: ['JT808'], + sourceStatus: [{ protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }], + sourceCount: 1, + onlineSourceCount: 1, + online: true, + bindingStatus: 'bound', + primaryProtocol: 'JT808', + longitude: 113.2, + latitude: 23.1, + speedKmh: 32, + socPercent: 76, + totalMileageKm: 1200.5, + lastSeen: '2026-07-03 20:12:10', + serviceStatus: { + status: 'healthy', + severity: 'ok', + title: '服务正常', + detail: '1/1 来源在线', + sourceCount: 1, + onlineSourceCount: 1 + } + }, + { + vin: 'VIN-RT-SUMMARY-002', + plate: '粤A摘要2', + phone: '13300000002', + oem: '现代', + protocols: ['GB32960', 'JT808'], + sourceStatus: [ + { protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, + { protocol: 'JT808', online: false, lastSeen: '2026-07-03 19:58:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true } + ], + sourceCount: 2, + onlineSourceCount: 1, + online: true, + bindingStatus: 'bound', + primaryProtocol: 'GB32960', + longitude: 113.4, + latitude: 23.2, + speedKmh: 18, + socPercent: 69, + totalMileageKm: 2200.5, + lastSeen: '2026-07-03 20:12:08', + serviceStatus: { + status: 'degraded', + severity: 'warning', + title: '来源不完整', + detail: 'JT808 离线,GB32960 仍可支撑车辆服务', + sourceCount: 2, + onlineSourceCount: 1 + } + }, + { + vin: 'VIN-RT-SUMMARY-003', + plate: '粤A摘要3', + phone: '13300000003', + oem: '宇通', + protocols: ['YUTONG_MQTT'], + sourceStatus: [{ protocol: 'YUTONG_MQTT', online: false, lastSeen: '2026-07-03 18:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }], + sourceCount: 1, + onlineSourceCount: 0, + online: false, + bindingStatus: 'bound', + primaryProtocol: 'YUTONG_MQTT', + longitude: 0, + latitude: 0, + speedKmh: 0, + socPercent: 60, + totalMileageKm: 3200.5, + lastSeen: '2026-07-03 18:12:10', + serviceStatus: { + status: 'offline', + severity: 'error', + title: '车辆离线', + detail: 'YUTONG_MQTT 超过在线窗口', + sourceCount: 1, + onlineSourceCount: 0 + } + } + ], + total: 3, + limit: 50, + offset: 0 + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + return { + ok: true, + json: async () => ({ + data: { items: [], total: 0, limit: 50, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + }); + + render(); + + expect((await screen.findAllByText('VIN-RT-SUMMARY-001')).length).toBeGreaterThan(0); + fireEvent.click(screen.getByRole('button', { name: '复制实时摘要' })); + + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【实时监控摘要】')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前筛选:数据来源:JT808;在线:在线')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆总数:3,当前页:3')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('在线车辆:2,定位有效:2')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('降级服务:2,来源类型:3')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('地图配置:已配置')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('重点车辆:')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('粤A摘要3 / YUTONG_MQTT / 车辆离线')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时页面:http://localhost:3000/#/realtime?protocol=JT808&online=online')); +}); + test('opens vehicle service from realtime map service queue', async () => { window.history.replaceState(null, '', '/#/realtime'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {