Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
fd73b887
Commit
fd73b887
authored
Jan 23, 2026
by
shaw
Browse files
feat(frontend): 优化账号限流状态显示,直接展示倒计时
parent
f9ab1daa
Changes
4
Show whitespace changes
Inline
Side-by-side
frontend/src/components/account/AccountStatusIndicator.vue
View file @
fd73b887
<
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 -->
<div
v-if=
"isRateLimited"
class=
"flex flex-col items-center gap-1"
>
<span
class=
"badge text-xs badge-warning"
>
{{
t
(
'
admin.accounts.status.rateLimited
'
)
}}
</span>
<span
class=
"text-[11px] text-gray-400 dark:text-gray-500"
>
{{
rateLimitCountdown
}}
</span>
</div>
<!-- Overload Display (529) - Two-line layout -->
<div
v-else-if=
"isOverloaded"
class=
"flex flex-col items-center gap-1"
>
<span
class=
"badge text-xs badge-danger"
>
{{
t
(
'
admin.accounts.status.overloaded
'
)
}}
</span>
<span
class=
"text-[11px] text-gray-400 dark:text-gray-500"
>
{{
overloadCountdown
}}
</span>
</div>
<!-- Main Status Badge (shown when not rate limited/overloaded) -->
<template
v-else
>
<button
<button
v-if=
"isTempUnschedulable"
v-if=
"isTempUnschedulable"
type=
"button"
type=
"button"
...
@@ -13,6 +26,7 @@
...
@@ -13,6 +26,7 @@
<span
v-else
:class=
"['badge text-xs', statusClass]"
>
<span
v-else
:class=
"['badge text-xs', statusClass]"
>
{{
statusText
}}
{{
statusText
}}
</span>
</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 @@
...
@@ -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 @@
...
@@ -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(() => {
...
@@ -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(() => {
...
@@ -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(() => {
...
@@ -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 = () => {
...
@@ -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
>
frontend/src/i18n/locales/en.ts
View file @
fd73b887
...
@@ -169,7 +169,13 @@ export default {
...
@@ -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 {
...
@@ -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}
'
,
...
...
frontend/src/i18n/locales/zh.ts
View file @
fd73b887
...
@@ -166,7 +166,13 @@ export default {
...
@@ -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 {
...
@@ -1212,6 +1218,8 @@ export default {
cooldown
:
'
冷却中
'
,
cooldown
:
'
冷却中
'
,
paused
:
'
暂停
'
,
paused
:
'
暂停
'
,
limited
:
'
限流
'
,
limited
:
'
限流
'
,
rateLimited
:
'
限流中
'
,
overloaded
:
'
过载中
'
,
tempUnschedulable
:
'
临时不可调度
'
,
tempUnschedulable
:
'
临时不可调度
'
,
rateLimitedUntil
:
'
限流中,重置时间:{time}
'
,
rateLimitedUntil
:
'
限流中,重置时间:{time}
'
,
overloadedUntil
:
'
负载过重,重置时间:{time}
'
,
overloadedUntil
:
'
负载过重,重置时间:{time}
'
,
...
...
frontend/src/utils/format.ts
View file @
fd73b887
...
@@ -216,3 +216,48 @@ export function formatTokensK(tokens: number): string {
...
@@ -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
})
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment