mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
refactor: move affinity config to quota section with unified style
- AffinityConfigCard redesigned to match QuotaLimitCard style (section title + toggle card) - Moved from standalone section to quota area, visible for all Anthropic account types - Green zone (base) allows empty = no limit, min=1 when set; yellow zone hidden when base unset - Deduplicated affinity save logic in EditAccountModal (single block for all Anthropic types) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,8 @@ const onBaseInput = (e: Event) => {
|
||||
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
|
||||
const bufferIsInfinite = ref(props.buffer === null || props.buffer === undefined)
|
||||
|
||||
@@ -78,85 +80,94 @@ const zonePreview = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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.affinityToggle') }}</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.affinityToggleHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="localEnabled = !localEnabled"
|
||||
: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',
|
||||
localEnabled ? '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',
|
||||
localEnabled ? 'translate-x-5' : 'translate-x-0'
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
<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 v-if="localEnabled" class="space-y-3">
|
||||
<!-- Base limit (green zone) -->
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.affinityBase') }}</label>
|
||||
<input
|
||||
:value="base"
|
||||
@input="onBaseInput"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
class="input"
|
||||
:placeholder="t('admin.accounts.affinityBasePlaceholder')"
|
||||
/>
|
||||
<p class="input-hint">{{ t('admin.accounts.affinityBaseHint') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Buffer (yellow zone) -->
|
||||
<div>
|
||||
<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">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="bufferIsInfinite"
|
||||
@change="toggleBufferInfinite"
|
||||
class="h-3.5 w-3.5 rounded border-gray-300 text-primary-600"
|
||||
/>
|
||||
{{ t('admin.accounts.affinityBufferInfinite') }}
|
||||
</label>
|
||||
<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.affinityToggle') }}</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.affinityToggleHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
v-if="!bufferIsInfinite"
|
||||
:value="buffer"
|
||||
@input="onBufferInput"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
class="input"
|
||||
:placeholder="t('admin.accounts.affinityBufferPlaceholder')"
|
||||
/>
|
||||
<p class="input-hint">{{ t('admin.accounts.affinityBufferHint') }}</p>
|
||||
<button
|
||||
type="button"
|
||||
@click="localEnabled = !localEnabled"
|
||||
: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',
|
||||
localEnabled ? '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',
|
||||
localEnabled ? 'translate-x-5' : 'translate-x-0'
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Zone preview -->
|
||||
<div v-if="zonePreview && base && base > 0" 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>
|
||||
<span v-if="zonePreview.yellow" class="inline-flex items-center gap-1 rounded-full bg-yellow-100 px-2 py-0.5 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400">
|
||||
{{ zonePreview.yellow }}
|
||||
</span>
|
||||
<span v-if="zonePreview.red" class="inline-flex items-center gap-1 rounded-full bg-red-100 px-2 py-0.5 text-red-700 dark:bg-red-900/30 dark:text-red-400">
|
||||
{{ zonePreview.red }}
|
||||
</span>
|
||||
<div v-if="localEnabled" class="space-y-3">
|
||||
<!-- Base limit (green zone) -->
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.affinityBase') }}</label>
|
||||
<input
|
||||
:value="base"
|
||||
@input="onBaseInput"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
class="input"
|
||||
:placeholder="t('admin.accounts.affinityBasePlaceholder')"
|
||||
/>
|
||||
<p class="input-hint">{{ t('admin.accounts.affinityBaseHint') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Buffer (yellow zone) - only shown when base is set -->
|
||||
<div v-if="hasBaseLimit">
|
||||
<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">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="bufferIsInfinite"
|
||||
@change="toggleBufferInfinite"
|
||||
class="h-3.5 w-3.5 rounded border-gray-300 text-primary-600"
|
||||
/>
|
||||
{{ t('admin.accounts.affinityBufferInfinite') }}
|
||||
</label>
|
||||
</div>
|
||||
<input
|
||||
v-if="!bufferIsInfinite"
|
||||
:value="buffer"
|
||||
@input="onBufferInput"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
class="input"
|
||||
:placeholder="t('admin.accounts.affinityBufferPlaceholder')"
|
||||
/>
|
||||
<p class="input-hint">{{ t('admin.accounts.affinityBufferHint') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Zone preview -->
|
||||
<div v-if="zonePreview && base && base > 0" 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>
|
||||
<span v-if="zonePreview.yellow" class="inline-flex items-center gap-1 rounded-full bg-yellow-100 px-2 py-0.5 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400">
|
||||
{{ zonePreview.yellow }}
|
||||
</span>
|
||||
<span v-if="zonePreview.red" class="inline-flex items-center gap-1 rounded-full bg-red-100 px-2 py-0.5 text-red-700 dark:bg-red-900/30 dark:text-red-400">
|
||||
{{ zonePreview.red }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -782,26 +782,6 @@
|
||||
</div>
|
||||
|
||||
<!-- Client Affinity (Anthropic accounts only) -->
|
||||
<div
|
||||
v-if="account?.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.affinitySection') }}</h3>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.affinitySectionHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<AffinityConfigCard
|
||||
:enabled="clientAffinityEnabled"
|
||||
:base="affinityBase"
|
||||
:buffer="affinityBuffer"
|
||||
@update:enabled="clientAffinityEnabled = $event"
|
||||
@update:base="affinityBase = $event"
|
||||
@update:buffer="affinityBuffer = $event"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.proxy') }}</label>
|
||||
<ProxySelector v-model="form.proxy_id" :proxies="proxies" />
|
||||
@@ -927,6 +907,17 @@
|
||||
<!-- 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
|
||||
v-if="account?.platform === 'anthropic'"
|
||||
:enabled="clientAffinityEnabled"
|
||||
:base="affinityBase"
|
||||
:buffer="affinityBuffer"
|
||||
@update:enabled="clientAffinityEnabled = $event"
|
||||
@update:base="affinityBase = $event"
|
||||
@update:buffer="affinityBuffer = $event"
|
||||
/>
|
||||
|
||||
<!-- OpenAI OAuth Codex 官方客户端限制开关 -->
|
||||
<div
|
||||
v-if="account?.platform === 'openai' && account?.type === 'oauth'"
|
||||
@@ -2539,54 +2530,18 @@ const handleSubmit = async () => {
|
||||
delete newExtra.cache_ttl_override_target
|
||||
}
|
||||
|
||||
// Client affinity setting
|
||||
if (clientAffinityEnabled.value) {
|
||||
newExtra.client_affinity_enabled = true
|
||||
if (affinityBase.value != null && affinityBase.value > 0) {
|
||||
newExtra.affinity_base = affinityBase.value
|
||||
} else {
|
||||
delete newExtra.affinity_base
|
||||
}
|
||||
if (affinityBase.value != null && affinityBase.value > 0 && affinityBuffer.value != null) {
|
||||
newExtra.affinity_buffer = affinityBuffer.value
|
||||
} else {
|
||||
delete newExtra.affinity_buffer
|
||||
}
|
||||
} else {
|
||||
delete newExtra.client_affinity_enabled
|
||||
delete newExtra.affinity_base
|
||||
delete newExtra.affinity_buffer
|
||||
}
|
||||
|
||||
updatePayload.extra = newExtra
|
||||
}
|
||||
|
||||
// For Anthropic API Key accounts, handle passthrough mode and client affinity in extra
|
||||
// For Anthropic API Key accounts, handle passthrough mode in extra
|
||||
if (props.account.platform === 'anthropic' && props.account.type === 'apikey') {
|
||||
const currentExtra = (props.account.extra as Record<string, unknown>) || {}
|
||||
const currentExtra = (updatePayload.extra as Record<string, unknown>) || (props.account.extra as Record<string, unknown>) || {}
|
||||
const newExtra: Record<string, unknown> = { ...currentExtra }
|
||||
if (anthropicPassthroughEnabled.value) {
|
||||
newExtra.anthropic_passthrough = true
|
||||
} else {
|
||||
delete newExtra.anthropic_passthrough
|
||||
}
|
||||
if (clientAffinityEnabled.value) {
|
||||
newExtra.client_affinity_enabled = true
|
||||
if (affinityBase.value != null && affinityBase.value > 0) {
|
||||
newExtra.affinity_base = affinityBase.value
|
||||
} else {
|
||||
delete newExtra.affinity_base
|
||||
}
|
||||
if (affinityBase.value != null && affinityBase.value > 0 && affinityBuffer.value != null) {
|
||||
newExtra.affinity_buffer = affinityBuffer.value
|
||||
} else {
|
||||
delete newExtra.affinity_buffer
|
||||
}
|
||||
} else {
|
||||
delete newExtra.client_affinity_enabled
|
||||
delete newExtra.affinity_base
|
||||
delete newExtra.affinity_buffer
|
||||
}
|
||||
updatePayload.extra = newExtra
|
||||
}
|
||||
|
||||
|
||||
@@ -2025,8 +2025,8 @@ export default {
|
||||
affinityToggle: 'Enable Client Affinity',
|
||||
affinityToggleHint: 'New sessions prefer accounts previously used by this client',
|
||||
affinityBase: 'Base Limit (Green Zone)',
|
||||
affinityBasePlaceholder: 'e.g. 5',
|
||||
affinityBaseHint: 'Max number of clients in the green zone (full priority scheduling)',
|
||||
affinityBasePlaceholder: 'Empty = no limit',
|
||||
affinityBaseHint: 'Max clients in green zone (full priority). Leave empty for unlimited.',
|
||||
affinityBuffer: 'Buffer (Yellow Zone)',
|
||||
affinityBufferPlaceholder: 'e.g. 3',
|
||||
affinityBufferHint: 'Additional clients allowed in the yellow zone (degraded priority)',
|
||||
|
||||
@@ -2159,8 +2159,8 @@ export default {
|
||||
affinityToggle: '启用客户端亲和',
|
||||
affinityToggleHint: '新会话优先调度到该客户端之前使用过的账号',
|
||||
affinityBase: '基础限额(绿区)',
|
||||
affinityBasePlaceholder: '例如 5',
|
||||
affinityBaseHint: '绿区最大客户端数量(完整优先级调度)',
|
||||
affinityBasePlaceholder: '留空表示不限制',
|
||||
affinityBaseHint: '绿区最大客户端数量(完整优先级调度),留空表示不限制',
|
||||
affinityBuffer: '缓冲区(黄区)',
|
||||
affinityBufferPlaceholder: '例如 3',
|
||||
affinityBufferHint: '黄区允许的额外客户端数量(降级优先级调度)',
|
||||
|
||||
Reference in New Issue
Block a user