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>
|
||||
|
||||
Reference in New Issue
Block a user