fix(usage): 抽取 ElapsedTimeText 组件、缩短缓存 TTL 提升实时性,修复 _KeyCandidate slots

- 将 UsageRecordsTable 中的内联计时逻辑抽取为独立的 ElapsedTimeText 组件
- usage records 缓存 TTL 从 15s 降为 3s,全局自动刷新间隔从 5s 改为 3s 并默认开启
- 补充 PoolManager._KeyCandidate 缺失的 __slots__ 字段
This commit is contained in:
fawney19
2026-03-04 02:24:46 +08:00
parent cdce817928
commit e181329a81
6 changed files with 112 additions and 59 deletions

View File

@@ -0,0 +1,76 @@
<template>
<span class="tabular-nums">{{ displayText }}</span>
</template>
<script setup lang="ts">
import { computed, onUnmounted, ref, watch } from 'vue'
const props = withDefaults(defineProps<{
createdAt?: string | null
status?: string | null
responseTimeMs?: number | null
precision?: number
intervalMs?: number
}>(), {
createdAt: null,
status: null,
responseTimeMs: null,
precision: 2,
intervalMs: 200
})
const now = ref(Date.now())
const precision = computed(() => Math.max(0, props.precision))
const isActive = computed(() => props.status === 'pending' || props.status === 'streaming')
let timer: ReturnType<typeof setInterval> | null = null
function parseCreatedAtMs(value: string | null | undefined): number {
if (!value) return Number.NaN
// 后端有时返回无时区时间,按 UTC 解析,和列表时间显示逻辑保持一致
const normalized = /(?:Z|[+-]\d{2}:\d{2})$/i.test(value) ? value : `${value}Z`
return new Date(normalized).getTime()
}
function stopTimer() {
if (!timer) return
clearInterval(timer)
timer = null
}
function startTimer() {
stopTimer()
const intervalMs = Math.max(100, props.intervalMs)
timer = setInterval(() => {
now.value = Date.now()
}, intervalMs)
}
watch([isActive, () => props.intervalMs], ([active]) => {
if (active) {
now.value = Date.now()
startTimer()
} else {
stopTimer()
}
}, { immediate: true })
onUnmounted(() => {
stopTimer()
})
const displayText = computed(() => {
if (!isActive.value) {
if (props.responseTimeMs == null) return '-'
return `${(props.responseTimeMs / 1000).toFixed(precision.value)}s`
}
if (!props.createdAt) return '-'
const createdAtMs = parseCreatedAtMs(props.createdAt)
if (Number.isNaN(createdAtMs)) return '-'
const elapsedMs = Math.max(0, now.value - createdAtMs)
return `${(elapsedMs / 1000).toFixed(precision.value)}s`
})
</script>

View File

@@ -161,7 +161,7 @@
size="icon"
class="h-8 w-8"
:class="autoRefresh ? 'text-primary' : ''"
:title="autoRefresh ? '点击关闭自动刷新' : '点击开启自动刷新(每5秒刷新)'"
:title="autoRefresh ? '点击关闭自动刷新' : '点击开启自动刷新(每3秒刷新)'"
@click="$emit('update:autoRefresh', !autoRefresh)"
>
<RefreshCcw
@@ -263,7 +263,11 @@
<span
v-if="record.status === 'pending' || record.status === 'streaming'"
class="text-primary tabular-nums"
>{{ getElapsedTime(record) }}</span>
><ElapsedTimeText
:created-at="record.created_at"
:status="record.status"
:response-time-ms="record.response_time_ms ?? null"
/></span>
<span
v-else-if="record.response_time_ms != null"
class="tabular-nums"
@@ -582,9 +586,11 @@
class="flex flex-col items-end text-xs gap-0.5"
>
<span class="text-muted-foreground">-</span>
<span class="text-primary tabular-nums">
{{ getElapsedTime(record) }}
</span>
<span class="text-primary tabular-nums"><ElapsedTimeText
:created-at="record.created_at"
:status="record.status"
:response-time-ms="record.response_time_ms ?? null"
/></span>
</div>
<!-- streaming 状态首字固定 + 总时间增长 -->
<div
@@ -599,9 +605,11 @@
v-else
class="text-muted-foreground"
>-</span>
<span class="text-primary tabular-nums">
{{ getElapsedTime(record) }}
</span>
<span class="text-primary tabular-nums"><ElapsedTimeText
:created-at="record.created_at"
:status="record.status"
:response-time-ms="record.response_time_ms ?? null"
/></span>
</div>
<!-- 已完成状态首字 + 总耗时 -->
<div
@@ -645,7 +653,7 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useDebounceFn, useIntervalFn } from '@vueuse/core'
import { useDebounceFn } from '@vueuse/core'
import {
TableCard,
Badge,
@@ -671,6 +679,7 @@ import { useRowClick } from '@/composables/useRowClick'
import { formatApiFormat } from '@/api/endpoints/types/api-format'
import type { DateRangeParams, UsageRecord } from '../types'
import { TimeRangePicker } from '@/components/common'
import ElapsedTimeText from './ElapsedTimeText.vue'
export interface UserOption {
id: string
@@ -756,50 +765,6 @@ watch(localSearch, (value) => {
emitSearchDebounced(value)
})
// 动态计时器相关
const now = ref(Date.now())
// 检查是否有活跃请求
const hasActiveRecords = computed(() => {
return props.records.some(r => r.status === 'pending' || r.status === 'streaming')
})
// 使用 VueUse 的 useIntervalFn 管理计时器(自动清理)
const { pause: stopTimer, resume: startTimer } = useIntervalFn(
() => { now.value = Date.now() },
500,
{ immediate: false }
)
// 计算活跃请求的实时耗时
function getElapsedTime(record: UsageRecord): string {
if (record.status !== 'pending' && record.status !== 'streaming') {
// 非活跃状态,显示实际响应时间
if (record.response_time_ms) {
return `${(record.response_time_ms / 1000).toFixed(2)}s`
}
return '-'
}
// 活跃状态,计算实时耗时
if (!record.created_at) return '-'
const createdAt = new Date(record.created_at).getTime()
const elapsed = now.value - createdAt
if (elapsed < 0) return '0.00s'
return `${(elapsed / 1000).toFixed(2)}s`
}
// 监听活跃记录状态,自动启动/停止计时器
watch(hasActiveRecords, (hasActive) => {
if (hasActive) {
startTimer()
} else {
stopTimer()
}
}, { immediate: true })
// 使用复用的行点击逻辑
const { handleMouseDown, shouldTriggerRowClick } = useRowClick()
@@ -810,7 +775,7 @@ function handleRowClick(event: MouseEvent, id: string) {
emit('showDetail', id)
}
// useIntervalFn 和 useDebounceFn 自动处理清理,无需 onUnmounted
// useDebounceFn 自动处理清理,无需 onUnmounted
// 判断是否应该显示格式转换信息
// 包括1. 跨格式转换has_format_conversion=true2. 同族格式差异(如 CLAUDE_CLI → CLAUDE

View File

@@ -249,8 +249,8 @@ let autoRefreshTimer: ReturnType<typeof setInterval> | null = null
let globalAutoRefreshTimer: ReturnType<typeof setInterval> | null = null
let refreshInFlight: Promise<void> | null = null
const AUTO_REFRESH_INTERVAL = 1000 // 1秒刷新一次用于活跃请求
const GLOBAL_AUTO_REFRESH_INTERVAL = 5000 // 5秒刷新一次(全局自动刷新)
const globalAutoRefresh = ref(false) // 全局自动刷新开关
const GLOBAL_AUTO_REFRESH_INTERVAL = 3000 // 3秒刷新一次(全局自动刷新)
const globalAutoRefresh = ref(true) // 全局自动刷新开关(默认开启)
const isPageVisible = ref(typeof document === 'undefined' ? true : !document.hidden)
// 轮询活跃请求状态(轻量级,只更新状态变化的记录)
@@ -471,6 +471,10 @@ onMounted(async () => {
// 用户页面loadStats 已包含记录加载,不需要单独调用 loadRecords
await Promise.allSettled(tasks)
if (globalAutoRefresh.value && isPageVisible.value) {
startGlobalAutoRefresh()
}
})
// 处理时间范围变化