From fd73b8875d86dc75b6a4f10a9c71605cba40161b Mon Sep 17 00:00:00 2001 From: shaw Date: Fri, 23 Jan 2026 15:48:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E4=BC=98=E5=8C=96=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E9=99=90=E6=B5=81=E7=8A=B6=E6=80=81=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=EF=BC=8C=E7=9B=B4=E6=8E=A5=E5=B1=95=E7=A4=BA=E5=80=92=E8=AE=A1?= =?UTF-8?q?=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../account/AccountStatusIndicator.vue | 97 ++++++++----------- frontend/src/i18n/locales/en.ts | 10 +- frontend/src/i18n/locales/zh.ts | 10 +- frontend/src/utils/format.ts | 45 +++++++++ 4 files changed, 102 insertions(+), 60 deletions(-) diff --git a/frontend/src/components/account/AccountStatusIndicator.vue b/frontend/src/components/account/AccountStatusIndicator.vue index 7dae33bb..02c962f1 100644 --- a/frontend/src/components/account/AccountStatusIndicator.vue +++ b/frontend/src/components/account/AccountStatusIndicator.vue @@ -1,18 +1,32 @@ @@ -87,8 +63,7 @@ import { computed } from 'vue' import { useI18n } from 'vue-i18n' import type { Account } from '@/types' -import { formatTime } from '@/utils/format' -import Icon from '@/components/icons/Icon.vue' +import { formatCountdownWithSuffix } from '@/utils/format' const { t } = useI18n() @@ -123,6 +98,16 @@ const hasError = computed(() => { 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 const statusClass = computed(() => { if (hasError.value) { @@ -131,7 +116,7 @@ const statusClass = computed(() => { if (isTempUnschedulable.value) { return 'badge-warning' } - if (!props.account.schedulable || isRateLimited.value || isOverloaded.value) { + if (!props.account.schedulable) { return 'badge-gray' } switch (props.account.status) { @@ -157,9 +142,6 @@ const statusText = computed(() => { if (!props.account.schedulable) { 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}`) }) @@ -167,5 +149,4 @@ const handleTempUnschedClick = () => { if (!isTempUnschedulable.value) return emit('show-temp-unsched', props.account) } - diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 366cb4e7..e293491b 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -169,7 +169,13 @@ export default { justNow: 'Just now', minutesAgo: '{n}m 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', paused: 'Paused', limited: 'Limited', + rateLimited: 'Rate Limited', + overloaded: 'Overloaded', tempUnschedulable: 'Temp Unschedulable', rateLimitedUntil: 'Rate limited until {time}', overloadedUntil: 'Overloaded until {time}', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 6864e0fb..dbeb3819 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -166,7 +166,13 @@ export default { justNow: '刚刚', minutesAgo: '{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: '冷却中', paused: '暂停', limited: '限流', + rateLimited: '限流中', + overloaded: '过载中', tempUnschedulable: '临时不可调度', rateLimitedUntil: '限流中,重置时间:{time}', overloadedUntil: '负载过重,重置时间:{time}', diff --git a/frontend/src/utils/format.ts b/frontend/src/utils/format.ts index bdc68660..78e45354 100644 --- a/frontend/src/utils/format.ts +++ b/frontend/src/utils/format.ts @@ -216,3 +216,48 @@ export function formatTokensK(tokens: number): string { if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}K` 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 }) +}