mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-03 06:52:13 +08:00
feat(admin): 分组管理列表新增用量列与账号数分类
分组管理列表增强: 1. 今日/累计用量列: - 新增独立端点 GET /admin/groups/usage-summary - 一次查询返回所有分组的今日费用和累计费用(actual_cost) - 前端异步加载后合并显示在分组列表中 2. 账号数区分可用/限流/总量: - 将账号数列从单一总量改为 badge 内多行展示 - 可用: active + schedulable 的账号数(绿色) - 限流: rate_limit/overload/temp_unschedulable 的账号数(橙色,无限流时隐藏) - 总量: 全部关联账号数 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -218,6 +218,22 @@ export async function batchSetGroupRateMultipliers(
|
||||
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
|
||||
}
|
||||
|
||||
export const groupsAPI = {
|
||||
list,
|
||||
getAll,
|
||||
@@ -232,7 +248,8 @@ export const groupsAPI = {
|
||||
getGroupRateMultipliers,
|
||||
clearGroupRateMultipliers,
|
||||
batchSetGroupRateMultipliers,
|
||||
updateSortOrder
|
||||
updateSortOrder,
|
||||
getUsageSummary
|
||||
}
|
||||
|
||||
export default groupsAPI
|
||||
|
||||
@@ -1505,6 +1505,7 @@ export default {
|
||||
rateMultiplier: 'Rate Multiplier',
|
||||
type: 'Type',
|
||||
accounts: 'Accounts',
|
||||
usage: 'Usage',
|
||||
status: 'Status',
|
||||
actions: 'Actions',
|
||||
billingType: 'Billing Type',
|
||||
@@ -1513,6 +1514,12 @@ export default {
|
||||
userNotes: 'Notes',
|
||||
userStatus: 'Status'
|
||||
},
|
||||
usageToday: 'Today',
|
||||
usageTotal: 'Total',
|
||||
accountsAvailable: 'Avail:',
|
||||
accountsRateLimited: 'Limited:',
|
||||
accountsTotal: 'Total:',
|
||||
accountsUnit: '',
|
||||
rateAndAccounts: '{rate}x rate · {count} accounts',
|
||||
accountsCount: '{count} accounts',
|
||||
form: {
|
||||
|
||||
@@ -1561,6 +1561,7 @@ export default {
|
||||
priority: '优先级',
|
||||
apiKeys: 'API 密钥数',
|
||||
accounts: '账号数',
|
||||
usage: '用量',
|
||||
status: '状态',
|
||||
actions: '操作',
|
||||
billingType: '计费类型',
|
||||
@@ -1569,6 +1570,12 @@ export default {
|
||||
userNotes: '备注',
|
||||
userStatus: '状态'
|
||||
},
|
||||
usageToday: '今日',
|
||||
usageTotal: '累计',
|
||||
accountsAvailable: '可用:',
|
||||
accountsRateLimited: '限流:',
|
||||
accountsTotal: '总量:',
|
||||
accountsUnit: '个账号',
|
||||
form: {
|
||||
name: '名称',
|
||||
description: '描述',
|
||||
|
||||
@@ -411,6 +411,8 @@ export interface AdminGroup extends Group {
|
||||
|
||||
// 分组下账号数量(仅管理员可见)
|
||||
account_count?: number
|
||||
active_account_count?: number
|
||||
rate_limited_account_count?: number
|
||||
|
||||
// OpenAI Messages 调度配置(仅 openai 平台使用)
|
||||
default_mapped_model?: string
|
||||
|
||||
@@ -158,12 +158,38 @@
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #cell-account_count="{ value }">
|
||||
<span
|
||||
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"
|
||||
>
|
||||
{{ t('admin.groups.accountsCount', { count: value || 0 }) }}
|
||||
</span>
|
||||
<template #cell-account_count="{ row }">
|
||||
<div class="space-y-0.5 text-xs">
|
||||
<div>
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ t('admin.groups.accountsAvailable') }}</span>
|
||||
<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 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-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 #cell-status="{ value }">
|
||||
@@ -1827,6 +1853,7 @@ const columns = computed<Column[]>(() => [
|
||||
{ key: 'rate_multiplier', label: t('admin.groups.columns.rateMultiplier'), 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: 'usage', label: t('admin.groups.columns.usage'), sortable: false },
|
||||
{ key: 'status', label: t('admin.groups.columns.status'), sortable: true },
|
||||
{ key: 'actions', label: t('admin.groups.columns.actions'), sortable: false }
|
||||
])
|
||||
@@ -1963,6 +1990,8 @@ const copyAccountsGroupOptionsForEdit = computed(() => {
|
||||
|
||||
const groups = ref<AdminGroup[]>([])
|
||||
const loading = ref(false)
|
||||
const usageMap = ref<Map<number, { today_cost: number; total_cost: number }>>(new Map())
|
||||
const usageLoading = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const filters = reactive({
|
||||
platform: '',
|
||||
@@ -2301,6 +2330,7 @@ const loadGroups = async () => {
|
||||
groups.value = response.items
|
||||
pagination.total = response.total
|
||||
pagination.pages = response.pages
|
||||
loadUsageSummary()
|
||||
} catch (error: any) {
|
||||
if (signal.aborted || error?.name === 'AbortError' || error?.code === 'ERR_CANCELED') {
|
||||
return
|
||||
@@ -2314,6 +2344,29 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
let searchTimeout: ReturnType<typeof setTimeout>
|
||||
const handleSearch = () => {
|
||||
clearTimeout(searchTimeout)
|
||||
|
||||
Reference in New Issue
Block a user