diff --git a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx index d03fc311..e0c16337 100644 --- a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx @@ -9,6 +9,7 @@ import { SourceStatusTags } from '../components/SourceStatusTags'; import { StatusTag } from '../components/StatusTag'; import { VehicleMap, type VehicleMapPoint } from '../components/VehicleMap'; import { getAMapConfig, isAMapConfigured } from '../config/appConfig'; +import { buildAppHash } from '../domain/appRoute'; import { buildCsv, downloadCsv, type CsvColumn } from '../domain/csvExport'; function canOpenVehicle(vin?: string) { @@ -223,6 +224,82 @@ function realtimeOperationsSummaryText({ ].join('\n'); } +function appURL(hash: string) { + return `${window.location.origin}${window.location.pathname}${hash}`; +} + +function realtimeIssueLabels(row: VehicleRealtimeRow) { + const labels: string[] = []; + const freshness = dataFreshness(row); + if (freshness.stale) { + labels.push(freshness.detail); + } + if (!isValidCoordinate(row)) { + labels.push('坐标无效'); + } + if (hasSourceIssue(row)) { + labels.push(row.serviceStatus?.detail || sourceIssueTags(row).join('、') || sourceEvidenceText(row)); + } + return labels.length > 0 ? labels : ['暂无异常']; +} + +function realtimeIssueAction(row: VehicleRealtimeRow) { + const issues = realtimeIssueLabels(row).join(';'); + if (!isValidCoordinate(row)) { + return `核对${row.primaryProtocol || '实时来源'}定位字段解析和平台转发`; + } + if (dataFreshness(row).stale) { + return `确认${row.primaryProtocol || '实时来源'}是否持续上报,必要时检查接入链路`; + } + if (hasSourceIssue(row)) { + return '补齐离线来源,确认多来源实时状态一致'; + } + return issues === '暂无异常' ? '持续观察' : '结合车辆服务详情继续排查'; +} + +function realtimeIssueChecklistText({ + filters, + rows, + total +}: { + filters: Record; + rows: VehicleRealtimeRow[]; + total: number; +}) { + const issueRows = rows + .filter((row) => canOpenVehicle(row.vin) && (dataFreshness(row).stale || !isValidCoordinate(row) || hasSourceIssue(row))) + .sort((a, b) => { + const statusDelta = serviceStatusWeight(b) - serviceStatusWeight(a); + if (statusDelta !== 0) return statusDelta; + return Number(dataFreshness(b).stale) - Number(dataFreshness(a).stale); + }); + const lines = [ + '【实时异常处置清单】', + `当前筛选:${realtimeFilterSummary(filters).join(';') || '全部实时车辆'}`, + `异常车辆:${issueRows.length.toLocaleString()} / 当前页 ${rows.length.toLocaleString()} / 总计 ${total.toLocaleString()}`, + '' + ]; + if (issueRows.length === 0) { + lines.push('当前页暂无实时异常车辆。'); + } + issueRows.forEach((row, index) => { + const protocol = filters.protocol || row.primaryProtocol || ''; + const status = vehicleServiceStatus(row); + const issues = realtimeIssueLabels(row).join(';'); + lines.push( + `${index + 1}. ${row.plate || '-'} / ${row.vin} / ${protocol || '-'}`, + ` 状态:${status.label};在线:${row.online ? '在线' : '离线'};问题:${issues}`, + ` 位置:${isValidCoordinate(row) ? `${row.longitude},${row.latitude}` : '无有效坐标'};速度:${row.speedKmh ?? '-'} km/h;SOC:${row.socPercent ?? '-'}%`, + ` 最后时间:${row.lastSeen || '-'};建议动作:${realtimeIssueAction(row)}`, + ` 车辆服务:${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'); +} + async function copyText(value: string, label: string) { const text = value.trim(); if (!text) { @@ -417,6 +494,7 @@ export function Realtime({ amapConfigured, sourceIssueRows }), '实时摘要'); + const copyRealtimeIssueChecklist = () => copyText(realtimeIssueChecklistText({ filters, rows, total: pagination.total }), '实时异常处置清单'); const selectRealtimeRow = (row: VehicleRealtimeRow) => { const index = rows.indexOf(row); if (index < 0) return; @@ -538,6 +616,7 @@ export function Realtime({ {locatedCount.toLocaleString()} 辆有定位 {onlineCount.toLocaleString()} 辆在线 + { 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')); + fireEvent.click(screen.getByRole('button', { name: '复制实时异常处置清单' })); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【实时异常处置清单】')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前筛选:数据来源:JT808;在线:在线')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('异常车辆:3 / 当前页 3 / 总计 3')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('粤A摘要3 / VIN-RT-SUMMARY-003 / JT808')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('状态:车辆离线;在线:离线;问题:')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('坐标无效')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('建议动作:核对YUTONG_MQTT定位字段解析和平台转发')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆服务:http://localhost:3000/#/detail?keyword=VIN-RT-SUMMARY-003&protocol=JT808')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时监控:http://localhost:3000/#/realtime?keyword=VIN-RT-SUMMARY-003&protocol=JT808')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹回放:http://localhost:3000/#/history?keyword=VIN-RT-SUMMARY-003&protocol=JT808')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('告警事件:http://localhost:3000/#/alert-events?keyword=VIN-RT-SUMMARY-003&protocol=JT808')); fireEvent.click(screen.getByRole('button', { name: '实时筛选 降级服务 2' })); expect(window.location.hash).toBe('#/realtime?protocol=JT808&serviceStatus=degraded&online=online'); });