feat: build vehicle data platform and production pipeline

This commit is contained in:
lingniu
2026-07-14 12:35:33 +08:00
parent b452be3b94
commit bb59303a4b
270 changed files with 88016 additions and 1975 deletions

View File

@@ -0,0 +1,45 @@
import type { AccessProtocolThreshold, AccessThresholdConfig, AccessVehicleRow } from '../../api/types';
export const accessStateLabels: Record<AccessVehicleRow['onlineState'], string> = {
online: '在线',
offline: '离线',
never_reported: '从未上报',
unknown: '未知'
};
export function formatSeconds(value: number | null | undefined) {
if (value === null || value === undefined || !Number.isFinite(value)) return '—';
const sign = value < 0 ? '-' : '';
const seconds = Math.abs(Math.round(value));
if (seconds < 60) return `${sign}${seconds}`;
if (seconds < 3600) return `${sign}${Math.floor(seconds / 60)}${seconds % 60}`;
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `${sign}${hours} 小时${minutes ? ` ${minutes}` : ''}`;
}
export function formatAccessTime(value: string) {
if (!value) return '—';
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) return '—';
return new Intl.DateTimeFormat('zh-CN', {
year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
}).format(parsed).replace(/\//g, '-');
}
export function thresholdForProtocol(config: AccessThresholdConfig, protocol: string) {
return config.protocols.find((item) => item.protocol === protocol)?.thresholdSec ?? config.defaultThresholdSec;
}
export function updateProtocolThreshold(items: AccessProtocolThreshold[], protocol: string, thresholdSec: number) {
const next = items.filter((item) => item.protocol !== protocol);
next.push({ protocol, thresholdSec });
return next.sort((a, b) => a.protocol.localeCompare(b.protocol));
}
export function accessRowsToCSV(rows: AccessVehicleRow[]) {
const columns = ['在线状态', '车牌', 'VIN', '厂家', '车型', '企业', '协议', '接入厂家', '首次接入', '首次接入证据', '最新事件时间', '最新接收时间', '上报间隔(秒)', '持久样本数', '上报间隔证据', '数据延迟(秒)', '动态阈值(秒)', '最新消息类型', '最近错误'];
const quote = (value: unknown) => `"${String(value ?? '').replace(/"/g, '""')}"`;
const lines = rows.map((row) => [accessStateLabels[row.onlineState], row.plate, row.vin, row.oem, row.model, row.company, row.protocol, row.provider, row.firstSeenAt, row.firstSeenEvidence, row.latestEventAt, row.latestReceivedAt, row.reportIntervalSec, row.reportSampleCount, row.reportIntervalEvidence, row.dataDelaySec, row.thresholdSec, row.latestMessageType, row.latestError].map(quote).join(','));
return `\uFEFF${columns.map(quote).join(',')}\n${lines.join('\n')}`;
}