import i18n from '@/i18n'; export const LATENCY_SOURCE_FIELD = 'latency_ms'; export const LATENCY_SOURCE_UNIT = 'ms'; export interface LatencyStats { averageMs: number | null; totalMs: number | null; sampleCount: number; } export interface DurationFormatOptions { maxUnits?: number; invalidText?: string; secondDecimals?: number | 'auto'; locale?: string; } export interface LatencyAccumulator { totalMs: number; sampleCount: number; } const isRecord = (value: unknown): value is Record => value !== null && typeof value === 'object' && !Array.isArray(value); const normalizeDurationMaxUnits = (value: number | undefined): number => { const parsed = Number(value); if (!Number.isFinite(parsed) || parsed <= 0) { return 2; } return Math.min(Math.floor(parsed), 4); }; const resolveSecondDecimalPlaces = ( seconds: number, secondDecimals: number | 'auto' | undefined ): number => { if (secondDecimals === 'auto' || secondDecimals === undefined) { return seconds < 10 ? 2 : 1; } const parsed = Math.floor(Number(secondDecimals)); if (!Number.isFinite(parsed) || parsed < 0) { return seconds < 10 ? 2 : 1; } return Math.min(parsed, 3); }; const resolveDurationLocale = (locale?: string): string | undefined => locale?.trim() || i18n.resolvedLanguage || i18n.language || undefined; const formatDurationNumber = ( value: number, locale: string | undefined, options: Intl.NumberFormatOptions = {} ): string => { try { return new Intl.NumberFormat(locale, { useGrouping: false, ...options, }).format(value); } catch { return String(value); } }; const getDurationUnitLabel = (unit: 'd' | 'h' | 'm' | 's' | 'ms'): string => i18n.t(`usage_stats.duration_unit_${unit}`, { defaultValue: unit }); const formatDurationPart = ( value: number, unit: 'd' | 'h' | 'm' | 's' | 'ms', locale: string | undefined, options: Intl.NumberFormatOptions = {} ): string => `${formatDurationNumber(value, locale, options)}${getDurationUnitLabel(unit)}`; /** * 从后端字段 latency_ms 提取耗时,并按毫秒解释。 */ export function extractLatencyMs(detail: unknown): number | null { const record = isRecord(detail) ? detail : null; const rawValue = record?.[LATENCY_SOURCE_FIELD]; if ( rawValue === null || rawValue === undefined || (typeof rawValue === 'string' && rawValue.trim() === '') ) { return null; } const parsed = Number(rawValue); if (!Number.isFinite(parsed) || parsed < 0) { return null; } return parsed; } export const createLatencyAccumulator = (): LatencyAccumulator => ({ totalMs: 0, sampleCount: 0, }); export const addLatencySample = ( accumulator: LatencyAccumulator, latencyMs: number | null | undefined ): void => { if (latencyMs === null || latencyMs === undefined) { return; } const parsed = Number(latencyMs); if (!Number.isFinite(parsed) || parsed < 0) { return; } accumulator.totalMs += parsed; accumulator.sampleCount += 1; }; export const finalizeLatencyStats = (accumulator: LatencyAccumulator): LatencyStats => ({ averageMs: accumulator.sampleCount > 0 ? accumulator.totalMs / accumulator.sampleCount : null, totalMs: accumulator.sampleCount > 0 ? accumulator.totalMs : null, sampleCount: accumulator.sampleCount, }); /** * 从明细列表计算耗时统计 */ export function calculateLatencyStatsFromDetails(details: Iterable): LatencyStats { const accumulator = createLatencyAccumulator(); for (const detail of details) { addLatencySample(accumulator, extractLatencyMs(detail)); } return finalizeLatencyStats(accumulator); } /** * 按当前语言格式化耗时显示。 */ export function formatDurationMs( value: number | null | undefined, options: DurationFormatOptions = {} ): string { const invalidText = options.invalidText ?? '--'; if (value === null || value === undefined) { return invalidText; } const parsed = Number(value); if (!Number.isFinite(parsed) || parsed < 0) { return invalidText; } const locale = resolveDurationLocale(options.locale); if (parsed < 1000) { return formatDurationPart(Math.round(parsed), 'ms', locale); } const seconds = parsed / 1000; if (seconds < 60) { const secondDecimalPlaces = resolveSecondDecimalPlaces(seconds, options.secondDecimals); return formatDurationPart(seconds, 's', locale, { minimumFractionDigits: 0, maximumFractionDigits: secondDecimalPlaces, }); } const totalSeconds = Math.floor(seconds); let remainingSeconds = totalSeconds; const days = Math.floor(remainingSeconds / 86_400); remainingSeconds -= days * 86_400; const hours = Math.floor(remainingSeconds / 3_600); remainingSeconds -= hours * 3_600; const minutes = Math.floor(remainingSeconds / 60); remainingSeconds -= minutes * 60; const parts = [ { unit: 'd' as const, value: days }, { unit: 'h' as const, value: hours }, { unit: 'm' as const, value: minutes }, { unit: 's' as const, value: remainingSeconds }, ].filter((part) => part.value > 0); if (!parts.length) { return formatDurationPart(0, 's', locale); } return parts .slice(0, normalizeDurationMaxUnits(options.maxUnits)) .map((part, index) => formatDurationPart(part.value, part.unit, locale, { minimumIntegerDigits: index > 0 && (part.unit === 'm' || part.unit === 's') ? 2 : 1, maximumFractionDigits: 0, }) ) .join(' '); }