refactor: 重新设计用量页布局并新增延迟健康列

- /admin/usage 明细区收进单卡片:三个下划线 tab(用量明细/错误请求/用户排行) + 自适应筛选栏 + 内容
- 用户 Token 排行收编为第三个 tab:去掉独立邮箱搜索,懒挂载,点击行下钻回用量明细并回显用户
- 首Token/耗时合并为延迟列(首字/总耗时):阈值 10s/30s/60s 与 1min/3min/5min 四档配色,色条上下两段短渐变过渡(from-40% to-60%),管理端与用户端 /usage 共用
- 时长格式 ≥1min 简化为 Xm Ys、≥1h 为 Xh Ym
- UsageFilters/UsageTable/OpsErrorLogTable 增加 flat 嵌入模式(默认关闭不影响其他使用方)
This commit is contained in:
shaw
2026-07-09 21:09:40 +08:00
parent e2c731abe4
commit 1a3cc2a787
14 changed files with 478 additions and 169 deletions
@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest'
import { durationSeverity, firstTokenSeverity } from '../latencyHealth'
describe('latencyHealth', () => {
it('classifies first-token latency at 10s/30s/60s boundaries', () => {
expect(firstTokenSeverity(0)).toBe('good')
expect(firstTokenSeverity(9_999)).toBe('good')
expect(firstTokenSeverity(10_000)).toBe('warn')
expect(firstTokenSeverity(29_999)).toBe('warn')
expect(firstTokenSeverity(30_000)).toBe('slow')
expect(firstTokenSeverity(59_999)).toBe('slow')
expect(firstTokenSeverity(60_000)).toBe('critical')
})
it('classifies total duration at 1min/3min/5min boundaries', () => {
expect(durationSeverity(0)).toBe('good')
expect(durationSeverity(59_999)).toBe('good')
expect(durationSeverity(60_000)).toBe('warn')
expect(durationSeverity(179_999)).toBe('warn')
expect(durationSeverity(180_000)).toBe('slow')
expect(durationSeverity(299_999)).toBe('slow')
expect(durationSeverity(300_000)).toBe('critical')
})
})
+69
View File
@@ -0,0 +1,69 @@
/**
* 请求延迟健康度分档(用于用量明细"延迟"列的纵向健康扫视)。
*
* 首 Token(TTFT):10s 内正常,10-30s 偏慢,30-60s 缓慢,60s 及以上严重。
* 总耗时:流式请求整体时长天然更长,阈值放宽为 1min / 3min / 5min。
*/
export type LatencySeverity = 'good' | 'warn' | 'slow' | 'critical'
export const FIRST_TOKEN_THRESHOLDS_MS = {
warn: 10_000,
slow: 30_000,
critical: 60_000,
} as const
export const DURATION_THRESHOLDS_MS = {
warn: 60_000,
slow: 180_000,
critical: 300_000,
} as const
interface Thresholds {
warn: number
slow: number
critical: number
}
const classify = (ms: number, thresholds: Thresholds): LatencySeverity => {
if (ms >= thresholds.critical) return 'critical'
if (ms >= thresholds.slow) return 'slow'
if (ms >= thresholds.warn) return 'warn'
return 'good'
}
export const firstTokenSeverity = (ms: number): LatencySeverity =>
classify(ms, FIRST_TOKEN_THRESHOLDS_MS)
export const durationSeverity = (ms: number): LatencySeverity =>
classify(ms, DURATION_THRESHOLDS_MS)
export const LATENCY_TEXT_CLASSES: Record<LatencySeverity, string> = {
good: 'text-emerald-600 dark:text-emerald-400',
warn: 'text-amber-600 dark:text-amber-400',
slow: 'text-orange-600 dark:text-orange-400',
critical: 'text-red-600 dark:text-red-400',
}
/** 无首字数据时的纯色色条(仅按总耗时档着色)。 */
export const LATENCY_BAR_CLASSES: Record<LatencySeverity, string> = {
good: 'bg-emerald-500',
warn: 'bg-amber-400',
slow: 'bg-orange-500',
critical: 'bg-red-500',
}
/** 渐变色条上端(首字档);与 LATENCY_BAR_TO_CLASSES 组合成上下渐变,避免两段硬切割裂感。 */
export const LATENCY_BAR_FROM_CLASSES: Record<LatencySeverity, string> = {
good: 'from-emerald-500',
warn: 'from-amber-400',
slow: 'from-orange-500',
critical: 'from-red-500',
}
/** 渐变色条下端(总耗时档)。 */
export const LATENCY_BAR_TO_CLASSES: Record<LatencySeverity, string> = {
good: 'to-emerald-500',
warn: 'to-amber-400',
slow: 'to-orange-500',
critical: 'to-red-500',
}