feat(platform): copy trajectory playback summary

This commit is contained in:
lingniu
2026-07-04 13:35:28 +08:00
parent ff833bc065
commit d77754b0a0
2 changed files with 170 additions and 2 deletions

View File

@@ -1,5 +1,5 @@
import { Button, Card, Form, Select, SideSheet, Space, Table, Tabs, Tag, Toast, Typography } from '@douyinfe/semi-ui';
import { IconRefresh, IconSearch } from '@douyinfe/semi-icons';
import { IconCopy, IconRefresh, IconSearch } from '@douyinfe/semi-icons';
import { useEffect, useMemo, useState } from 'react';
import { api, type RawFrameQuery } from '../api/client';
import type { HistoryLocationRow, Page, RawFrameRow } from '../api/types';
@@ -125,6 +125,58 @@ function exportFileName(prefix: string, filters: HistoryFilters) {
return `${prefix}-${keyword}-${protocol}.csv`;
}
function trajectorySummaryText({
filters,
totalPoints,
validPointCount,
mileageDelta,
maxSpeed,
firstLocation,
lastLocation
}: {
filters: HistoryFilters;
totalPoints: number;
validPointCount: number;
mileageDelta?: number;
maxSpeed?: number;
firstLocation?: HistoryLocationRow;
lastLocation?: HistoryLocationRow;
}) {
const vehicle = filters.keyword?.trim() || '全部车辆';
const protocol = filters.protocol?.trim() || '全部来源';
const dateFrom = filters.dateFrom?.trim();
const dateTo = filters.dateTo?.trim();
const range = dateFrom || dateTo ? `${dateFrom || '-'}${dateTo || '-'}` : '全部时间';
const startTime = firstLocation?.deviceTime || firstLocation?.serverTime || '-';
const endTime = lastLocation?.deviceTime || lastLocation?.serverTime || '-';
return [
'【轨迹回放摘要】',
`车辆:${vehicle}`,
`数据来源:${protocol}`,
`查询范围:${range}`,
`轨迹点:${totalPoints.toLocaleString()},有效定位:${validPointCount.toLocaleString()}`,
`区间里程:${formatNumber(mileageDelta, ' km')}`,
`最高速度:${formatNumber(maxSpeed, ' km/h')}`,
`起点:${startTime}`,
`终点:${endTime}`,
`当前位置服务:${window.location.origin}${window.location.pathname}${window.location.hash}`
].join('\n');
}
async function copyText(value: string, label: string) {
const text = value.trim();
if (!text) {
Toast.warning(`${label}为空`);
return;
}
try {
await navigator.clipboard.writeText(text);
Toast.success(`已复制${label}`);
} catch {
Toast.error(`复制${label}失败`);
}
}
export function History({
initialVin,
initialProtocol,
@@ -299,6 +351,20 @@ export function History({
downloadCsv(exportFileName('raw-frames', filters), buildCsv(rawExportColumns, rawFrames.items));
Toast.success(`已导出 ${rawFrames.items.length} 条 RAW 帧`);
};
const copyTrajectorySummary = () => {
copyText(
trajectorySummaryText({
filters,
totalPoints: locations.total,
validPointCount: validLocations.length,
mileageDelta,
maxSpeed,
firstLocation,
lastLocation
}),
'轨迹摘要'
);
};
const movePlayback = (delta: number) => {
if (playbackRows.length === 0) return;
setPlaybackIndex((current) => Math.min(Math.max(current + delta, 0), playbackRows.length - 1));
@@ -388,7 +454,16 @@ export function History({
</Space>
</Card>
) : null}
<Card bordered title="轨迹回放作业台" style={{ marginTop: 16 }}>
<Card
bordered
title={(
<Space>
<span></span>
<Button size="small" theme="light" icon={<IconCopy />} onClick={copyTrajectorySummary}></Button>
</Space>
)}
style={{ marginTop: 16 }}
>
<div className="vp-playback-layout">
<div className="vp-playback-map">
<div className="vp-monitor-map-header">

View File

@@ -3380,6 +3380,99 @@ test('opens same-day raw evidence from quality issue row', async () => {
expect(await screen.findByRole('heading', { name: '轨迹回放' })).toBeInTheDocument();
});
test('copies trajectory playback summary from history page', async () => {
window.history.replaceState(null, '', '/#/history?keyword=VIN-HISTORY-SUMMARY&protocol=JT808&dateFrom=2026-07-03&dateTo=2026-07-04');
const writeText = vi.fn(() => Promise.resolve());
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: { writeText }
});
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
const path = String(input);
if (path.includes('/api/ops/health')) {
return {
ok: true,
json: async () => ({
data: { linkHealth: [], kafkaLag: 0, activeConnections: 0, capacityFindings: [], redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } },
traceId: 'trace-test',
timestamp: 1783094400000
})
} as Response;
}
if (path.includes('/api/history/locations')) {
return {
ok: true,
json: async () => ({
data: {
items: [
{
vin: 'VIN-HISTORY-SUMMARY',
plate: '粤A轨迹1',
protocol: 'JT808',
longitude: 113.201,
latitude: 23.122,
speedKmh: 12.5,
totalMileageKm: 1000,
deviceTime: '2026-07-03 10:00:00',
serverTime: '2026-07-03 10:00:02'
},
{
vin: 'VIN-HISTORY-SUMMARY',
plate: '粤A轨迹1',
protocol: 'JT808',
longitude: 113.301,
latitude: 23.222,
speedKmh: 58.8,
totalMileageKm: 1022.6,
deviceTime: '2026-07-03 11:00:00',
serverTime: '2026-07-03 11:00:02'
}
],
total: 2,
limit: 10,
offset: 0
},
traceId: 'trace-test',
timestamp: 1783094400000
})
} as Response;
}
if (path.includes('/api/history/raw-frames/query')) {
return {
ok: true,
json: async () => ({
data: { items: [], total: 0, limit: 10, offset: 0 },
traceId: 'trace-test',
timestamp: 1783094400000
})
} as Response;
}
return {
ok: true,
json: async () => ({
data: { items: [], total: 0, limit: 20, offset: 0 },
traceId: 'trace-test',
timestamp: 1783094400000
})
} as Response;
});
render(<App />);
expect((await screen.findAllByText('VIN-HISTORY-SUMMARY')).length).toBeGreaterThan(0);
fireEvent.click(screen.getByRole('button', { name: /复制轨迹摘要/ }));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【轨迹回放摘要】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆VIN-HISTORY-SUMMARY'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('数据来源JT808'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('查询范围2026-07-03 至 2026-07-04'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹点2有效定位2'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('区间里程22.6 km'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('最高速度58.8 km/h'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('起点2026-07-03 10:00:00'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('终点2026-07-03 11:00:00'));
});
test('opens same-day mileage statistics from quality issue row', async () => {
window.history.replaceState(null, '', '/#/quality');
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {