mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
fix(usage): 抽取 ElapsedTimeText 组件、缩短缓存 TTL 提升实时性,修复 _KeyCandidate slots
- 将 UsageRecordsTable 中的内联计时逻辑抽取为独立的 ElapsedTimeText 组件 - usage records 缓存 TTL 从 15s 降为 3s,全局自动刷新间隔从 5s 改为 3s 并默认开启 - 补充 PoolManager._KeyCandidate 缺失的 __slots__ 字段
This commit is contained in:
76
frontend/src/features/usage/components/ElapsedTimeText.vue
Normal file
76
frontend/src/features/usage/components/ElapsedTimeText.vue
Normal 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>
|
||||
@@ -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=true)2. 同族格式差异(如 CLAUDE_CLI → CLAUDE)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
|
||||
// 处理时间范围变化
|
||||
|
||||
Reference in New Issue
Block a user