refactor(accounts): 优化用量窗口显示,统一 OAuth 和 Setup Token 处理
- Setup Token 账号现在也调用 API 获取 5h 窗口用量数据 - 重新设计 UsageProgressBar UI,将用量统计移到进度条上方 - 删除冗余的 SetupTokenTimeWindow 组件 - 请求数/Token数支持 K/M/B 单位显示
This commit is contained in:
@@ -1,24 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="showUsageWindows">
|
<div v-if="showUsageWindows">
|
||||||
<!-- Anthropic OAuth accounts: fetch real usage data -->
|
<!-- Anthropic OAuth and Setup Token accounts: fetch real usage data -->
|
||||||
<template v-if="account.platform === 'anthropic' && account.type === 'oauth'">
|
<template v-if="account.platform === 'anthropic' && (account.type === 'oauth' || account.type === 'setup-token')">
|
||||||
<!-- Loading state -->
|
<!-- Loading state -->
|
||||||
<div v-if="loading" class="space-y-1.5">
|
<div v-if="loading" class="space-y-1.5">
|
||||||
|
<!-- OAuth: 3 rows, Setup Token: 1 row -->
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<div class="w-[32px] h-3 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
<div class="w-[32px] h-3 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
||||||
<div class="w-8 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse"></div>
|
<div class="w-8 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse"></div>
|
||||||
<div class="w-[32px] h-3 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
<div class="w-[32px] h-3 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1">
|
<template v-if="account.type === 'oauth'">
|
||||||
<div class="w-[32px] h-3 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
<div class="flex items-center gap-1">
|
||||||
<div class="w-8 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse"></div>
|
<div class="w-[32px] h-3 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
||||||
<div class="w-[32px] h-3 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
<div class="w-8 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse"></div>
|
||||||
</div>
|
<div class="w-[32px] h-3 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
||||||
<div class="flex items-center gap-1">
|
</div>
|
||||||
<div class="w-[32px] h-3 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
<div class="flex items-center gap-1">
|
||||||
<div class="w-8 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse"></div>
|
<div class="w-[32px] h-3 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
||||||
<div class="w-[32px] h-3 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
<div class="w-8 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse"></div>
|
||||||
</div>
|
<div class="w-[32px] h-3 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error state -->
|
<!-- Error state -->
|
||||||
@@ -38,7 +41,7 @@
|
|||||||
color="indigo"
|
color="indigo"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 7d Window -->
|
<!-- 7d Window (OAuth only) -->
|
||||||
<UsageProgressBar
|
<UsageProgressBar
|
||||||
v-if="usageInfo.seven_day"
|
v-if="usageInfo.seven_day"
|
||||||
label="7d"
|
label="7d"
|
||||||
@@ -47,7 +50,7 @@
|
|||||||
color="emerald"
|
color="emerald"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 7d Sonnet Window -->
|
<!-- 7d Sonnet Window (OAuth only) -->
|
||||||
<UsageProgressBar
|
<UsageProgressBar
|
||||||
v-if="usageInfo.seven_day_sonnet"
|
v-if="usageInfo.seven_day_sonnet"
|
||||||
label="7d S"
|
label="7d S"
|
||||||
@@ -63,11 +66,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Anthropic Setup Token accounts: show time-based window progress -->
|
|
||||||
<template v-else-if="account.platform === 'anthropic' && account.type === 'setup-token'">
|
|
||||||
<SetupTokenTimeWindow :account="account" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- OpenAI OAuth accounts: show Codex usage from extra field -->
|
<!-- OpenAI OAuth accounts: show Codex usage from extra field -->
|
||||||
<template v-else-if="account.platform === 'openai' && account.type === 'oauth'">
|
<template v-else-if="account.platform === 'openai' && account.type === 'oauth'">
|
||||||
<div v-if="hasCodexUsage" class="space-y-1">
|
<div v-if="hasCodexUsage" class="space-y-1">
|
||||||
@@ -109,7 +107,6 @@ import { ref, computed, onMounted } from 'vue'
|
|||||||
import { adminAPI } from '@/api/admin'
|
import { adminAPI } from '@/api/admin'
|
||||||
import type { Account, AccountUsageInfo } from '@/types'
|
import type { Account, AccountUsageInfo } from '@/types'
|
||||||
import UsageProgressBar from './UsageProgressBar.vue'
|
import UsageProgressBar from './UsageProgressBar.vue'
|
||||||
import SetupTokenTimeWindow from './SetupTokenTimeWindow.vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
account: Account
|
account: Account
|
||||||
@@ -160,9 +157,10 @@ const codexSecondaryResetAt = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const loadUsage = async () => {
|
const loadUsage = async () => {
|
||||||
// Only fetch usage for Anthropic OAuth accounts
|
// Fetch usage for Anthropic OAuth and Setup Token accounts
|
||||||
// OpenAI usage comes from account.extra field (updated during forwarding)
|
// OpenAI usage comes from account.extra field (updated during forwarding)
|
||||||
if (props.account.platform !== 'anthropic' || props.account.type !== 'oauth') return
|
if (props.account.platform !== 'anthropic') return
|
||||||
|
if (props.account.type !== 'oauth' && props.account.type !== 'setup-token') return
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
|
|||||||
@@ -1,200 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<!-- 5h Time Window Progress -->
|
|
||||||
<div v-if="hasWindowInfo" class="flex items-center gap-1">
|
|
||||||
<!-- Label badge -->
|
|
||||||
<span class="text-[10px] font-medium px-1 rounded w-[32px] text-center shrink-0 bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300">
|
|
||||||
5h
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<!-- Progress bar container -->
|
|
||||||
<div class="w-8 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden shrink-0">
|
|
||||||
<div
|
|
||||||
:class="['h-full transition-all duration-300', barColorClass]"
|
|
||||||
:style="{ width: progressWidth }"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Percentage -->
|
|
||||||
<span :class="['text-[10px] font-medium w-[32px] text-right shrink-0', textColorClass]">
|
|
||||||
{{ displayPercent }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<!-- Reset time -->
|
|
||||||
<span class="text-[10px] text-gray-400 shrink-0">
|
|
||||||
{{ formatResetTime }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- No recent activity (had activity but window expired > 1 hour) -->
|
|
||||||
<div v-else-if="hasExpiredWindow" class="flex items-center gap-1">
|
|
||||||
<span class="text-[10px] font-medium px-1 rounded w-[32px] text-center shrink-0 bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300">
|
|
||||||
5h
|
|
||||||
</span>
|
|
||||||
<span class="text-[10px] text-gray-400 italic">
|
|
||||||
No recent activity
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- No window info yet (never had activity) -->
|
|
||||||
<div v-else class="flex items-center gap-1">
|
|
||||||
<span class="text-[10px] font-medium px-1 rounded w-[32px] text-center shrink-0 bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300">
|
|
||||||
5h
|
|
||||||
</span>
|
|
||||||
<span class="text-[10px] text-gray-400 italic">
|
|
||||||
No activity yet
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Hint -->
|
|
||||||
<div class="text-[10px] text-gray-400 italic">
|
|
||||||
Setup Token (time-based)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
|
||||||
import type { Account } from '@/types'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
account: Account
|
|
||||||
}>()
|
|
||||||
|
|
||||||
// Update timer
|
|
||||||
const currentTime = ref(new Date())
|
|
||||||
let timer: ReturnType<typeof setInterval> | null = null
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// Update every second for more accurate countdown
|
|
||||||
timer = setInterval(() => {
|
|
||||||
currentTime.value = new Date()
|
|
||||||
}, 1000)
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (timer) {
|
|
||||||
clearInterval(timer)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Check if we have window information but it's been expired for more than 1 hour
|
|
||||||
const hasExpiredWindow = computed(() => {
|
|
||||||
if (!props.account.session_window_start || !props.account.session_window_end) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const end = new Date(props.account.session_window_end).getTime()
|
|
||||||
const now = currentTime.value.getTime()
|
|
||||||
const expiredMs = now - end
|
|
||||||
|
|
||||||
// Window exists and expired more than 1 hour ago
|
|
||||||
return expiredMs > 1000 * 60 * 60
|
|
||||||
})
|
|
||||||
|
|
||||||
// Check if we have valid window information (not expired for more than 1 hour)
|
|
||||||
const hasWindowInfo = computed(() => {
|
|
||||||
if (!props.account.session_window_start || !props.account.session_window_end) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// If window is expired more than 1 hour, don't show progress bar
|
|
||||||
if (hasExpiredWindow.value) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
// Calculate time-based progress (0-100)
|
|
||||||
const timeProgress = computed(() => {
|
|
||||||
if (!props.account.session_window_start || !props.account.session_window_end) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = new Date(props.account.session_window_start).getTime()
|
|
||||||
const end = new Date(props.account.session_window_end).getTime()
|
|
||||||
const now = currentTime.value.getTime()
|
|
||||||
|
|
||||||
// Window hasn't started yet
|
|
||||||
if (now < start) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Window has ended
|
|
||||||
if (now >= end) {
|
|
||||||
return 100
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate progress within window
|
|
||||||
const total = end - start
|
|
||||||
const elapsed = now - start
|
|
||||||
return Math.round((elapsed / total) * 100)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Progress bar width
|
|
||||||
const progressWidth = computed(() => {
|
|
||||||
return `${Math.min(timeProgress.value, 100)}%`
|
|
||||||
})
|
|
||||||
|
|
||||||
// Display percentage
|
|
||||||
const displayPercent = computed(() => {
|
|
||||||
return `${timeProgress.value}%`
|
|
||||||
})
|
|
||||||
|
|
||||||
// Progress bar color based on progress
|
|
||||||
const barColorClass = computed(() => {
|
|
||||||
if (timeProgress.value >= 100) {
|
|
||||||
return 'bg-red-500'
|
|
||||||
} else if (timeProgress.value >= 80) {
|
|
||||||
return 'bg-amber-500'
|
|
||||||
} else {
|
|
||||||
return 'bg-green-500'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Text color based on progress
|
|
||||||
const textColorClass = computed(() => {
|
|
||||||
if (timeProgress.value >= 100) {
|
|
||||||
return 'text-red-600 dark:text-red-400'
|
|
||||||
} else if (timeProgress.value >= 80) {
|
|
||||||
return 'text-amber-600 dark:text-amber-400'
|
|
||||||
} else {
|
|
||||||
return 'text-gray-600 dark:text-gray-400'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Format reset time (time remaining until window end)
|
|
||||||
const formatResetTime = computed(() => {
|
|
||||||
if (!props.account.session_window_end) {
|
|
||||||
return 'N/A'
|
|
||||||
}
|
|
||||||
|
|
||||||
const end = new Date(props.account.session_window_end)
|
|
||||||
const now = currentTime.value
|
|
||||||
const diffMs = end.getTime() - now.getTime()
|
|
||||||
|
|
||||||
if (diffMs <= 0) {
|
|
||||||
// 窗口已过期,计算过期了多久
|
|
||||||
const expiredMs = Math.abs(diffMs)
|
|
||||||
const expiredHours = Math.floor(expiredMs / (1000 * 60 * 60))
|
|
||||||
|
|
||||||
if (expiredHours >= 1) {
|
|
||||||
return 'No recent activity'
|
|
||||||
}
|
|
||||||
return 'Window expired'
|
|
||||||
}
|
|
||||||
|
|
||||||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
|
|
||||||
const diffMins = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60))
|
|
||||||
const diffSecs = Math.floor((diffMs % (1000 * 60)) / 1000)
|
|
||||||
|
|
||||||
if (diffHours > 0) {
|
|
||||||
return `${diffHours}h ${diffMins}m`
|
|
||||||
} else if (diffMins > 0) {
|
|
||||||
return `${diffMins}m ${diffSecs}s`
|
|
||||||
} else {
|
|
||||||
return `${diffSecs}s`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,37 +1,50 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center gap-1">
|
<div>
|
||||||
<!-- Label badge (fixed width for alignment) -->
|
<!-- Window stats row (above progress bar, left-right aligned with progress bar) -->
|
||||||
<span
|
<div v-if="windowStats" class="flex items-center justify-between mb-0.5" :title="`5h 窗口用量统计`">
|
||||||
:class="[
|
<div class="flex items-center gap-1.5 text-[9px] text-gray-500 dark:text-gray-400 cursor-help">
|
||||||
'text-[10px] font-medium px-1 rounded w-[32px] text-center shrink-0',
|
<span class="px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-800">
|
||||||
labelClass
|
{{ formatRequests }} req
|
||||||
]"
|
</span>
|
||||||
>
|
<span class="px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-800">
|
||||||
{{ label }}
|
{{ formatTokens }}
|
||||||
</span>
|
</span>
|
||||||
|
<span class="px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-800">
|
||||||
<!-- Progress bar container -->
|
${{ formatCost }}
|
||||||
<div class="w-8 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden shrink-0">
|
</span>
|
||||||
<div
|
</div>
|
||||||
:class="['h-full transition-all duration-300', barClass]"
|
|
||||||
:style="{ width: barWidth }"
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Percentage -->
|
<!-- Progress bar row -->
|
||||||
<span :class="['text-[10px] font-medium w-[32px] text-right shrink-0', textClass]">
|
<div class="flex items-center gap-1">
|
||||||
{{ displayPercent }}
|
<!-- Label badge (fixed width for alignment) -->
|
||||||
</span>
|
<span
|
||||||
|
:class="[
|
||||||
|
'text-[10px] font-medium px-1 rounded w-[32px] text-center shrink-0',
|
||||||
|
labelClass
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</span>
|
||||||
|
|
||||||
<!-- Reset time -->
|
<!-- Progress bar container -->
|
||||||
<span v-if="resetsAt" class="text-[10px] text-gray-400 shrink-0">
|
<div class="w-8 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden shrink-0">
|
||||||
{{ formatResetTime }}
|
<div
|
||||||
</span>
|
:class="['h-full transition-all duration-300', barClass]"
|
||||||
|
:style="{ width: barWidth }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Window stats (only for 5h window) -->
|
<!-- Percentage -->
|
||||||
<span v-if="windowStats" class="text-[10px] text-gray-400 shrink-0 ml-1">
|
<span :class="['text-[10px] font-medium w-[32px] text-right shrink-0', textClass]">
|
||||||
({{ formatStats }})
|
{{ displayPercent }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<!-- Reset time -->
|
||||||
|
<span v-if="resetsAt" class="text-[10px] text-gray-400 shrink-0">
|
||||||
|
{{ formatResetTime }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -113,17 +126,25 @@ const formatResetTime = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Format window stats
|
// Format window stats
|
||||||
const formatStats = computed(() => {
|
const formatRequests = computed(() => {
|
||||||
if (!props.windowStats) return ''
|
if (!props.windowStats) return ''
|
||||||
const { requests, tokens, cost } = props.windowStats
|
const r = props.windowStats.requests
|
||||||
|
if (r >= 1000000) return `${(r / 1000000).toFixed(1)}M`
|
||||||
|
if (r >= 1000) return `${(r / 1000).toFixed(1)}K`
|
||||||
|
return r.toString()
|
||||||
|
})
|
||||||
|
|
||||||
// Format tokens (e.g., 1234567 -> 1.2M)
|
const formatTokens = computed(() => {
|
||||||
const formatTokens = (t: number): string => {
|
if (!props.windowStats) return ''
|
||||||
if (t >= 1000000) return `${(t / 1000000).toFixed(1)}M`
|
const t = props.windowStats.tokens
|
||||||
if (t >= 1000) return `${(t / 1000).toFixed(1)}K`
|
if (t >= 1000000000) return `${(t / 1000000000).toFixed(1)}B`
|
||||||
return t.toString()
|
if (t >= 1000000) return `${(t / 1000000).toFixed(1)}M`
|
||||||
}
|
if (t >= 1000) return `${(t / 1000).toFixed(1)}K`
|
||||||
|
return t.toString()
|
||||||
|
})
|
||||||
|
|
||||||
return `${requests}req ${formatTokens(tokens)}tok $${cost.toFixed(2)}`
|
const formatCost = computed(() => {
|
||||||
|
if (!props.windowStats) return '0.00'
|
||||||
|
return props.windowStats.cost.toFixed(2)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user