perf(web): reuse locale formatters

This commit is contained in:
lingniu
2026-07-16 08:30:23 +08:00
parent 22d069a404
commit e06778e38c
11 changed files with 53 additions and 14 deletions

View File

@@ -1,5 +1,9 @@
import type { AccessProtocolThreshold, AccessThresholdConfig, AccessVehicleRow } from '../../api/types';
const accessTimeFormatter = new Intl.DateTimeFormat('zh-CN', {
year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
});
export const accessStateLabels: Record<AccessVehicleRow['onlineState'], string> = {
online: '在线',
offline: '离线',
@@ -22,9 +26,7 @@ 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, '-');
return accessTimeFormatter.format(parsed).replace(/\//g, '-');
}
export function thresholdForProtocol(config: AccessThresholdConfig, protocol: string) {

View File

@@ -1,4 +1,7 @@
import type { AlertEvent, AlertRule, AlertSeverity, AlertStatus } from '../../api/types';
import { formatZhNumber } from './formatters';
const alertTimeFormatter = new Intl.DateTimeFormat('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
export const severityLabels: Record<AlertSeverity, string> = { critical: '紧急', major: '重要', minor: '一般' };
export const statusLabels: Record<AlertStatus, string> = { unprocessed: '未处理', processing: '处理中', recovered: '已恢复', closed: '已关闭', ignored: '已忽略' };
@@ -10,18 +13,18 @@ export function formatAlertTime(value: string) {
if (!value) return '—';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value.replace('T', ' ').slice(0, 19);
return new Intl.DateTimeFormat('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).format(date).replace(/\//g, '-');
return alertTimeFormatter.format(date).replace(/\//g, '-');
}
export function alertValue(event: Pick<AlertEvent, 'triggerValue' | 'unit'>) {
return `${Number(event.triggerValue.toFixed(2)).toLocaleString('zh-CN')} ${event.unit}`.trim();
return `${formatZhNumber(Number(event.triggerValue.toFixed(2)), 2)} ${event.unit}`.trim();
}
export function thresholdText(event: Pick<AlertEvent, 'operator' | 'threshold' | 'thresholdHigh' | 'unit' | 'durationSec'>) {
const duration = event.durationSec > 0 ? `,持续 ${event.durationSec}` : '';
if (event.operator === 'between' || event.operator === 'outside') return `${operatorLabels[event.operator]} ${event.threshold}${event.thresholdHigh} ${event.unit}${duration}`.trim();
if (event.operator === 'changed') return `状态发生变化${duration}`;
return `${operatorLabels[event.operator] ?? event.operator} ${Number(event.threshold.toFixed(2)).toLocaleString('zh-CN')} ${event.unit}${duration}`.trim();
return `${operatorLabels[event.operator] ?? event.operator} ${formatZhNumber(Number(event.threshold.toFixed(2)), 2)} ${event.unit}${duration}`.trim();
}
export function ruleCondition(rule: AlertRule, labels: Record<string, string> = metricLabels) {

View File

@@ -0,0 +1,15 @@
import { describe, expect, it } from 'vitest';
import { formatZhNumber } from './formatters';
describe('formatZhNumber', () => {
it('formats Chinese locale numbers with independent precision caches', () => {
expect(formatZhNumber(1234.56)).toBe('1,235');
expect(formatZhNumber(1234.56, 1)).toBe('1,234.6');
expect(formatZhNumber(1234.56, 2)).toBe('1,234.56');
expect(formatZhNumber(1234.56, 1)).toBe('1,234.6');
});
it('preserves Intl handling for non-finite numeric values', () => {
expect(formatZhNumber(Number.NaN, 2)).toBe('NaN');
});
});

View File

@@ -0,0 +1,10 @@
const zhNumberFormatters = new Map<number, Intl.NumberFormat>();
export function formatZhNumber(value: number, maximumFractionDigits = 0) {
let formatter = zhNumberFormatters.get(maximumFractionDigits);
if (!formatter) {
formatter = new Intl.NumberFormat('zh-CN', { maximumFractionDigits });
zhNumberFormatters.set(maximumFractionDigits, formatter);
}
return formatter.format(value);
}

View File

@@ -1,4 +1,5 @@
import type { HistoryDataRow, HistoryExportJob, HistoryMetricDefinition, HistorySeries, HistorySeriesResponse } from '../../api/types';
import { formatZhNumber } from './formatters';
export const HISTORY_EXPORT_POLL_MS = {
running: 3_000,
@@ -23,7 +24,7 @@ export function parseHistoryKeywords(value: string) {
export function formatHistoryValue(value: unknown, metric?: HistoryMetricDefinition) {
if (value == null || value === '') return '—';
const formatted = typeof value === 'number' ? new Intl.NumberFormat('zh-CN', { maximumFractionDigits: 6 }).format(value) : String(value);
const formatted = typeof value === 'number' ? formatZhNumber(value, 6) : String(value);
return metric?.unit ? `${formatted} ${metric.unit}` : formatted;
}

View File

@@ -1,4 +1,5 @@
import type { VehicleRealtimeRow } from '../../api/types';
import { formatZhNumber } from './formatters';
export type FleetStatus = 'online' | 'offline' | 'driving' | 'idle' | 'alert' | 'unknown';
@@ -31,5 +32,5 @@ export function relativeFreshness(value: string) {
}
export function formatNumber(value: number, maximumFractionDigits = 0) {
return new Intl.NumberFormat('zh-CN', { maximumFractionDigits }).format(value);
return formatZhNumber(value, maximumFractionDigits);
}

View File

@@ -1,6 +1,8 @@
import { formatZhNumber } from './formatters';
export function formatTelemetryValue(value: unknown) {
if (typeof value === 'number' && Number.isFinite(value)) {
return new Intl.NumberFormat('zh-CN', { maximumFractionDigits: 2 }).format(value);
return formatZhNumber(value, 2);
}
if (typeof value === 'boolean') return value ? '是' : '否';
if (typeof value === 'string') return value || '—';