feat(platform): show trajectory evidence quality
This commit is contained in:
@@ -84,6 +84,23 @@ function formatNumber(value?: number, suffix = '') {
|
||||
return `${value.toLocaleString(undefined, { maximumFractionDigits: 1 })}${suffix}`;
|
||||
}
|
||||
|
||||
function formatPercent(value?: number) {
|
||||
if (!isFiniteNumber(value)) return '-';
|
||||
return `${value.toLocaleString(undefined, { maximumFractionDigits: 1 })}%`;
|
||||
}
|
||||
|
||||
function timeValue(row?: HistoryLocationRow) {
|
||||
const value = row?.deviceTime || row?.serverTime || row?.lastSeen;
|
||||
const ms = Date.parse(String(value ?? '').replace(' ', 'T'));
|
||||
return Number.isFinite(ms) ? ms : undefined;
|
||||
}
|
||||
|
||||
function formatDurationMinutes(value?: number, suffix = '') {
|
||||
if (!isFiniteNumber(value)) return '-';
|
||||
if (value < 1) return `${Math.round(value * 60)} 秒${suffix}`;
|
||||
return `${value.toLocaleString(undefined, { maximumFractionDigits: 1 })} 分钟${suffix}`;
|
||||
}
|
||||
|
||||
function mergeInitialFilters(initialVin: string, initialProtocol?: string, initialFilters: Record<string, string> = {}): HistoryFilters {
|
||||
const hasExplicitFilters = Object.keys(initialFilters).length > 0;
|
||||
return {
|
||||
@@ -332,6 +349,11 @@ export function History({
|
||||
const maxSpeed = locationItems.map((item) => item.speedKmh).filter(isFiniteNumber).reduce<number | undefined>((max, item) => max == null ? item : Math.max(max, item), undefined);
|
||||
const firstLocation = locationItems[0];
|
||||
const lastLocation = locationItems[locationItems.length - 1];
|
||||
const trajectoryCoverageRate = locationItems.length > 0 ? (validLocations.length / locationItems.length) * 100 : undefined;
|
||||
const playbackStartMs = timeValue(firstLocation);
|
||||
const playbackEndMs = timeValue(lastLocation);
|
||||
const playbackSpanMinutes = playbackStartMs != null && playbackEndMs != null ? Math.abs(playbackEndMs - playbackStartMs) / 60000 : undefined;
|
||||
const playbackIntervalMinutes = isFiniteNumber(playbackSpanMinutes) && locationItems.length > 1 ? playbackSpanMinutes / (locationItems.length - 1) : undefined;
|
||||
const currentVehicleKeyword = filters.keyword?.trim() ?? '';
|
||||
const currentProtocol = filters.protocol?.trim() ?? '';
|
||||
const amapConfigured = isAMapConfigured();
|
||||
@@ -566,6 +588,36 @@ export function History({
|
||||
<Typography.Text type="secondary">当前筛选范围暂无轨迹点,请调整车辆、来源或时间范围。</Typography.Text>
|
||||
) : null}
|
||||
</div>
|
||||
<Card bordered title="轨迹证据质量" style={{ marginTop: 12 }}>
|
||||
<div className="vp-stat-insight-grid">
|
||||
{[
|
||||
{
|
||||
label: '定位覆盖率',
|
||||
value: formatPercent(trajectoryCoverageRate),
|
||||
color: (trajectoryCoverageRate ?? 0) >= 80 ? 'green' as const : 'orange' as const,
|
||||
detail: `${validLocations.length.toLocaleString()} / ${locationItems.length.toLocaleString()} 个点有有效坐标。`
|
||||
},
|
||||
{
|
||||
label: '回放跨度',
|
||||
value: formatDurationMinutes(playbackSpanMinutes),
|
||||
color: isFiniteNumber(playbackSpanMinutes) ? 'blue' as const : 'grey' as const,
|
||||
detail: '当前页首末轨迹点设备时间跨度。'
|
||||
},
|
||||
{
|
||||
label: '采样间隔',
|
||||
value: formatDurationMinutes(playbackIntervalMinutes, '/点'),
|
||||
color: isFiniteNumber(playbackIntervalMinutes) && playbackIntervalMinutes <= 10 ? 'green' as const : 'orange' as const,
|
||||
detail: '当前页估算的平均轨迹采样间隔。'
|
||||
}
|
||||
].map((item) => (
|
||||
<Card key={item.label} bordered>
|
||||
<Tag color={item.color}>{item.label}</Tag>
|
||||
<div className="vp-monitor-metric-value">{item.value}</div>
|
||||
<Typography.Text type="secondary">{item.detail}</Typography.Text>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<Space wrap style={{ marginTop: 12 }}>
|
||||
<Tag color="grey">起点:{firstLocation?.deviceTime || firstLocation?.serverTime || '-'}</Tag>
|
||||
<Tag color="grey">终点:{lastLocation?.deviceTime || lastLocation?.serverTime || '-'}</Tag>
|
||||
|
||||
@@ -5267,6 +5267,52 @@ test('shows trajectory playback workspace from history locations', async () => {
|
||||
expect(screen.getByText('终点')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('shows trajectory evidence quality on history playback workspace', async () => {
|
||||
window.history.replaceState(null, '', '/#/history?keyword=VIN-TRACK-QUALITY&protocol=JT808');
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
|
||||
const path = String(input);
|
||||
if (path.includes('/api/history/locations')) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: {
|
||||
items: [
|
||||
{ vin: 'VIN-TRACK-QUALITY', plate: '粤A轨迹质量', protocol: 'JT808', longitude: 113.2, latitude: 23.1, speedKmh: 10, socPercent: 0, totalMileageKm: 100, lastSeen: '2026-07-03 10:00:00', deviceTime: '2026-07-03 10:00:00', serverTime: '2026-07-03 10:00:01' },
|
||||
{ vin: 'VIN-TRACK-QUALITY', plate: '粤A轨迹质量', protocol: 'JT808', longitude: 113.3, latitude: 23.2, speedKmh: 42, socPercent: 0, totalMileageKm: 112.4, lastSeen: '2026-07-03 10:10:00', deviceTime: '2026-07-03 10:10:00', serverTime: '2026-07-03 10:10:01' }
|
||||
],
|
||||
total: 2,
|
||||
limit: 10,
|
||||
offset: 0
|
||||
},
|
||||
traceId: 'trace-test',
|
||||
timestamp: 1783094400000
|
||||
})
|
||||
} as Response;
|
||||
}
|
||||
if (path.includes('/api/history/raw-frames/query')) {
|
||||
expect(init?.method).toBe('POST');
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: { items: [], total: 0, limit: 10, offset: 0 },
|
||||
traceId: 'trace-test',
|
||||
timestamp: 1783094400000
|
||||
})
|
||||
} as Response;
|
||||
});
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(await screen.findByText('轨迹证据质量')).toBeInTheDocument();
|
||||
expect(screen.getByText('定位覆盖率')).toBeInTheDocument();
|
||||
expect(screen.getByText('100%')).toBeInTheDocument();
|
||||
expect(screen.getByText('回放跨度')).toBeInTheDocument();
|
||||
expect(screen.getByText('10 分钟')).toBeInTheDocument();
|
||||
expect(screen.getByText('采样间隔')).toBeInTheDocument();
|
||||
expect(screen.getByText('10 分钟/点')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('opens amap route from trajectory playback workspace', async () => {
|
||||
window.history.replaceState(null, '', '/#/history?keyword=VIN-TRACK-AMAP&protocol=JT808');
|
||||
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
|
||||
|
||||
Reference in New Issue
Block a user