refactor: extract QuotaLimitCard component for reuse in create and edit modals
- Extract quota limit card/toggle UI into QuotaLimitCard.vue component - Use v-model pattern for clean parent-child data flow - Integrate into both EditAccountModal and CreateAccountModal - All apikey accounts (all platforms) now support quota limit on creation - Bump version to 0.1.90.6
This commit is contained in:
@@ -1227,6 +1227,9 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- API Key 账号配额限制 -->
|
||||||
|
<QuotaLimitCard v-if="form.type === 'apikey'" v-model="editQuotaLimit" />
|
||||||
|
|
||||||
<!-- Temp Unschedulable Rules -->
|
<!-- Temp Unschedulable Rules -->
|
||||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4">
|
<div class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4">
|
||||||
<div class="mb-3 flex items-center justify-between">
|
<div class="mb-3 flex items-center justify-between">
|
||||||
@@ -2337,6 +2340,7 @@ import Icon from '@/components/icons/Icon.vue'
|
|||||||
import ProxySelector from '@/components/common/ProxySelector.vue'
|
import ProxySelector from '@/components/common/ProxySelector.vue'
|
||||||
import GroupSelector from '@/components/common/GroupSelector.vue'
|
import GroupSelector from '@/components/common/GroupSelector.vue'
|
||||||
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
|
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
|
||||||
|
import QuotaLimitCard from '@/components/account/QuotaLimitCard.vue'
|
||||||
import { applyInterceptWarmup } from '@/components/account/credentialsBuilder'
|
import { applyInterceptWarmup } from '@/components/account/credentialsBuilder'
|
||||||
import { formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
|
import { formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
|
||||||
import { createStableObjectKeyResolver } from '@/utils/stableObjectKey'
|
import { createStableObjectKeyResolver } from '@/utils/stableObjectKey'
|
||||||
@@ -2460,6 +2464,7 @@ const accountCategory = ref<'oauth-based' | 'apikey'>('oauth-based') // UI selec
|
|||||||
const addMethod = ref<AddMethod>('oauth') // For oauth-based: 'oauth' or 'setup-token'
|
const addMethod = ref<AddMethod>('oauth') // For oauth-based: 'oauth' or 'setup-token'
|
||||||
const apiKeyBaseUrl = ref('https://api.anthropic.com')
|
const apiKeyBaseUrl = ref('https://api.anthropic.com')
|
||||||
const apiKeyValue = ref('')
|
const apiKeyValue = ref('')
|
||||||
|
const editQuotaLimit = ref<number | null>(null)
|
||||||
const modelMappings = ref<ModelMapping[]>([])
|
const modelMappings = ref<ModelMapping[]>([])
|
||||||
const modelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist')
|
const modelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist')
|
||||||
const allowedModels = ref<string[]>([])
|
const allowedModels = ref<string[]>([])
|
||||||
@@ -3120,6 +3125,7 @@ const resetForm = () => {
|
|||||||
addMethod.value = 'oauth'
|
addMethod.value = 'oauth'
|
||||||
apiKeyBaseUrl.value = 'https://api.anthropic.com'
|
apiKeyBaseUrl.value = 'https://api.anthropic.com'
|
||||||
apiKeyValue.value = ''
|
apiKeyValue.value = ''
|
||||||
|
editQuotaLimit.value = null
|
||||||
modelMappings.value = []
|
modelMappings.value = []
|
||||||
modelRestrictionMode.value = 'whitelist'
|
modelRestrictionMode.value = 'whitelist'
|
||||||
allowedModels.value = [...claudeModels] // Default fill related models
|
allowedModels.value = [...claudeModels] // Default fill related models
|
||||||
@@ -3533,13 +3539,18 @@ const createAccountAndFinish = async (
|
|||||||
if (!applyTempUnschedConfig(credentials)) {
|
if (!applyTempUnschedConfig(credentials)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Inject quota_limit for apikey accounts
|
||||||
|
let finalExtra = extra
|
||||||
|
if (type === 'apikey' && editQuotaLimit.value != null && editQuotaLimit.value > 0) {
|
||||||
|
finalExtra = { ...(extra || {}), quota_limit: editQuotaLimit.value }
|
||||||
|
}
|
||||||
await doCreateAccount({
|
await doCreateAccount({
|
||||||
name: form.name,
|
name: form.name,
|
||||||
notes: form.notes,
|
notes: form.notes,
|
||||||
platform,
|
platform,
|
||||||
type,
|
type,
|
||||||
credentials,
|
credentials,
|
||||||
extra,
|
extra: finalExtra,
|
||||||
proxy_id: form.proxy_id,
|
proxy_id: form.proxy_id,
|
||||||
concurrency: form.concurrency,
|
concurrency: form.concurrency,
|
||||||
priority: form.priority,
|
priority: form.priority,
|
||||||
|
|||||||
@@ -760,61 +760,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- API Key 账号配额限制 -->
|
<!-- API Key 账号配额限制 -->
|
||||||
<div
|
<QuotaLimitCard v-if="account?.type === 'apikey'" v-model="editQuotaLimit" />
|
||||||
v-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>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{{ t('admin.accounts.quotaLimitToggleHint') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
@click="quotaLimitEnabled = !quotaLimitEnabled"
|
|
||||||
: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',
|
|
||||||
quotaLimitEnabled ? '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',
|
|
||||||
quotaLimitEnabled ? 'translate-x-5' : 'translate-x-0'
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="quotaLimitEnabled" class="space-y-3">
|
|
||||||
<div>
|
|
||||||
<label class="input-label">{{ t('admin.accounts.quotaLimitAmount') }}</label>
|
|
||||||
<div class="relative">
|
|
||||||
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 dark:text-gray-400">$</span>
|
|
||||||
<input
|
|
||||||
v-model.number="editQuotaLimit"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="0.01"
|
|
||||||
class="input pl-7"
|
|
||||||
:placeholder="t('admin.accounts.quotaLimitPlaceholder')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p class="input-hint">{{ t('admin.accounts.quotaLimitAmountHint') }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- OpenAI OAuth Codex 官方客户端限制开关 -->
|
<!-- OpenAI OAuth Codex 官方客户端限制开关 -->
|
||||||
<div
|
<div
|
||||||
@@ -1326,6 +1272,7 @@ import Icon from '@/components/icons/Icon.vue'
|
|||||||
import ProxySelector from '@/components/common/ProxySelector.vue'
|
import ProxySelector from '@/components/common/ProxySelector.vue'
|
||||||
import GroupSelector from '@/components/common/GroupSelector.vue'
|
import GroupSelector from '@/components/common/GroupSelector.vue'
|
||||||
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
|
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
|
||||||
|
import QuotaLimitCard from '@/components/account/QuotaLimitCard.vue'
|
||||||
import { applyInterceptWarmup } from '@/components/account/credentialsBuilder'
|
import { applyInterceptWarmup } from '@/components/account/credentialsBuilder'
|
||||||
import { formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
|
import { formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
|
||||||
import { createStableObjectKeyResolver } from '@/utils/stableObjectKey'
|
import { createStableObjectKeyResolver } from '@/utils/stableObjectKey'
|
||||||
@@ -1444,7 +1391,6 @@ const openaiAPIKeyResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OF
|
|||||||
const codexCLIOnlyEnabled = ref(false)
|
const codexCLIOnlyEnabled = ref(false)
|
||||||
const anthropicPassthroughEnabled = ref(false)
|
const anthropicPassthroughEnabled = ref(false)
|
||||||
const editQuotaLimit = ref<number | null>(null)
|
const editQuotaLimit = ref<number | null>(null)
|
||||||
const quotaLimitEnabled = ref(false)
|
|
||||||
const openAIWSModeOptions = computed(() => [
|
const openAIWSModeOptions = computed(() => [
|
||||||
{ value: OPENAI_WS_MODE_OFF, label: t('admin.accounts.openai.wsModeOff') },
|
{ value: OPENAI_WS_MODE_OFF, label: t('admin.accounts.openai.wsModeOff') },
|
||||||
// TODO: ctx_pool 选项暂时隐藏,待测试完成后恢复
|
// TODO: ctx_pool 选项暂时隐藏,待测试完成后恢复
|
||||||
@@ -1473,13 +1419,6 @@ const isOpenAIModelRestrictionDisabled = computed(() =>
|
|||||||
props.account?.platform === 'openai' && openaiPassthroughEnabled.value
|
props.account?.platform === 'openai' && openaiPassthroughEnabled.value
|
||||||
)
|
)
|
||||||
|
|
||||||
// When quota limit toggle is turned off, clear the value
|
|
||||||
watch(quotaLimitEnabled, (enabled) => {
|
|
||||||
if (!enabled) {
|
|
||||||
editQuotaLimit.value = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Computed: current preset mappings based on platform
|
// Computed: current preset mappings based on platform
|
||||||
const presetMappings = computed(() => getPresetMappingsByPlatform(props.account?.platform || 'anthropic'))
|
const presetMappings = computed(() => getPresetMappingsByPlatform(props.account?.platform || 'anthropic'))
|
||||||
const tempUnschedPresets = computed(() => [
|
const tempUnschedPresets = computed(() => [
|
||||||
@@ -1610,10 +1549,8 @@ watch(
|
|||||||
// Load quota limit for apikey accounts
|
// Load quota limit for apikey accounts
|
||||||
if (newAccount.type === 'apikey') {
|
if (newAccount.type === 'apikey') {
|
||||||
const quotaVal = extra?.quota_limit as number | undefined
|
const quotaVal = extra?.quota_limit as number | undefined
|
||||||
quotaLimitEnabled.value = !!(quotaVal && quotaVal > 0)
|
|
||||||
editQuotaLimit.value = (quotaVal && quotaVal > 0) ? quotaVal : null
|
editQuotaLimit.value = (quotaVal && quotaVal > 0) ? quotaVal : null
|
||||||
} else {
|
} else {
|
||||||
quotaLimitEnabled.value = false
|
|
||||||
editQuotaLimit.value = null
|
editQuotaLimit.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2364,7 +2301,7 @@ const handleSubmit = async () => {
|
|||||||
const currentExtra = (updatePayload.extra as Record<string, unknown>) ||
|
const currentExtra = (updatePayload.extra as Record<string, unknown>) ||
|
||||||
(props.account.extra as Record<string, unknown>) || {}
|
(props.account.extra as Record<string, unknown>) || {}
|
||||||
const newExtra: Record<string, unknown> = { ...currentExtra }
|
const newExtra: Record<string, unknown> = { ...currentExtra }
|
||||||
if (quotaLimitEnabled.value && editQuotaLimit.value != null && editQuotaLimit.value > 0) {
|
if (editQuotaLimit.value != null && editQuotaLimit.value > 0) {
|
||||||
newExtra.quota_limit = editQuotaLimit.value
|
newExtra.quota_limit = editQuotaLimit.value
|
||||||
} else {
|
} else {
|
||||||
delete newExtra.quota_limit
|
delete newExtra.quota_limit
|
||||||
|
|||||||
92
frontend/src/components/account/QuotaLimitCard.vue
Normal file
92
frontend/src/components/account/QuotaLimitCard.vue
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: number | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: number | null]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const enabled = ref(props.modelValue != null && props.modelValue > 0)
|
||||||
|
|
||||||
|
// Sync enabled state when modelValue changes externally (e.g. account load)
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(val) => {
|
||||||
|
enabled.value = val != null && val > 0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// When toggle is turned off, clear the value
|
||||||
|
watch(enabled, (val) => {
|
||||||
|
if (!val) {
|
||||||
|
emit('update:modelValue', null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const onInput = (e: Event) => {
|
||||||
|
const raw = (e.target as HTMLInputElement).valueAsNumber
|
||||||
|
emit('update:modelValue', Number.isNaN(raw) ? null : raw)
|
||||||
|
}
|
||||||
|
</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="mb-3 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<label class="input-label mb-0">{{ t('admin.accounts.quotaLimitToggle') }}</label>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.accounts.quotaLimitToggleHint') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="enabled = !enabled"
|
||||||
|
: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',
|
||||||
|
enabled ? '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',
|
||||||
|
enabled ? 'translate-x-5' : 'translate-x-0'
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="enabled" class="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label class="input-label">{{ t('admin.accounts.quotaLimitAmount') }}</label>
|
||||||
|
<div class="relative">
|
||||||
|
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 dark:text-gray-400">$</span>
|
||||||
|
<input
|
||||||
|
:value="modelValue"
|
||||||
|
@input="onInput"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
class="input pl-7"
|
||||||
|
:placeholder="t('admin.accounts.quotaLimitPlaceholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="input-hint">{{ t('admin.accounts.quotaLimitAmountHint') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user