mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-31 00:24:38 +00:00
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:
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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',
|
||||
}
|
||||
Reference in New Issue
Block a user