feat(frontend): 优化账号限流状态显示,直接展示倒计时
This commit is contained in:
@@ -1,18 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<!-- Main Status Badge -->
|
<!-- Rate Limit Display (429) - Two-line layout -->
|
||||||
<button
|
<div v-if="isRateLimited" class="flex flex-col items-center gap-1">
|
||||||
v-if="isTempUnschedulable"
|
<span class="badge text-xs badge-warning">{{ t('admin.accounts.status.rateLimited') }}</span>
|
||||||
type="button"
|
<span class="text-[11px] text-gray-400 dark:text-gray-500">{{ rateLimitCountdown }}</span>
|
||||||
:class="['badge text-xs', statusClass, 'cursor-pointer']"
|
</div>
|
||||||
:title="t('admin.accounts.status.viewTempUnschedDetails')"
|
|
||||||
@click="handleTempUnschedClick"
|
<!-- Overload Display (529) - Two-line layout -->
|
||||||
>
|
<div v-else-if="isOverloaded" class="flex flex-col items-center gap-1">
|
||||||
{{ statusText }}
|
<span class="badge text-xs badge-danger">{{ t('admin.accounts.status.overloaded') }}</span>
|
||||||
</button>
|
<span class="text-[11px] text-gray-400 dark:text-gray-500">{{ overloadCountdown }}</span>
|
||||||
<span v-else :class="['badge text-xs', statusClass]">
|
</div>
|
||||||
{{ statusText }}
|
|
||||||
</span>
|
<!-- Main Status Badge (shown when not rate limited/overloaded) -->
|
||||||
|
<template v-else>
|
||||||
|
<button
|
||||||
|
v-if="isTempUnschedulable"
|
||||||
|
type="button"
|
||||||
|
:class="['badge text-xs', statusClass, 'cursor-pointer']"
|
||||||
|
:title="t('admin.accounts.status.viewTempUnschedDetails')"
|
||||||
|
@click="handleTempUnschedClick"
|
||||||
|
>
|
||||||
|
{{ statusText }}
|
||||||
|
</button>
|
||||||
|
<span v-else :class="['badge text-xs', statusClass]">
|
||||||
|
{{ statusText }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Error Info Indicator -->
|
<!-- Error Info Indicator -->
|
||||||
<div v-if="hasError && account.error_message" class="group/error relative">
|
<div v-if="hasError && account.error_message" class="group/error relative">
|
||||||
@@ -42,44 +56,6 @@
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Rate Limit Indicator (429) -->
|
|
||||||
<div v-if="isRateLimited" class="group relative">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center gap-1 rounded bg-amber-100 px-1.5 py-0.5 text-xs font-medium text-amber-700 dark:bg-amber-900/30 dark:text-amber-400"
|
|
||||||
>
|
|
||||||
<Icon name="exclamationTriangle" size="xs" :stroke-width="2" />
|
|
||||||
429
|
|
||||||
</span>
|
|
||||||
<!-- Tooltip -->
|
|
||||||
<div
|
|
||||||
class="pointer-events-none absolute bottom-full left-1/2 z-50 mb-2 -translate-x-1/2 whitespace-nowrap rounded bg-gray-900 px-2 py-1 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700"
|
|
||||||
>
|
|
||||||
{{ t('admin.accounts.status.rateLimitedUntil', { time: formatTime(account.rate_limit_reset_at) }) }}
|
|
||||||
<div
|
|
||||||
class="absolute left-1/2 top-full -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Overload Indicator (529) -->
|
|
||||||
<div v-if="isOverloaded" class="group relative">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center gap-1 rounded bg-red-100 px-1.5 py-0.5 text-xs font-medium text-red-700 dark:bg-red-900/30 dark:text-red-400"
|
|
||||||
>
|
|
||||||
<Icon name="exclamationTriangle" size="xs" :stroke-width="2" />
|
|
||||||
529
|
|
||||||
</span>
|
|
||||||
<!-- Tooltip -->
|
|
||||||
<div
|
|
||||||
class="pointer-events-none absolute bottom-full left-1/2 z-50 mb-2 -translate-x-1/2 whitespace-nowrap rounded bg-gray-900 px-2 py-1 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700"
|
|
||||||
>
|
|
||||||
{{ t('admin.accounts.status.overloadedUntil', { time: formatTime(account.overload_until) }) }}
|
|
||||||
<div
|
|
||||||
class="absolute left-1/2 top-full -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -87,8 +63,7 @@
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import type { Account } from '@/types'
|
import type { Account } from '@/types'
|
||||||
import { formatTime } from '@/utils/format'
|
import { formatCountdownWithSuffix } from '@/utils/format'
|
||||||
import Icon from '@/components/icons/Icon.vue'
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
@@ -123,6 +98,16 @@ const hasError = computed(() => {
|
|||||||
return props.account.status === 'error'
|
return props.account.status === 'error'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Computed: countdown text for rate limit (429)
|
||||||
|
const rateLimitCountdown = computed(() => {
|
||||||
|
return formatCountdownWithSuffix(props.account.rate_limit_reset_at)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed: countdown text for overload (529)
|
||||||
|
const overloadCountdown = computed(() => {
|
||||||
|
return formatCountdownWithSuffix(props.account.overload_until)
|
||||||
|
})
|
||||||
|
|
||||||
// Computed: status badge class
|
// Computed: status badge class
|
||||||
const statusClass = computed(() => {
|
const statusClass = computed(() => {
|
||||||
if (hasError.value) {
|
if (hasError.value) {
|
||||||
@@ -131,7 +116,7 @@ const statusClass = computed(() => {
|
|||||||
if (isTempUnschedulable.value) {
|
if (isTempUnschedulable.value) {
|
||||||
return 'badge-warning'
|
return 'badge-warning'
|
||||||
}
|
}
|
||||||
if (!props.account.schedulable || isRateLimited.value || isOverloaded.value) {
|
if (!props.account.schedulable) {
|
||||||
return 'badge-gray'
|
return 'badge-gray'
|
||||||
}
|
}
|
||||||
switch (props.account.status) {
|
switch (props.account.status) {
|
||||||
@@ -157,9 +142,6 @@ const statusText = computed(() => {
|
|||||||
if (!props.account.schedulable) {
|
if (!props.account.schedulable) {
|
||||||
return t('admin.accounts.status.paused')
|
return t('admin.accounts.status.paused')
|
||||||
}
|
}
|
||||||
if (isRateLimited.value || isOverloaded.value) {
|
|
||||||
return t('admin.accounts.status.limited')
|
|
||||||
}
|
|
||||||
return t(`admin.accounts.status.${props.account.status}`)
|
return t(`admin.accounts.status.${props.account.status}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -167,5 +149,4 @@ const handleTempUnschedClick = () => {
|
|||||||
if (!isTempUnschedulable.value) return
|
if (!isTempUnschedulable.value) return
|
||||||
emit('show-temp-unsched', props.account)
|
emit('show-temp-unsched', props.account)
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -169,7 +169,13 @@ export default {
|
|||||||
justNow: 'Just now',
|
justNow: 'Just now',
|
||||||
minutesAgo: '{n}m ago',
|
minutesAgo: '{n}m ago',
|
||||||
hoursAgo: '{n}h ago',
|
hoursAgo: '{n}h ago',
|
||||||
daysAgo: '{n}d ago'
|
daysAgo: '{n}d ago',
|
||||||
|
countdown: {
|
||||||
|
daysHours: '{d}d {h}h',
|
||||||
|
hoursMinutes: '{h}h {m}m',
|
||||||
|
minutes: '{m}m',
|
||||||
|
withSuffix: '{time} to lift'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1090,6 +1096,8 @@ export default {
|
|||||||
cooldown: 'Cooldown',
|
cooldown: 'Cooldown',
|
||||||
paused: 'Paused',
|
paused: 'Paused',
|
||||||
limited: 'Limited',
|
limited: 'Limited',
|
||||||
|
rateLimited: 'Rate Limited',
|
||||||
|
overloaded: 'Overloaded',
|
||||||
tempUnschedulable: 'Temp Unschedulable',
|
tempUnschedulable: 'Temp Unschedulable',
|
||||||
rateLimitedUntil: 'Rate limited until {time}',
|
rateLimitedUntil: 'Rate limited until {time}',
|
||||||
overloadedUntil: 'Overloaded until {time}',
|
overloadedUntil: 'Overloaded until {time}',
|
||||||
|
|||||||
@@ -166,7 +166,13 @@ export default {
|
|||||||
justNow: '刚刚',
|
justNow: '刚刚',
|
||||||
minutesAgo: '{n}分钟前',
|
minutesAgo: '{n}分钟前',
|
||||||
hoursAgo: '{n}小时前',
|
hoursAgo: '{n}小时前',
|
||||||
daysAgo: '{n}天前'
|
daysAgo: '{n}天前',
|
||||||
|
countdown: {
|
||||||
|
daysHours: '{d}d {h}h',
|
||||||
|
hoursMinutes: '{h}h {m}m',
|
||||||
|
minutes: '{m}m',
|
||||||
|
withSuffix: '{time} 后解除'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1212,6 +1218,8 @@ export default {
|
|||||||
cooldown: '冷却中',
|
cooldown: '冷却中',
|
||||||
paused: '暂停',
|
paused: '暂停',
|
||||||
limited: '限流',
|
limited: '限流',
|
||||||
|
rateLimited: '限流中',
|
||||||
|
overloaded: '过载中',
|
||||||
tempUnschedulable: '临时不可调度',
|
tempUnschedulable: '临时不可调度',
|
||||||
rateLimitedUntil: '限流中,重置时间:{time}',
|
rateLimitedUntil: '限流中,重置时间:{time}',
|
||||||
overloadedUntil: '负载过重,重置时间:{time}',
|
overloadedUntil: '负载过重,重置时间:{time}',
|
||||||
|
|||||||
@@ -216,3 +216,48 @@ export function formatTokensK(tokens: number): string {
|
|||||||
if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}K`
|
if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}K`
|
||||||
return tokens.toString()
|
return tokens.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化倒计时(从现在到目标时间的剩余时间)
|
||||||
|
* @param targetDate 目标日期字符串或 Date 对象
|
||||||
|
* @returns 倒计时字符串,如 "2h 41m", "3d 5h", "15m"
|
||||||
|
*/
|
||||||
|
export function formatCountdown(targetDate: string | Date | null | undefined): string | null {
|
||||||
|
if (!targetDate) return null
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
const target = new Date(targetDate)
|
||||||
|
const diffMs = target.getTime() - now.getTime()
|
||||||
|
|
||||||
|
// 如果目标时间已过或无效
|
||||||
|
if (diffMs <= 0 || isNaN(diffMs)) return null
|
||||||
|
|
||||||
|
const diffMins = Math.floor(diffMs / (1000 * 60))
|
||||||
|
const diffHours = Math.floor(diffMins / 60)
|
||||||
|
const diffDays = Math.floor(diffHours / 24)
|
||||||
|
|
||||||
|
const remainingHours = diffHours % 24
|
||||||
|
const remainingMins = diffMins % 60
|
||||||
|
|
||||||
|
if (diffDays > 0) {
|
||||||
|
// 超过1天:显示 "Xd Yh"
|
||||||
|
return i18n.global.t('common.time.countdown.daysHours', { d: diffDays, h: remainingHours })
|
||||||
|
}
|
||||||
|
if (diffHours > 0) {
|
||||||
|
// 小于1天:显示 "Xh Ym"
|
||||||
|
return i18n.global.t('common.time.countdown.hoursMinutes', { h: diffHours, m: remainingMins })
|
||||||
|
}
|
||||||
|
// 小于1小时:显示 "Ym"
|
||||||
|
return i18n.global.t('common.time.countdown.minutes', { m: diffMins })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化倒计时并带后缀(如 "2h 41m 后解除")
|
||||||
|
* @param targetDate 目标日期字符串或 Date 对象
|
||||||
|
* @returns 完整的倒计时字符串,如 "2h 41m to lift", "2小时41分钟后解除"
|
||||||
|
*/
|
||||||
|
export function formatCountdownWithSuffix(targetDate: string | Date | null | undefined): string | null {
|
||||||
|
const countdown = formatCountdown(targetDate)
|
||||||
|
if (!countdown) return null
|
||||||
|
return i18n.global.t('common.time.countdown.withSuffix', { time: countdown })
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user