From fa9c088dec47acfd5b75778773064eb7bc82c5c9 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 21:02:59 +0800 Subject: [PATCH] feat(platform): add realtime duty handoff --- .../apps/web/src/pages/Realtime.tsx | 100 ++++++++++++++++++ .../apps/web/src/test/App.test.tsx | 9 ++ 2 files changed, 109 insertions(+) diff --git a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx index e0c16337..6a853d4a 100644 --- a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx @@ -228,6 +228,16 @@ function appURL(hash: string) { return `${window.location.origin}${window.location.pathname}${hash}`; } +function rawFrameAPIURL(row: VehicleRealtimeRow, protocol: string) { + const params = new URLSearchParams({ + protocol, + vin: row.vin || '', + limit: '20', + includeFields: 'true' + }); + return `${window.location.origin}/api/history/raw-frames?${params.toString()}`; +} + function realtimeIssueLabels(row: VehicleRealtimeRow) { const labels: string[] = []; const freshness = dataFreshness(row); @@ -300,6 +310,83 @@ function realtimeIssueChecklistText({ return lines.join('\n'); } +function realtimeDutyHandoffText({ + filters, + rows, + total, + onlineCount, + locatedCount, + degradedCount, + staleCount, + sourceTypeCount, + amapConfigured, + runtimeRelease +}: { + filters: Record; + rows: VehicleRealtimeRow[]; + total: number; + onlineCount: number; + locatedCount: number; + degradedCount: number; + staleCount: number; + sourceTypeCount: number; + amapConfigured: boolean; + runtimeRelease?: string; +}) { + const sourceCoverage = new Map(); + rows.forEach((row) => { + row.sourceStatus?.forEach((source) => { + const current = sourceCoverage.get(source.protocol) ?? { total: 0, online: 0, realtime: 0 }; + current.total += 1; + current.online += source.online ? 1 : 0; + current.realtime += source.hasRealtime ? 1 : 0; + sourceCoverage.set(source.protocol, current); + }); + }); + const sourceLines = [...sourceCoverage.entries()] + .sort(([left], [right]) => left.localeCompare(right)) + .map(([protocol, item]) => `${protocol}:在线 ${item.online}/${item.total},实时 ${item.realtime}/${item.total}`); + const sampleRows = rows + .filter((row) => canOpenVehicle(row.vin)) + .sort((a, b) => { + const statusDelta = serviceStatusWeight(b) - serviceStatusWeight(a); + if (statusDelta !== 0) return statusDelta; + return Number(dataFreshness(b).stale) - Number(dataFreshness(a).stale); + }) + .slice(0, 8); + const lines = [ + '【实时值班交接包】', + `当前筛选:${realtimeFilterSummary(filters).join(';') || '全部实时车辆'}`, + `页面车辆:${rows.length.toLocaleString()} / 总计 ${total.toLocaleString()};版本:${runtimeRelease || '未标记'}`, + `在线:${onlineCount.toLocaleString()};离线:${(rows.length - onlineCount).toLocaleString()};定位有效:${locatedCount.toLocaleString()};降级:${degradedCount.toLocaleString()};超时:${staleCount.toLocaleString()};来源类型:${sourceTypeCount.toLocaleString()}`, + `地图:${amapConfigured ? '高德已配置' : '高德未配置'}`, + `来源覆盖:${sourceLines.length > 0 ? sourceLines.join(';') : '当前页暂无来源明细'}`, + `值班入口:${appURL(buildAppHash({ page: 'realtime', protocol: filters.protocol, filters }))}`, + '' + ]; + if (sampleRows.length === 0) { + lines.push('当前页暂无可交接 VIN 样本。'); + return lines.join('\n'); + } + lines.push('车辆样本:'); + sampleRows.forEach((row, index) => { + const protocol = filters.protocol || row.primaryProtocol || ''; + const status = vehicleServiceStatus(row); + const freshness = dataFreshness(row); + lines.push( + `${index + 1}. ${row.plate || '-'} / ${row.vin} / ${protocol || '-'} / ${status.label}`, + ` 最后上报:${row.lastSeen || '-'};新鲜度:${freshness.label};位置:${isValidCoordinate(row) ? `${row.longitude},${row.latitude}` : '无有效坐标'};速度:${row.speedKmh ?? '-'} km/h;SOC:${row.socPercent ?? '-'}%`, + ` 来源:${sourceEvidenceText(row)};问题:${realtimeIssueLabels(row).join(';')}`, + ` 车辆服务:${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: 'mileage', keyword: row.vin, protocol }))}`, + ` 历史RAW:${protocol ? rawFrameAPIURL(row, protocol) : '-'}` + ); + }); + return lines.join('\n'); +} + async function copyText(value: string, label: string) { const text = value.trim(); if (!text) { @@ -495,6 +582,18 @@ export function Realtime({ sourceIssueRows }), '实时摘要'); const copyRealtimeIssueChecklist = () => copyText(realtimeIssueChecklistText({ filters, rows, total: pagination.total }), '实时异常处置清单'); + const copyRealtimeDutyHandoff = () => copyText(realtimeDutyHandoffText({ + filters, + rows, + total: pagination.total, + onlineCount, + locatedCount, + degradedCount, + staleCount, + sourceTypeCount: primaryProtocols.size, + amapConfigured, + runtimeRelease: runtime?.platformRelease + }), '实时值班交接包'); const selectRealtimeRow = (row: VehicleRealtimeRow) => { const index = rows.indexOf(row); if (index < 0) return; @@ -617,6 +716,7 @@ export function Realtime({ {onlineCount.toLocaleString()} 辆在线 + { 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: '复制实时值班交接包' })); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【实时值班交接包】')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('页面车辆:3 / 总计 3;版本:')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('在线:2;离线:1;定位有效:2;降级:2;超时:')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('来源覆盖:GB32960:在线 1/1,实时 1/1;JT808:在线 1/2,实时 2/2;YUTONG_MQTT:在线 0/1,实时 1/1')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('值班入口:http://localhost:3000/#/realtime?protocol=JT808&online=online')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆服务:http://localhost:3000/#/detail?keyword=VIN-RT-SUMMARY-003&protocol=JT808')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('里程统计:http://localhost:3000/#/mileage?keyword=VIN-RT-SUMMARY-003&protocol=JT808')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('历史RAW:http://localhost:3000/api/history/raw-frames?protocol=JT808&vin=VIN-RT-SUMMARY-003&limit=20&includeFields=true')); fireEvent.click(screen.getByRole('button', { name: '实时筛选 降级服务 2' })); expect(window.location.hash).toBe('#/realtime?protocol=JT808&serviceStatus=degraded&online=online'); });