refactor: unify QuotaLimitCard and AffinityConfigCard under single section

Move both cards under a unified "配额限制" section wrapper for Anthropic
platform. Remove independent section wrappers from both components so
they render as plain rounded cards. Non-Anthropic apikey accounts get
their own quota section with just QuotaLimitCard.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
erio
2026-03-07 18:07:25 +08:00
co-authored by Claude Opus 4.6
parent 146f0baebc
commit 0381414ca3
6 changed files with 136 additions and 77 deletions
@@ -30,14 +30,29 @@ watch(localEnabled, (val) => {
}
})
// Green zone: toggle + input
const baseLimitEnabled = ref(props.base != null && props.base > 0)
watch(() => props.base, (val) => {
baseLimitEnabled.value = val != null && val > 0
})
const toggleBaseLimit = () => {
baseLimitEnabled.value = !baseLimitEnabled.value
if (baseLimitEnabled.value) {
emit('update:base', 5) // default base
} else {
emit('update:base', null)
emit('update:buffer', null)
}
}
const onBaseInput = (e: Event) => {
const raw = (e.target as HTMLInputElement).valueAsNumber
emit('update:base', Number.isNaN(raw) ? null : Math.max(1, Math.floor(raw)))
}
const hasBaseLimit = computed(() => props.base != null && props.base > 0)
// buffer: empty = null (infinite yellow), 0 = no yellow, >0 = yellow range
// Yellow zone: "unlimited" checkbox + input
const bufferIsInfinite = ref(props.buffer === null || props.buffer === undefined)
watch(() => props.buffer, (val) => {
@@ -80,16 +95,8 @@ const zonePreview = computed(() => {
</script>
<template>
<div class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4">
<div class="mb-3">
<h3 class="input-label mb-0 text-base font-semibold">{{ t('admin.accounts.affinitySection') }}</h3>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.affinitySectionHint') }}
</p>
</div>
<div class="rounded-lg border border-gray-200 p-4 dark:border-dark-600">
<div class="mb-3 flex items-center justify-between">
<div class="rounded-lg border border-gray-200 p-4 dark:border-dark-600">
<div class="flex items-center justify-between" :class="{ 'mb-3': localEnabled }">
<div>
<label class="input-label mb-0">{{ t('admin.accounts.affinityToggle') }}</label>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
@@ -114,10 +121,28 @@ const zonePreview = computed(() => {
</div>
<div v-if="localEnabled" class="space-y-3">
<!-- Base limit (green zone) -->
<!-- Green zone toggle + input -->
<div>
<label class="input-label">{{ t('admin.accounts.affinityBase') }}</label>
<div class="flex items-center justify-between mb-1">
<label class="input-label mb-0">{{ t('admin.accounts.affinityBase') }}</label>
<button
type="button"
@click="toggleBaseLimit"
:class="[
'relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none',
baseLimitEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
]"
>
<span
:class="[
'pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
baseLimitEnabled ? 'translate-x-4' : 'translate-x-0'
]"
/>
</button>
</div>
<input
v-if="baseLimitEnabled"
:value="base"
@input="onBaseInput"
type="number"
@@ -126,11 +151,11 @@ const zonePreview = computed(() => {
class="input"
:placeholder="t('admin.accounts.affinityBasePlaceholder')"
/>
<p class="input-hint">{{ t('admin.accounts.affinityBaseHint') }}</p>
<p class="input-hint">{{ baseLimitEnabled ? t('admin.accounts.affinityBaseHint') : t('admin.accounts.affinityBaseOffHint') }}</p>
</div>
<!-- Buffer (yellow zone) - only shown when base is set -->
<div v-if="hasBaseLimit">
<div v-if="baseLimitEnabled">
<div class="flex items-center justify-between mb-1">
<label class="input-label mb-0">{{ t('admin.accounts.affinityBuffer') }}</label>
<label class="flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400 cursor-pointer">
@@ -157,7 +182,7 @@ const zonePreview = computed(() => {
</div>
<!-- Zone preview -->
<div v-if="zonePreview && base && base > 0" class="flex items-center gap-2 text-xs">
<div v-if="zonePreview" class="flex items-center gap-2 text-xs">
<span class="inline-flex items-center gap-1 rounded-full bg-emerald-100 px-2 py-0.5 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400">
{{ zonePreview.green }}
</span>
@@ -169,6 +194,5 @@ const zonePreview = computed(() => {
</span>
</div>
</div>
</div>
</div>
</template>
@@ -1227,8 +1227,41 @@
</div>
<!-- API Key 账号配额限制 -->
<QuotaLimitCard v-if="form.type === 'apikey'" :totalLimit="editQuotaLimit" :dailyLimit="editQuotaDailyLimit" :weeklyLimit="editQuotaWeeklyLimit" @update:totalLimit="editQuotaLimit = $event" @update:dailyLimit="editQuotaDailyLimit = $event" @update:weeklyLimit="editQuotaWeeklyLimit = $event" />
<!-- 配额限制 (Anthropic 平台: 配额 + 亲和) -->
<div
v-if="form.platform === 'anthropic'"
class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4"
>
<div class="mb-3">
<h3 class="input-label mb-0 text-base font-semibold">{{ t('admin.accounts.quotaLimit') }}</h3>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.quotaLimitHint') }}
</p>
</div>
<QuotaLimitCard v-if="form.type === 'apikey'" :totalLimit="editQuotaLimit" :dailyLimit="editQuotaDailyLimit" :weeklyLimit="editQuotaWeeklyLimit" @update:totalLimit="editQuotaLimit = $event" @update:dailyLimit="editQuotaDailyLimit = $event" @update:weeklyLimit="editQuotaWeeklyLimit = $event" />
<AffinityConfigCard
:enabled="clientAffinityEnabled"
:base="affinityBase"
:buffer="affinityBuffer"
@update:enabled="clientAffinityEnabled = $event"
@update:base="affinityBase = $event"
@update:buffer="affinityBuffer = $event"
/>
</div>
<!-- 配额限制 (非 Anthropic apikey) -->
<div
v-else-if="form.type === 'apikey'"
class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4"
>
<div class="mb-3">
<h3 class="input-label mb-0 text-base font-semibold">{{ t('admin.accounts.quotaLimit') }}</h3>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.quotaLimitHint') }}
</p>
</div>
<QuotaLimitCard :totalLimit="editQuotaLimit" :dailyLimit="editQuotaDailyLimit" :weeklyLimit="editQuotaWeeklyLimit" @update:totalLimit="editQuotaLimit = $event" @update:dailyLimit="editQuotaDailyLimit = $event" @update:weeklyLimit="editQuotaWeeklyLimit = $event" />
</div>
<!-- OpenAI OAuth Model Mapping (OAuth 类型没有 apikey 容器,需要独立的模型映射区域) -->
<div
@@ -1546,36 +1579,6 @@
</div>
</div>
<!-- Client Affinity (Anthropic accounts only) -->
<div
v-if="form.platform === 'anthropic'"
class="border-t border-gray-200 pt-4 dark:border-dark-600"
>
<div class="flex items-center justify-between">
<div>
<label class="input-label mb-0">{{ t('admin.accounts.quotaControl.clientAffinity.label') }}</label>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.quotaControl.clientAffinity.hint') }}
</p>
</div>
<button
type="button"
@click="clientAffinityEnabled = !clientAffinityEnabled"
:class="[
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
clientAffinityEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
]"
>
<span
:class="[
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
clientAffinityEnabled ? 'translate-x-5' : 'translate-x-0'
]"
/>
</button>
</div>
</div>
<!-- Quota Control Section (Anthropic OAuth/SetupToken only) -->
<div
v-if="form.platform === 'anthropic' && accountCategory === 'oauth-based'"
@@ -2515,6 +2518,7 @@ import ProxySelector from '@/components/common/ProxySelector.vue'
import GroupSelector from '@/components/common/GroupSelector.vue'
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
import QuotaLimitCard from '@/components/account/QuotaLimitCard.vue'
import AffinityConfigCard from '@/components/account/AffinityConfigCard.vue'
import { applyInterceptWarmup } from '@/components/account/credentialsBuilder'
import { formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
import { createStableObjectKeyResolver } from '@/utils/stableObjectKey'
@@ -2683,6 +2687,8 @@ const showGeminiHelpDialog = ref(false)
// Client affinity (all Anthropic accounts)
const clientAffinityEnabled = ref(false)
const affinityBase = ref<number | null>(null)
const affinityBuffer = ref<number | null>(null)
// Quota control state (Anthropic OAuth/SetupToken only)
const windowCostEnabled = ref(false)
@@ -3310,6 +3316,8 @@ const resetForm = () => {
editQuotaDailyLimit.value = null
editQuotaWeeklyLimit.value = null
clientAffinityEnabled.value = false
affinityBase.value = null
affinityBuffer.value = null
modelMappings.value = []
modelRestrictionMode.value = 'whitelist'
allowedModels.value = [...claudeModels] // Default fill related models
@@ -3422,8 +3430,20 @@ const buildAnthropicExtra = (base?: Record<string, unknown>): Record<string, unk
const applyClientAffinity = (extra: Record<string, unknown>) => {
if (clientAffinityEnabled.value) {
extra.client_affinity_enabled = true
if (affinityBase.value != null && affinityBase.value > 0) {
extra.affinity_base = affinityBase.value
} else {
delete extra.affinity_base
}
if (affinityBase.value != null && affinityBase.value > 0 && affinityBuffer.value != null) {
extra.affinity_buffer = affinityBuffer.value
} else {
delete extra.affinity_buffer
}
} else {
delete extra.client_affinity_enabled
delete extra.affinity_base
delete extra.affinity_buffer
}
}
@@ -904,19 +904,41 @@
</div>
</div>
<!-- API Key 账号配额限制 -->
<QuotaLimitCard v-if="account?.type === 'apikey'" :totalLimit="editQuotaLimit" :dailyLimit="editQuotaDailyLimit" :weeklyLimit="editQuotaWeeklyLimit" @update:totalLimit="editQuotaLimit = $event" @update:dailyLimit="editQuotaDailyLimit = $event" @update:weeklyLimit="editQuotaWeeklyLimit = $event" />
<!-- 客户端亲和限制 (all Anthropic account types) -->
<AffinityConfigCard
<!-- 配额限制 (Anthropic 平台: 配额 + 亲和) -->
<div
v-if="account?.platform === 'anthropic'"
:enabled="clientAffinityEnabled"
:base="affinityBase"
:buffer="affinityBuffer"
@update:enabled="clientAffinityEnabled = $event"
@update:base="affinityBase = $event"
@update:buffer="affinityBuffer = $event"
/>
class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4"
>
<div class="mb-3">
<h3 class="input-label mb-0 text-base font-semibold">{{ t('admin.accounts.quotaLimit') }}</h3>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.quotaLimitHint') }}
</p>
</div>
<QuotaLimitCard v-if="account?.type === 'apikey'" :totalLimit="editQuotaLimit" :dailyLimit="editQuotaDailyLimit" :weeklyLimit="editQuotaWeeklyLimit" @update:totalLimit="editQuotaLimit = $event" @update:dailyLimit="editQuotaDailyLimit = $event" @update:weeklyLimit="editQuotaWeeklyLimit = $event" />
<AffinityConfigCard
:enabled="clientAffinityEnabled"
:base="affinityBase"
:buffer="affinityBuffer"
@update:enabled="clientAffinityEnabled = $event"
@update:base="affinityBase = $event"
@update:buffer="affinityBuffer = $event"
/>
</div>
<!-- 配额限制 (非 Anthropic apikey) -->
<div
v-else-if="account?.type === 'apikey'"
class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4"
>
<div class="mb-3">
<h3 class="input-label mb-0 text-base font-semibold">{{ t('admin.accounts.quotaLimit') }}</h3>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.quotaLimitHint') }}
</p>
</div>
<QuotaLimitCard :totalLimit="editQuotaLimit" :dailyLimit="editQuotaDailyLimit" :weeklyLimit="editQuotaWeeklyLimit" @update:totalLimit="editQuotaLimit = $event" @update:dailyLimit="editQuotaDailyLimit = $event" @update:weeklyLimit="editQuotaWeeklyLimit = $event" />
</div>
<!-- OpenAI OAuth Codex 官方客户端限制开关 -->
<div
@@ -53,15 +53,7 @@ const onWeeklyInput = (e: Event) => {
</script>
<template>
<div class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4">
<div class="mb-3">
<h3 class="input-label mb-0 text-base font-semibold">{{ t('admin.accounts.quotaLimit') }}</h3>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.quotaLimitHint') }}
</p>
</div>
<div class="rounded-lg border border-gray-200 p-4 dark:border-dark-600">
<div class="rounded-lg border border-gray-200 p-4 dark:border-dark-600">
<div class="mb-3 flex items-center justify-between">
<div>
<label class="input-label mb-0">{{ t('admin.accounts.quotaLimitToggle') }}</label>
@@ -141,6 +133,5 @@ const onWeeklyInput = (e: Event) => {
<p class="input-hint">{{ t('admin.accounts.quotaTotalLimitHint') }}</p>
</div>
</div>
</div>
</div>
</template>
+3 -2
View File
@@ -1795,7 +1795,7 @@ export default {
resetQuota: 'Reset Quota',
quotaLimit: 'Quota Limit',
quotaLimitPlaceholder: '0 means unlimited',
quotaLimitHint: 'Set max spending limit (USD). Account will be paused when reached. Changing limit won\'t reset usage.',
quotaLimitHint: 'Configure account usage quota and client affinity limits.',
quotaLimitToggle: 'Enable Quota Limit',
quotaLimitToggleHint: 'When enabled, account will be paused when usage reaches the set limit',
quotaLimitAmount: 'Total Limit',
@@ -2026,7 +2026,8 @@ export default {
affinityToggleHint: 'New sessions prefer accounts previously used by this client',
affinityBase: 'Base Limit (Green Zone)',
affinityBasePlaceholder: 'Empty = no limit',
affinityBaseHint: 'Max clients in green zone (full priority). Leave empty for unlimited.',
affinityBaseHint: 'Max clients in green zone (full priority scheduling)',
affinityBaseOffHint: 'No green zone limit. All clients receive full priority scheduling.',
affinityBuffer: 'Buffer (Yellow Zone)',
affinityBufferPlaceholder: 'e.g. 3',
affinityBufferHint: 'Additional clients allowed in the yellow zone (degraded priority)',
+3 -2
View File
@@ -1794,7 +1794,7 @@ export default {
resetQuota: '重置配额',
quotaLimit: '配额限制',
quotaLimitPlaceholder: '0 表示不限制',
quotaLimitHint: '设置最大使用额度(美元),达到后账号暂停调度。修改限额不会重置已用额度。',
quotaLimitHint: '配置账号用量配额和客户端亲和限制。',
quotaLimitToggle: '启用配额限制',
quotaLimitToggleHint: '开启后,当账号用量达到设定额度时自动暂停调度',
quotaLimitAmount: '总限额',
@@ -2160,7 +2160,8 @@ export default {
affinityToggleHint: '新会话优先调度到该客户端之前使用过的账号',
affinityBase: '基础限额(绿区)',
affinityBasePlaceholder: '留空表示不限制',
affinityBaseHint: '绿区最大客户端数量(完整优先级调度),留空表示不限制',
affinityBaseHint: '绿区最大客户端数量(完整优先级调度)',
affinityBaseOffHint: '未开启绿区限制,所有客户端均享受完整优先级调度',
affinityBuffer: '缓冲区(黄区)',
affinityBufferPlaceholder: '例如 3',
affinityBufferHint: '黄区允许的额外客户端数量(降级优先级调度)',