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}`;
|
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) {
|
function realtimeIssueLabels(row: VehicleRealtimeRow) {
|
||||||
const labels: string[] = [];
|
const labels: string[] = [];
|
||||||
const freshness = dataFreshness(row);
|
const freshness = dataFreshness(row);
|
||||||
@@ -300,6 +310,83 @@ function realtimeIssueChecklistText({
|
|||||||
return lines.join('\n');
|
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) {
|
async function copyText(value: string, label: string) {
|
||||||
const text = value.trim();
|
const text = value.trim();
|
||||||
if (!text) {
|
if (!text) {
|
||||||
@@ -495,6 +582,18 @@ export function Realtime({
|
|||||||
sourceIssueRows
|
sourceIssueRows
|
||||||
}), '实时摘要');
|
}), '实时摘要');
|
||||||
const copyRealtimeIssueChecklist = () => copyText(realtimeIssueChecklistText({ filters, rows, total: pagination.total }), '实时异常处置清单');
|
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 selectRealtimeRow = (row: VehicleRealtimeRow) => {
|
||||||
const index = rows.indexOf(row);
|
const index = rows.indexOf(row);
|
||||||
if (index < 0) return;
|
if (index < 0) return;
|
||||||
@@ -617,6 +716,7 @@ export function Realtime({
|
|||||||
<Tag color="green">{onlineCount.toLocaleString()} 辆在线</Tag>
|
<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={copyRealtimeSummary}>复制实时摘要</Button>
|
||||||
<Button size="small" theme="light" icon={<IconCopy />} aria-label="复制实时异常处置清单" onClick={copyRealtimeIssueChecklist}>复制异常处置清单</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>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
<VehicleMap
|
<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/#/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/#/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'));
|
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' }));
|
fireEvent.click(screen.getByRole('button', { name: '实时筛选 降级服务 2' }));
|
||||||
expect(window.location.hash).toBe('#/realtime?protocol=JT808&serviceStatus=degraded&online=online');
|
expect(window.location.hash).toBe('#/realtime?protocol=JT808&serviceStatus=degraded&online=online');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user