feat(platform): add realtime duty handoff
This commit is contained in:
@@ -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/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({
|
||||
<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
|
||||
|
||||
@@ -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/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');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user