feat: restyle API Key quota limit UI to card/toggle format

- Redesign quota limit section with card layout and toggle switch
- Add watch to clear quota value when toggle is disabled
- Add i18n keys for toggle labels and hints (zh/en)
- Bump version to 0.1.90.5
This commit is contained in:
erio
2026-03-05 22:01:40 +08:00
parent 05527b13db
commit 1893b0eb30
3 changed files with 69 additions and 14 deletions

View File

@@ -762,21 +762,58 @@
<!-- API Key 账号配额限制 --> <!-- API Key 账号配额限制 -->
<div <div
v-if="account?.type === 'apikey'" v-if="account?.type === 'apikey'"
class="border-t border-gray-200 pt-4 dark:border-dark-600" class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4"
> >
<label class="input-label">{{ t('admin.accounts.quotaLimit') }}</label> <div class="mb-3">
<div class="flex items-center gap-2"> <h3 class="input-label mb-0 text-base font-semibold">{{ t('admin.accounts.quotaLimit') }}</h3>
<span class="text-gray-500">$</span> <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 <input
v-model.number="editQuotaLimit" v-model.number="editQuotaLimit"
type="number" type="number"
min="0" min="0"
step="0.01" step="0.01"
class="input" class="input pl-7"
:placeholder="t('admin.accounts.quotaLimitPlaceholder')" :placeholder="t('admin.accounts.quotaLimitPlaceholder')"
/> />
</div> </div>
<p class="input-hint">{{ t('admin.accounts.quotaLimitHint') }}</p> <p class="input-hint">{{ t('admin.accounts.quotaLimitAmountHint') }}</p>
</div>
</div>
</div>
</div> </div>
<!-- OpenAI OAuth Codex 官方客户端限制开关 --> <!-- OpenAI OAuth Codex 官方客户端限制开关 -->
@@ -1407,6 +1444,7 @@ 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 选项暂时隐藏,待测试完成后恢复
@@ -1435,6 +1473,13 @@ 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(() => [
@@ -1565,8 +1610,10 @@ 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
} }
@@ -2317,7 +2364,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 (editQuotaLimit.value != null && editQuotaLimit.value > 0) { if (quotaLimitEnabled.value && 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

View File

@@ -1787,6 +1787,10 @@ export default {
quotaLimit: 'Quota Limit', quotaLimit: 'Quota Limit',
quotaLimitPlaceholder: '0 means unlimited', quotaLimitPlaceholder: '0 means unlimited',
quotaLimitHint: 'Set max spending limit (USD). Account will be paused when reached. Changing limit won\'t reset usage.', quotaLimitHint: 'Set max spending limit (USD). Account will be paused when reached. Changing limit won\'t reset usage.',
quotaLimitToggle: 'Enable Quota Limit',
quotaLimitToggleHint: 'When enabled, account will be paused when usage reaches the set limit',
quotaLimitAmount: 'Limit Amount',
quotaLimitAmountHint: 'Maximum spending limit (USD). Account will be auto-paused when reached. Changing limit won\'t reset usage.',
testConnection: 'Test Connection', testConnection: 'Test Connection',
reAuthorize: 'Re-Authorize', reAuthorize: 'Re-Authorize',
refreshToken: 'Refresh Token', refreshToken: 'Refresh Token',

View File

@@ -1794,6 +1794,10 @@ export default {
quotaLimit: '配额限制', quotaLimit: '配额限制',
quotaLimitPlaceholder: '0 表示不限制', quotaLimitPlaceholder: '0 表示不限制',
quotaLimitHint: '设置最大使用额度(美元),达到后账号暂停调度。修改限额不会重置已用额度。', quotaLimitHint: '设置最大使用额度(美元),达到后账号暂停调度。修改限额不会重置已用额度。',
quotaLimitToggle: '启用配额限制',
quotaLimitToggleHint: '开启后,当账号用量达到设定额度时自动暂停调度',
quotaLimitAmount: '限额金额',
quotaLimitAmountHint: '账号最大可用额度(美元),达到后自动暂停。修改限额不会重置已用额度。',
testConnection: '测试连接', testConnection: '测试连接',
reAuthorize: '重新授权', reAuthorize: '重新授权',
refreshToken: '刷新令牌', refreshToken: '刷新令牌',