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
e6326b29
Unverified
Commit
e6326b29
authored
Mar 18, 2026
by
Wesley Liddick
Committed by
GitHub
Mar 18, 2026
Browse files
Merge pull request #1108 from DaydreamCoding/feat/admin-group-capacity-and-usage
feat(admin): 分组管理列表新增用量、账号分类与容量列
parents
17cdcebd
d4cc9871
Changes
32
Show whitespace changes
Inline
Side-by-side
backend/internal/service/group.go
View file @
e6326b29
...
@@ -66,6 +66,8 @@ type Group struct {
...
@@ -66,6 +66,8 @@ type Group struct {
AccountGroups
[]
AccountGroup
AccountGroups
[]
AccountGroup
AccountCount
int64
AccountCount
int64
ActiveAccountCount
int64
RateLimitedAccountCount
int64
}
}
func
(
g
*
Group
)
IsActive
()
bool
{
func
(
g
*
Group
)
IsActive
()
bool
{
...
...
backend/internal/service/group_capacity_service.go
0 → 100644
View file @
e6326b29
package
service
import
(
"context"
"time"
)
// GroupCapacitySummary holds aggregated capacity for a single group.
type
GroupCapacitySummary
struct
{
GroupID
int64
`json:"group_id"`
ConcurrencyUsed
int
`json:"concurrency_used"`
ConcurrencyMax
int
`json:"concurrency_max"`
SessionsUsed
int
`json:"sessions_used"`
SessionsMax
int
`json:"sessions_max"`
RPMUsed
int
`json:"rpm_used"`
RPMMax
int
`json:"rpm_max"`
}
// GroupCapacityService aggregates per-group capacity from runtime data.
type
GroupCapacityService
struct
{
accountRepo
AccountRepository
groupRepo
GroupRepository
concurrencyService
*
ConcurrencyService
sessionLimitCache
SessionLimitCache
rpmCache
RPMCache
}
// NewGroupCapacityService creates a new GroupCapacityService.
func
NewGroupCapacityService
(
accountRepo
AccountRepository
,
groupRepo
GroupRepository
,
concurrencyService
*
ConcurrencyService
,
sessionLimitCache
SessionLimitCache
,
rpmCache
RPMCache
,
)
*
GroupCapacityService
{
return
&
GroupCapacityService
{
accountRepo
:
accountRepo
,
groupRepo
:
groupRepo
,
concurrencyService
:
concurrencyService
,
sessionLimitCache
:
sessionLimitCache
,
rpmCache
:
rpmCache
,
}
}
// GetAllGroupCapacity returns capacity summary for all active groups.
func
(
s
*
GroupCapacityService
)
GetAllGroupCapacity
(
ctx
context
.
Context
)
([]
GroupCapacitySummary
,
error
)
{
groups
,
err
:=
s
.
groupRepo
.
ListActive
(
ctx
)
if
err
!=
nil
{
return
nil
,
err
}
results
:=
make
([]
GroupCapacitySummary
,
0
,
len
(
groups
))
for
i
:=
range
groups
{
cap
,
err
:=
s
.
getGroupCapacity
(
ctx
,
groups
[
i
]
.
ID
)
if
err
!=
nil
{
// Skip groups with errors, return partial results
continue
}
cap
.
GroupID
=
groups
[
i
]
.
ID
results
=
append
(
results
,
cap
)
}
return
results
,
nil
}
func
(
s
*
GroupCapacityService
)
getGroupCapacity
(
ctx
context
.
Context
,
groupID
int64
)
(
GroupCapacitySummary
,
error
)
{
accounts
,
err
:=
s
.
accountRepo
.
ListSchedulableByGroupID
(
ctx
,
groupID
)
if
err
!=
nil
{
return
GroupCapacitySummary
{},
err
}
if
len
(
accounts
)
==
0
{
return
GroupCapacitySummary
{},
nil
}
// Collect account IDs and config values
accountIDs
:=
make
([]
int64
,
0
,
len
(
accounts
))
sessionTimeouts
:=
make
(
map
[
int64
]
time
.
Duration
)
var
concurrencyMax
,
sessionsMax
,
rpmMax
int
for
i
:=
range
accounts
{
acc
:=
&
accounts
[
i
]
accountIDs
=
append
(
accountIDs
,
acc
.
ID
)
concurrencyMax
+=
acc
.
Concurrency
if
ms
:=
acc
.
GetMaxSessions
();
ms
>
0
{
sessionsMax
+=
ms
timeout
:=
time
.
Duration
(
acc
.
GetSessionIdleTimeoutMinutes
())
*
time
.
Minute
if
timeout
<=
0
{
timeout
=
5
*
time
.
Minute
}
sessionTimeouts
[
acc
.
ID
]
=
timeout
}
if
rpm
:=
acc
.
GetBaseRPM
();
rpm
>
0
{
rpmMax
+=
rpm
}
}
// Batch query runtime data from Redis
concurrencyMap
,
_
:=
s
.
concurrencyService
.
GetAccountConcurrencyBatch
(
ctx
,
accountIDs
)
var
sessionsMap
map
[
int64
]
int
if
sessionsMax
>
0
&&
s
.
sessionLimitCache
!=
nil
{
sessionsMap
,
_
=
s
.
sessionLimitCache
.
GetActiveSessionCountBatch
(
ctx
,
accountIDs
,
sessionTimeouts
)
}
var
rpmMap
map
[
int64
]
int
if
rpmMax
>
0
&&
s
.
rpmCache
!=
nil
{
rpmMap
,
_
=
s
.
rpmCache
.
GetRPMBatch
(
ctx
,
accountIDs
)
}
// Aggregate
var
concurrencyUsed
,
sessionsUsed
,
rpmUsed
int
for
_
,
id
:=
range
accountIDs
{
concurrencyUsed
+=
concurrencyMap
[
id
]
if
sessionsMap
!=
nil
{
sessionsUsed
+=
sessionsMap
[
id
]
}
if
rpmMap
!=
nil
{
rpmUsed
+=
rpmMap
[
id
]
}
}
return
GroupCapacitySummary
{
ConcurrencyUsed
:
concurrencyUsed
,
ConcurrencyMax
:
concurrencyMax
,
SessionsUsed
:
sessionsUsed
,
SessionsMax
:
sessionsMax
,
RPMUsed
:
rpmUsed
,
RPMMax
:
rpmMax
,
},
nil
}
backend/internal/service/group_service.go
View file @
e6326b29
...
@@ -27,7 +27,7 @@ type GroupRepository interface {
...
@@ -27,7 +27,7 @@ type GroupRepository interface {
ListActiveByPlatform
(
ctx
context
.
Context
,
platform
string
)
([]
Group
,
error
)
ListActiveByPlatform
(
ctx
context
.
Context
,
platform
string
)
([]
Group
,
error
)
ExistsByName
(
ctx
context
.
Context
,
name
string
)
(
bool
,
error
)
ExistsByName
(
ctx
context
.
Context
,
name
string
)
(
bool
,
error
)
GetAccountCount
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
GetAccountCount
(
ctx
context
.
Context
,
groupID
int64
)
(
total
int64
,
active
int64
,
err
error
)
DeleteAccountGroupsByGroupID
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
DeleteAccountGroupsByGroupID
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
// GetAccountIDsByGroupIDs 获取多个分组的所有账号 ID(去重)
// GetAccountIDsByGroupIDs 获取多个分组的所有账号 ID(去重)
GetAccountIDsByGroupIDs
(
ctx
context
.
Context
,
groupIDs
[]
int64
)
([]
int64
,
error
)
GetAccountIDsByGroupIDs
(
ctx
context
.
Context
,
groupIDs
[]
int64
)
([]
int64
,
error
)
...
@@ -202,7 +202,7 @@ func (s *GroupService) GetStats(ctx context.Context, id int64) (map[string]any,
...
@@ -202,7 +202,7 @@ func (s *GroupService) GetStats(ctx context.Context, id int64) (map[string]any,
}
}
// 获取账号数量
// 获取账号数量
accountCount
,
err
:=
s
.
groupRepo
.
GetAccountCount
(
ctx
,
id
)
accountCount
,
_
,
err
:=
s
.
groupRepo
.
GetAccountCount
(
ctx
,
id
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"get account count: %w"
,
err
)
return
nil
,
fmt
.
Errorf
(
"get account count: %w"
,
err
)
}
}
...
...
backend/internal/service/sora_quota_service_test.go
View file @
e6326b29
...
@@ -52,8 +52,8 @@ func (r *stubGroupRepoForQuota) ListActiveByPlatform(context.Context, string) ([
...
@@ -52,8 +52,8 @@ func (r *stubGroupRepoForQuota) ListActiveByPlatform(context.Context, string) ([
func
(
r
*
stubGroupRepoForQuota
)
ExistsByName
(
context
.
Context
,
string
)
(
bool
,
error
)
{
func
(
r
*
stubGroupRepoForQuota
)
ExistsByName
(
context
.
Context
,
string
)
(
bool
,
error
)
{
return
false
,
nil
return
false
,
nil
}
}
func
(
r
*
stubGroupRepoForQuota
)
GetAccountCount
(
context
.
Context
,
int64
)
(
int64
,
error
)
{
func
(
r
*
stubGroupRepoForQuota
)
GetAccountCount
(
context
.
Context
,
int64
)
(
int64
,
int64
,
error
)
{
return
0
,
nil
return
0
,
0
,
nil
}
}
func
(
r
*
stubGroupRepoForQuota
)
DeleteAccountGroupsByGroupID
(
context
.
Context
,
int64
)
(
int64
,
error
)
{
func
(
r
*
stubGroupRepoForQuota
)
DeleteAccountGroupsByGroupID
(
context
.
Context
,
int64
)
(
int64
,
error
)
{
return
0
,
nil
return
0
,
nil
...
...
backend/internal/service/subscription_assign_idempotency_test.go
View file @
e6326b29
...
@@ -40,7 +40,7 @@ func (groupRepoNoop) ListActiveByPlatform(context.Context, string) ([]Group, err
...
@@ -40,7 +40,7 @@ func (groupRepoNoop) ListActiveByPlatform(context.Context, string) ([]Group, err
func
(
groupRepoNoop
)
ExistsByName
(
context
.
Context
,
string
)
(
bool
,
error
)
{
func
(
groupRepoNoop
)
ExistsByName
(
context
.
Context
,
string
)
(
bool
,
error
)
{
panic
(
"unexpected ExistsByName call"
)
panic
(
"unexpected ExistsByName call"
)
}
}
func
(
groupRepoNoop
)
GetAccountCount
(
context
.
Context
,
int64
)
(
int64
,
error
)
{
func
(
groupRepoNoop
)
GetAccountCount
(
context
.
Context
,
int64
)
(
int64
,
int64
,
error
)
{
panic
(
"unexpected GetAccountCount call"
)
panic
(
"unexpected GetAccountCount call"
)
}
}
func
(
groupRepoNoop
)
DeleteAccountGroupsByGroupID
(
context
.
Context
,
int64
)
(
int64
,
error
)
{
func
(
groupRepoNoop
)
DeleteAccountGroupsByGroupID
(
context
.
Context
,
int64
)
(
int64
,
error
)
{
...
...
backend/internal/service/wire.go
View file @
e6326b29
...
@@ -486,4 +486,5 @@ var ProviderSet = wire.NewSet(
...
@@ -486,4 +486,5 @@ var ProviderSet = wire.NewSet(
ProvideIdempotencyCleanupService
,
ProvideIdempotencyCleanupService
,
ProvideScheduledTestService
,
ProvideScheduledTestService
,
ProvideScheduledTestRunnerService
,
ProvideScheduledTestRunnerService
,
NewGroupCapacityService
,
)
)
frontend/src/api/admin/groups.ts
View file @
e6326b29
...
@@ -218,6 +218,34 @@ export async function batchSetGroupRateMultipliers(
...
@@ -218,6 +218,34 @@ export async function batchSetGroupRateMultipliers(
return
data
return
data
}
}
/**
* Get usage summary (today + cumulative cost) for all groups
* @param timezone - IANA timezone string (e.g. "Asia/Shanghai")
* @returns Array of group usage summaries
*/
export
async
function
getUsageSummary
(
timezone
?:
string
):
Promise
<
{
group_id
:
number
;
today_cost
:
number
;
total_cost
:
number
}[]
>
{
const
{
data
}
=
await
apiClient
.
get
<
{
group_id
:
number
;
today_cost
:
number
;
total_cost
:
number
}[]
>
(
'
/admin/groups/usage-summary
'
,
{
params
:
timezone
?
{
timezone
}
:
undefined
})
return
data
}
/**
* Get capacity summary (concurrency/sessions/RPM) for all active groups
*/
export
async
function
getCapacitySummary
():
Promise
<
{
group_id
:
number
;
concurrency_used
:
number
;
concurrency_max
:
number
;
sessions_used
:
number
;
sessions_max
:
number
;
rpm_used
:
number
;
rpm_max
:
number
}[]
>
{
const
{
data
}
=
await
apiClient
.
get
<
{
group_id
:
number
;
concurrency_used
:
number
;
concurrency_max
:
number
;
sessions_used
:
number
;
sessions_max
:
number
;
rpm_used
:
number
;
rpm_max
:
number
}[]
>
(
'
/admin/groups/capacity-summary
'
)
return
data
}
export
const
groupsAPI
=
{
export
const
groupsAPI
=
{
list
,
list
,
getAll
,
getAll
,
...
@@ -232,7 +260,9 @@ export const groupsAPI = {
...
@@ -232,7 +260,9 @@ export const groupsAPI = {
getGroupRateMultipliers
,
getGroupRateMultipliers
,
clearGroupRateMultipliers
,
clearGroupRateMultipliers
,
batchSetGroupRateMultipliers
,
batchSetGroupRateMultipliers
,
updateSortOrder
updateSortOrder
,
getUsageSummary
,
getCapacitySummary
}
}
export
default
groupsAPI
export
default
groupsAPI
frontend/src/components/common/GroupCapacityBadge.vue
0 → 100644
View file @
e6326b29
<
template
>
<div
class=
"flex flex-col gap-1"
>
<!-- 并发槽位 -->
<div
class=
"flex items-center gap-1"
>
<span
:class=
"[
'inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] font-medium',
capacityClass(concurrencyUsed, concurrencyMax)
]"
>
<svg
class=
"h-2.5 w-2.5"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
stroke-width=
"2"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z"
/>
</svg>
<span
class=
"font-mono"
>
{{
concurrencyUsed
}}
</span>
<span
class=
"text-gray-400 dark:text-gray-500"
>
/
</span>
<span
class=
"font-mono"
>
{{
concurrencyMax
}}
</span>
</span>
</div>
<!-- 会话数 -->
<div
v-if=
"sessionsMax > 0"
class=
"flex items-center gap-1"
>
<span
:class=
"[
'inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] font-medium',
capacityClass(sessionsUsed, sessionsMax)
]"
>
<svg
class=
"h-2.5 w-2.5"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
stroke-width=
"2"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z"
/>
</svg>
<span
class=
"font-mono"
>
{{
sessionsUsed
}}
</span>
<span
class=
"text-gray-400 dark:text-gray-500"
>
/
</span>
<span
class=
"font-mono"
>
{{
sessionsMax
}}
</span>
</span>
</div>
<!-- RPM -->
<div
v-if=
"rpmMax > 0"
class=
"flex items-center gap-1"
>
<span
:class=
"[
'inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] font-medium',
capacityClass(rpmUsed, rpmMax)
]"
>
<svg
class=
"h-2.5 w-2.5"
fill=
"none"
viewBox=
"0 0 24 24"
stroke-width=
"1.5"
stroke=
"currentColor"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
<span
class=
"font-mono"
>
{{
rpmUsed
}}
</span>
<span
class=
"text-gray-400 dark:text-gray-500"
>
/
</span>
<span
class=
"font-mono"
>
{{
rpmMax
}}
</span>
</span>
</div>
</div>
</
template
>
<
script
setup
lang=
"ts"
>
interface
Props
{
concurrencyUsed
:
number
concurrencyMax
:
number
sessionsUsed
:
number
sessionsMax
:
number
rpmUsed
:
number
rpmMax
:
number
}
withDefaults
(
defineProps
<
Props
>
(),
{
concurrencyUsed
:
0
,
concurrencyMax
:
0
,
sessionsUsed
:
0
,
sessionsMax
:
0
,
rpmUsed
:
0
,
rpmMax
:
0
})
function
capacityClass
(
used
:
number
,
max
:
number
):
string
{
if
(
max
>
0
&&
used
>=
max
)
{
return
'
bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400
'
}
if
(
used
>
0
)
{
return
'
bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400
'
}
return
'
bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400
'
}
</
script
>
frontend/src/i18n/locales/en.ts
View file @
e6326b29
...
@@ -1505,6 +1505,8 @@ export default {
...
@@ -1505,6 +1505,8 @@ export default {
rateMultiplier
:
'
Rate Multiplier
'
,
rateMultiplier
:
'
Rate Multiplier
'
,
type
:
'
Type
'
,
type
:
'
Type
'
,
accounts
:
'
Accounts
'
,
accounts
:
'
Accounts
'
,
capacity
:
'
Capacity
'
,
usage
:
'
Usage
'
,
status
:
'
Status
'
,
status
:
'
Status
'
,
actions
:
'
Actions
'
,
actions
:
'
Actions
'
,
billingType
:
'
Billing Type
'
,
billingType
:
'
Billing Type
'
,
...
@@ -1513,6 +1515,12 @@ export default {
...
@@ -1513,6 +1515,12 @@ export default {
userNotes
:
'
Notes
'
,
userNotes
:
'
Notes
'
,
userStatus
:
'
Status
'
userStatus
:
'
Status
'
},
},
usageToday
:
'
Today
'
,
usageTotal
:
'
Total
'
,
accountsAvailable
:
'
Avail:
'
,
accountsRateLimited
:
'
Limited:
'
,
accountsTotal
:
'
Total:
'
,
accountsUnit
:
''
,
rateAndAccounts
:
'
{rate}x rate · {count} accounts
'
,
rateAndAccounts
:
'
{rate}x rate · {count} accounts
'
,
accountsCount
:
'
{count} accounts
'
,
accountsCount
:
'
{count} accounts
'
,
form
:
{
form
:
{
...
...
frontend/src/i18n/locales/zh.ts
View file @
e6326b29
...
@@ -1561,6 +1561,8 @@ export default {
...
@@ -1561,6 +1561,8 @@ export default {
priority
:
'
优先级
'
,
priority
:
'
优先级
'
,
apiKeys
:
'
API 密钥数
'
,
apiKeys
:
'
API 密钥数
'
,
accounts
:
'
账号数
'
,
accounts
:
'
账号数
'
,
capacity
:
'
容量
'
,
usage
:
'
用量
'
,
status
:
'
状态
'
,
status
:
'
状态
'
,
actions
:
'
操作
'
,
actions
:
'
操作
'
,
billingType
:
'
计费类型
'
,
billingType
:
'
计费类型
'
,
...
@@ -1569,6 +1571,12 @@ export default {
...
@@ -1569,6 +1571,12 @@ export default {
userNotes
:
'
备注
'
,
userNotes
:
'
备注
'
,
userStatus
:
'
状态
'
userStatus
:
'
状态
'
},
},
usageToday
:
'
今日
'
,
usageTotal
:
'
累计
'
,
accountsAvailable
:
'
可用:
'
,
accountsRateLimited
:
'
限流:
'
,
accountsTotal
:
'
总量:
'
,
accountsUnit
:
'
个账号
'
,
form
:
{
form
:
{
name
:
'
名称
'
,
name
:
'
名称
'
,
description
:
'
描述
'
,
description
:
'
描述
'
,
...
...
frontend/src/types/index.ts
View file @
e6326b29
...
@@ -411,6 +411,8 @@ export interface AdminGroup extends Group {
...
@@ -411,6 +411,8 @@ export interface AdminGroup extends Group {
// 分组下账号数量(仅管理员可见)
// 分组下账号数量(仅管理员可见)
account_count
?:
number
account_count
?:
number
active_account_count
?:
number
rate_limited_account_count
?:
number
// OpenAI Messages 调度配置(仅 openai 平台使用)
// OpenAI Messages 调度配置(仅 openai 平台使用)
default_mapped_model
?:
string
default_mapped_model
?:
string
...
...
frontend/src/views/admin/GroupsView.vue
View file @
e6326b29
...
@@ -158,12 +158,51 @@
...
@@ -158,12 +158,51 @@
</span>
</span>
</
template
>
</
template
>
<
template
#cell-account_count=
"{ value }"
>
<
template
#cell-account_count=
"{ row }"
>
<span
<div
class=
"space-y-0.5 text-xs"
>
class=
"inline-flex items-center rounded bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-800 dark:bg-dark-600 dark:text-gray-300"
<div>
>
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.groups.accountsAvailable
'
)
}}
</span>
{{
t
(
'
admin.groups.accountsCount
'
,
{
count
:
value
||
0
}
)
}}
<span
class=
"ml-1 font-medium text-emerald-600 dark:text-emerald-400"
>
{{
(
row
.
active_account_count
||
0
)
-
(
row
.
rate_limited_account_count
||
0
)
}}
</span>
<
/span
>
<span
class=
"ml-1 inline-flex items-center rounded bg-gray-100 px-1.5 py-0.5 font-medium text-gray-800 dark:bg-dark-600 dark:text-gray-300"
>
{{
t
(
'
admin.groups.accountsUnit
'
)
}}
</span>
</div>
<div
v-if=
"row.rate_limited_account_count"
>
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.groups.accountsRateLimited
'
)
}}
</span>
<span
class=
"ml-1 font-medium text-amber-600 dark:text-amber-400"
>
{{
row
.
rate_limited_account_count
}}
</span>
<span
class=
"ml-1 inline-flex items-center rounded bg-gray-100 px-1.5 py-0.5 font-medium text-gray-800 dark:bg-dark-600 dark:text-gray-300"
>
{{
t
(
'
admin.groups.accountsUnit
'
)
}}
</span>
</div>
<div>
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.groups.accountsTotal
'
)
}}
</span>
<span
class=
"ml-1 font-medium text-gray-700 dark:text-gray-300"
>
{{
row
.
account_count
||
0
}}
</span>
<span
class=
"ml-1 inline-flex items-center rounded bg-gray-100 px-1.5 py-0.5 font-medium text-gray-800 dark:bg-dark-600 dark:text-gray-300"
>
{{
t
(
'
admin.groups.accountsUnit
'
)
}}
</span>
</div>
</div>
</
template
>
<
template
#cell-capacity=
"{ row }"
>
<GroupCapacityBadge
v-if=
"capacityMap.get(row.id)"
:concurrency-used=
"capacityMap.get(row.id)!.concurrencyUsed"
:concurrency-max=
"capacityMap.get(row.id)!.concurrencyMax"
:sessions-used=
"capacityMap.get(row.id)!.sessionsUsed"
:sessions-max=
"capacityMap.get(row.id)!.sessionsMax"
:rpm-used=
"capacityMap.get(row.id)!.rpmUsed"
:rpm-max=
"capacityMap.get(row.id)!.rpmMax"
/>
<span
v-else
class=
"text-xs text-gray-400"
>
—
</span>
</
template
>
<
template
#cell-usage=
"{ row }"
>
<div
v-if=
"usageLoading"
class=
"text-xs text-gray-400"
>
—
</div>
<div
v-else
class=
"space-y-0.5 text-xs"
>
<div
class=
"text-gray-500 dark:text-gray-400"
>
<span
class=
"text-gray-400 dark:text-gray-500"
>
{{
t
(
'
admin.groups.usageToday
'
)
}}
</span>
<span
class=
"ml-1 font-medium text-gray-700 dark:text-gray-300"
>
$
{{
formatCost
(
usageMap
.
get
(
row
.
id
)?.
today_cost
??
0
)
}}
</span>
</div>
<div
class=
"text-gray-500 dark:text-gray-400"
>
<span
class=
"text-gray-400 dark:text-gray-500"
>
{{
t
(
'
admin.groups.usageTotal
'
)
}}
</span>
<span
class=
"ml-1 font-medium text-gray-700 dark:text-gray-300"
>
$
{{
formatCost
(
usageMap
.
get
(
row
.
id
)?.
total_cost
??
0
)
}}
</span>
</div>
</div>
</
template
>
</
template
>
<
template
#cell-status=
"{ value }"
>
<
template
#cell-status=
"{ value }"
>
...
@@ -1812,6 +1851,7 @@ import Select from '@/components/common/Select.vue'
...
@@ -1812,6 +1851,7 @@ import Select from '@/components/common/Select.vue'
import
PlatformIcon
from
'
@/components/common/PlatformIcon.vue
'
import
PlatformIcon
from
'
@/components/common/PlatformIcon.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
GroupRateMultipliersModal
from
'
@/components/admin/group/GroupRateMultipliersModal.vue
'
import
GroupRateMultipliersModal
from
'
@/components/admin/group/GroupRateMultipliersModal.vue
'
import
GroupCapacityBadge
from
'
@/components/common/GroupCapacityBadge.vue
'
import
{
VueDraggable
}
from
'
vue-draggable-plus
'
import
{
VueDraggable
}
from
'
vue-draggable-plus
'
import
{
createStableObjectKeyResolver
}
from
'
@/utils/stableObjectKey
'
import
{
createStableObjectKeyResolver
}
from
'
@/utils/stableObjectKey
'
import
{
useKeyedDebouncedSearch
}
from
'
@/composables/useKeyedDebouncedSearch
'
import
{
useKeyedDebouncedSearch
}
from
'
@/composables/useKeyedDebouncedSearch
'
...
@@ -1827,6 +1867,8 @@ const columns = computed<Column[]>(() => [
...
@@ -1827,6 +1867,8 @@ const columns = computed<Column[]>(() => [
{
key
:
'
rate_multiplier
'
,
label
:
t
(
'
admin.groups.columns.rateMultiplier
'
),
sortable
:
true
},
{
key
:
'
rate_multiplier
'
,
label
:
t
(
'
admin.groups.columns.rateMultiplier
'
),
sortable
:
true
},
{
key
:
'
is_exclusive
'
,
label
:
t
(
'
admin.groups.columns.type
'
),
sortable
:
true
},
{
key
:
'
is_exclusive
'
,
label
:
t
(
'
admin.groups.columns.type
'
),
sortable
:
true
},
{
key
:
'
account_count
'
,
label
:
t
(
'
admin.groups.columns.accounts
'
),
sortable
:
true
},
{
key
:
'
account_count
'
,
label
:
t
(
'
admin.groups.columns.accounts
'
),
sortable
:
true
},
{
key
:
'
capacity
'
,
label
:
t
(
'
admin.groups.columns.capacity
'
),
sortable
:
false
},
{
key
:
'
usage
'
,
label
:
t
(
'
admin.groups.columns.usage
'
),
sortable
:
false
},
{
key
:
'
status
'
,
label
:
t
(
'
admin.groups.columns.status
'
),
sortable
:
true
},
{
key
:
'
status
'
,
label
:
t
(
'
admin.groups.columns.status
'
),
sortable
:
true
},
{
key
:
'
actions
'
,
label
:
t
(
'
admin.groups.columns.actions
'
),
sortable
:
false
}
{
key
:
'
actions
'
,
label
:
t
(
'
admin.groups.columns.actions
'
),
sortable
:
false
}
])
])
...
@@ -1963,6 +2005,9 @@ const copyAccountsGroupOptionsForEdit = computed(() => {
...
@@ -1963,6 +2005,9 @@ const copyAccountsGroupOptionsForEdit = computed(() => {
const
groups
=
ref
<
AdminGroup
[]
>
([])
const
groups
=
ref
<
AdminGroup
[]
>
([])
const
loading
=
ref
(
false
)
const
loading
=
ref
(
false
)
const
usageMap
=
ref
<
Map
<
number
,
{
today_cost
:
number
;
total_cost
:
number
}
>>
(
new
Map
())
const
usageLoading
=
ref
(
false
)
const
capacityMap
=
ref
<
Map
<
number
,
{
concurrencyUsed
:
number
;
concurrencyMax
:
number
;
sessionsUsed
:
number
;
sessionsMax
:
number
;
rpmUsed
:
number
;
rpmMax
:
number
}
>>
(
new
Map
())
const
searchQuery
=
ref
(
''
)
const
searchQuery
=
ref
(
''
)
const
filters
=
reactive
({
const
filters
=
reactive
({
platform
:
''
,
platform
:
''
,
...
@@ -2301,6 +2346,8 @@ const loadGroups = async () => {
...
@@ -2301,6 +2346,8 @@ const loadGroups = async () => {
groups
.
value
=
response
.
items
groups
.
value
=
response
.
items
pagination
.
total
=
response
.
total
pagination
.
total
=
response
.
total
pagination
.
pages
=
response
.
pages
pagination
.
pages
=
response
.
pages
loadUsageSummary
()
loadCapacitySummary
()
}
catch
(
error
:
any
)
{
}
catch
(
error
:
any
)
{
if
(
signal
.
aborted
||
error
?.
name
===
'
AbortError
'
||
error
?.
code
===
'
ERR_CANCELED
'
)
{
if
(
signal
.
aborted
||
error
?.
name
===
'
AbortError
'
||
error
?.
code
===
'
ERR_CANCELED
'
)
{
return
return
...
@@ -2314,6 +2361,49 @@ const loadGroups = async () => {
...
@@ -2314,6 +2361,49 @@ const loadGroups = async () => {
}
}
}
}
const
formatCost
=
(
cost
:
number
):
string
=>
{
if
(
cost
>=
1000
)
return
cost
.
toFixed
(
0
)
if
(
cost
>=
100
)
return
cost
.
toFixed
(
1
)
return
cost
.
toFixed
(
2
)
}
const
loadUsageSummary
=
async
()
=>
{
usageLoading
.
value
=
true
try
{
const
tz
=
Intl
.
DateTimeFormat
().
resolvedOptions
().
timeZone
const
data
=
await
adminAPI
.
groups
.
getUsageSummary
(
tz
)
const
map
=
new
Map
<
number
,
{
today_cost
:
number
;
total_cost
:
number
}
>
()
for
(
const
item
of
data
)
{
map
.
set
(
item
.
group_id
,
{
today_cost
:
item
.
today_cost
,
total_cost
:
item
.
total_cost
})
}
usageMap
.
value
=
map
}
catch
(
error
)
{
console
.
error
(
'
Error loading group usage summary:
'
,
error
)
}
finally
{
usageLoading
.
value
=
false
}
}
const
loadCapacitySummary
=
async
()
=>
{
try
{
const
data
=
await
adminAPI
.
groups
.
getCapacitySummary
()
const
map
=
new
Map
<
number
,
{
concurrencyUsed
:
number
;
concurrencyMax
:
number
;
sessionsUsed
:
number
;
sessionsMax
:
number
;
rpmUsed
:
number
;
rpmMax
:
number
}
>
()
for
(
const
item
of
data
)
{
map
.
set
(
item
.
group_id
,
{
concurrencyUsed
:
item
.
concurrency_used
,
concurrencyMax
:
item
.
concurrency_max
,
sessionsUsed
:
item
.
sessions_used
,
sessionsMax
:
item
.
sessions_max
,
rpmUsed
:
item
.
rpm_used
,
rpmMax
:
item
.
rpm_max
})
}
capacityMap
.
value
=
map
}
catch
(
error
)
{
console
.
error
(
'
Error loading group capacity summary:
'
,
error
)
}
}
let
searchTimeout
:
ReturnType
<
typeof
setTimeout
>
let
searchTimeout
:
ReturnType
<
typeof
setTimeout
>
const
handleSearch
=
()
=>
{
const
handleSearch
=
()
=>
{
clearTimeout
(
searchTimeout
)
clearTimeout
(
searchTimeout
)
...
...
Prev
1
2
Next
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