mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-16 06:53:30 +08:00
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:
@@ -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>
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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: '黄区允许的额外客户端数量(降级优先级调度)',
|
||||
|
||||
Reference in New Issue
Block a user