Merge pull request #195 from AAEE86/keys

feat(pool): 新增账号配额展示并清理 Codex 旧限额字段
This commit is contained in:
fawney19
2026-03-01 00:59:49 +08:00
committed by GitHub
11 changed files with 349 additions and 68 deletions

View File

@@ -78,6 +78,7 @@ export interface PoolKeyDetail {
key_name: string
is_active: boolean
auth_type: string
account_quota: string | null
cooldown_reason: string | null
cooldown_ttl_seconds: number | null
cost_window_usage: number

View File

@@ -284,10 +284,6 @@ export interface CodexUpstreamMetadata {
secondary_reset_seconds?: number // 5H限额重置剩余秒数
secondary_reset_at?: number // 5H限额重置时间Unix 时间戳)
secondary_window_minutes?: number // 5H限额窗口大小分钟
code_review_used_percent?: number // 代码审查限额使用百分比
code_review_reset_seconds?: number // 代码审查限额重置剩余秒数
code_review_reset_at?: number // 代码审查限额重置时间Unix 时间戳)
code_review_window_minutes?: number // 代码审查限额窗口大小(分钟)
has_credits?: boolean // 是否有积分
credits_balance?: number // 积分余额
}

View File

@@ -544,10 +544,10 @@
</span>
</div>
</div>
<!-- 限额并排显示Team/Plus/Enterprise 账号 3列, Free 账号 2列 -->
<!-- 限额并排显示Team/Plus/Enterprise 账号 2列, Free 账号 1列 -->
<div
class="grid gap-3"
:class="isCodexTeamPlan(key) ? 'grid-cols-3' : 'grid-cols-2'"
:class="isCodexTeamPlan(key) ? 'grid-cols-2' : 'grid-cols-1'"
>
<!-- 周限额 -->
<div v-if="key.upstream_metadata.codex?.primary_used_percent !== undefined">
@@ -595,28 +595,6 @@
</template>
</div>
</div>
<!-- 代码审查限额 -->
<div v-if="key.upstream_metadata.codex?.code_review_used_percent !== undefined">
<div class="flex items-center justify-between text-[10px] mb-0.5">
<span class="text-muted-foreground">审查限额</span>
<span :class="getQuotaRemainingClass(key.upstream_metadata.codex.code_review_used_percent)">
{{ (100 - key.upstream_metadata.codex.code_review_used_percent).toFixed(1) }}%
</span>
</div>
<div class="relative w-full h-1.5 bg-border rounded-full overflow-hidden">
<div
class="absolute left-0 top-0 h-full transition-all duration-300"
:class="getQuotaRemainingBarColor(key.upstream_metadata.codex.code_review_used_percent)"
:style="{ width: `${Math.max(100 - key.upstream_metadata.codex.code_review_used_percent, 0)}%` }"
/>
</div>
<div
v-if="key.upstream_metadata.codex.code_review_reset_seconds"
class="text-[9px] text-muted-foreground/70 mt-0.5"
>
{{ formatResetTime(key.upstream_metadata.codex.code_review_reset_seconds) }}后重置
</div>
</div>
</div>
</div>
<!-- Antigravity 上游额度摘要(按家族分组展示关键配额) -->

View File

@@ -290,6 +290,12 @@
<TableHead class="font-semibold">
名称
</TableHead>
<TableHead
v-if="showAccountQuotaColumn"
class="w-64 font-semibold"
>
配额
</TableHead>
<TableHead class="w-20 font-semibold">
状态
</TableHead>
@@ -330,6 +336,51 @@
{{ key.key_name || '未命名' }}
</span>
</TableCell>
<TableCell
v-if="showAccountQuotaColumn"
class="py-3"
>
<div
v-if="quotaProgressMap[key.key_id]?.length"
class="flex items-stretch gap-2 overflow-x-auto pb-0.5"
>
<div
v-for="(item, idx) in quotaProgressMap[key.key_id]"
:key="`${key.key_id}-quota-${idx}`"
class="min-w-[140px] max-w-[180px] shrink-0"
>
<div class="flex items-center justify-between text-[10px] mb-0.5">
<span class="text-muted-foreground">{{ item.label }}</span>
<span :class="getQuotaRemainingClassByRemaining(item.remainingPercent)">
{{ item.remainingPercent.toFixed(1) }}%
</span>
</div>
<div class="relative w-full h-1.5 bg-border rounded-full overflow-hidden">
<div
class="absolute left-0 top-0 h-full transition-all duration-300"
:class="getQuotaRemainingBarColorByRemaining(item.remainingPercent)"
:style="{ width: `${item.remainingPercent}%` }"
/>
</div>
<div
v-if="item.detail"
class="text-[9px] text-muted-foreground/70 mt-0.5 truncate"
>
{{ item.detail }}
</div>
</div>
</div>
<span
v-else-if="key.account_quota"
:class="getQuotaTextClass(key.account_quota)"
>
{{ key.account_quota }}
</span>
<span
v-else
class="text-xs text-muted-foreground"
>-</span>
</TableCell>
<TableCell class="py-3">
<Badge
:variant="key.is_active ? (key.cooldown_reason ? 'destructive' : 'default') : 'secondary'"
@@ -488,7 +539,10 @@
</Button>
</div>
</div>
<div class="mt-2.5 ml-7 grid grid-cols-3 gap-2">
<div
class="mt-2.5 ml-7 grid gap-2"
:class="showAccountQuotaColumn ? 'grid-cols-4' : 'grid-cols-3'"
>
<div class="p-2 bg-muted/50 rounded-lg text-xs">
<div class="text-muted-foreground mb-0.5">
冷却
@@ -543,6 +597,56 @@
{{ key.last_used_at ? formatRelativeTime(key.last_used_at) : '-' }}
</div>
</div>
<div
v-if="showAccountQuotaColumn"
class="p-2 bg-muted/50 rounded-lg text-xs"
>
<div class="text-muted-foreground mb-0.5">
配额
</div>
<div
v-if="quotaProgressMap[key.key_id]?.length"
class="flex items-stretch gap-2 overflow-x-auto pb-0.5"
>
<div
v-for="(item, idx) in quotaProgressMap[key.key_id]"
:key="`${key.key_id}-quota-mobile-${idx}`"
class="min-w-[120px] max-w-[160px] shrink-0"
>
<div class="flex items-center justify-between text-[10px] mb-0.5">
<span class="text-muted-foreground">{{ item.label }}</span>
<span :class="getQuotaRemainingClassByRemaining(item.remainingPercent)">
{{ item.remainingPercent.toFixed(1) }}%
</span>
</div>
<div class="relative w-full h-1.5 bg-border rounded-full overflow-hidden">
<div
class="absolute left-0 top-0 h-full transition-all duration-300"
:class="getQuotaRemainingBarColorByRemaining(item.remainingPercent)"
:style="{ width: `${item.remainingPercent}%` }"
/>
</div>
<div
v-if="item.detail"
class="text-[9px] text-muted-foreground/70 mt-0.5 truncate"
>
{{ item.detail }}
</div>
</div>
</div>
<div
v-else-if="key.account_quota"
:class="getQuotaTextClass(key.account_quota)"
>
{{ key.account_quota }}
</div>
<div
v-else
class="text-muted-foreground"
>
-
</div>
</div>
</div>
</div>
</div>
@@ -682,6 +786,19 @@ const selectedProviderConfig = computed<PoolAdvancedConfig | null>(() => {
return (selectedProviderData.value as Record<string, unknown> | null)?.pool_advanced as PoolAdvancedConfig | null ?? null
})
const selectedProviderType = computed(() => {
const fromDetail = String(selectedProviderData.value?.provider_type || '').trim().toLowerCase()
if (fromDetail) return fromDetail
const fromOverview = poolProviders.value.find(item => item.provider_id === selectedProviderId.value)?.provider_type
return String(fromOverview || '').trim().toLowerCase()
})
const showAccountQuotaColumn = computed(() => {
return selectedProviderType.value === 'codex'
|| selectedProviderType.value === 'kiro'
|| selectedProviderType.value === 'antigravity'
})
async function selectProvider(id: string) {
selectedProviderId.value = id
selectedKeys.value.clear()
@@ -714,6 +831,20 @@ const statusFilter = ref('all')
const currentPage = ref(1)
const pageSize = ref(50)
interface QuotaProgressItem {
label: string
remainingPercent: number
detail?: string
}
const quotaProgressMap = computed<Record<string, QuotaProgressItem[]>>(() => {
const map: Record<string, QuotaProgressItem[]> = {}
for (const key of keyPage.value.keys) {
map[key.key_id] = parseQuotaProgressItems(key.account_quota)
}
return map
})
async function loadKeys() {
if (!selectedProviderId.value) return
keysLoading.value = true
@@ -840,6 +971,70 @@ function getCostBarColor(usage: number, limit: number): string {
return 'bg-green-500'
}
function normalizeQuotaLabel(label: string): string {
const normalized = label.trim()
if (!normalized) return '额度'
if (normalized.includes('周剩余')) return '周限额'
if (normalized.includes('5H剩余')) return '5H限额'
if (normalized.includes('最低剩余')) return '最低剩余'
if (normalized === '剩余') return '剩余额度'
return normalized
}
function clampPercent(value: number): number {
if (!Number.isFinite(value)) return 0
if (value < 0) return 0
if (value > 100) return 100
return value
}
function parseQuotaProgressItems(quotaText: string | null | undefined): QuotaProgressItem[] {
if (!quotaText) return []
const segments = quotaText
.split('|')
.map(s => s.trim())
.filter(Boolean)
const items: QuotaProgressItem[] = []
for (const segment of segments) {
const match = segment.match(/^(.*?)(-?\d+(?:\.\d+)?)%\s*(.*)$/)
if (!match) continue
const [, rawLabel, rawPercent, rawTail] = match
const remainingPercent = clampPercent(Number(rawPercent))
const label = normalizeQuotaLabel(rawLabel)
const detail = rawTail.trim().replace(/^[()]+|[()]+$/g, '').trim()
items.push({
label,
remainingPercent,
detail: detail || undefined,
})
}
return items
}
function getQuotaRemainingClassByRemaining(remaining: number): string {
if (remaining <= 10) return 'text-red-600 dark:text-red-400'
if (remaining <= 30) return 'text-yellow-600 dark:text-yellow-400'
return 'text-green-600 dark:text-green-400'
}
function getQuotaRemainingBarColorByRemaining(remaining: number): string {
if (remaining <= 10) return 'bg-red-500 dark:bg-red-400'
if (remaining <= 30) return 'bg-yellow-500 dark:bg-yellow-400'
return 'bg-green-500 dark:bg-green-400'
}
function getQuotaTextClass(quotaText: string): string {
if (quotaText.includes('封禁') || quotaText.includes('受限')) {
return 'text-[11px] text-destructive leading-4'
}
return 'text-[11px] text-foreground/90 leading-4'
}
function formatRelativeTime(isoStr: string): string {
const diff = (Date.now() - new Date(isoStr).getTime()) / 1000
if (diff < 60) return '刚刚'