feat(platform): add realtime duty handoff

This commit is contained in:
lingniu
2026-07-04 21:02:59 +08:00
parent 4077de9296
commit fa9c088dec
2 changed files with 109 additions and 0 deletions

View File

@@ -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<string, string>;
rows: VehicleRealtimeRow[];
total: number;
onlineCount: number;
locatedCount: number;
degradedCount: number;
staleCount: number;
sourceTypeCount: number;
amapConfigured: boolean;
runtimeRelease?: string;
}) {
const sourceCoverage = new Map<string, { total: number; online: number; realtime: number }>();
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/hSOC${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({
<Tag color="green">{onlineCount.toLocaleString()} 线</Tag>
<Button size="small" theme="light" icon={<IconCopy />} aria-label="复制实时摘要" onClick={copyRealtimeSummary}></Button>
<Button size="small" theme="light" icon={<IconCopy />} aria-label="复制实时异常处置清单" onClick={copyRealtimeIssueChecklist}></Button>
<Button size="small" theme="light" icon={<IconCopy />} aria-label="复制实时值班交接包" onClick={copyRealtimeDutyHandoff}></Button>
</Space>
</div>
<VehicleMap

View File

@@ -8541,6 +8541,15 @@ test('copies realtime operations summary from realtime page', async () => {
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/1JT808在线 1/2实时 2/2YUTONG_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('历史RAWhttp://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');
});