feat(platform): harden telemetry pipeline and unify Semi UI workspaces
This commit is contained in:
@@ -10,6 +10,7 @@ describe('history domain', () => {
|
||||
it('formats units without fabricating missing values', () => {
|
||||
expect(formatHistoryValue(undefined)).toBe('—');
|
||||
expect(formatHistoryValue(42.5, { unit: 'km/h' } as never)).toBe('42.5 km/h');
|
||||
expect(formatHistoryValue(1, { valueMappings: [{ value: '1', label: '启动' }] } as never)).toBe('启动(1)');
|
||||
});
|
||||
|
||||
it('builds only numeric series with at least two evidence points', () => {
|
||||
|
||||
@@ -24,6 +24,9 @@ export function parseHistoryKeywords(value: string) {
|
||||
|
||||
export function formatHistoryValue(value: unknown, metric?: HistoryMetricDefinition) {
|
||||
if (value == null || value === '') return '—';
|
||||
const raw = typeof value === 'number' && Number.isInteger(value) ? String(value) : String(value);
|
||||
const mapped = metric?.valueMappings?.find((item) => item.value === raw);
|
||||
if (mapped) return `${mapped.label}(${mapped.value})`;
|
||||
const formatted = typeof value === 'number' ? formatZhNumber(value, 6) : String(value);
|
||||
return metric?.unit ? `${formatted} ${metric.unit}` : formatted;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { VehicleRealtimeRow } from '../../api/types';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { formatNumber, statusLabel, vehicleStatus } from './monitor';
|
||||
import { formatNumber, normalizeMonitorBounds, statusLabel, vehicleStatus } from './monitor';
|
||||
|
||||
function vehicle(overrides: Partial<VehicleRealtimeRow> = {}): VehicleRealtimeRow {
|
||||
return {
|
||||
@@ -37,4 +37,11 @@ describe('monitor domain', () => {
|
||||
expect(formatNumber(12560)).toBe('12,560');
|
||||
expect(statusLabel('driving')).toBe('行驶');
|
||||
});
|
||||
|
||||
it('normalizes valid WGS-84 viewport bounds and drops unsafe map ranges', () => {
|
||||
expect(normalizeMonitorBounds('113,22,114,24')).toBe('113.000000,22.000000,114.000000,24.000000');
|
||||
expect(normalizeMonitorBounds('105,31,103,29')).toBe('');
|
||||
expect(normalizeMonitorBounds('-181,0,181,1')).toBe('');
|
||||
expect(normalizeMonitorBounds('113,22,Infinity,24')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,3 +34,12 @@ export function relativeFreshness(value: string) {
|
||||
export function formatNumber(value: number, maximumFractionDigits = 0) {
|
||||
return formatZhNumber(value, maximumFractionDigits);
|
||||
}
|
||||
|
||||
export function normalizeMonitorBounds(value?: string | null) {
|
||||
if (!value) return '';
|
||||
const coordinates = value.split(',').map(Number);
|
||||
if (coordinates.length !== 4 || coordinates.some((coordinate) => !Number.isFinite(coordinate))) return '';
|
||||
const [west, south, east, north] = coordinates;
|
||||
if (west < -180 || east > 180 || south < -90 || north > 90 || west >= east || south >= north) return '';
|
||||
return coordinates.map((coordinate) => coordinate.toFixed(6)).join(',');
|
||||
}
|
||||
|
||||
@@ -3,15 +3,15 @@ import { parseVehicleProfileSyncCSV, vehicleProfileSyncCSVHeader } from './profi
|
||||
|
||||
describe('parseVehicleProfileSyncCSV', () => {
|
||||
it('parses quoted values, BOM, CRLF and normalizes VIN/status', () => {
|
||||
const rows = parseVehicleProfileSyncCSV(`\uFEFF${vehicleProfileSyncCSVHeader}\r\nvin001,"车型,一",重卡,示范物流,ACTIVE,车厂平台,2026-07-01T08:30:00+08:00,3600\r\n`);
|
||||
expect(rows).toEqual([expect.objectContaining({ vin: 'VIN001', modelName: '车型,一', operationStatus: 'active', runtimeSeconds: 3600 })]);
|
||||
const rows = parseVehicleProfileSyncCSV(`\uFEFF${vehicleProfileSyncCSVHeader}\r\nvin001,宇通,"车型,一",重卡,示范物流,ACTIVE,车厂平台,2026-07-01T08:30:00+08:00,3600\r\n`);
|
||||
expect(rows).toEqual([expect.objectContaining({ vin: 'VIN001', brandName: '宇通', modelName: '车型,一', operationStatus: 'active', runtimeSeconds: 3600 })]);
|
||||
});
|
||||
|
||||
it('rejects duplicate VINs and malformed source rows before upload', () => {
|
||||
const duplicate = `${vehicleProfileSyncCSVHeader}\nVIN001,,,,unknown,,,\nvin001,,,,unknown,,,`;
|
||||
const duplicate = `${vehicleProfileSyncCSVHeader}\nVIN001,,,,,unknown,,,\nvin001,,,,,unknown,,,`;
|
||||
expect(() => parseVehicleProfileSyncCSV(duplicate)).toThrow(/VIN 重复/);
|
||||
expect(() => parseVehicleProfileSyncCSV(`${vehicleProfileSyncCSVHeader}\nVIN001,,,,active,,,1.5`)).toThrow(/累计运行秒数/);
|
||||
expect(() => parseVehicleProfileSyncCSV(`${vehicleProfileSyncCSVHeader}\nVIN001,,,,,active,,,1.5`)).toThrow(/累计运行秒数/);
|
||||
expect(() => parseVehicleProfileSyncCSV('vin,modelName\nVIN001,车型')).toThrow(/表头/);
|
||||
expect(() => parseVehicleProfileSyncCSV(`${vehicleProfileSyncCSVHeader}\nVIN001,"车型"x,,,active,,,`)).toThrow(/引号结束/);
|
||||
expect(() => parseVehicleProfileSyncCSV(`${vehicleProfileSyncCSVHeader}\nVIN001,宇通,"车型"x,,,active,,,`)).toThrow(/引号结束/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { VehicleProfileSyncItem } from '../../api/types';
|
||||
|
||||
const headers = ['vin', 'modelName', 'vehicleType', 'companyName', 'operationStatus', 'accessProvider', 'firstAccessAt', 'runtimeSeconds'] as const;
|
||||
const headers = ['vin', 'brandName', 'modelName', 'vehicleType', 'companyName', 'operationStatus', 'accessProvider', 'firstAccessAt', 'runtimeSeconds'] as const;
|
||||
const allowedStatuses = new Set(['', 'unknown', 'active', 'inactive', 'maintenance', 'retired']);
|
||||
|
||||
export const vehicleProfileSyncCSVHeader = headers.join(',');
|
||||
@@ -18,7 +18,7 @@ export function parseVehicleProfileSyncCSV(text: string): VehicleProfileSyncItem
|
||||
return dataRows.map((row, index) => {
|
||||
const line = index + 2;
|
||||
if (row.length !== headers.length) throw new Error(`CSV 第 ${line} 行列数不正确`);
|
||||
const [rawVIN, modelName, vehicleType, companyName, rawStatus, accessProvider, firstAccessAt, rawRuntime] = row.map((value) => value.trim());
|
||||
const [rawVIN, brandName, modelName, vehicleType, companyName, rawStatus, accessProvider, firstAccessAt, rawRuntime] = row.map((value) => value.trim());
|
||||
const vin = rawVIN.toUpperCase();
|
||||
if (!vin || vin.length > 32) throw new Error(`CSV 第 ${line} 行 VIN 无效`);
|
||||
if (seen.has(vin)) throw new Error(`CSV 第 ${line} 行 VIN 重复:${vin}`);
|
||||
@@ -28,7 +28,7 @@ export function parseVehicleProfileSyncCSV(text: string): VehicleProfileSyncItem
|
||||
const runtimeSeconds = rawRuntime === '' ? null : Number(rawRuntime);
|
||||
if (runtimeSeconds !== null && (!Number.isSafeInteger(runtimeSeconds) || runtimeSeconds < 0)) throw new Error(`CSV 第 ${line} 行累计运行秒数无效`);
|
||||
return {
|
||||
vin, modelName, vehicleType, companyName,
|
||||
vin, brandName, modelName, vehicleType, companyName,
|
||||
operationStatus: (operationStatus || 'unknown') as VehicleProfileSyncItem['operationStatus'],
|
||||
accessProvider, firstAccessAt, runtimeSeconds
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ describe('latest telemetry presentation', () => {
|
||||
expect(formatTelemetryValue(42.567)).toBe('42.57');
|
||||
expect(formatTelemetryValue(true)).toBe('是');
|
||||
expect(formatTelemetryValue(null)).toBe('—');
|
||||
expect(formatTelemetryValue(1, '启动(1)')).toBe('启动(1)');
|
||||
});
|
||||
|
||||
it('translates server quality states', () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { formatZhNumber } from './formatters';
|
||||
|
||||
export function formatTelemetryValue(value: unknown) {
|
||||
export function formatTelemetryValue(value: unknown, displayValue?: string) {
|
||||
if (displayValue) return displayValue;
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return formatZhNumber(value, 2);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user