feat: 完成web/admin-spa/src/components/apikeys的国际化并修复语法错误和警告

This commit is contained in:
Wangnov
2025-09-10 16:03:01 +08:00
parent 9836f88068
commit 97b94eeff9
35 changed files with 4766 additions and 2061 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,9 @@
>
<i class="fas fa-layer-group text-sm text-white sm:text-base" />
</div>
<h3 class="text-lg font-bold text-gray-900 sm:text-xl">{{ t('groupManagement.title') }}</h3>
<h3 class="text-lg font-bold text-gray-900 sm:text-xl">
{{ t('groupManagement.title') }}
</h3>
</div>
<button
class="p-1 text-gray-400 transition-colors hover:text-gray-600"
@@ -31,10 +33,14 @@
<!-- 创建分组表单 -->
<div v-if="showCreateForm" class="mb-6 rounded-lg border border-blue-200 bg-blue-50 p-4">
<h4 class="mb-4 text-lg font-semibold text-gray-900">{{ t('groupManagement.createGroup') }}</h4>
<h4 class="mb-4 text-lg font-semibold text-gray-900">
{{ t('groupManagement.createGroup') }}
</h4>
<div class="space-y-4">
<div>
<label class="mb-2 block text-sm font-semibold text-gray-700">{{ t('groupManagement.groupNameRequired') }}</label>
<label class="mb-2 block text-sm font-semibold text-gray-700">{{
t('groupManagement.groupNameRequired')
}}</label>
<input
v-model="createForm.name"
class="form-input w-full"
@@ -44,7 +50,9 @@
</div>
<div>
<label class="mb-2 block text-sm font-semibold text-gray-700">{{ t('groupManagement.platformTypeRequired') }}</label>
<label class="mb-2 block text-sm font-semibold text-gray-700">{{
t('groupManagement.platformTypeRequired')
}}</label>
<div class="flex gap-4">
<label class="flex cursor-pointer items-center">
<input v-model="createForm.platform" class="mr-2" type="radio" value="claude" />
@@ -62,7 +70,9 @@
</div>
<div>
<label class="mb-2 block text-sm font-semibold text-gray-700">{{ t('groupManagement.descriptionOptional') }}</label>
<label class="mb-2 block text-sm font-semibold text-gray-700">{{
t('groupManagement.descriptionOptional')
}}</label>
<textarea
v-model="createForm.description"
class="form-input w-full resize-none"
@@ -80,7 +90,9 @@
<div v-if="creating" class="loading-spinner mr-2" />
{{ creating ? t('groupManagement.creating') : t('groupManagement.create') }}
</button>
<button class="btn btn-secondary px-4 py-2" @click="cancelCreate">{{ t('groupManagement.cancel') }}</button>
<button class="btn btn-secondary px-4 py-2" @click="cancelCreate">
{{ t('groupManagement.cancel') }}
</button>
</div>
</div>
</div>
@@ -184,7 +196,9 @@
<div class="space-y-4">
<div>
<label class="mb-2 block text-sm font-semibold text-gray-700">{{ t('groupManagement.groupNameRequired') }}</label>
<label class="mb-2 block text-sm font-semibold text-gray-700">{{
t('groupManagement.groupNameRequired')
}}</label>
<input
v-model="editForm.name"
class="form-input w-full"
@@ -194,7 +208,9 @@
</div>
<div>
<label class="mb-2 block text-sm font-semibold text-gray-700">{{ t('groupManagement.platformTypeLabel') }}</label>
<label class="mb-2 block text-sm font-semibold text-gray-700">{{
t('groupManagement.platformTypeLabel')
}}</label>
<div class="rounded-lg bg-gray-100 px-3 py-2 text-sm text-gray-600">
{{
editForm.platform === 'claude'
@@ -203,12 +219,16 @@
? 'Gemini'
: 'OpenAI'
}}
<span class="ml-2 text-xs text-gray-500">{{ t('groupManagement.cannotModify') }}</span>
<span class="ml-2 text-xs text-gray-500">{{
t('groupManagement.cannotModify')
}}</span>
</div>
</div>
<div>
<label class="mb-2 block text-sm font-semibold text-gray-700">{{ t('groupManagement.descriptionOptional') }}</label>
<label class="mb-2 block text-sm font-semibold text-gray-700">{{
t('groupManagement.descriptionOptional')
}}</label>
<textarea
v-model="editForm.description"
class="form-input w-full resize-none"
@@ -226,7 +246,9 @@
<div v-if="updating" class="loading-spinner mr-2" />
{{ updating ? t('groupManagement.updating') : t('groupManagement.update') }}
</button>
<button class="btn btn-secondary flex-1 px-4 py-2" @click="cancelEdit">{{ t('groupManagement.cancel') }}</button>
<button class="btn btn-secondary flex-1 px-4 py-2" @click="cancelEdit">
{{ t('groupManagement.cancel') }}
</button>
</div>
</div>
</div>

View File

@@ -12,7 +12,9 @@
<i class="fas fa-link text-white" />
</div>
<div class="flex-1">
<h4 class="mb-3 font-semibold text-blue-900 dark:text-blue-200">{{ t('oauthFlow.claudeAccountAuth') }}</h4>
<h4 class="mb-3 font-semibold text-blue-900 dark:text-blue-200">
{{ t('oauthFlow.claudeAccountAuth') }}
</h4>
<p class="mb-4 text-sm text-blue-800 dark:text-blue-300">
{{ t('oauthFlow.claudeAuthDescription') }}
</p>
@@ -115,14 +117,17 @@
</p>
<p class="mb-3 text-sm text-blue-700 dark:text-blue-300">
{{ t('oauthFlow.step3Description') }}
<strong>{{ t('oauthFlow.authorizationCode') }}</strong>{{ t('oauthFlow.step3DescriptionMiddle') }}
<strong>{{ t('oauthFlow.authorizationCode') }}</strong
>{{ t('oauthFlow.step3DescriptionMiddle') }}
</p>
<div class="space-y-3">
<div>
<label
class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>
<i class="fas fa-key mr-2 text-blue-500" />{{ t('oauthFlow.authorizationCode') }}
<i class="fas fa-key mr-2 text-blue-500" />{{
t('oauthFlow.authorizationCode')
}}
</label>
<textarea
v-model="authCode"
@@ -157,7 +162,9 @@
<i class="fas fa-robot text-white" />
</div>
<div class="flex-1">
<h4 class="mb-3 font-semibold text-green-900 dark:text-green-200">{{ t('oauthFlow.geminiAccountAuth') }}</h4>
<h4 class="mb-3 font-semibold text-green-900 dark:text-green-200">
{{ t('oauthFlow.geminiAccountAuth') }}
</h4>
<p class="mb-4 text-sm text-green-800 dark:text-green-300">
{{ t('oauthFlow.geminiAuthDescription') }}
</p>
@@ -266,7 +273,9 @@
<label
class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>
<i class="fas fa-key mr-2 text-green-500" />{{ t('oauthFlow.authorizationCode') }}
<i class="fas fa-key mr-2 text-green-500" />{{
t('oauthFlow.authorizationCode')
}}
</label>
<textarea
v-model="authCode"
@@ -303,7 +312,9 @@
<i class="fas fa-brain text-white" />
</div>
<div class="flex-1">
<h4 class="mb-3 font-semibold text-orange-900 dark:text-orange-200">{{ t('oauthFlow.openaiAccountAuth') }}</h4>
<h4 class="mb-3 font-semibold text-orange-900 dark:text-orange-200">
{{ t('oauthFlow.openaiAccountAuth') }}
</h4>
<p class="mb-4 text-sm text-orange-800 dark:text-orange-300">
{{ t('oauthFlow.openaiAuthDescription') }}
</p>
@@ -382,7 +393,8 @@
>
<p class="text-xs text-amber-800 dark:text-amber-300">
<i class="fas fa-clock mr-1" />
<strong>{{ t('oauthFlow.openaiImportantNote') }}</strong>{{ t('oauthFlow.openaiLoadingNote') }}
<strong>{{ t('oauthFlow.openaiImportantNote') }}</strong
>{{ t('oauthFlow.openaiLoadingNote') }}
</p>
<p class="mt-2 text-xs text-amber-700 dark:text-amber-400">
{{ t('oauthFlow.openaiAddressNote') }}
@@ -419,14 +431,17 @@
</p>
<p class="mb-3 text-sm text-orange-700 dark:text-orange-300">
{{ t('oauthFlow.step3DescriptionOpenAI') }}
<strong class="font-mono">http://localhost:1455/...</strong> {{ t('oauthFlow.step3DescriptionOpenAIMiddle') }}
<strong class="font-mono">http://localhost:1455/...</strong>
{{ t('oauthFlow.step3DescriptionOpenAIMiddle') }}
</p>
<div class="space-y-3">
<div>
<label
class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>
<i class="fas fa-link mr-2 text-orange-500" />{{ t('oauthFlow.authLinkOrCode') }}
<i class="fas fa-link mr-2 text-orange-500" />{{
t('oauthFlow.authLinkOrCode')
}}
</label>
<textarea
v-model="authCode"
@@ -440,17 +455,18 @@
>
<p class="text-xs text-blue-700 dark:text-blue-300">
<i class="fas fa-lightbulb mr-1" />
<strong>{{ t('oauthFlow.openaiTip') }}</strong>{{ t('oauthFlow.openaiTipText') }}
<strong>{{ t('oauthFlow.openaiTip') }}</strong
>{{ t('oauthFlow.openaiTipText') }}
</p>
<p class="mt-1 text-xs text-blue-600 dark:text-blue-400">
{{ t('oauthFlow.openaiLinkExample') }}<span class="font-mono"
{{ t('oauthFlow.openaiLinkExample')
}}<span class="font-mono"
>http://localhost:1455/auth/callback?code=ac_4hm8...</span
>
</p>
<p class="text-xs text-blue-600">
{{ t('oauthFlow.openaiCodeExample') }}<span class="font-mono"
>ac_4hm8iqmx9A2fzMy_cwye7U3W7...</span
>
{{ t('oauthFlow.openaiCodeExample')
}}<span class="font-mono">ac_4hm8iqmx9A2fzMy_cwye7U3W7...</span>
</p>
</div>
</div>

View File

@@ -1,14 +1,18 @@
<template>
<div class="space-y-4">
<div class="flex items-center justify-between">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">{{ t('proxyConfig.title') }}</h4>
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">
{{ t('proxyConfig.title') }}
</h4>
<label class="flex cursor-pointer items-center">
<input
v-model="proxy.enabled"
class="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-blue-500"
type="checkbox"
/>
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">{{ t('proxyConfig.enableProxy') }}</span>
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">{{
t('proxyConfig.enableProxy')
}}</span>
</label>
</div>
@@ -70,9 +74,9 @@
<div class="my-3 border-t border-gray-200 dark:border-gray-600"></div>
<div>
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>{{ t('proxyConfig.proxyType') }}</label
>
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">{{
t('proxyConfig.proxyType')
}}</label>
<select
v-model="proxy.type"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
@@ -85,9 +89,9 @@
<div class="grid grid-cols-2 gap-4">
<div>
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>{{ t('proxyConfig.hostAddress') }}</label
>
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">{{
t('proxyConfig.hostAddress')
}}</label>
<input
v-model="proxy.host"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
@@ -96,9 +100,9 @@
/>
</div>
<div>
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>{{ t('proxyConfig.port') }}</label
>
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">{{
t('proxyConfig.port')
}}</label>
<input
v-model="proxy.port"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
@@ -126,9 +130,9 @@
<div v-if="showAuth" class="grid grid-cols-2 gap-4">
<div>
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>{{ t('proxyConfig.username') }}</label
>
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">{{
t('proxyConfig.username')
}}</label>
<input
v-model="proxy.username"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
@@ -137,9 +141,9 @@
/>
</div>
<div>
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>{{ t('proxyConfig.password') }}</label
>
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">{{
t('proxyConfig.password')
}}</label>
<div class="relative">
<input
v-model="proxy.password"

View File

@@ -64,7 +64,9 @@
<!-- Role Selection -->
<form class="space-y-4" @submit.prevent="handleSubmit">
<div>
<label class="mb-2 block text-sm font-medium text-gray-700"> {{ $t('user.changeRoleModal.newRole') }} </label>
<label class="mb-2 block text-sm font-medium text-gray-700">
{{ $t('user.changeRoleModal.newRole') }}
</label>
<div class="space-y-2">
<label class="flex items-center">
<input
@@ -75,8 +77,12 @@
value="user"
/>
<div class="ml-3">
<div class="text-sm font-medium text-gray-900">{{ $t('user.changeRoleModal.roles.user') }}</div>
<div class="text-xs text-gray-500">{{ $t('user.changeRoleModal.roles.userDesc') }}</div>
<div class="text-sm font-medium text-gray-900">
{{ $t('user.changeRoleModal.roles.user') }}
</div>
<div class="text-xs text-gray-500">
{{ $t('user.changeRoleModal.roles.userDesc') }}
</div>
</div>
</label>
<label class="flex items-center">
@@ -88,8 +94,12 @@
value="admin"
/>
<div class="ml-3">
<div class="text-sm font-medium text-gray-900">{{ $t('user.changeRoleModal.roles.admin') }}</div>
<div class="text-xs text-gray-500">{{ $t('user.changeRoleModal.roles.adminDesc') }}</div>
<div class="text-sm font-medium text-gray-900">
{{ $t('user.changeRoleModal.roles.admin') }}
</div>
<div class="text-xs text-gray-500">
{{ $t('user.changeRoleModal.roles.adminDesc') }}
</div>
</div>
</label>
</div>
@@ -111,7 +121,9 @@
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800">{{ $t('user.changeRoleModal.roleChangeWarning.title') }}</h3>
<h3 class="text-sm font-medium text-yellow-800">
{{ $t('user.changeRoleModal.roleChangeWarning.title') }}
</h3>
<div class="mt-2 text-sm text-yellow-700">
<p v-if="selectedRole === 'admin'">
{{ $t('user.changeRoleModal.roleChangeWarning.grantAdmin') }}

View File

@@ -8,7 +8,11 @@
<div class="mb-6 flex items-center justify-between">
<div>
<h3 class="text-lg font-medium text-gray-900">
{{ $t('user.usageStatsModal.titleWithUser', { displayName: user?.displayName || user?.username }) }}
{{
$t('user.usageStatsModal.titleWithUser', {
displayName: user?.displayName || user?.username
})
}}
</h3>
<p class="text-sm text-gray-500">@{{ user?.username }} {{ user?.role }}</p>
</div>
@@ -34,7 +38,9 @@
<option value="day">{{ $t('user.usageStatsModal.periodSelection.day') }}</option>
<option value="week">{{ $t('user.usageStatsModal.periodSelection.week') }}</option>
<option value="month">{{ $t('user.usageStatsModal.periodSelection.month') }}</option>
<option value="quarter">{{ $t('user.usageStatsModal.periodSelection.quarter') }}</option>
<option value="quarter">
{{ $t('user.usageStatsModal.periodSelection.quarter') }}
</option>
</select>
</div>
@@ -87,7 +93,9 @@
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="truncate text-sm font-medium text-blue-600">{{ $t('user.usageStatsModal.summaryCards.requests') }}</dt>
<dt class="truncate text-sm font-medium text-blue-600">
{{ $t('user.usageStatsModal.summaryCards.requests') }}
</dt>
<dd class="text-lg font-medium text-blue-900">
{{ formatNumber(usageStats?.totalRequests || 0) }}
</dd>
@@ -117,7 +125,9 @@
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="truncate text-sm font-medium text-green-600">{{ $t('user.usageStatsModal.summaryCards.inputTokens') }}</dt>
<dt class="truncate text-sm font-medium text-green-600">
{{ $t('user.usageStatsModal.summaryCards.inputTokens') }}
</dt>
<dd class="text-lg font-medium text-green-900">
{{ formatNumber(usageStats?.totalInputTokens || 0) }}
</dd>
@@ -147,7 +157,9 @@
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="truncate text-sm font-medium text-purple-600">{{ $t('user.usageStatsModal.summaryCards.outputTokens') }}</dt>
<dt class="truncate text-sm font-medium text-purple-600">
{{ $t('user.usageStatsModal.summaryCards.outputTokens') }}
</dt>
<dd class="text-lg font-medium text-purple-900">
{{ formatNumber(usageStats?.totalOutputTokens || 0) }}
</dd>
@@ -177,7 +189,9 @@
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="truncate text-sm font-medium text-yellow-600">{{ $t('user.usageStatsModal.summaryCards.totalCost') }}</dt>
<dt class="truncate text-sm font-medium text-yellow-600">
{{ $t('user.usageStatsModal.summaryCards.totalCost') }}
</dt>
<dd class="text-lg font-medium text-yellow-900">
${{ (usageStats?.totalCost || 0).toFixed(4) }}
</dd>
@@ -194,7 +208,9 @@
class="rounded-lg border border-gray-200 bg-white"
>
<div class="border-b border-gray-200 px-4 py-5 sm:px-6">
<h4 class="text-lg font-medium leading-6 text-gray-900">{{ $t('user.usageStatsModal.apiKeysTable.title') }}</h4>
<h4 class="text-lg font-medium leading-6 text-gray-900">
{{ $t('user.usageStatsModal.apiKeysTable.title') }}
</h4>
</div>
<div class="overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
@@ -253,21 +269,35 @@
: 'bg-red-100 text-red-800'
]"
>
{{ apiKey.isActive ? $t('user.usageStatsModal.apiKeysTable.status.active') : $t('user.usageStatsModal.apiKeysTable.status.disabled') }}
{{
apiKey.isActive
? $t('user.usageStatsModal.apiKeysTable.status.active')
: $t('user.usageStatsModal.apiKeysTable.status.disabled')
}}
</span>
</td>
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">
{{ formatNumber(apiKey.usage?.requests || 0) }}
</td>
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">
<div>{{ $t('user.usageStatsModal.apiKeysTable.tokensFormat.input') }}: {{ formatNumber(apiKey.usage?.inputTokens || 0) }}</div>
<div>{{ $t('user.usageStatsModal.apiKeysTable.tokensFormat.output') }}: {{ formatNumber(apiKey.usage?.outputTokens || 0) }}</div>
<div>
{{ $t('user.usageStatsModal.apiKeysTable.tokensFormat.input') }}:
{{ formatNumber(apiKey.usage?.inputTokens || 0) }}
</div>
<div>
{{ $t('user.usageStatsModal.apiKeysTable.tokensFormat.output') }}:
{{ formatNumber(apiKey.usage?.outputTokens || 0) }}
</div>
</td>
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">
${{ (apiKey.usage?.totalCost || 0).toFixed(4) }}
</td>
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
{{ apiKey.lastUsedAt ? formatDate(apiKey.lastUsedAt) : $t('user.usageStatsModal.apiKeysTable.never') }}
{{
apiKey.lastUsedAt
? formatDate(apiKey.lastUsedAt)
: $t('user.usageStatsModal.apiKeysTable.never')
}}
</td>
</tr>
</tbody>
@@ -278,7 +308,9 @@
<!-- Chart Placeholder -->
<div class="rounded-lg border border-gray-200 bg-white">
<div class="border-b border-gray-200 px-4 py-5 sm:px-6">
<h4 class="text-lg font-medium leading-6 text-gray-900">{{ $t('user.usageStatsModal.usageTrend.title') }}</h4>
<h4 class="text-lg font-medium leading-6 text-gray-900">
{{ $t('user.usageStatsModal.usageTrend.title') }}
</h4>
</div>
<div class="p-6">
<div
@@ -298,9 +330,13 @@
stroke-width="2"
/>
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">{{ $t('user.usageStatsModal.usageTrend.chartTitle') }}</h3>
<h3 class="mt-2 text-sm font-medium text-gray-900">
{{ $t('user.usageStatsModal.usageTrend.chartTitle') }}
</h3>
<p class="mt-1 text-sm text-gray-500">
{{ $t('user.usageStatsModal.usageTrend.dailyTrends', { period: selectedPeriod }) }}
{{
$t('user.usageStatsModal.usageTrend.dailyTrends', { period: selectedPeriod })
}}
</p>
<p class="mt-2 text-xs text-gray-400">
{{ $t('user.usageStatsModal.usageTrend.chartNote') }}
@@ -325,7 +361,9 @@
stroke-width="2"
/>
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">{{ $t('user.usageStatsModal.noData.title') }}</h3>
<h3 class="mt-2 text-sm font-medium text-gray-900">
{{ $t('user.usageStatsModal.noData.title') }}
</h3>
<p class="mt-1 text-sm text-gray-500">
{{ $t('user.usageStatsModal.noData.description') }}
</p>
@@ -349,9 +387,7 @@
import { ref, watch } from 'vue'
import { apiClient } from '@/config/api'
import { showToast } from '@/utils/toast'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
// import { useI18n } from 'vue-i18n' - using $t in template instead
const props = defineProps({
show: {

View File

@@ -12,13 +12,17 @@
<i class="fas fa-layer-group text-lg text-white" />
</div>
<div>
<h3 class="text-xl font-bold text-gray-900">批量创建成功</h3>
<p class="text-sm text-gray-600">成功创建 {{ apiKeys.length }} API Key</p>
<h3 class="text-xl font-bold text-gray-900">
{{ $t('apiKeys.batchApiKeyModal.title') }}
</h3>
<p class="text-sm text-gray-600">
{{ $t('apiKeys.batchApiKeyModal.successMessage', { count: apiKeys.length }) }}
</p>
</div>
</div>
<button
class="text-gray-400 transition-colors hover:text-gray-600"
title="直接关闭(不推荐)"
:title="$t('apiKeys.batchApiKeyModal.directCloseTooltip')"
@click="handleDirectClose"
>
<i class="fas fa-times text-xl" />
@@ -34,10 +38,11 @@
<i class="fas fa-exclamation-triangle text-sm text-white" />
</div>
<div class="ml-3">
<h5 class="mb-1 font-semibold text-amber-900">重要提醒</h5>
<h5 class="mb-1 font-semibold text-amber-900">
{{ $t('apiKeys.batchApiKeyModal.importantReminder') }}
</h5>
<p class="text-sm text-amber-800">
这是您唯一能看到所有 API Key 的机会关闭此窗口后系统将不再显示完整的 API
Key请立即下载并妥善保存
{{ $t('apiKeys.batchApiKeyModal.warningMessage') }}
</p>
</div>
</div>
@@ -50,7 +55,9 @@
>
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-blue-600">创建数量</p>
<p class="text-xs font-medium text-blue-600">
{{ $t('apiKeys.batchApiKeyModal.createdCount') }}
</p>
<p class="mt-1 text-2xl font-bold text-blue-900">
{{ apiKeys.length }}
</p>
@@ -68,7 +75,9 @@
>
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-green-600">基础名称</p>
<p class="text-xs font-medium text-green-600">
{{ $t('apiKeys.batchApiKeyModal.baseName') }}
</p>
<p class="mt-1 truncate text-lg font-bold text-green-900">
{{ baseName }}
</p>
@@ -86,7 +95,9 @@
>
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-purple-600">权限范围</p>
<p class="text-xs font-medium text-purple-600">
{{ $t('apiKeys.batchApiKeyModal.permissionScope') }}
</p>
<p class="mt-1 text-lg font-bold text-purple-900">
{{ getPermissionText() }}
</p>
@@ -104,7 +115,9 @@
>
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-orange-600">过期时间</p>
<p class="text-xs font-medium text-orange-600">
{{ $t('apiKeys.batchApiKeyModal.expiryTime') }}
</p>
<p class="mt-1 text-lg font-bold text-orange-900">
{{ getExpiryText() }}
</p>
@@ -121,7 +134,9 @@
<!-- API Keys 预览 -->
<div class="mb-6">
<div class="mb-3 flex items-center justify-between">
<label class="text-sm font-semibold text-gray-700">API Keys 预览</label>
<label class="text-sm font-semibold text-gray-700">{{
$t('apiKeys.batchApiKeyModal.previewTitle')
}}</label>
<div class="flex items-center gap-2">
<button
class="flex items-center gap-1 text-xs text-blue-600 hover:text-blue-800"
@@ -129,9 +144,15 @@
@click="togglePreview"
>
<i :class="['fas', showPreview ? 'fa-eye-slash' : 'fa-eye']" />
{{ showPreview ? '隐藏' : '显示' }}预览
{{
showPreview
? $t('apiKeys.batchApiKeyModal.hide')
: $t('apiKeys.batchApiKeyModal.show')
}}{{ $t('apiKeys.batchApiKeyModal.preview') }}
</button>
<span class="text-xs text-gray-500">最多显示前10个</span>
<span class="text-xs text-gray-500">{{
$t('apiKeys.batchApiKeyModal.maxDisplayNote')
}}</span>
</div>
</div>
@@ -150,13 +171,13 @@
@click="downloadApiKeys"
>
<i class="fas fa-download" />
下载所有 API Keys
{{ $t('apiKeys.batchApiKeyModal.downloadAll') }}
</button>
<button
class="rounded-xl border border-gray-300 bg-gray-200 px-6 py-3 font-semibold text-gray-800 transition-colors hover:bg-gray-300"
@click="handleClose"
>
我已保存
{{ $t('apiKeys.batchApiKeyModal.alreadySaved') }}
</button>
</div>
@@ -165,8 +186,7 @@
<p class="flex items-start text-xs text-blue-700">
<i class="fas fa-info-circle mr-2 mt-0.5 flex-shrink-0" />
<span>
下载的文件格式为文本文件.txt每行包含一个 API Key
请将文件保存在安全的位置避免泄露
{{ $t('apiKeys.batchApiKeyModal.fileFormatInfo') }}
</span>
</p>
</div>
@@ -177,8 +197,11 @@
<script setup>
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { showToast } from '@/utils/toast'
const { t } = useI18n()
const props = defineProps({
apiKeys: {
type: Array,
@@ -203,30 +226,28 @@ const baseName = computed(() => {
// 获取权限文本
const getPermissionText = () => {
if (props.apiKeys.length === 0) return '未知'
if (props.apiKeys.length === 0) return t('apiKeys.batchApiKeyModal.permissions.unknown')
const permissions = props.apiKeys[0].permissions
const permissionMap = {
all: '全部服务',
claude: '仅 Claude',
gemini: '仅 Gemini'
}
return permissionMap[permissions] || permissions
const permissionKey = `apiKeys.batchApiKeyModal.permissions.${permissions}`
return t(permissionKey, t('apiKeys.batchApiKeyModal.permissions.unknown'))
}
// 获取过期时间文本
const getExpiryText = () => {
if (props.apiKeys.length === 0) return '未知'
if (props.apiKeys.length === 0) return t('apiKeys.batchApiKeyModal.permissions.unknown')
const expiresAt = props.apiKeys[0].expiresAt
if (!expiresAt) return '永不过期'
if (!expiresAt) return t('apiKeys.batchApiKeyModal.neverExpire')
const expiryDate = new Date(expiresAt)
const now = new Date()
const diffDays = Math.ceil((expiryDate - now) / (1000 * 60 * 60 * 24))
if (diffDays <= 7) return `${diffDays}`
if (diffDays <= 30) return `${Math.ceil(diffDays / 7)}`
if (diffDays <= 365) return `${Math.ceil(diffDays / 30)}个月`
return `${Math.ceil(diffDays / 365)}`
if (diffDays <= 7) return t('apiKeys.batchApiKeyModal.daysFormat', { days: diffDays })
if (diffDays <= 30)
return t('apiKeys.batchApiKeyModal.weeksFormat', { weeks: Math.ceil(diffDays / 7) })
if (diffDays <= 365)
return t('apiKeys.batchApiKeyModal.monthsFormat', { months: Math.ceil(diffDays / 30) })
return t('apiKeys.batchApiKeyModal.yearsFormat', { years: Math.ceil(diffDays / 365) })
}
// 切换预览显示
@@ -242,7 +263,7 @@ const getPreviewText = () => {
})
if (props.apiKeys.length > 10) {
lines.push(`... 还有 ${props.apiKeys.length - 10} 个 API Key`)
lines.push(t('apiKeys.batchApiKeyModal.moreKeysNote', { count: props.apiKeys.length - 10 }))
}
return lines.join('\n')
@@ -277,26 +298,24 @@ const downloadApiKeys = () => {
// 释放 URL 对象
URL.revokeObjectURL(url)
showToast('API Keys 文件已下载', 'success')
showToast(t('apiKeys.batchApiKeyModal.downloadSuccess'), 'success')
}
// 关闭弹窗(带确认)
const handleClose = async () => {
if (window.showConfirm) {
const confirmed = await window.showConfirm(
'关闭提醒',
'关闭后将无法再次查看这些 API Key请确保已经下载并妥善保存。\n\n确定要关闭吗',
'确定关闭',
'返回下载'
t('apiKeys.batchApiKeyModal.closeReminderTitle'),
t('apiKeys.batchApiKeyModal.closeReminderMessage'),
t('apiKeys.batchApiKeyModal.confirmCloseButton'),
t('apiKeys.batchApiKeyModal.goBackDownloadButton')
)
if (confirmed) {
emit('close')
}
} else {
// 降级方案
const confirmed = confirm(
'关闭后将无法再次查看这些 API Key请确保已经下载并妥善保存。\n\n确定要关闭吗'
)
const confirmed = confirm(t('apiKeys.batchApiKeyModal.closeReminderMessage'))
if (confirmed) {
emit('close')
}
@@ -307,17 +326,17 @@ const handleClose = async () => {
const handleDirectClose = async () => {
if (window.showConfirm) {
const confirmed = await window.showConfirm(
'确定要关闭吗?',
'您还没有下载 API Keys关闭后将无法再次查看。\n\n强烈建议您先下载保存。',
'仍然关闭',
'返回下载'
t('apiKeys.batchApiKeyModal.directCloseTitle'),
t('apiKeys.batchApiKeyModal.directCloseMessage'),
t('apiKeys.batchApiKeyModal.stillCloseButton'),
t('apiKeys.batchApiKeyModal.goBackDownloadButton')
)
if (confirmed) {
emit('close')
}
} else {
// 降级方案
const confirmed = confirm('您还没有下载 API Keys关闭后将无法再次查看。\n\n确定要关闭吗')
const confirmed = confirm(t('apiKeys.batchApiKeyModal.directCloseFallbackMessage'))
if (confirmed) {
emit('close')
}

View File

@@ -12,7 +12,7 @@
<i class="fas fa-edit text-sm text-white sm:text-base" />
</div>
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100 sm:text-xl">
批量编辑 API Keys ({{ selectedCount }} )
{{ $t('apiKeys.batchEditApiKeyModal.title', { count: selectedCount }) }}
</h3>
</div>
<button
@@ -32,10 +32,11 @@
<div class="flex items-start gap-3">
<i class="fas fa-info-circle mt-1 text-blue-500" />
<div>
<p class="text-sm font-medium text-blue-800 dark:text-blue-300">批量编辑说明</p>
<p class="text-sm font-medium text-blue-800 dark:text-blue-300">
{{ $t('apiKeys.batchEditApiKeyModal.infoTitle') }}
</p>
<p class="mt-1 text-sm text-blue-700 dark:text-blue-400">
以下设置将应用到所选的 {{ selectedCount }} API
Key只有填写或修改的字段才会被更新空白字段将保持原值不变
{{ $t('apiKeys.batchEditApiKeyModal.infoContent', { count: selectedCount }) }}
</p>
</div>
</div>
@@ -46,26 +47,34 @@
<label
class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-3 sm:text-sm"
>
标签 (批量操作)
{{ $t('apiKeys.batchEditApiKeyModal.tagLabel') }}
</label>
<div class="space-y-4">
<!-- 标签操作模式选择 -->
<div class="flex flex-wrap gap-4">
<label class="flex cursor-pointer items-center">
<input v-model="tagOperation" class="mr-2" type="radio" value="replace" />
<span class="text-sm text-gray-700 dark:text-gray-300">替换标签</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{{
$t('apiKeys.batchEditApiKeyModal.tagOperations.replace')
}}</span>
</label>
<label class="flex cursor-pointer items-center">
<input v-model="tagOperation" class="mr-2" type="radio" value="add" />
<span class="text-sm text-gray-700 dark:text-gray-300">添加标签</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{{
$t('apiKeys.batchEditApiKeyModal.tagOperations.add')
}}</span>
</label>
<label class="flex cursor-pointer items-center">
<input v-model="tagOperation" class="mr-2" type="radio" value="remove" />
<span class="text-sm text-gray-700 dark:text-gray-300">移除标签</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{{
$t('apiKeys.batchEditApiKeyModal.tagOperations.remove')
}}</span>
</label>
<label class="flex cursor-pointer items-center">
<input v-model="tagOperation" class="mr-2" type="radio" value="none" />
<span class="text-sm text-gray-700 dark:text-gray-300">不修改标签</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{{
$t('apiKeys.batchEditApiKeyModal.tagOperations.none')
}}</span>
</label>
</div>
@@ -76,10 +85,10 @@
<div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
{{
tagOperation === 'replace'
? '新标签列表:'
? $t('apiKeys.batchEditApiKeyModal.newTagsList')
: tagOperation === 'add'
? '要添加的标签:'
: '要移除的标签:'
? $t('apiKeys.batchEditApiKeyModal.tagsToAdd')
: $t('apiKeys.batchEditApiKeyModal.tagsToRemove')
}}
</div>
<div class="flex flex-wrap gap-2">
@@ -103,7 +112,7 @@
<!-- 可选择的已有标签 -->
<div v-if="unselectedTags.length > 0">
<div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
点击选择已有标签:
{{ $t('apiKeys.batchEditApiKeyModal.clickToSelectTags') }}
</div>
<div class="flex flex-wrap gap-2">
<button
@@ -122,13 +131,13 @@
<!-- 创建新标签 -->
<div>
<div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
创建新标签:
{{ $t('apiKeys.batchEditApiKeyModal.createNewTag') }}
</div>
<div class="flex gap-2">
<input
v-model="newTag"
class="form-input flex-1 border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
placeholder="输入新标签名称"
:placeholder="$t('apiKeys.batchEditApiKeyModal.inputNewTagPlaceholder')"
type="text"
@keypress.enter.prevent="addTag"
/>
@@ -155,46 +164,48 @@
>
<i class="fas fa-tachometer-alt text-xs text-white" />
</div>
<h4 class="text-sm font-semibold text-gray-800 dark:text-gray-200">速率限制设置</h4>
<h4 class="text-sm font-semibold text-gray-800 dark:text-gray-200">
{{ $t('apiKeys.batchEditApiKeyModal.rateLimitTitle') }}
</h4>
</div>
<div class="space-y-2">
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3">
<div>
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">
时间窗口 (分钟)
{{ $t('apiKeys.batchEditApiKeyModal.rateLimitWindow') }}
</label>
<input
v-model="form.rateLimitWindow"
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
min="1"
placeholder="不修改"
:placeholder="$t('apiKeys.batchEditApiKeyModal.noModifyPlaceholder')"
type="number"
/>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300"
>请求次数限制</label
>
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">{{
$t('apiKeys.batchEditApiKeyModal.rateLimitRequests')
}}</label>
<input
v-model="form.rateLimitRequests"
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
min="1"
placeholder="不修改"
:placeholder="$t('apiKeys.batchEditApiKeyModal.noModifyPlaceholder')"
type="number"
/>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300"
>费用限制 (美元)</label
>
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">{{
$t('apiKeys.batchEditApiKeyModal.rateLimitCost')
}}</label>
<input
v-model="form.rateLimitCost"
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
min="0"
placeholder="不修改"
:placeholder="$t('apiKeys.batchEditApiKeyModal.noModifyPlaceholder')"
step="0.01"
type="number"
/>
@@ -206,13 +217,13 @@
<!-- 每日费用限制 -->
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
每日费用限制 (美元)
{{ $t('apiKeys.batchEditApiKeyModal.dailyCostLimit') }}
</label>
<input
v-model="form.dailyCostLimit"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
min="0"
placeholder="不修改 (0 表示无限制)"
:placeholder="$t('apiKeys.batchEditApiKeyModal.dailyCostLimitPlaceholder')"
step="0.01"
type="number"
/>
@@ -221,31 +232,31 @@
<!-- Opus 模型周费用限制 -->
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
Opus 模型周费用限制 (美元)
{{ $t('apiKeys.batchEditApiKeyModal.weeklyOpusCostLimit') }}
</label>
<input
v-model="form.weeklyOpusCostLimit"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
min="0"
placeholder="不修改 (0 表示无限制)"
:placeholder="$t('apiKeys.batchEditApiKeyModal.weeklyOpusCostLimitPlaceholder')"
step="0.01"
type="number"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
设置 Opus 模型的周费用限制周一到周日仅限 Claude 官方账户
{{ $t('apiKeys.batchEditApiKeyModal.opusLimitDescription') }}
</p>
</div>
<!-- 并发限制 -->
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>并发限制</label
>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
$t('apiKeys.batchEditApiKeyModal.concurrencyLimit')
}}</label>
<input
v-model="form.concurrencyLimit"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
min="0"
placeholder="不修改 (0 表示无限制)"
:placeholder="$t('apiKeys.batchEditApiKeyModal.concurrencyLimitPlaceholder')"
type="number"
/>
</div>
@@ -253,19 +264,27 @@
<!-- 激活状态 -->
<div>
<div class="mb-3 flex items-center gap-4">
<label class="text-sm font-semibold text-gray-700 dark:text-gray-300">激活状态</label>
<label class="text-sm font-semibold text-gray-700 dark:text-gray-300">{{
$t('apiKeys.batchEditApiKeyModal.activeStatus')
}}</label>
<div class="flex gap-4">
<label class="flex cursor-pointer items-center">
<input v-model="form.isActive" class="mr-2" type="radio" :value="true" />
<span class="text-sm text-gray-700 dark:text-gray-300">激活</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{{
$t('apiKeys.batchEditApiKeyModal.statusOptions.active')
}}</span>
</label>
<label class="flex cursor-pointer items-center">
<input v-model="form.isActive" class="mr-2" type="radio" :value="false" />
<span class="text-sm text-gray-700 dark:text-gray-300">禁用</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{{
$t('apiKeys.batchEditApiKeyModal.statusOptions.disabled')
}}</span>
</label>
<label class="flex cursor-pointer items-center">
<input v-model="form.isActive" class="mr-2" type="radio" :value="null" />
<span class="text-sm text-gray-700 dark:text-gray-300">不修改</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{{
$t('apiKeys.batchEditApiKeyModal.statusOptions.noChange')
}}</span>
</label>
</div>
</div>
@@ -273,29 +292,39 @@
<!-- 服务权限 -->
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>服务权限</label
>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
$t('apiKeys.batchEditApiKeyModal.servicePermissions')
}}</label>
<div class="flex flex-wrap gap-4">
<label class="flex cursor-pointer items-center">
<input v-model="form.permissions" class="mr-2" type="radio" value="" />
<span class="text-sm text-gray-700">不修改</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{{
$t('apiKeys.batchEditApiKeyModal.permissionOptions.noChange')
}}</span>
</label>
<label class="flex cursor-pointer items-center">
<input v-model="form.permissions" class="mr-2" type="radio" value="all" />
<span class="text-sm text-gray-700">全部服务</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{{
$t('apiKeys.batchEditApiKeyModal.permissionOptions.all')
}}</span>
</label>
<label class="flex cursor-pointer items-center">
<input v-model="form.permissions" class="mr-2" type="radio" value="claude" />
<span class="text-sm text-gray-700"> Claude</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{{
$t('apiKeys.batchEditApiKeyModal.permissionOptions.claude')
}}</span>
</label>
<label class="flex cursor-pointer items-center">
<input v-model="form.permissions" class="mr-2" type="radio" value="gemini" />
<span class="text-sm text-gray-700"> Gemini</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{{
$t('apiKeys.batchEditApiKeyModal.permissionOptions.gemini')
}}</span>
</label>
<label class="flex cursor-pointer items-center">
<input v-model="form.permissions" class="mr-2" type="radio" value="openai" />
<span class="text-sm text-gray-700"> OpenAI</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{{
$t('apiKeys.batchEditApiKeyModal.permissionOptions.openai')
}}</span>
</label>
</div>
</div>
@@ -303,13 +332,13 @@
<!-- 专属账号绑定 -->
<div>
<div class="mb-3 flex items-center justify-between">
<label class="text-sm font-semibold text-gray-700 dark:text-gray-300"
>专属账号绑定</label
>
<label class="text-sm font-semibold text-gray-700 dark:text-gray-300">{{
$t('apiKeys.batchEditApiKeyModal.accountBinding')
}}</label>
<button
class="flex items-center gap-1 text-sm text-blue-600 transition-colors hover:text-blue-800 disabled:cursor-not-allowed disabled:opacity-50 dark:text-blue-400 dark:hover:text-blue-300"
:disabled="accountsLoading"
title="刷新账号列表"
:title="$t('apiKeys.batchEditApiKeyModal.refreshAccounts')"
type="button"
@click="refreshAccounts"
>
@@ -320,31 +349,46 @@
'text-xs'
]"
/>
<span>{{ accountsLoading ? '刷新中...' : '刷新账号' }}</span>
<span>{{
accountsLoading
? $t('apiKeys.batchEditApiKeyModal.refreshing')
: $t('apiKeys.batchEditApiKeyModal.refreshAccounts')
}}</span>
</button>
</div>
<div class="grid grid-cols-1 gap-3">
<div>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
>Claude 专属账号</label
>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
$t('apiKeys.batchEditApiKeyModal.claudeAccount')
}}</label>
<select
v-model="form.claudeAccountId"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
>
<option value="">不修改</option>
<option value="SHARED_POOL">使用共享账号池</option>
<optgroup v-if="localAccounts.claudeGroups.length > 0" label="账号分组">
<option value="">
{{ $t('apiKeys.batchEditApiKeyModal.accountOptions.noChange') }}
</option>
<option value="SHARED_POOL">
{{ $t('apiKeys.batchEditApiKeyModal.accountOptions.sharedPool') }}
</option>
<optgroup
v-if="localAccounts.claudeGroups.length > 0"
:label="$t('apiKeys.batchEditApiKeyModal.optgroupLabels.accountGroups')"
>
<option
v-for="group in localAccounts.claudeGroups"
:key="group.id"
:value="`group:${group.id}`"
>
分组 - {{ group.name }}
{{ $t('apiKeys.batchEditApiKeyModal.accountOptions.groupPrefix')
}}{{ group.name }}
</option>
</optgroup>
<optgroup v-if="localAccounts.claude.length > 0" label="专属账号">
<optgroup
v-if="localAccounts.claude.length > 0"
:label="$t('apiKeys.batchEditApiKeyModal.optgroupLabels.dedicatedAccounts')"
>
<option
v-for="account in localAccounts.claude"
:key="account.id"
@@ -360,26 +404,37 @@
</select>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
>Gemini 专属账号</label
>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
$t('apiKeys.batchEditApiKeyModal.geminiAccount')
}}</label>
<select
v-model="form.geminiAccountId"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
:disabled="form.permissions === 'claude' || form.permissions === 'openai'"
>
<option value="">不修改</option>
<option value="SHARED_POOL">使用共享账号池</option>
<optgroup v-if="localAccounts.geminiGroups.length > 0" label="账号分组">
<option value="">
{{ $t('apiKeys.batchEditApiKeyModal.accountOptions.noChange') }}
</option>
<option value="SHARED_POOL">
{{ $t('apiKeys.batchEditApiKeyModal.accountOptions.sharedPool') }}
</option>
<optgroup
v-if="localAccounts.geminiGroups.length > 0"
:label="$t('apiKeys.batchEditApiKeyModal.optgroupLabels.accountGroups')"
>
<option
v-for="group in localAccounts.geminiGroups"
:key="group.id"
:value="`group:${group.id}`"
>
分组 - {{ group.name }}
{{ $t('apiKeys.batchEditApiKeyModal.accountOptions.groupPrefix')
}}{{ group.name }}
</option>
</optgroup>
<optgroup v-if="localAccounts.gemini.length > 0" label="专属账号">
<optgroup
v-if="localAccounts.gemini.length > 0"
:label="$t('apiKeys.batchEditApiKeyModal.optgroupLabels.dedicatedAccounts')"
>
<option
v-for="account in localAccounts.gemini"
:key="account.id"
@@ -391,26 +446,37 @@
</select>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
>OpenAI 专属账号</label
>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
$t('apiKeys.batchEditApiKeyModal.openaiAccount')
}}</label>
<select
v-model="form.openaiAccountId"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
:disabled="form.permissions === 'claude' || form.permissions === 'gemini'"
>
<option value="">不修改</option>
<option value="SHARED_POOL">使用共享账号池</option>
<optgroup v-if="localAccounts.openaiGroups.length > 0" label="账号分组">
<option value="">
{{ $t('apiKeys.batchEditApiKeyModal.accountOptions.noChange') }}
</option>
<option value="SHARED_POOL">
{{ $t('apiKeys.batchEditApiKeyModal.accountOptions.sharedPool') }}
</option>
<optgroup
v-if="localAccounts.openaiGroups.length > 0"
:label="$t('apiKeys.batchEditApiKeyModal.optgroupLabels.accountGroups')"
>
<option
v-for="group in localAccounts.openaiGroups"
:key="group.id"
:value="`group:${group.id}`"
>
分组 - {{ group.name }}
{{ $t('apiKeys.batchEditApiKeyModal.accountOptions.groupPrefix')
}}{{ group.name }}
</option>
</optgroup>
<optgroup v-if="localAccounts.openai.length > 0" label="专属账号">
<optgroup
v-if="localAccounts.openai.length > 0"
:label="$t('apiKeys.batchEditApiKeyModal.optgroupLabels.dedicatedAccounts')"
>
<option
v-for="account in localAccounts.openai"
:key="account.id"
@@ -422,17 +488,24 @@
</select>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
>Bedrock 专属账号</label
>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
$t('apiKeys.batchEditApiKeyModal.bedrockAccount')
}}</label>
<select
v-model="form.bedrockAccountId"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
>
<option value="">不修改</option>
<option value="SHARED_POOL">使用共享账号池</option>
<optgroup v-if="localAccounts.bedrock.length > 0" label="专属账号">
<option value="">
{{ $t('apiKeys.batchEditApiKeyModal.accountOptions.noChange') }}
</option>
<option value="SHARED_POOL">
{{ $t('apiKeys.batchEditApiKeyModal.accountOptions.sharedPool') }}
</option>
<optgroup
v-if="localAccounts.bedrock.length > 0"
:label="$t('apiKeys.batchEditApiKeyModal.optgroupLabels.dedicatedAccounts')"
>
<option
v-for="account in localAccounts.bedrock"
:key="account.id"
@@ -452,7 +525,7 @@
type="button"
@click="$emit('close')"
>
取消
{{ $t('apiKeys.batchEditApiKeyModal.cancel') }}
</button>
<button
class="btn btn-primary flex-1 px-6 py-3 font-semibold"
@@ -461,7 +534,11 @@
>
<div v-if="loading" class="loading-spinner mr-2" />
<i v-else class="fas fa-save mr-2" />
{{ loading ? '保存中...' : '批量保存' }}
{{
loading
? $t('apiKeys.batchEditApiKeyModal.saving')
: $t('apiKeys.batchEditApiKeyModal.batchSave')
}}
</button>
</div>
</form>
@@ -472,10 +549,13 @@
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { showToast } from '@/utils/toast'
import { useApiKeysStore } from '@/stores/apiKeys'
import { apiClient } from '@/config/api'
const { t } = useI18n()
const props = defineProps({
selectedKeys: {
type: Array,
@@ -620,9 +700,9 @@ const refreshAccounts = async () => {
localAccounts.value.openaiGroups = allGroups.filter((g) => g.platform === 'openai')
}
showToast('账号列表已刷新', 'success')
showToast(t('apiKeys.batchEditApiKeyModal.refreshAccountsSuccess'), 'success')
} catch (error) {
showToast('刷新账号列表失败', 'error')
showToast(t('apiKeys.batchEditApiKeyModal.refreshAccountsFailed'), 'error')
} finally {
accountsLoading.value = false
}
@@ -722,24 +802,33 @@ const batchUpdateApiKeys = async () => {
const { successCount, failedCount, errors } = result.data
if (successCount > 0) {
showToast(`成功批量编辑 ${successCount} 个 API Keys`, 'success')
showToast(
t('apiKeys.batchEditApiKeyModal.batchEditSuccess', { count: successCount }),
'success'
)
if (failedCount > 0) {
const errorMessages = errors.map((e) => `${e.keyId}: ${e.error}`).join('\n')
showToast(`${failedCount} 个编辑失败:\n${errorMessages}`, 'warning')
showToast(
t('apiKeys.batchEditApiKeyModal.batchEditPartialFail', {
failedCount,
errors: errorMessages
}),
'warning'
)
}
} else {
showToast('所有 API Keys 编辑失败', 'error')
showToast(t('apiKeys.batchEditApiKeyModal.batchEditAllFailed'), 'error')
}
emit('success')
emit('close')
} else {
showToast(result.message || '批量编辑失败', 'error')
showToast(result.message || t('apiKeys.batchEditApiKeyModal.batchEditFailed'), 'error')
}
} catch (error) {
showToast('批量编辑失败', 'error')
console.error('批量编辑 API Keys 失败:', error)
showToast(t('apiKeys.batchEditApiKeyModal.batchEditFailed'), 'error')
console.error(t('apiKeys.batchEditApiKeyModal.batchEditErrorLog'), error)
} finally {
loading.value = false
}

View File

@@ -10,7 +10,7 @@
<i class="fas fa-key text-sm text-white sm:text-base" />
</div>
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100 sm:text-xl">
创建新的 API Key
{{ $t('apiKeys.createApiKeyModal.title') }}
</h3>
</div>
<button
@@ -37,7 +37,7 @@
>
<label
class="flex h-full items-center text-xs font-semibold text-gray-700 dark:text-gray-300 sm:text-sm"
>创建类型</label
>{{ $t('apiKeys.createApiKeyModal.createType') }}</label
>
<div class="flex items-center gap-3 sm:gap-4">
<label class="flex cursor-pointer items-center">
@@ -51,7 +51,7 @@
class="flex items-center text-xs text-gray-700 dark:text-gray-300 sm:text-sm"
>
<i class="fas fa-key mr-1 text-xs" />
单个创建
{{ $t('apiKeys.createApiKeyModal.singleCreate') }}
</span>
</label>
<label class="flex cursor-pointer items-center">
@@ -65,7 +65,7 @@
class="flex items-center text-xs text-gray-700 dark:text-gray-300 sm:text-sm"
>
<i class="fas fa-layer-group mr-1 text-xs" />
批量创建
{{ $t('apiKeys.createApiKeyModal.batchCreate') }}
</span>
</label>
</div>
@@ -75,32 +75,30 @@
<div v-if="form.createType === 'batch'" class="mt-3">
<div class="flex items-center gap-4">
<div class="flex-1">
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400"
>创建数量</label
>
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">{{
$t('apiKeys.createApiKeyModal.batchCount')
}}</label>
<div class="flex items-center gap-2">
<input
v-model.number="form.batchCount"
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
max="500"
min="2"
placeholder="输入数量 (2-500)"
:placeholder="$t('apiKeys.createApiKeyModal.batchCountPlaceholder')"
required
type="number"
/>
<div class="whitespace-nowrap text-xs text-gray-500 dark:text-gray-400">
最大支持 500
{{ $t('apiKeys.createApiKeyModal.maxSupported') }}
</div>
</div>
</div>
</div>
<p class="mt-2 flex items-start text-xs text-amber-600 dark:text-amber-400">
<i class="fas fa-info-circle mr-1 mt-0.5 flex-shrink-0" />
<span
>批量创建时每个 Key 的名称会自动添加序号后缀例如{{
form.name || 'MyKey'
}}_1, {{ form.name || 'MyKey' }}_2 ...</span
>
<span>{{
$t('apiKeys.createApiKeyModal.batchHint', { name: form.name || 'MyKey' })
}}</span>
</p>
</div>
</div>
@@ -108,23 +106,24 @@
<div>
<label
class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-2 sm:text-sm"
>名称 <span class="text-red-500">*</span></label
>{{ $t('apiKeys.createApiKeyModal.name') }}
<span class="text-red-500">{{
$t('apiKeys.createApiKeyModal.nameRequired')
}}</span></label
>
<div>
<input
v-model="form.name"
class="form-input flex-1 border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
:class="{ 'border-red-500': errors.name }"
:placeholder="
form.createType === 'batch'
? '输入基础名称(将自动添加序号)'
: '为您的 API Key 取一个名称'
? $t('apiKeys.createApiKeyModal.batchNamePlaceholder')
: $t('apiKeys.createApiKeyModal.singleNamePlaceholder')
"
required
type="text"
@input="errors.name = ''"
/>
</div>
<p v-if="errors.name" class="mt-1 text-xs text-red-500 dark:text-red-400">
{{ errors.name }}
</p>
@@ -132,14 +131,14 @@
<!-- 标签 -->
<div>
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>标签</label
>
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
$t('apiKeys.createApiKeyModal.tags')
}}</label>
<div class="space-y-4">
<!-- 已选择的标签 -->
<div v-if="form.tags.length > 0">
<div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
已选择的标签:
{{ $t('apiKeys.createApiKeyModal.selectedTags') }}
</div>
<div class="flex flex-wrap gap-2">
<span
@@ -162,7 +161,7 @@
<!-- 可选择的已有标签 -->
<div v-if="unselectedTags.length > 0">
<div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
点击选择已有标签:
{{ $t('apiKeys.createApiKeyModal.clickToSelectTags') }}
</div>
<div class="flex flex-wrap gap-2">
<button
@@ -181,13 +180,13 @@
<!-- 创建新标签 -->
<div>
<div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
创建新标签:
{{ $t('apiKeys.createApiKeyModal.createNewTag') }}
</div>
<div class="flex gap-2">
<input
v-model="newTag"
class="form-input flex-1 border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
placeholder="输入新标签名称"
:placeholder="$t('apiKeys.createApiKeyModal.newTagPlaceholder')"
type="text"
@keypress.enter.prevent="addTag"
/>
@@ -202,7 +201,7 @@
</div>
<p class="text-xs text-gray-500 dark:text-gray-400">
用于标记不同团队或用途方便筛选管理
{{ $t('apiKeys.createApiKeyModal.tagHint') }}
</p>
</div>
</div>
@@ -218,68 +217,76 @@
<i class="fas fa-tachometer-alt text-xs text-white" />
</div>
<h4 class="text-sm font-semibold text-gray-800 dark:text-gray-200">
速率限制设置 (可选)
{{ $t('apiKeys.createApiKeyModal.rateLimitTitle') }}
</h4>
</div>
<div class="space-y-2">
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3">
<div>
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300"
>时间窗口 (分钟)</label
>
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">{{
$t('apiKeys.createApiKeyModal.rateLimitWindow')
}}</label>
<input
v-model="form.rateLimitWindow"
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
min="1"
placeholder="无限制"
:placeholder="$t('apiKeys.createApiKeyModal.rateLimitWindowPlaceholder')"
type="number"
/>
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">时间段单位</p>
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{{ $t('apiKeys.createApiKeyModal.rateLimitWindowHint') }}
</p>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300"
>请求次数限制</label
>
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">{{
$t('apiKeys.createApiKeyModal.rateLimitRequests')
}}</label>
<input
v-model="form.rateLimitRequests"
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
min="1"
placeholder="无限制"
:placeholder="$t('apiKeys.createApiKeyModal.rateLimitRequestsPlaceholder')"
type="number"
/>
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">窗口内最大请求</p>
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{{ $t('apiKeys.createApiKeyModal.rateLimitRequestsHint') }}
</p>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300"
>费用限制 (美元)</label
>
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">{{
$t('apiKeys.createApiKeyModal.rateLimitCost')
}}</label>
<input
v-model="form.rateLimitCost"
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
min="0"
placeholder="无限制"
:placeholder="$t('apiKeys.createApiKeyModal.rateLimitCostPlaceholder')"
step="0.01"
type="number"
/>
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">窗口内最大费用</p>
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{{ $t('apiKeys.createApiKeyModal.rateLimitCostHint') }}
</p>
</div>
</div>
<!-- 示例说明 -->
<div class="rounded-lg bg-blue-100 p-2 dark:bg-blue-900/30">
<h5 class="mb-1 text-xs font-semibold text-blue-800 dark:text-blue-400">
💡 使用示例
{{ $t('apiKeys.createApiKeyModal.exampleTitle') }}
</h5>
<div class="space-y-0.5 text-xs text-blue-700 dark:text-blue-300">
<div>
<strong>示例1:</strong> 时间窗口=60请求次数=1000 每60分钟最多1000次请求
<strong>{{ $t('apiKeys.createApiKeyModal.example1') }}</strong>
</div>
<div><strong>示例2:</strong> 时间窗口=1费用=0.1 每分钟最多$0.1费用</div>
<div>
<strong>示例3:</strong> 窗口=30请求=50费用=5 每30分钟50次请求且不超$5费用
<strong>{{ $t('apiKeys.createApiKeyModal.example2') }}</strong>
</div>
<div>
<strong>{{ $t('apiKeys.createApiKeyModal.example3') }}</strong>
</div>
</div>
</div>
@@ -287,9 +294,9 @@
</div>
<div>
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>每日费用限制 (美元)</label
>
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
$t('apiKeys.createApiKeyModal.dailyCostLimit')
}}</label>
<div class="space-y-2">
<div class="flex gap-2">
<button
@@ -318,27 +325,27 @@
type="button"
@click="form.dailyCostLimit = ''"
>
自定义
{{ $t('apiKeys.createApiKeyModal.custom') }}
</button>
</div>
<input
v-model="form.dailyCostLimit"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
min="0"
placeholder="0 表示无限制"
:placeholder="$t('apiKeys.createApiKeyModal.dailyCostLimitPlaceholder')"
step="0.01"
type="number"
/>
<p class="text-xs text-gray-500 dark:text-gray-400">
设置此 API Key 每日的费用限制超过限制将拒绝请求0 或留空表示无限制
{{ $t('apiKeys.createApiKeyModal.dailyCostHint') }}
</p>
</div>
</div>
<div>
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>Opus 模型周费用限制 (美元)</label
>
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
$t('apiKeys.createApiKeyModal.weeklyOpusCostLimit')
}}</label>
<div class="space-y-2">
<div class="flex gap-2">
<button
@@ -367,55 +374,55 @@
type="button"
@click="form.weeklyOpusCostLimit = ''"
>
自定义
{{ $t('apiKeys.createApiKeyModal.custom') }}
</button>
</div>
<input
v-model="form.weeklyOpusCostLimit"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
min="0"
placeholder="0 表示无限制"
:placeholder="$t('apiKeys.createApiKeyModal.weeklyOpusCostLimitPlaceholder')"
step="0.01"
type="number"
/>
<p class="text-xs text-gray-500 dark:text-gray-400">
设置 Opus 模型的周费用限制周一到周日仅限 Claude 官方账户0 或留空表示无限制
{{ $t('apiKeys.createApiKeyModal.weeklyOpusHint') }}
</p>
</div>
</div>
<div>
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>并发限制 (可选)</label
>
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
$t('apiKeys.createApiKeyModal.concurrencyLimit')
}}</label>
<input
v-model="form.concurrencyLimit"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
min="0"
placeholder="0 表示无限制"
:placeholder="$t('apiKeys.createApiKeyModal.concurrencyLimitPlaceholder')"
type="number"
/>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
设置此 API Key 可同时处理的最大请求数0 或留空表示无限制
{{ $t('apiKeys.createApiKeyModal.concurrencyHint') }}
</p>
</div>
<div>
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>备注 (可选)</label
>
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
$t('apiKeys.createApiKeyModal.description')
}}</label>
<textarea
v-model="form.description"
class="form-input w-full resize-none border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
placeholder="描述此 API Key 的用途..."
:placeholder="$t('apiKeys.createApiKeyModal.descriptionPlaceholder')"
rows="2"
/>
</div>
<div>
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>过期设置</label
>
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
$t('apiKeys.createApiKeyModal.expirationSettings')
}}</label>
<!-- 过期模式选择 -->
<div
class="mb-3 rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800"
@@ -428,7 +435,9 @@
type="radio"
value="fixed"
/>
<span class="text-sm text-gray-700 dark:text-gray-300">固定时间过期</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{{
$t('apiKeys.createApiKeyModal.fixedTimeExpiry')
}}</span>
</label>
<label class="flex cursor-pointer items-center">
<input
@@ -437,17 +446,19 @@
type="radio"
value="activation"
/>
<span class="text-sm text-gray-700 dark:text-gray-300">首次使用后激活</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{{
$t('apiKeys.createApiKeyModal.activationExpiry')
}}</span>
</label>
</div>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
<span v-if="form.expirationMode === 'fixed'">
<i class="fas fa-info-circle mr-1" />
固定时间模式Key 创建后立即生效按设定时间过期
{{ $t('apiKeys.createApiKeyModal.fixedModeHint') }}
</span>
<span v-else>
<i class="fas fa-info-circle mr-1" />
激活模式Key 首次使用时激活激活后按设定天数过期适合批量销售
{{ $t('apiKeys.createApiKeyModal.activationModeHint') }}
</span>
</p>
</div>
@@ -459,14 +470,14 @@
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
@change="updateExpireAt"
>
<option value="">永不过期</option>
<option value="1d">1 </option>
<option value="7d">7 </option>
<option value="30d">30 </option>
<option value="90d">90 </option>
<option value="180d">180 </option>
<option value="365d">365 </option>
<option value="custom">自定义日期</option>
<option value="">{{ $t('apiKeys.createApiKeyModal.neverExpire') }}</option>
<option value="1d">{{ $t('apiKeys.createApiKeyModal.1d') }}</option>
<option value="7d">{{ $t('apiKeys.createApiKeyModal.7d') }}</option>
<option value="30d">{{ $t('apiKeys.createApiKeyModal.30d') }}</option>
<option value="90d">{{ $t('apiKeys.createApiKeyModal.90d') }}</option>
<option value="180d">{{ $t('apiKeys.createApiKeyModal.180d') }}</option>
<option value="365d">{{ $t('apiKeys.createApiKeyModal.365d') }}</option>
<option value="custom">{{ $t('apiKeys.createApiKeyModal.customDate') }}</option>
</select>
<div v-if="form.expireDuration === 'custom'" class="mt-3">
<input
@@ -478,7 +489,11 @@
/>
</div>
<p v-if="form.expiresAt" class="mt-2 text-xs text-gray-500 dark:text-gray-400">
将于 {{ formatExpireDate(form.expiresAt) }} 过期
{{
$t('apiKeys.createApiKeyModal.willExpireOn', {
date: formatExpireDate(form.expiresAt)
})
}}
</p>
</div>
@@ -490,10 +505,12 @@
class="form-input flex-1 border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
max="3650"
min="1"
placeholder="输入天数"
:placeholder="$t('apiKeys.createApiKeyModal.activationDays')"
type="number"
/>
<span class="text-sm text-gray-600 dark:text-gray-400"></span>
<span class="text-sm text-gray-600 dark:text-gray-400">{{
$t('apiKeys.createApiKeyModal.daysUnit')
}}</span>
</div>
<div class="mt-2 flex flex-wrap gap-2">
<button
@@ -503,20 +520,24 @@
type="button"
@click="form.activationDays = days"
>
{{ days }}
{{ days }}{{ $t('apiKeys.createApiKeyModal.daysUnit') }}
</button>
</div>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
<i class="fas fa-clock mr-1" />
Key 将在首次使用后激活激活后 {{ form.activationDays || 30 }} 天过期
{{
$t('apiKeys.createApiKeyModal.activationHint', {
days: form.activationDays || 30
})
}}
</p>
</div>
</div>
<div>
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>服务权限</label
>
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
$t('apiKeys.createApiKeyModal.servicePermissions')
}}</label>
<div class="flex gap-4">
<label class="flex cursor-pointer items-center">
<input
@@ -525,7 +546,9 @@
type="radio"
value="all"
/>
<span class="text-sm text-gray-700 dark:text-gray-300">全部服务</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{{
$t('apiKeys.createApiKeyModal.allServices')
}}</span>
</label>
<label class="flex cursor-pointer items-center">
<input
@@ -534,7 +557,9 @@
type="radio"
value="claude"
/>
<span class="text-sm text-gray-700 dark:text-gray-300"> Claude</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{{
$t('apiKeys.createApiKeyModal.claudeOnly')
}}</span>
</label>
<label class="flex cursor-pointer items-center">
<input
@@ -543,7 +568,9 @@
type="radio"
value="gemini"
/>
<span class="text-sm text-gray-700 dark:text-gray-300"> Gemini</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{{
$t('apiKeys.createApiKeyModal.geminiOnly')
}}</span>
</label>
<label class="flex cursor-pointer items-center">
<input
@@ -552,23 +579,25 @@
type="radio"
value="openai"
/>
<span class="text-sm text-gray-700 dark:text-gray-300"> OpenAI</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{{
$t('apiKeys.createApiKeyModal.openaiOnly')
}}</span>
</label>
</div>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
控制此 API Key 可以访问哪些服务
{{ $t('apiKeys.createApiKeyModal.permissionHint') }}
</p>
</div>
<div>
<div class="mb-2 flex items-center justify-between">
<label class="text-sm font-semibold text-gray-700 dark:text-gray-300"
>专属账号绑定 (可选)</label
>
<label class="text-sm font-semibold text-gray-700 dark:text-gray-300">{{
$t('apiKeys.createApiKeyModal.dedicatedAccountBinding')
}}</label>
<button
class="flex items-center gap-1 text-sm text-blue-600 transition-colors hover:text-blue-800 disabled:cursor-not-allowed disabled:opacity-50 dark:text-blue-400 dark:hover:text-blue-300"
:disabled="accountsLoading"
title="刷新账号列表"
title="{{ $t('apiKeys.createApiKeyModal.refreshAccounts') }}"
type="button"
@click="refreshAccounts"
>
@@ -579,69 +608,73 @@
'text-xs'
]"
/>
<span>{{ accountsLoading ? '刷新中...' : '刷新账号' }}</span>
<span>{{
accountsLoading
? $t('apiKeys.createApiKeyModal.refreshing')
: $t('apiKeys.createApiKeyModal.refreshAccounts')
}}</span>
</button>
</div>
<div class="grid grid-cols-1 gap-3">
<div>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
>Claude 专属账号</label
>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
$t('apiKeys.createApiKeyModal.claudeDedicatedAccount')
}}</label>
<AccountSelector
v-model="form.claudeAccountId"
:accounts="localAccounts.claude"
default-option-text="使用共享账号池"
:default-option-text="$t('apiKeys.createApiKeyModal.useSharedPool')"
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
:groups="localAccounts.claudeGroups"
placeholder="请选择Claude账号"
:placeholder="$t('apiKeys.createApiKeyModal.selectClaudeAccount')"
platform="claude"
/>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
>Gemini 专属账号</label
>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
$t('apiKeys.createApiKeyModal.geminiDedicatedAccount')
}}</label>
<AccountSelector
v-model="form.geminiAccountId"
:accounts="localAccounts.gemini"
default-option-text="使用共享账号池"
:default-option-text="$t('apiKeys.createApiKeyModal.useSharedPool')"
:disabled="form.permissions === 'claude' || form.permissions === 'openai'"
:groups="localAccounts.geminiGroups"
placeholder="请选择Gemini账号"
:placeholder="$t('apiKeys.createApiKeyModal.selectGeminiAccount')"
platform="gemini"
/>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
>OpenAI 专属账号</label
>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
$t('apiKeys.createApiKeyModal.openaiDedicatedAccount')
}}</label>
<AccountSelector
v-model="form.openaiAccountId"
:accounts="localAccounts.openai"
default-option-text="使用共享账号池"
:default-option-text="$t('apiKeys.createApiKeyModal.useSharedPool')"
:disabled="form.permissions === 'claude' || form.permissions === 'gemini'"
:groups="localAccounts.openaiGroups"
placeholder="请选择OpenAI账号"
:placeholder="$t('apiKeys.createApiKeyModal.selectOpenaiAccount')"
platform="openai"
/>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
>Bedrock 专属账号</label
>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
$t('apiKeys.createApiKeyModal.bedrockDedicatedAccount')
}}</label>
<AccountSelector
v-model="form.bedrockAccountId"
:accounts="localAccounts.bedrock"
default-option-text="使用共享账号池"
:default-option-text="$t('apiKeys.createApiKeyModal.useSharedPool')"
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
:groups="[]"
placeholder="请选择Bedrock账号"
:placeholder="$t('apiKeys.createApiKeyModal.selectBedrockAccount')"
platform="bedrock"
/>
</div>
</div>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
选择专属账号后此API Key将只使用该账号不选择则使用共享账号池
{{ $t('apiKeys.createApiKeyModal.accountBindingHint') }}
</p>
</div>
@@ -657,13 +690,15 @@
class="ml-2 cursor-pointer text-sm font-semibold text-gray-700 dark:text-gray-300"
for="enableModelRestriction"
>
启用模型限制
{{ $t('apiKeys.createApiKeyModal.enableModelRestriction') }}
</label>
</div>
<div v-if="form.enableModelRestriction" class="space-y-3">
<div>
<label class="mb-2 block text-sm font-medium text-gray-600">限制的模型列表</label>
<label class="mb-2 block text-sm font-medium text-gray-600">{{
$t('apiKeys.createApiKeyModal.restrictedModelsList')
}}</label>
<div
class="mb-3 flex min-h-[32px] flex-wrap gap-2 rounded-lg border border-gray-200 bg-gray-50 p-2"
>
@@ -682,7 +717,7 @@
</button>
</span>
<span v-if="form.restrictedModels.length === 0" class="text-sm text-gray-400">
暂无限制的模型
{{ $t('apiKeys.createApiKeyModal.noRestrictedModels') }}
</span>
</div>
<div class="space-y-3">
@@ -701,7 +736,7 @@
v-if="availableQuickModels.length === 0"
class="text-sm italic text-gray-400"
>
所有常用模型已在限制列表中
{{ $t('apiKeys.createApiKeyModal.allCommonModelsRestricted') }}
</span>
</div>
@@ -710,7 +745,7 @@
<input
v-model="form.modelInput"
class="form-input flex-1"
placeholder="输入模型名称,按回车添加"
:placeholder="$t('apiKeys.createApiKeyModal.addRestrictedModelPlaceholder')"
type="text"
@keydown.enter.prevent="addRestrictedModel"
/>
@@ -724,7 +759,7 @@
</div>
</div>
<p class="mt-2 text-xs text-gray-500">
设置此API Key无法访问的模型例如claude-opus-4-20250514
{{ $t('apiKeys.createApiKeyModal.modelRestrictionHint') }}
</p>
</div>
</div>
@@ -743,7 +778,7 @@
class="ml-2 cursor-pointer text-sm font-semibold text-gray-700 dark:text-gray-300"
for="enableClientRestriction"
>
启用客户端限制
{{ $t('apiKeys.createApiKeyModal.enableClientRestriction') }}
</label>
</div>
@@ -752,9 +787,9 @@
class="rounded-lg border border-green-200 bg-green-50 p-3 dark:border-green-700 dark:bg-green-900/20"
>
<div>
<label class="mb-2 block text-xs font-medium text-gray-700 dark:text-gray-300"
>允许的客户端</label
>
<label class="mb-2 block text-xs font-medium text-gray-700 dark:text-gray-300">{{
$t('apiKeys.createApiKeyModal.allowedClients')
}}</label>
<div class="space-y-1">
<div v-for="client in supportedClients" :key="client.id" class="flex items-start">
<input
@@ -784,7 +819,7 @@
type="button"
@click="$emit('close')"
>
取消
{{ $t('apiKeys.createApiKeyModal.cancel') }}
</button>
<button
class="btn btn-primary flex-1 px-4 py-2.5 text-sm font-semibold"
@@ -793,7 +828,11 @@
>
<div v-if="loading" class="loading-spinner mr-2" />
<i v-else class="fas fa-plus mr-2" />
{{ loading ? '创建中...' : '创建' }}
{{
loading
? $t('apiKeys.createApiKeyModal.creating')
: $t('apiKeys.createApiKeyModal.create')
}}
</button>
</div>
</form>
@@ -804,12 +843,15 @@
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { showToast } from '@/utils/toast'
import { useClientsStore } from '@/stores/clients'
import { useApiKeysStore } from '@/stores/apiKeys'
import { apiClient } from '@/config/api'
import AccountSelector from '@/components/common/AccountSelector.vue'
const { t } = useI18n()
const props = defineProps({
accounts: {
type: Object,
@@ -886,58 +928,28 @@ onMounted(async () => {
availableTags.value = await apiKeysStore.fetchTags()
// 初始化账号数据
if (props.accounts) {
// 合并 OpenAI 和 OpenAI-Responses 账号
const openaiAccounts = []
if (props.accounts.openai) {
props.accounts.openai.forEach((account) => {
openaiAccounts.push({
...account,
platform: 'openai'
})
})
}
if (props.accounts.openaiResponses) {
props.accounts.openaiResponses.forEach((account) => {
openaiAccounts.push({
...account,
platform: 'openai-responses'
})
})
}
localAccounts.value = {
claude: props.accounts.claude || [],
gemini: props.accounts.gemini || [],
openai: openaiAccounts,
openai: props.accounts.openai || [],
bedrock: props.accounts.bedrock || [], // 添加 Bedrock 账号
claudeGroups: props.accounts.claudeGroups || [],
geminiGroups: props.accounts.geminiGroups || [],
openaiGroups: props.accounts.openaiGroups || []
}
}
// 自动加载账号数据
await refreshAccounts()
})
// 刷新账号列表
const refreshAccounts = async () => {
accountsLoading.value = true
try {
const [
claudeData,
claudeConsoleData,
geminiData,
openaiData,
openaiResponsesData,
bedrockData,
groupsData
] = await Promise.all([
const [claudeData, claudeConsoleData, geminiData, openaiData, bedrockData, groupsData] =
await Promise.all([
apiClient.get('/admin/claude-accounts'),
apiClient.get('/admin/claude-console-accounts'),
apiClient.get('/admin/gemini-accounts'),
apiClient.get('/admin/openai-accounts'),
apiClient.get('/admin/openai-responses-accounts'), // 获取 OpenAI-Responses 账号
apiClient.get('/admin/bedrock-accounts'), // 添加 Bedrock 账号获取
apiClient.get('/admin/account-groups')
])
@@ -974,31 +986,13 @@ const refreshAccounts = async () => {
}))
}
// 合并 OpenAI 和 OpenAI-Responses 账号
const openaiAccounts = []
if (openaiData.success) {
;(openaiData.data || []).forEach((account) => {
openaiAccounts.push({
localAccounts.value.openai = (openaiData.data || []).map((account) => ({
...account,
platform: 'openai',
isDedicated: account.accountType === 'dedicated' // 保留以便向后兼容
})
})
}))
}
if (openaiResponsesData.success) {
;(openaiResponsesData.data || []).forEach((account) => {
openaiAccounts.push({
...account,
platform: 'openai-responses',
isDedicated: account.accountType === 'dedicated' // 保留以便向后兼容
})
})
}
localAccounts.value.openai = openaiAccounts
if (bedrockData.success) {
localAccounts.value.bedrock = (bedrockData.data || []).map((account) => ({
...account,
@@ -1014,9 +1008,9 @@ const refreshAccounts = async () => {
localAccounts.value.openaiGroups = allGroups.filter((g) => g.platform === 'openai')
}
showToast('账号列表已刷新', 'success')
showToast(t('apiKeys.createApiKeyModal.refreshAccountsSuccess'), 'success')
} catch (error) {
showToast('刷新账号列表失败', 'error')
showToast(t('apiKeys.createApiKeyModal.refreshAccountsFailed'), 'error')
} finally {
accountsLoading.value = false
}
@@ -1077,13 +1071,17 @@ const updateCustomExpireAt = () => {
// 格式化过期日期
const formatExpireDate = (dateString) => {
const date = new Date(dateString)
return date.toLocaleString('zh-CN', {
const { locale } = useI18n()
return date.toLocaleString(
locale.value === 'zh-cn' ? 'zh-CN' : locale.value === 'zh-tw' ? 'zh-TW' : 'en-US',
{
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
)
}
// 添加限制的模型
@@ -1141,14 +1139,14 @@ const createApiKey = async () => {
errors.value.name = ''
if (!form.name || !form.name.trim()) {
errors.value.name = '请输入API Key名称'
errors.value.name = t('apiKeys.createApiKeyModal.nameError')
return
}
// 批量创建时验证数量
if (form.createType === 'batch') {
if (!form.batchCount || form.batchCount < 2 || form.batchCount > 500) {
showToast('批量创建数量必须在 2-500 之间', 'error')
showToast(t('apiKeys.createApiKeyModal.batchCountError'), 'error')
return
}
}
@@ -1158,14 +1156,14 @@ const createApiKey = async () => {
let confirmed = false
if (window.showConfirm) {
confirmed = await window.showConfirm(
'费用限制提醒',
'您设置了时间窗口但费用限制为0这意味着不会有费用限制。\n\n是否继续',
'继续创建',
'返回修改'
t('apiKeys.createApiKeyModal.costLimitConfirmTitle'),
t('apiKeys.createApiKeyModal.costLimitConfirmMessage'),
t('apiKeys.createApiKeyModal.costLimitConfirmContinue'),
t('apiKeys.createApiKeyModal.costLimitConfirmBack')
)
} else {
// 降级方案
confirmed = confirm('您设置了时间窗口但费用限制为0这意味着不会有费用限制。\n是否继续')
confirmed = confirm(t('apiKeys.createApiKeyModal.costLimitFallbackMessage'))
}
if (!confirmed) {
return
@@ -1254,11 +1252,11 @@ const createApiKey = async () => {
const result = await apiClient.post('/admin/api-keys', data)
if (result.success) {
showToast('API Key 创建成功', 'success')
showToast(t('apiKeys.createApiKeyModal.createSuccess'), 'success')
emit('success', result.data)
emit('close')
} else {
showToast(result.message || '创建失败', 'error')
showToast(result.message || t('apiKeys.createApiKeyModal.createFailed'), 'error')
}
} else {
// 批量创建
@@ -1272,15 +1270,18 @@ const createApiKey = async () => {
const result = await apiClient.post('/admin/api-keys/batch', data)
if (result.success) {
showToast(`成功创建 ${result.data.length} 个 API Key`, 'success')
showToast(
t('apiKeys.createApiKeyModal.batchCreateSuccess', { count: result.data.length }),
'success'
)
emit('batch-success', result.data)
emit('close')
} else {
showToast(result.message || '批量创建失败', 'error')
showToast(result.message || t('apiKeys.createApiKeyModal.batchCreateFailed'), 'error')
}
}
} catch (error) {
showToast('创建失败', 'error')
showToast(t('apiKeys.createApiKeyModal.createFailed'), 'error')
} finally {
loading.value = false
}

View File

@@ -12,7 +12,7 @@
<i class="fas fa-edit text-sm text-white sm:text-base" />
</div>
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100 sm:text-xl">
编辑 API Key
{{ t('apiKeys.editApiKeyModal.title') }}
</h3>
</div>
<button
@@ -30,20 +30,18 @@
<div>
<label
class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-3 sm:text-sm"
>名称</label
>{{ t('apiKeys.editApiKeyModal.name') }}</label
>
<div>
<input
v-model="form.name"
class="form-input flex-1 border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
maxlength="100"
placeholder="请输入API Key名称"
:placeholder="t('apiKeys.editApiKeyModal.namePlaceholder')"
required
type="text"
/>
</div>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 sm:mt-2">
用于识别此 API Key 的用途
{{ t('apiKeys.editApiKeyModal.nameHint') }}
</p>
</div>
@@ -51,7 +49,7 @@
<div>
<label
class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-3 sm:text-sm"
>所有者</label
>{{ t('apiKeys.editApiKeyModal.owner') }}</label
>
<select
v-model="form.ownerId"
@@ -59,11 +57,13 @@
>
<option v-for="user in availableUsers" :key="user.id" :value="user.id">
{{ user.displayName }} ({{ user.username }})
<span v-if="user.role === 'admin'" class="text-gray-500">- 管理员</span>
<span v-if="user.role === 'admin'" class="text-gray-500">{{
t('apiKeys.editApiKeyModal.adminLabel')
}}</span>
</option>
</select>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 sm:mt-2">
分配此 API Key 给指定用户或管理员管理员分配时不受用户 API Key 数量限制
{{ t('apiKeys.editApiKeyModal.ownerHint') }}
</p>
</div>
@@ -71,13 +71,13 @@
<div>
<label
class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-3 sm:text-sm"
>标签</label
>{{ t('apiKeys.editApiKeyModal.tags') }}</label
>
<div class="space-y-4">
<!-- 已选择的标签 -->
<div v-if="form.tags.length > 0">
<div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
已选择的标签:
{{ t('apiKeys.editApiKeyModal.selectedTags') }}
</div>
<div class="flex flex-wrap gap-2">
<span
@@ -100,7 +100,7 @@
<!-- 可选择的已有标签 -->
<div v-if="unselectedTags.length > 0">
<div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
点击选择已有标签:
{{ t('apiKeys.editApiKeyModal.clickToSelectTags') }}
</div>
<div class="flex flex-wrap gap-2">
<button
@@ -119,13 +119,13 @@
<!-- 创建新标签 -->
<div>
<div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
创建新标签:
{{ t('apiKeys.editApiKeyModal.createNewTag') }}
</div>
<div class="flex gap-2">
<input
v-model="newTag"
class="form-input flex-1 border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
placeholder="输入新标签名称"
:placeholder="t('apiKeys.editApiKeyModal.newTagPlaceholder')"
type="text"
@keypress.enter.prevent="addTag"
/>
@@ -140,7 +140,7 @@
</div>
<p class="text-xs text-gray-500 dark:text-gray-400">
用于标记不同团队或用途方便筛选管理
{{ t('apiKeys.editApiKeyModal.tagsHint') }}
</p>
</div>
</div>
@@ -156,68 +156,76 @@
<i class="fas fa-tachometer-alt text-xs text-white" />
</div>
<h4 class="text-sm font-semibold text-gray-800 dark:text-gray-200">
速率限制设置 (可选)
{{ t('apiKeys.editApiKeyModal.rateLimitTitle') }}
</h4>
</div>
<div class="space-y-2">
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3">
<div>
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300"
>时间窗口 (分钟)</label
>
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">{{
t('apiKeys.editApiKeyModal.rateLimitWindow')
}}</label>
<input
v-model="form.rateLimitWindow"
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
min="1"
placeholder="无限制"
:placeholder="t('apiKeys.editApiKeyModal.noLimit')"
type="number"
/>
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">时间段单位</p>
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{{ t('apiKeys.editApiKeyModal.rateLimitWindowHint') }}
</p>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300"
>请求次数限制</label
>
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">{{
t('apiKeys.editApiKeyModal.rateLimitRequests')
}}</label>
<input
v-model="form.rateLimitRequests"
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
min="1"
placeholder="无限制"
:placeholder="t('apiKeys.editApiKeyModal.noLimit')"
type="number"
/>
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">窗口内最大请求</p>
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{{ t('apiKeys.editApiKeyModal.rateLimitRequestsHint') }}
</p>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300"
>费用限制 (美元)</label
>
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">{{
t('apiKeys.editApiKeyModal.rateLimitCost')
}}</label>
<input
v-model="form.rateLimitCost"
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
min="0"
placeholder="无限制"
:placeholder="t('apiKeys.editApiKeyModal.noLimit')"
step="0.01"
type="number"
/>
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">窗口内最大费用</p>
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{{ t('apiKeys.editApiKeyModal.rateLimitCostHint') }}
</p>
</div>
</div>
<!-- 示例说明 -->
<div class="rounded-lg bg-blue-100 p-2 dark:bg-blue-900/30">
<h5 class="mb-1 text-xs font-semibold text-blue-800 dark:text-blue-400">
💡 使用示例
{{ t('apiKeys.editApiKeyModal.usageExamples') }}
</h5>
<div class="space-y-0.5 text-xs text-blue-700 dark:text-blue-300">
<div>
<strong>示例1:</strong> 时间窗口=60请求次数=1000 每60分钟最多1000次请求
<strong>{{ t('apiKeys.editApiKeyModal.example1') }}</strong>
</div>
<div><strong>示例2:</strong> 时间窗口=1费用=0.1 每分钟最多$0.1费用</div>
<div>
<strong>示例3:</strong> 窗口=30请求=50费用=5 每30分钟50次请求且不超$5费用
<strong>{{ t('apiKeys.editApiKeyModal.example2') }}</strong>
</div>
<div>
<strong>{{ t('apiKeys.editApiKeyModal.example3') }}</strong>
</div>
</div>
</div>
@@ -225,9 +233,9 @@
</div>
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>每日费用限制 (美元)</label
>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
t('apiKeys.editApiKeyModal.dailyCostLimit')
}}</label>
<div class="space-y-3">
<div class="flex gap-2">
<button
@@ -256,27 +264,27 @@
type="button"
@click="form.dailyCostLimit = ''"
>
自定义
{{ t('apiKeys.editApiKeyModal.custom') }}
</button>
</div>
<input
v-model="form.dailyCostLimit"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
min="0"
placeholder="0 表示无限制"
:placeholder="t('apiKeys.editApiKeyModal.dailyCostLimitPlaceholder')"
step="0.01"
type="number"
/>
<p class="text-xs text-gray-500 dark:text-gray-400">
设置此 API Key 每日的费用限制超过限制将拒绝请求0 或留空表示无限制
{{ t('apiKeys.editApiKeyModal.dailyCostHint') }}
</p>
</div>
</div>
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>Opus 模型周费用限制 (美元)</label
>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
t('apiKeys.editApiKeyModal.weeklyOpusCostLimit')
}}</label>
<div class="space-y-3">
<div class="flex gap-2">
<button
@@ -305,36 +313,36 @@
type="button"
@click="form.weeklyOpusCostLimit = ''"
>
自定义
{{ t('apiKeys.editApiKeyModal.custom') }}
</button>
</div>
<input
v-model="form.weeklyOpusCostLimit"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
min="0"
placeholder="0 表示无限制"
:placeholder="t('apiKeys.editApiKeyModal.weeklyOpusCostLimitPlaceholder')"
step="0.01"
type="number"
/>
<p class="text-xs text-gray-500 dark:text-gray-400">
设置 Opus 模型的周费用限制周一到周日仅限 Claude 官方账户0 或留空表示无限制
{{ t('apiKeys.editApiKeyModal.weeklyOpusHint') }}
</p>
</div>
</div>
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>并发限制</label
>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
t('apiKeys.editApiKeyModal.concurrencyLimit')
}}</label>
<input
v-model="form.concurrencyLimit"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
min="0"
placeholder="0 表示无限制"
:placeholder="t('apiKeys.editApiKeyModal.concurrencyLimitPlaceholder')"
type="number"
/>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
设置此 API Key 可同时处理的最大请求数
{{ t('apiKeys.editApiKeyModal.concurrencyHint') }}
</p>
</div>
@@ -351,18 +359,18 @@
class="ml-2 cursor-pointer text-sm font-semibold text-gray-700 dark:text-gray-300"
for="editIsActive"
>
激活账号
{{ t('apiKeys.editApiKeyModal.activeStatus') }}
</label>
</div>
<p class="mb-4 text-xs text-gray-500 dark:text-gray-400">
取消勾选将禁用此 API Key暂停所有请求客户端返回 401 错误
{{ t('apiKeys.editApiKeyModal.activeStatusHint') }}
</p>
</div>
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>服务权限</label
>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
t('apiKeys.editApiKeyModal.servicePermissions')
}}</label>
<div class="flex gap-4">
<label class="flex cursor-pointer items-center">
<input
@@ -371,7 +379,9 @@
type="radio"
value="all"
/>
<span class="text-sm text-gray-700 dark:text-gray-300">全部服务</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{{
t('apiKeys.editApiKeyModal.allServices')
}}</span>
</label>
<label class="flex cursor-pointer items-center">
<input
@@ -380,7 +390,9 @@
type="radio"
value="claude"
/>
<span class="text-sm text-gray-700 dark:text-gray-300"> Claude</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{{
t('apiKeys.editApiKeyModal.claudeOnly')
}}</span>
</label>
<label class="flex cursor-pointer items-center">
<input
@@ -389,7 +401,9 @@
type="radio"
value="gemini"
/>
<span class="text-sm text-gray-700 dark:text-gray-300"> Gemini</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{{
t('apiKeys.editApiKeyModal.geminiOnly')
}}</span>
</label>
<label class="flex cursor-pointer items-center">
<input
@@ -398,23 +412,25 @@
type="radio"
value="openai"
/>
<span class="text-sm text-gray-700 dark:text-gray-300"> OpenAI</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{{
t('apiKeys.editApiKeyModal.openaiOnly')
}}</span>
</label>
</div>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
控制此 API Key 可以访问哪些服务
{{ t('apiKeys.editApiKeyModal.permissionsHint') }}
</p>
</div>
<div>
<div class="mb-3 flex items-center justify-between">
<label class="text-sm font-semibold text-gray-700 dark:text-gray-300"
>专属账号绑定</label
>
<label class="text-sm font-semibold text-gray-700 dark:text-gray-300">{{
t('apiKeys.editApiKeyModal.accountBinding')
}}</label>
<button
class="flex items-center gap-1 text-sm text-blue-600 transition-colors hover:text-blue-800 disabled:cursor-not-allowed disabled:opacity-50 dark:text-blue-400 dark:hover:text-blue-300"
:disabled="accountsLoading"
title="刷新账号列表"
:title="t('apiKeys.editApiKeyModal.refreshAccounts')"
type="button"
@click="refreshAccounts"
>
@@ -425,69 +441,73 @@
'text-xs'
]"
/>
<span>{{ accountsLoading ? '刷新中...' : '刷新账号' }}</span>
<span>{{
accountsLoading
? t('apiKeys.editApiKeyModal.refreshing')
: t('apiKeys.editApiKeyModal.refreshAccounts')
}}</span>
</button>
</div>
<div class="grid grid-cols-1 gap-3">
<div>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
>Claude 专属账号</label
>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
t('apiKeys.editApiKeyModal.claudeAccount')
}}</label>
<AccountSelector
v-model="form.claudeAccountId"
:accounts="localAccounts.claude"
default-option-text="使用共享账号池"
:default-option-text="t('apiKeys.editApiKeyModal.useSharedPool')"
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
:groups="localAccounts.claudeGroups"
placeholder="请选择Claude账号"
:placeholder="t('apiKeys.editApiKeyModal.selectClaudeAccount')"
platform="claude"
/>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
>Gemini 专属账号</label
>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
t('apiKeys.editApiKeyModal.geminiAccount')
}}</label>
<AccountSelector
v-model="form.geminiAccountId"
:accounts="localAccounts.gemini"
default-option-text="使用共享账号池"
:default-option-text="t('apiKeys.editApiKeyModal.useSharedPool')"
:disabled="form.permissions === 'claude' || form.permissions === 'openai'"
:groups="localAccounts.geminiGroups"
placeholder="请选择Gemini账号"
:placeholder="t('apiKeys.editApiKeyModal.selectGeminiAccount')"
platform="gemini"
/>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
>OpenAI 专属账号</label
>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
t('apiKeys.editApiKeyModal.openaiAccount')
}}</label>
<AccountSelector
v-model="form.openaiAccountId"
:accounts="localAccounts.openai"
default-option-text="使用共享账号池"
:default-option-text="t('apiKeys.editApiKeyModal.useSharedPool')"
:disabled="form.permissions === 'claude' || form.permissions === 'gemini'"
:groups="localAccounts.openaiGroups"
placeholder="请选择OpenAI账号"
:placeholder="t('apiKeys.editApiKeyModal.selectOpenaiAccount')"
platform="openai"
/>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
>Bedrock 专属账号</label
>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
t('apiKeys.editApiKeyModal.bedrockAccount')
}}</label>
<AccountSelector
v-model="form.bedrockAccountId"
:accounts="localAccounts.bedrock"
default-option-text="使用共享账号池"
:default-option-text="t('apiKeys.editApiKeyModal.useSharedPool')"
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
:groups="[]"
placeholder="请选择Bedrock账号"
:placeholder="t('apiKeys.editApiKeyModal.selectBedrockAccount')"
platform="bedrock"
/>
</div>
</div>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
修改绑定账号将影响此API Key的请求路由
{{ t('apiKeys.editApiKeyModal.accountBindingHint') }}
</p>
</div>
@@ -503,15 +523,15 @@
class="ml-2 cursor-pointer text-sm font-semibold text-gray-700 dark:text-gray-300"
for="editEnableModelRestriction"
>
启用模型限制
{{ t('apiKeys.editApiKeyModal.enableModelRestriction') }}
</label>
</div>
<div v-if="form.enableModelRestriction" class="space-y-3">
<div>
<label class="mb-2 block text-sm font-medium text-gray-600 dark:text-gray-400"
>限制的模型列表</label
>
<label class="mb-2 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
t('apiKeys.editApiKeyModal.restrictedModels')
}}</label>
<div
class="mb-3 flex min-h-[32px] flex-wrap gap-2 rounded-lg border border-gray-200 bg-gray-50 p-2 dark:border-gray-600 dark:bg-gray-700"
>
@@ -533,7 +553,7 @@
v-if="form.restrictedModels.length === 0"
class="text-sm text-gray-400 dark:text-gray-500"
>
暂无限制的模型
{{ t('apiKeys.editApiKeyModal.noRestrictedModels') }}
</span>
</div>
<div class="space-y-3">
@@ -552,7 +572,7 @@
v-if="availableQuickModels.length === 0"
class="text-sm italic text-gray-400 dark:text-gray-500"
>
所有常用模型已在限制列表中
{{ t('apiKeys.editApiKeyModal.allCommonModelsRestricted') }}
</span>
</div>
@@ -561,7 +581,7 @@
<input
v-model="form.modelInput"
class="form-input flex-1 border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
placeholder="输入模型名称,按回车添加"
:placeholder="t('apiKeys.editApiKeyModal.addRestrictedModelPlaceholder')"
type="text"
@keydown.enter.prevent="addRestrictedModel"
/>
@@ -575,7 +595,7 @@
</div>
</div>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
设置此API Key无法访问的模型例如claude-opus-4-20250514
{{ t('apiKeys.editApiKeyModal.modelRestrictionHint') }}
</p>
</div>
</div>
@@ -594,17 +614,17 @@
class="ml-2 cursor-pointer text-sm font-semibold text-gray-700 dark:text-gray-300"
for="editEnableClientRestriction"
>
启用客户端限制
{{ t('apiKeys.editApiKeyModal.enableClientRestriction') }}
</label>
</div>
<div v-if="form.enableClientRestriction" class="space-y-3">
<div>
<label class="mb-2 block text-sm font-medium text-gray-600 dark:text-gray-400"
>允许的客户端</label
>
<label class="mb-2 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
t('apiKeys.editApiKeyModal.allowedClients')
}}</label>
<p class="mb-3 text-xs text-gray-500 dark:text-gray-400">
勾选允许使用此API Key的客户端
{{ t('apiKeys.editApiKeyModal.clientRestrictionHint') }}
</p>
<div class="space-y-2">
<div v-for="client in supportedClients" :key="client.id" class="flex items-start">
@@ -635,7 +655,7 @@
type="button"
@click="$emit('close')"
>
取消
{{ t('apiKeys.editApiKeyModal.cancel') }}
</button>
<button
class="btn btn-primary flex-1 px-6 py-3 font-semibold"
@@ -644,7 +664,9 @@
>
<div v-if="loading" class="loading-spinner mr-2" />
<i v-else class="fas fa-save mr-2" />
{{ loading ? '保存中...' : '保存修改' }}
{{
loading ? t('apiKeys.editApiKeyModal.saving') : t('apiKeys.editApiKeyModal.save')
}}
</button>
</div>
</form>
@@ -655,6 +677,7 @@
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { showToast } from '@/utils/toast'
import { useClientsStore } from '@/stores/clients'
import { useApiKeysStore } from '@/stores/apiKeys'
@@ -674,6 +697,9 @@ const props = defineProps({
const emit = defineEmits(['close', 'success'])
// 国际化
const { t } = useI18n()
// const authStore = useAuthStore()
const clientsStore = useClientsStore()
const apiKeysStore = useApiKeysStore()
@@ -785,14 +811,14 @@ const updateApiKey = async () => {
let confirmed = false
if (window.showConfirm) {
confirmed = await window.showConfirm(
'费用限制提醒',
'您设置了时间窗口但费用限制为0这意味着不会有费用限制。\n\n是否继续',
'继续保存',
'返回修改'
t('apiKeys.editApiKeyModal.costLimitConfirmTitle'),
t('apiKeys.editApiKeyModal.costLimitConfirmMessage'),
t('apiKeys.editApiKeyModal.costLimitConfirmContinue'),
t('apiKeys.editApiKeyModal.costLimitConfirmBack')
)
} else {
// 降级方案
confirmed = confirm('您设置了时间窗口但费用限制为0这意味着不会有费用限制。\n是否继续')
confirmed = confirm(t('apiKeys.editApiKeyModal.costLimitConfirmMessage'))
}
if (!confirmed) {
return
@@ -898,10 +924,10 @@ const updateApiKey = async () => {
emit('success')
emit('close')
} else {
showToast(result.message || '更新失败', 'error')
showToast(result.message || t('apiKeys.editApiKeyModal.updateFailed'), 'error')
}
} catch (error) {
showToast('更新失败', 'error')
showToast(t('apiKeys.editApiKeyModal.updateFailed'), 'error')
} finally {
loading.value = false
}
@@ -911,20 +937,12 @@ const updateApiKey = async () => {
const refreshAccounts = async () => {
accountsLoading.value = true
try {
const [
claudeData,
claudeConsoleData,
geminiData,
openaiData,
openaiResponsesData,
bedrockData,
groupsData
] = await Promise.all([
const [claudeData, claudeConsoleData, geminiData, openaiData, bedrockData, groupsData] =
await Promise.all([
apiClient.get('/admin/claude-accounts'),
apiClient.get('/admin/claude-console-accounts'),
apiClient.get('/admin/gemini-accounts'),
apiClient.get('/admin/openai-accounts'),
apiClient.get('/admin/openai-responses-accounts'), // 获取 OpenAI-Responses 账号
apiClient.get('/admin/bedrock-accounts'), // 添加 Bedrock 账号获取
apiClient.get('/admin/account-groups')
])
@@ -961,31 +979,13 @@ const refreshAccounts = async () => {
}))
}
// 合并 OpenAI 和 OpenAI-Responses 账号
const openaiAccounts = []
if (openaiData.success) {
;(openaiData.data || []).forEach((account) => {
openaiAccounts.push({
localAccounts.value.openai = (openaiData.data || []).map((account) => ({
...account,
platform: 'openai',
isDedicated: account.accountType === 'dedicated'
})
})
}))
}
if (openaiResponsesData.success) {
;(openaiResponsesData.data || []).forEach((account) => {
openaiAccounts.push({
...account,
platform: 'openai-responses',
isDedicated: account.accountType === 'dedicated'
})
})
}
localAccounts.value.openai = openaiAccounts
if (bedrockData.success) {
localAccounts.value.bedrock = (bedrockData.data || []).map((account) => ({
...account,
@@ -1001,9 +1001,9 @@ const refreshAccounts = async () => {
localAccounts.value.openaiGroups = allGroups.filter((g) => g.platform === 'openai')
}
showToast('账号列表已刷新', 'success')
showToast(t('apiKeys.editApiKeyModal.refreshAccountsSuccess'), 'success')
} catch (error) {
showToast('刷新账号列表失败', 'error')
showToast(t('apiKeys.editApiKeyModal.refreshAccountsFailed'), 'error')
} finally {
accountsLoading.value = false
}
@@ -1017,7 +1017,7 @@ const loadUsers = async () => {
availableUsers.value = response.data || []
}
} catch (error) {
// console.error('Failed to load users:', error)
console.error('Failed to load users:', error)
availableUsers.value = [
{
id: 'admin',
@@ -1043,7 +1043,7 @@ onMounted(async () => {
supportedClients.value = clients || []
availableTags.value = tags || []
} catch (error) {
// console.error('Error loading initial data:', error)
console.error('Error loading initial data:', error)
// Fallback to empty arrays if loading fails
supportedClients.value = []
availableTags.value = []
@@ -1051,29 +1051,10 @@ onMounted(async () => {
// 初始化账号数据
if (props.accounts) {
// 合并 OpenAI 和 OpenAI-Responses 账号
const openaiAccounts = []
if (props.accounts.openai) {
props.accounts.openai.forEach((account) => {
openaiAccounts.push({
...account,
platform: 'openai'
})
})
}
if (props.accounts.openaiResponses) {
props.accounts.openaiResponses.forEach((account) => {
openaiAccounts.push({
...account,
platform: 'openai-responses'
})
})
}
localAccounts.value = {
claude: props.accounts.claude || [],
gemini: props.accounts.gemini || [],
openai: openaiAccounts,
openai: props.accounts.openai || [],
bedrock: props.accounts.bedrock || [], // 添加 Bedrock 账号
claudeGroups: props.accounts.claudeGroups || [],
geminiGroups: props.accounts.geminiGroups || [],
@@ -1081,9 +1062,6 @@ onMounted(async () => {
}
}
// 自动加载账号数据
await refreshAccounts()
form.name = props.apiKey.name
// 处理速率限制迁移如果有tokenLimit且没有rateLimitCost提示用户
@@ -1093,7 +1071,7 @@ onMounted(async () => {
// 如果有历史tokenLimit但没有rateLimitCost提示用户需要重新设置
if (props.apiKey.tokenLimit > 0 && !props.apiKey.rateLimitCost) {
// 可以根据需要添加提示,或者自动迁移(这里选择让用户手动设置)
// console.log('检测到历史Token限制请考虑设置费用限制')
console.log('Token limit migration detected, consider setting cost limit')
}
form.rateLimitWindow = props.apiKey.rateLimitWindow || ''
@@ -1109,10 +1087,7 @@ onMounted(async () => {
form.claudeAccountId = props.apiKey.claudeAccountId || ''
}
form.geminiAccountId = props.apiKey.geminiAccountId || ''
// 处理 OpenAI 账号 - 直接使用后端传来的值(已包含 responses: 前缀)
form.openaiAccountId = props.apiKey.openaiAccountId || ''
form.bedrockAccountId = props.apiKey.bedrockAccountId || '' // 添加 Bedrock 账号ID初始化
form.restrictedModels = props.apiKey.restrictedModels || []
form.allowedClients = props.apiKey.allowedClients || []

View File

@@ -18,9 +18,11 @@
<i class="fas fa-clock text-white" />
</div>
<div>
<h3 class="text-xl font-bold text-gray-900 dark:text-gray-100">修改过期时间</h3>
<h3 class="text-xl font-bold text-gray-900 dark:text-gray-100">
{{ $t('apiKeys.expiryEditModal.title') }}
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">
"{{ apiKey.name || 'API Key' }}" 设置新的过期时间
{{ $t('apiKeys.expiryEditModal.subtitle', { name: apiKey.name || 'API Key' }) }}
</p>
</div>
</div>
@@ -39,14 +41,20 @@
>
<div class="flex items-center justify-between">
<div>
<p class="mb-1 text-xs font-medium text-gray-600 dark:text-gray-400">当前状态</p>
<p class="mb-1 text-xs font-medium text-gray-600 dark:text-gray-400">
{{ $t('apiKeys.expiryEditModal.currentStatus') }}
</p>
<p class="text-sm font-semibold text-gray-800 dark:text-gray-200">
<!-- 未激活状态 -->
<template v-if="apiKey.expirationMode === 'activation' && !apiKey.isActivated">
<i class="fas fa-pause-circle mr-1 text-blue-500" />
未激活
{{ $t('apiKeys.expiryEditModal.notActivated') }}
<span class="ml-2 text-xs font-normal text-gray-600">
(激活后 {{ apiKey.activationDays || 30 }} 天过期)
{{
$t('apiKeys.expiryEditModal.activationDaysHint', {
days: apiKey.activationDays || 30
})
}}
</span>
</template>
<!-- 已设置过期时间 -->
@@ -63,7 +71,7 @@
<!-- 永不过期 -->
<template v-else>
<i class="fas fa-infinity mr-1 text-gray-500" />
永不过期
{{ $t('apiKeys.expiryEditModal.neverExpire') }}
</template>
</p>
</div>
@@ -89,19 +97,23 @@
@click="handleActivateNow"
>
<i class="fas fa-rocket mr-2" />
立即激活 (激活后 {{ apiKey.activationDays || 30 }} 天过期)
{{
$t('apiKeys.expiryEditModal.activateButton', { days: apiKey.activationDays || 30 })
}}
</button>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
<i class="fas fa-info-circle mr-1" />
点击立即激活此 API Key激活后将在 {{ apiKey.activationDays || 30 }} 天后过期
{{
$t('apiKeys.expiryEditModal.activationInfo', { days: apiKey.activationDays || 30 })
}}
</p>
</div>
<!-- 快捷选项 -->
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>选择新的期限</label
>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
{{ $t('apiKeys.expiryEditModal.selectNewDuration') }}
</label>
<div class="mb-3 grid grid-cols-3 gap-2">
<button
v-for="option in quickOptions"
@@ -126,16 +138,16 @@
@click="selectQuickOption('custom')"
>
<i class="fas fa-calendar-alt mr-1" />
自定义
{{ $t('apiKeys.expiryEditModal.custom') }}
</button>
</div>
</div>
<!-- 自定义日期选择 -->
<div v-if="localForm.expireDuration === 'custom'" class="animate-fadeIn">
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>选择日期和时间</label
>
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300">
{{ $t('apiKeys.expiryEditModal.selectDateAndTime') }}
</label>
<input
v-model="localForm.customExpireDate"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
@@ -144,7 +156,7 @@
@change="updateCustomExpiryPreview"
/>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
选择一个未来的日期和时间作为过期时间
{{ $t('apiKeys.expiryEditModal.selectFutureDateTime') }}
</p>
</div>
@@ -157,7 +169,7 @@
<div>
<p class="mb-1 text-xs font-medium text-blue-700 dark:text-blue-400">
<i class="fas fa-arrow-right mr-1" />
新的过期时间
{{ $t('apiKeys.expiryEditModal.newExpiryTime') }}
</p>
<p class="text-sm font-semibold text-blue-900 dark:text-blue-200">
<template v-if="localForm.expiresAt">
@@ -172,7 +184,7 @@
</template>
<template v-else>
<i class="fas fa-infinity mr-1" />
永不过期
{{ $t('apiKeys.expiryEditModal.neverExpire') }}
</template>
</p>
</div>
@@ -190,7 +202,7 @@
class="flex-1 rounded-lg bg-gray-100 px-4 py-2.5 font-semibold text-gray-700 transition-colors hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
@click="$emit('close')"
>
取消
{{ $t('apiKeys.expiryEditModal.cancel') }}
</button>
<button
class="btn btn-primary flex-1 px-4 py-2.5 font-semibold"
@@ -199,7 +211,11 @@
>
<div v-if="saving" class="loading-spinner mr-2" />
<i v-else class="fas fa-save mr-2" />
{{ saving ? '保存中...' : '保存更改' }}
{{
saving
? $t('apiKeys.expiryEditModal.saving')
: $t('apiKeys.expiryEditModal.saveChanges')
}}
</button>
</div>
</div>
@@ -210,6 +226,9 @@
<script setup>
import { ref, reactive, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const props = defineProps({
show: {
@@ -235,14 +254,14 @@ const localForm = reactive({
// 快捷选项
const quickOptions = [
{ value: '', label: '永不过期' },
{ value: '1d', label: '1 天' },
{ value: '7d', label: '7 天' },
{ value: '30d', label: '30 天' },
{ value: '90d', label: '90 天' },
{ value: '180d', label: '180 天' },
{ value: '365d', label: '1 年' },
{ value: '730d', label: '2 年' }
{ value: '', label: t('apiKeys.expiryEditModal.neverExpireOption') },
{ value: '1d', label: t('apiKeys.expiryEditModal.oneDay') },
{ value: '7d', label: t('apiKeys.expiryEditModal.sevenDays') },
{ value: '30d', label: t('apiKeys.expiryEditModal.thirtyDays') },
{ value: '90d', label: t('apiKeys.expiryEditModal.ninetyDays') },
{ value: '180d', label: t('apiKeys.expiryEditModal.oneHundredEightyDays') },
{ value: '365d', label: t('apiKeys.expiryEditModal.threeSixtyFiveDays') },
{ value: '730d', label: t('apiKeys.expiryEditModal.twoYears') }
]
// 计算最小日期时间
@@ -337,13 +356,17 @@ const updateCustomExpiryPreview = () => {
const formatExpireDate = (dateString) => {
if (!dateString) return ''
const date = new Date(dateString)
return date.toLocaleString('zh-CN', {
const { locale } = useI18n()
return date.toLocaleString(
locale.value === 'zh-cn' ? 'zh-CN' : locale.value === 'zh-tw' ? 'zh-TW' : 'en-US',
{
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
)
}
// 检查是否已过期
@@ -363,22 +386,22 @@ const getExpiryStatus = (expiresAt) => {
if (diffMs < 0) {
return {
text: '已过期',
text: t('apiKeys.expiryEditModal.expired'),
class: 'text-red-600'
}
} else if (diffDays <= 7) {
return {
text: `${diffDays} 天后过期`,
text: t('apiKeys.expiryEditModal.daysToExpire', { days: diffDays }),
class: 'text-orange-600'
}
} else if (diffDays <= 30) {
return {
text: `${diffDays} 天后过期`,
text: t('apiKeys.expiryEditModal.daysToExpire', { days: diffDays }),
class: 'text-yellow-600'
}
} else {
return {
text: `${Math.ceil(diffDays / 30)} 个月后过期`,
text: t('apiKeys.expiryEditModal.monthsToExpire', { months: Math.ceil(diffDays / 30) }),
class: 'text-green-600'
}
}
@@ -399,15 +422,19 @@ const handleActivateNow = async () => {
let confirmed = true
if (window.showConfirm) {
confirmed = await window.showConfirm(
'激活 API Key',
`确定要立即激活此 API Key 吗?激活后将在 ${props.apiKey.activationDays || 30} 天后自动过期。`,
'确定激活',
'取消'
t('apiKeys.expiryEditModal.activateConfirmTitle'),
t('apiKeys.expiryEditModal.activateConfirmMessage', {
days: props.apiKey.activationDays || 30
}),
t('apiKeys.expiryEditModal.confirmActivate'),
t('apiKeys.expiryEditModal.confirmCancel')
)
} else {
// 降级方案
confirmed = confirm(
`确定要立即激活此 API Key 吗?激活后将在 ${props.apiKey.activationDays || 30} 天后自动过期。`
t('apiKeys.expiryEditModal.activateConfirmMessage', {
days: props.apiKey.activationDays || 30
})
)
}

View File

@@ -12,13 +12,17 @@
<i class="fas fa-check text-lg text-white" />
</div>
<div>
<h3 class="text-xl font-bold text-gray-900 dark:text-gray-100">API Key 创建成功</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">请妥善保存您的 API Key</p>
<h3 class="text-xl font-bold text-gray-900 dark:text-gray-100">
{{ t('apiKeys.newApiKeyModal.title') }}
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ t('apiKeys.newApiKeyModal.subtitle') }}
</p>
</div>
</div>
<button
class="text-gray-400 transition-colors hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
title="直接关闭(不推荐)"
:title="t('apiKeys.newApiKeyModal.directCloseTooltip')"
@click="handleDirectClose"
>
<i class="fas fa-times text-xl" />
@@ -36,10 +40,11 @@
<i class="fas fa-exclamation-triangle text-sm text-white" />
</div>
<div class="ml-3">
<h5 class="mb-1 font-semibold text-amber-900 dark:text-amber-400">重要提醒</h5>
<h5 class="mb-1 font-semibold text-amber-900 dark:text-amber-400">
{{ t('apiKeys.newApiKeyModal.warningTitle') }}
</h5>
<p class="text-sm text-amber-800 dark:text-amber-300">
这是您唯一能看到完整 API Key 的机会关闭此窗口后系统将不再显示完整的 API
Key请立即复制并妥善保存
{{ t('apiKeys.newApiKeyModal.warningMessage') }}
</p>
</div>
</div>
@@ -48,9 +53,9 @@
<!-- API Key 信息 -->
<div class="mb-6 space-y-4">
<div>
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>API Key 名称</label
>
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
t('apiKeys.newApiKeyModal.apiKeyName')
}}</label>
<div
class="rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-600 dark:bg-gray-800"
>
@@ -59,22 +64,22 @@
</div>
<div v-if="apiKey.description">
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>备注</label
>
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
t('apiKeys.newApiKeyModal.remarks')
}}</label>
<div
class="rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-600 dark:bg-gray-800"
>
<span class="text-gray-700 dark:text-gray-300">{{
apiKey.description || '无描述'
apiKey.description || t('apiKeys.newApiKeyModal.noDescription')
}}</span>
</div>
</div>
<div>
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>API Key</label
>
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
t('apiKeys.newApiKeyModal.apiKey')
}}</label>
<div class="relative">
<div
class="flex min-h-[60px] items-center break-all rounded-lg border border-gray-700 bg-gray-900 p-4 pr-14 font-mono text-sm text-white dark:border-gray-600 dark:bg-gray-900"
@@ -84,7 +89,11 @@
<div class="absolute right-3 top-3">
<button
class="btn-icon-sm bg-gray-700 hover:bg-gray-800 dark:bg-gray-700 dark:hover:bg-gray-600"
:title="showFullKey ? '隐藏API Key' : '显示完整API Key'"
:title="
showFullKey
? t('apiKeys.newApiKeyModal.hideApiKey')
: t('apiKeys.newApiKeyModal.showFullApiKey')
"
type="button"
@click="toggleKeyVisibility"
>
@@ -93,7 +102,7 @@
</div>
</div>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
点击眼睛图标切换显示模式使用下方按钮复制完整 API Key
{{ t('apiKeys.newApiKeyModal.visibilityHint') }}
</p>
</div>
</div>
@@ -105,13 +114,13 @@
@click="copyApiKey"
>
<i class="fas fa-copy" />
复制 API Key
{{ t('apiKeys.newApiKeyModal.copyApiKey') }}
</button>
<button
class="rounded-xl border border-gray-300 bg-gray-200 px-6 py-3 font-semibold text-gray-800 transition-colors hover:bg-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600"
@click="handleClose"
>
我已保存
{{ t('apiKeys.newApiKeyModal.alreadySaved') }}
</button>
</div>
</div>
@@ -121,8 +130,11 @@
<script setup>
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { showToast } from '@/utils/toast'
const { t } = useI18n()
const props = defineProps({
apiKey: {
type: Object,
@@ -159,13 +171,13 @@ const getDisplayedApiKey = () => {
const copyApiKey = async () => {
const key = props.apiKey.apiKey || props.apiKey.key || ''
if (!key) {
showToast('API Key 不存在', 'error')
showToast(t('apiKeys.newApiKeyModal.apiKeyNotFound'), 'error')
return
}
try {
await navigator.clipboard.writeText(key)
showToast('API Key 已复制到剪贴板', 'success')
showToast(t('apiKeys.newApiKeyModal.copySuccess'), 'success')
} catch (error) {
// console.error('Failed to copy:', error)
// 降级方案:创建一个临时文本区域
@@ -175,9 +187,9 @@ const copyApiKey = async () => {
textArea.select()
try {
document.execCommand('copy')
showToast('API Key 已复制到剪贴板', 'success')
showToast(t('apiKeys.newApiKeyModal.copySuccess'), 'success')
} catch (fallbackError) {
showToast('复制失败,请手动复制', 'error')
showToast(t('apiKeys.newApiKeyModal.copyFailed'), 'error')
} finally {
document.body.removeChild(textArea)
}
@@ -188,19 +200,17 @@ const copyApiKey = async () => {
const handleClose = async () => {
if (window.showConfirm) {
const confirmed = await window.showConfirm(
'关闭提醒',
'关闭后将无法再次查看完整的API Key请确保已经妥善保存。\n\n确定要关闭吗',
'确定关闭',
'取消'
t('apiKeys.newApiKeyModal.closeReminderTitle'),
t('apiKeys.newApiKeyModal.closeReminderMessage'),
t('apiKeys.newApiKeyModal.confirmClose'),
t('apiKeys.newApiKeyModal.cancel')
)
if (confirmed) {
emit('close')
}
} else {
// 降级方案
const confirmed = confirm(
'关闭后将无法再次查看完整的API Key请确保已经妥善保存。\n\n确定要关闭吗'
)
const confirmed = confirm(t('apiKeys.newApiKeyModal.closeReminderMessage'))
if (confirmed) {
emit('close')
}
@@ -211,17 +221,17 @@ const handleClose = async () => {
const handleDirectClose = async () => {
if (window.showConfirm) {
const confirmed = await window.showConfirm(
'确定要关闭吗?',
'您还没有保存API Key关闭后将无法再次查看。\n\n建议您先复制API Key再关闭。',
'仍然关闭',
'返回复制'
t('apiKeys.newApiKeyModal.directCloseTitle'),
t('apiKeys.newApiKeyModal.directCloseMessage'),
t('apiKeys.newApiKeyModal.stillClose'),
t('apiKeys.newApiKeyModal.goBack')
)
if (confirmed) {
emit('close')
}
} else {
// 降级方案
const confirmed = confirm('您还没有保存API Key关闭后将无法再次查看。\n\n确定要关闭吗')
const confirmed = confirm(t('apiKeys.newApiKeyModal.directCloseFallback'))
if (confirmed) {
emit('close')
}

View File

@@ -9,7 +9,9 @@
>
<i class="fas fa-clock text-white" />
</div>
<h3 class="text-xl font-bold text-gray-900">续期 API Key</h3>
<h3 class="text-xl font-bold text-gray-900">
{{ $t('apiKeys.renewApiKeyModal.title') }}
</h3>
</div>
<button
class="text-gray-400 transition-colors hover:text-gray-600"
@@ -28,13 +30,18 @@
<i class="fas fa-info text-sm text-white" />
</div>
<div>
<h4 class="mb-1 font-semibold text-gray-800">API Key 信息</h4>
<h4 class="mb-1 font-semibold text-gray-800">
{{ $t('apiKeys.renewApiKeyModal.apiKeyInfo') }}
</h4>
<p class="text-sm text-gray-700">
{{ apiKey.name }}
</p>
<p class="mt-1 text-xs text-gray-600">
当前过期时间{{
apiKey.expiresAt ? formatExpireDate(apiKey.expiresAt) : '永不过期'
{{ $t('apiKeys.renewApiKeyModal.currentExpiry')
}}{{
apiKey.expiresAt
? formatExpireDate(apiKey.expiresAt)
: $t('apiKeys.renewApiKeyModal.neverExpires')
}}
</p>
</div>
@@ -42,19 +49,21 @@
</div>
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700">续期时长</label>
<label class="mb-3 block text-sm font-semibold text-gray-700">{{
$t('apiKeys.renewApiKeyModal.renewDuration')
}}</label>
<select
v-model="form.renewDuration"
class="form-input w-full"
@change="updateRenewExpireAt"
>
<option value="7d">延长 7 </option>
<option value="30d">延长 30 </option>
<option value="90d">延长 90 </option>
<option value="180d">延长 180 </option>
<option value="365d">延长 365 </option>
<option value="custom">自定义日期</option>
<option value="permanent">设为永不过期</option>
<option value="7d">{{ $t('apiKeys.renewApiKeyModal.extend7Days') }}</option>
<option value="30d">{{ $t('apiKeys.renewApiKeyModal.extend30Days') }}</option>
<option value="90d">{{ $t('apiKeys.renewApiKeyModal.extend90Days') }}</option>
<option value="180d">{{ $t('apiKeys.renewApiKeyModal.extend180Days') }}</option>
<option value="365d">{{ $t('apiKeys.renewApiKeyModal.extend365Days') }}</option>
<option value="custom">{{ $t('apiKeys.renewApiKeyModal.customDate') }}</option>
<option value="permanent">{{ $t('apiKeys.renewApiKeyModal.setPermanent') }}</option>
</select>
<div v-if="form.renewDuration === 'custom'" class="mt-3">
<input
@@ -66,7 +75,8 @@
/>
</div>
<p v-if="form.newExpiresAt" class="mt-2 text-xs text-gray-500">
新的过期时间{{ formatExpireDate(form.newExpiresAt) }}
{{ $t('apiKeys.renewApiKeyModal.newExpiry')
}}{{ formatExpireDate(form.newExpiresAt) }}
</p>
</div>
</div>
@@ -77,7 +87,7 @@
type="button"
@click="$emit('close')"
>
取消
{{ $t('apiKeys.renewApiKeyModal.cancel') }}
</button>
<button
class="btn btn-primary flex-1 px-6 py-3 font-semibold"
@@ -87,7 +97,11 @@
>
<div v-if="loading" class="loading-spinner mr-2" />
<i v-else class="fas fa-clock mr-2" />
{{ loading ? '续期中...' : '确认续期' }}
{{
loading
? $t('apiKeys.renewApiKeyModal.renewing')
: $t('apiKeys.renewApiKeyModal.confirmRenew')
}}
</button>
</div>
</div>
@@ -97,9 +111,12 @@
<script setup>
import { ref, reactive, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { showToast } from '@/utils/toast'
import { apiClient } from '@/config/api'
const { t } = useI18n()
const props = defineProps({
apiKey: {
type: Object,
@@ -133,7 +150,10 @@ const minDateTime = computed(() => {
// 格式化过期日期
const formatExpireDate = (dateString) => {
const date = new Date(dateString)
return date.toLocaleString('zh-CN', {
// 根据当前语言设置选择合适的locale
const locale =
t('common.locale') === 'en' ? 'en-US' : t('common.locale') === 'zh-TW' ? 'zh-TW' : 'zh-CN'
return date.toLocaleString(locale, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
@@ -209,14 +229,14 @@ const renewApiKey = async () => {
const result = await apiClient.put(`/admin/api-keys/${props.apiKey.id}`, data)
if (result.success) {
showToast('API Key 续期成功', 'success')
showToast(t('apiKeys.renewApiKeyModal.renewSuccess'), 'success')
emit('success')
emit('close')
} else {
showToast(result.message || '续期失败', 'error')
showToast(result.message || t('apiKeys.renewApiKeyModal.renewFailed'), 'error')
}
} catch (error) {
showToast('续期失败', 'error')
showToast(t('apiKeys.renewApiKeyModal.renewFailed'), 'error')
} finally {
loading.value = false
}

View File

@@ -17,7 +17,7 @@
<i class="fas fa-chart-line text-sm text-white sm:text-base" />
</div>
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100 sm:text-xl">
使用统计详情 - {{ apiKey.name }}
{{ t('apiKeys.usageDetailModal.title') }} - {{ apiKey.name }}
</h3>
</div>
<button class="p-1 text-gray-400 transition-colors hover:text-gray-600" @click="close">
@@ -34,14 +34,17 @@
class="rounded-lg border border-blue-200 bg-gradient-to-br from-blue-50 to-blue-100 p-4 dark:border-blue-700 dark:from-blue-900/20 dark:to-blue-800/20"
>
<div class="mb-3 flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">总请求数</span>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{
t('apiKeys.usageDetailModal.totalRequests')
}}</span>
<i class="fas fa-paper-plane text-blue-500" />
</div>
<div class="text-2xl font-bold text-gray-900 dark:text-gray-100">
{{ formatNumber(totalRequests) }}
</div>
<div class="mt-1 text-xs text-gray-600 dark:text-gray-400">
今日: {{ formatNumber(dailyRequests) }}
{{ t('apiKeys.usageDetailModal.today') }}: {{ formatNumber(dailyRequests) }}
{{ t('apiKeys.usageDetailModal.times') }}
</div>
</div>
@@ -50,14 +53,16 @@
class="rounded-lg border border-green-200 bg-gradient-to-br from-green-50 to-green-100 p-4 dark:border-green-700 dark:from-green-900/20 dark:to-green-800/20"
>
<div class="mb-3 flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">总Token数</span>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{
t('apiKeys.usageDetailModal.totalTokens')
}}</span>
<i class="fas fa-coins text-green-500" />
</div>
<div class="text-2xl font-bold text-gray-900 dark:text-gray-100">
{{ formatTokenCount(totalTokens) }}
</div>
<div class="mt-1 text-xs text-gray-600 dark:text-gray-400">
今日: {{ formatTokenCount(dailyTokens) }}
{{ t('apiKeys.usageDetailModal.today') }}: {{ formatTokenCount(dailyTokens) }}
</div>
</div>
@@ -66,14 +71,16 @@
class="rounded-lg border border-yellow-200 bg-gradient-to-br from-yellow-50 to-yellow-100 p-4 dark:border-yellow-700 dark:from-yellow-900/20 dark:to-yellow-800/20"
>
<div class="mb-3 flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">总费用</span>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{
t('apiKeys.usageDetailModal.totalCost')
}}</span>
<i class="fas fa-dollar-sign text-yellow-600" />
</div>
<div class="text-2xl font-bold text-gray-900 dark:text-gray-100">
${{ totalCost.toFixed(4) }}
</div>
<div class="mt-1 text-xs text-gray-600 dark:text-gray-400">
今日: ${{ dailyCost.toFixed(4) }}
{{ t('apiKeys.usageDetailModal.today') }}: ${{ dailyCost.toFixed(4) }}
</div>
</div>
@@ -82,7 +89,9 @@
class="rounded-lg border border-purple-200 bg-gradient-to-br from-purple-50 to-purple-100 p-4 dark:border-purple-700 dark:from-purple-900/20 dark:to-purple-800/20"
>
<div class="mb-3 flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">平均速率</span>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{
t('apiKeys.usageDetailModal.averageRate')
}}</span>
<i class="fas fa-tachometer-alt text-purple-500" />
</div>
<div class="space-y-1 text-sm">
@@ -104,13 +113,15 @@
class="mb-3 flex items-center text-sm font-semibold text-gray-700 dark:text-gray-300"
>
<i class="fas fa-chart-pie mr-2 text-indigo-500" />
Token 使用分布
{{ t('apiKeys.usageDetailModal.tokenDistribution') }}
</h4>
<div class="space-y-3 rounded-lg bg-gray-50 p-4 dark:bg-gray-700/50">
<div class="flex items-center justify-between">
<div class="flex items-center">
<i class="fas fa-arrow-down mr-2 text-green-500" />
<span class="text-sm text-gray-600 dark:text-gray-400">输入 Token</span>
<span class="text-sm text-gray-600 dark:text-gray-400">{{
t('apiKeys.usageDetailModal.inputTokens')
}}</span>
</div>
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ formatTokenCount(inputTokens) }}
@@ -119,7 +130,9 @@
<div class="flex items-center justify-between">
<div class="flex items-center">
<i class="fas fa-arrow-up mr-2 text-blue-500" />
<span class="text-sm text-gray-600 dark:text-gray-400">输出 Token</span>
<span class="text-sm text-gray-600 dark:text-gray-400">{{
t('apiKeys.usageDetailModal.outputTokens')
}}</span>
</div>
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ formatTokenCount(outputTokens) }}
@@ -128,7 +141,9 @@
<div v-if="cacheCreateTokens > 0" class="flex items-center justify-between">
<div class="flex items-center">
<i class="fas fa-save mr-2 text-purple-500" />
<span class="text-sm text-gray-600 dark:text-gray-400">缓存创建 Token</span>
<span class="text-sm text-gray-600 dark:text-gray-400">{{
t('apiKeys.usageDetailModal.cacheCreateTokens')
}}</span>
</div>
<span class="text-sm font-semibold text-purple-600">
{{ formatTokenCount(cacheCreateTokens) }}
@@ -137,7 +152,9 @@
<div v-if="cacheReadTokens > 0" class="flex items-center justify-between">
<div class="flex items-center">
<i class="fas fa-download mr-2 text-purple-500" />
<span class="text-sm text-gray-600 dark:text-gray-400">缓存读取 Token</span>
<span class="text-sm text-gray-600 dark:text-gray-400">{{
t('apiKeys.usageDetailModal.cacheReadTokens')
}}</span>
</div>
<span class="text-sm font-semibold text-purple-600">
{{ formatTokenCount(cacheReadTokens) }}
@@ -152,12 +169,14 @@
class="mb-3 flex items-center text-sm font-semibold text-gray-700 dark:text-gray-300"
>
<i class="fas fa-shield-alt mr-2 text-red-500" />
限制设置
{{ t('apiKeys.usageDetailModal.limitSettings') }}
</h4>
<div class="space-y-3 rounded-lg bg-gray-50 p-4 dark:bg-gray-700/50">
<div v-if="apiKey.dailyCostLimit > 0" class="space-y-2">
<div class="flex items-center justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400">每日费用限制</span>
<span class="text-gray-600 dark:text-gray-400">{{
t('apiKeys.usageDetailModal.dailyCostLimit')
}}</span>
<span class="font-semibold text-gray-900 dark:text-gray-100">
${{ apiKey.dailyCostLimit.toFixed(2) }}
</span>
@@ -176,7 +195,11 @@
/>
</div>
<div class="text-right text-xs text-gray-500 dark:text-gray-400">
已使用 {{ dailyCostPercentage.toFixed(1) }}%
{{
t('apiKeys.usageDetailModal.usedPercentage', {
percentage: dailyCostPercentage.toFixed(1)
})
}}
</div>
</div>
@@ -184,7 +207,9 @@
v-if="apiKey.concurrencyLimit > 0"
class="flex items-center justify-between text-sm"
>
<span class="text-gray-600 dark:text-gray-400">并发限制</span>
<span class="text-gray-600 dark:text-gray-400">{{
t('apiKeys.usageDetailModal.concurrencyLimit')
}}</span>
<span class="font-semibold text-purple-600">
{{ apiKey.currentConcurrency || 0 }} / {{ apiKey.concurrencyLimit }}
</span>
@@ -193,14 +218,14 @@
<div v-if="apiKey.rateLimitWindow > 0" class="space-y-2">
<h5 class="text-sm font-medium text-gray-700 dark:text-gray-300">
<i class="fas fa-clock mr-1 text-blue-500" />
时间窗口限制
{{ t('apiKeys.usageDetailModal.timeWindowLimit') }}
</h5>
<WindowCountdown
:cost-limit="apiKey.rateLimitCost"
:current-cost="apiKey.currentWindowCost"
:current-requests="apiKey.currentWindowRequests"
:current-tokens="apiKey.currentWindowTokens"
label="窗口状态"
:label="t('apiKeys.usageDetailModal.windowStatus')"
:rate-limit-window="apiKey.rateLimitWindow"
:request-limit="apiKey.rateLimitRequests"
:show-progress="true"
@@ -218,7 +243,7 @@
<!-- 底部按钮 -->
<div class="mt-4 flex justify-end gap-2 sm:mt-6 sm:gap-3">
<button class="btn btn-secondary px-4 py-2 text-sm" type="button" @click="close">
关闭
{{ t('apiKeys.usageDetailModal.close') }}
</button>
</div>
</div>
@@ -228,8 +253,11 @@
<script setup>
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import WindowCountdown from './WindowCountdown.vue'
const { t } = useI18n()
const props = defineProps({
show: {
type: Boolean,
@@ -274,7 +302,9 @@ const dailyCostPercentage = computed(() => {
// 方法
const formatNumber = (num) => {
if (!num && num !== 0) return '0'
return num.toLocaleString('zh-CN')
// 根据当前语言环境自动选择合适的地区设置
const currentLocale = t('common.locale')
return num.toLocaleString(currentLocale)
}
// 格式化Token数量使用K/M单位

View File

@@ -1,27 +1,29 @@
<template>
<div class="space-y-1">
<div class="flex items-center justify-between text-xs">
<span class="text-gray-500">{{ label }}</span>
<span class="text-gray-500">{{ displayLabel }}</span>
<span v-if="windowState === 'active'" class="font-medium text-gray-700">
<i class="fas fa-clock mr-1 text-blue-500" />
{{ formatTime(remainingSeconds) }}
</span>
<span v-else-if="windowState === 'expired'" class="font-medium text-orange-600">
<i class="fas fa-sync-alt mr-1" />
窗口已过期
{{ t('apiKeys.windowCountdown.expired') }}
</span>
<span v-else-if="windowState === 'notStarted'" class="font-medium text-gray-500">
<i class="fas fa-pause-circle mr-1" />
窗口未激活
{{ t('apiKeys.windowCountdown.notStarted') }}
</span>
<span v-else class="font-medium text-gray-400">
{{ rateLimitWindow }} {{ t('apiKeys.windowCountdown.minutes') }}
</span>
<span v-else class="font-medium text-gray-400"> {{ rateLimitWindow }} 分钟 </span>
</div>
<!-- 进度条仅在有限制时显示 -->
<div v-if="showProgress" class="space-y-0.5">
<div v-if="hasRequestLimit" class="space-y-0.5">
<div class="flex items-center justify-between text-xs">
<span class="text-gray-400">请求</span>
<span class="text-gray-400">{{ t('apiKeys.windowCountdown.requests') }}</span>
<span class="text-gray-600"> {{ currentRequests || 0 }}/{{ requestLimit }} </span>
</div>
<div class="h-1 w-full rounded-full bg-gray-200">
@@ -36,7 +38,7 @@
<!-- Token限制向后兼容 -->
<div v-if="hasTokenLimit" class="space-y-0.5">
<div class="flex items-center justify-between text-xs">
<span class="text-gray-400">Token</span>
<span class="text-gray-400">{{ t('apiKeys.windowCountdown.tokens') }}</span>
<span class="text-gray-600">
{{ formatTokenCount(currentTokens || 0) }}/{{ formatTokenCount(tokenLimit) }}
</span>
@@ -53,7 +55,7 @@
<!-- 费用限制新功能 -->
<div v-if="hasCostLimit" class="space-y-0.5">
<div class="flex items-center justify-between text-xs">
<span class="text-gray-400">费用</span>
<span class="text-gray-400">{{ t('apiKeys.windowCountdown.cost') }}</span>
<span class="text-gray-600">
${{ (currentCost || 0).toFixed(2) }}/${{ costLimit.toFixed(2) }}
</span>
@@ -71,22 +73,29 @@
<!-- 额外提示信息 -->
<div v-if="windowState === 'active' && showTooltip" class="text-xs text-gray-500">
<i class="fas fa-info-circle mr-1" />
<span v-if="remainingSeconds < 60">即将重置</span>
<span v-if="remainingSeconds < 60">{{ t('apiKeys.windowCountdown.aboutToReset') }}</span>
<span v-else-if="remainingSeconds < 300"
>{{ Math.ceil(remainingSeconds / 60) }} 分钟后重置</span
>{{ Math.ceil(remainingSeconds / 60) }}
{{ t('apiKeys.windowCountdown.minutesUntilReset') }}</span
>
<span v-else
>{{ formatDetailedTime(remainingSeconds)
}}{{ t('apiKeys.windowCountdown.untilReset') }}</span
>
<span v-else>{{ formatDetailedTime(remainingSeconds) }}后重置</span>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const props = defineProps({
label: {
type: String,
default: '窗口限制'
default: ''
},
rateLimitWindow: {
type: Number,
@@ -143,6 +152,10 @@ const remainingSeconds = ref(props.windowRemainingSeconds)
let intervalId = null
// 计算属性
const displayLabel = computed(() => {
return props.label || t('apiKeys.windowCountdown.windowLimit')
})
const windowState = computed(() => {
if (props.windowStartTime === null) {
return 'notStarted' // 窗口未开始
@@ -182,9 +195,9 @@ const formatDetailedTime = (seconds) => {
const minutes = Math.floor((seconds % 3600) / 60)
if (hours > 0) {
return `${hours}小时${minutes}分钟`
return `${hours}${t('apiKeys.windowCountdown.hours')}${minutes}${t('apiKeys.windowCountdown.minutes')}`
} else {
return `${minutes}分钟`
return `${minutes}${t('apiKeys.windowCountdown.minutes')}`
}
}

View File

@@ -33,7 +33,9 @@
<div
class="mt-1 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400"
>
<span>{{ formatNumber(getStatUsage(stat)?.requests || 0) }}{{ t('apiStats.requests') }}</span>
<span
>{{ formatNumber(getStatUsage(stat)?.requests || 0) }}{{ t('apiStats.requests') }}</span
>
<span>{{ getStatUsage(stat)?.formattedCost || '$0.00' }}</span>
</div>
</div>
@@ -41,7 +43,10 @@
<!-- 其他Keys汇总 -->
<div v-if="otherKeysCount > 0" class="border-t border-gray-200 pt-2 dark:border-gray-700">
<div class="flex items-center justify-between text-sm text-gray-600 dark:text-gray-400">
<span>{{ t('apiStats.otherKeys') }} {{ otherKeysCount }} {{ t('apiStats.individual') }}{{ t('apiStats.keys') }}</span>
<span
>{{ t('apiStats.otherKeys') }} {{ otherKeysCount }} {{ t('apiStats.individual')
}}{{ t('apiStats.keys') }}</span
>
<span>{{ otherPercentage }}%</span>
</div>
</div>

View File

@@ -6,7 +6,9 @@
<i class="fas fa-chart-line mr-3" />
{{ t('apiStats.usageStatsQuery') }}
</h2>
<p class="text-base text-gray-600 dark:text-gray-400">{{ t('apiStats.apiKeyDescription') }}</p>
<p class="text-base text-gray-600 dark:text-gray-400">
{{ t('apiStats.apiKeyDescription') }}
</p>
</div>
<!-- 输入区域 -->

View File

@@ -31,13 +31,17 @@
<div class="text-lg font-bold text-gray-900 dark:text-gray-100">
{{ aggregatedStats.totalKeys }}
</div>
<div class="text-xs text-gray-600 dark:text-gray-400">{{ t('apiStats.totalKeys') }}</div>
<div class="text-xs text-gray-600 dark:text-gray-400">
{{ t('apiStats.totalKeys') }}
</div>
</div>
<div class="text-center">
<div class="text-lg font-bold text-green-600">
{{ aggregatedStats.activeKeys }}
</div>
<div class="text-xs text-gray-600 dark:text-gray-400">{{ t('apiStats.activeKeys') }}</div>
<div class="text-xs text-gray-600 dark:text-gray-400">
{{ t('apiStats.activeKeys') }}
</div>
</div>
</div>
</div>
@@ -48,7 +52,9 @@
>
<div class="mb-3 flex items-center">
<i class="fas fa-chart-pie mr-2 text-purple-500" />
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('apiStats.aggregateStatsSummary') }}</span>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{
t('apiStats.aggregateStatsSummary')
}}</span>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between">
@@ -106,9 +112,9 @@
<!-- 每日费用限制 -->
<div>
<div class="mb-2 flex items-center justify-between">
<span class="text-sm font-medium text-gray-600 dark:text-gray-400 md:text-base"
>{{ t('apiStats.dailyCostLimit') }}</span
>
<span class="text-sm font-medium text-gray-600 dark:text-gray-400 md:text-base">{{
t('apiStats.dailyCostLimit')
}}</span>
<span class="text-xs text-gray-500 dark:text-gray-400 md:text-sm">
<span v-if="statsData.limits.dailyCostLimit > 0">
${{ statsData.limits.currentDailyCost.toFixed(4) }} / ${{
@@ -175,7 +181,9 @@
<!-- 其他限制信息 -->
<div class="space-y-2 border-t border-gray-100 pt-2 dark:border-gray-700">
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.concurrencyLimit') }}</span>
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
t('apiStats.concurrencyLimit')
}}</span>
<span class="text-sm font-medium text-gray-900 md:text-base">
<span v-if="statsData.limits.concurrencyLimit > 0">
{{ statsData.limits.concurrencyLimit }}
@@ -186,7 +194,9 @@
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.modelLimit') }}</span>
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
t('apiStats.modelLimit')
}}</span>
<span class="text-sm font-medium text-gray-900 md:text-base">
<span
v-if="
@@ -196,7 +206,11 @@
class="text-orange-600"
>
<i class="fas fa-exclamation-triangle mr-1 text-xs md:text-sm" />
{{ t('apiStats.restrictedModelsCount', { count: statsData.restrictions.restrictedModels.length }) }}
{{
t('apiStats.restrictedModelsCount', {
count: statsData.restrictions.restrictedModels.length
})
}}
</span>
<span v-else class="text-green-600">
<i class="fas fa-check-circle mr-1 text-xs md:text-sm" />
@@ -205,7 +219,9 @@
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.clientLimit') }}</span>
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
t('apiStats.clientLimit')
}}</span>
<span class="text-sm font-medium text-gray-900 md:text-base">
<span
v-if="
@@ -215,7 +231,11 @@
class="text-orange-600"
>
<i class="fas fa-exclamation-triangle mr-1 text-xs md:text-sm" />
{{ t('apiStats.restrictedClientsCount', { count: statsData.restrictions.allowedClients.length }) }}
{{
t('apiStats.restrictedClientsCount', {
count: statsData.restrictions.allowedClients.length
})
}}
</span>
<span v-else class="text-green-600">
<i class="fas fa-check-circle mr-1 text-xs md:text-sm" />

View File

@@ -19,7 +19,9 @@
<i
class="fas fa-spinner loading-spinner mb-2 text-xl text-gray-600 dark:text-gray-400 md:text-2xl"
/>
<p class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.loadingModelStats') }}</p>
<p class="text-sm text-gray-600 dark:text-gray-400 md:text-base">
{{ t('apiStats.loadingModelStats') }}
</p>
</div>
<!-- 模型统计数据 -->
@@ -38,7 +40,9 @@
<div class="text-base font-bold text-green-600 md:text-lg">
{{ model.formatted?.total || '$0.000000' }}
</div>
<div class="text-xs text-gray-600 dark:text-gray-400 md:text-sm">{{ t('apiStats.totalCost') }}</div>
<div class="text-xs text-gray-600 dark:text-gray-400 md:text-sm">
{{ t('apiStats.totalCost') }}
</div>
</div>
</div>
@@ -56,7 +60,9 @@
</div>
</div>
<div class="rounded bg-gray-50 p-2 dark:bg-gray-700">
<div class="text-gray-600 dark:text-gray-400">{{ t('apiStats.cacheCreateTokens') }}</div>
<div class="text-gray-600 dark:text-gray-400">
{{ t('apiStats.cacheCreateTokens') }}
</div>
<div class="font-medium text-gray-900 dark:text-gray-100">
{{ formatNumber(model.cacheCreateTokens) }}
</div>
@@ -75,7 +81,11 @@
<div v-else class="py-6 text-center text-gray-500 dark:text-gray-400 md:py-8">
<i class="fas fa-chart-pie mb-3 text-2xl md:text-3xl" />
<p class="text-sm md:text-base">
{{ t('apiStats.noModelData', { period: statsPeriod === 'daily' ? t('apiStats.today') : t('apiStats.thisMonth') }) }}
{{
t('apiStats.noModelData', {
period: statsPeriod === 'daily' ? t('apiStats.today') : t('apiStats.thisMonth')
})
}}
</p>
</div>
</div>

View File

@@ -17,39 +17,51 @@
<!-- Key 模式下的概要信息 -->
<div v-if="multiKeyMode && aggregatedStats" class="space-y-2 md:space-y-3">
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.queryKeysCount') }}</span>
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
t('apiStats.queryKeysCount')
}}</span>
<span class="text-sm font-medium text-gray-900 dark:text-gray-100 md:text-base">
{{ aggregatedStats.totalKeys }} {{ t('apiStats.individual') }}
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.activeKeysCount') }}</span>
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
t('apiStats.activeKeysCount')
}}</span>
<span class="text-sm font-medium text-green-600 md:text-base">
<i class="fas fa-check-circle mr-1 text-xs md:text-sm" />
{{ aggregatedStats.activeKeys }} {{ t('apiStats.individual') }}
</span>
</div>
<div v-if="invalidKeys.length > 0" class="flex items-center justify-between">
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.invalidKeysCount') }}</span>
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
t('apiStats.invalidKeysCount')
}}</span>
<span class="text-sm font-medium text-red-600 md:text-base">
<i class="fas fa-times-circle mr-1 text-xs md:text-sm" />
{{ invalidKeys.length }} {{ t('apiStats.individual') }}
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.totalRequests') }}</span>
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
t('apiStats.totalRequests')
}}</span>
<span class="text-sm font-medium text-gray-900 dark:text-gray-100 md:text-base">
{{ formatNumber(aggregatedStats.usage.requests) }}
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.totalTokens') }}</span>
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
t('apiStats.totalTokens')
}}</span>
<span class="text-sm font-medium text-gray-900 dark:text-gray-100 md:text-base">
{{ formatNumber(aggregatedStats.usage.allTokens) }}
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.totalCost') }}</span>
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
t('apiStats.totalCost')
}}</span>
<span class="text-sm font-medium text-indigo-600 md:text-base">
{{ aggregatedStats.usage.formattedCost }}
</span>
@@ -60,7 +72,9 @@
v-if="individualStats.length > 1"
class="border-t border-gray-200 pt-2 dark:border-gray-700"
>
<div class="mb-2 text-xs text-gray-500 dark:text-gray-400">{{ t('apiStats.keyContribution') }}</div>
<div class="mb-2 text-xs text-gray-500 dark:text-gray-400">
{{ t('apiStats.keyContribution') }}
</div>
<div class="space-y-1">
<div
v-for="stat in topContributors"
@@ -79,14 +93,18 @@
<!-- Key 模式下的详细信息 -->
<div v-else class="space-y-2 md:space-y-3">
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.name') }}</span>
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
t('apiStats.name')
}}</span>
<span
class="break-all text-sm font-medium text-gray-900 dark:text-gray-100 md:text-base"
>{{ statsData.name }}</span
>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.status') }}</span>
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
t('apiStats.status')
}}</span>
<span
class="text-sm font-medium md:text-base"
:class="statsData.isActive ? 'text-green-600' : 'text-red-600'"
@@ -99,22 +117,26 @@
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.permissions') }}</span>
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
t('apiStats.permissions')
}}</span>
<span class="text-sm font-medium text-gray-900 dark:text-gray-100 md:text-base">{{
formatPermissions(statsData.permissions)
}}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.createdAt') }}</span>
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
t('apiStats.createdAt')
}}</span>
<span
class="break-all text-xs font-medium text-gray-900 dark:text-gray-100 md:text-base"
>{{ formatDate(statsData.createdAt) }}</span
>
</div>
<div class="flex items-start justify-between">
<span class="mt-1 flex-shrink-0 text-sm text-gray-600 dark:text-gray-400 md:text-base"
>{{ t('apiStats.expiresAt') }}</span
>
<span class="mt-1 flex-shrink-0 text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
t('apiStats.expiresAt')
}}</span>
<!-- 未激活状态 -->
<div
v-if="statsData.expirationMode === 'activation' && !statsData.isActivated"
@@ -177,7 +199,9 @@
{{ formatNumber(currentPeriodData.requests) }}
</div>
<div class="text-xs text-gray-600 dark:text-gray-400 md:text-sm">
{{ statsPeriod === 'daily' ? t('apiStats.todayRequests') : t('apiStats.monthlyRequests') }}
{{
statsPeriod === 'daily' ? t('apiStats.todayRequests') : t('apiStats.monthlyRequests')
}}
</div>
</div>
<div class="stat-card text-center">
@@ -201,7 +225,11 @@
{{ formatNumber(currentPeriodData.inputTokens) }}
</div>
<div class="text-xs text-gray-600 dark:text-gray-400 md:text-sm">
{{ statsPeriod === 'daily' ? t('apiStats.todayInputTokens') : t('apiStats.monthlyInputTokens') }}
{{
statsPeriod === 'daily'
? t('apiStats.todayInputTokens')
: t('apiStats.monthlyInputTokens')
}}
</div>
</div>
</div>

View File

@@ -51,9 +51,9 @@
</div>
<div class="mt-3 border-t border-gray-200 pt-3 dark:border-gray-700 md:mt-4 md:pt-4">
<div class="flex items-center justify-between font-bold text-gray-900 dark:text-gray-100">
<span class="text-sm md:text-base"
>{{ statsPeriod === 'daily' ? t('apiStats.todayTotal') : t('apiStats.monthlyTotal') }}</span
>
<span class="text-sm md:text-base">{{
statsPeriod === 'daily' ? t('apiStats.todayTotal') : t('apiStats.monthlyTotal')
}}</span>
<span class="text-lg md:text-xl">{{ formatNumber(currentPeriodData.allTokens) }}</span>
</div>
</div>

View File

@@ -77,7 +77,9 @@
<!-- 版本信息 -->
<div class="border-b border-gray-100 px-4 py-3 dark:border-gray-700">
<div class="flex items-center justify-between text-sm">
<span class="text-gray-500 dark:text-gray-400">{{ t('header.currentVersion') }}</span>
<span class="text-gray-500 dark:text-gray-400">{{
t('header.currentVersion')
}}</span>
<span class="font-mono text-gray-700 dark:text-gray-300"
>v{{ versionInfo.current || '...' }}</span
>
@@ -165,7 +167,9 @@
>
<i class="fas fa-key text-white" />
</div>
<h3 class="text-xl font-bold text-gray-900 dark:text-gray-100">{{ t('header.changePasswordModal.title') }}</h3>
<h3 class="text-xl font-bold text-gray-900 dark:text-gray-100">
{{ t('header.changePasswordModal.title') }}
</h3>
</div>
<button
class="text-gray-400 transition-colors hover:text-gray-600 dark:hover:text-gray-300"
@@ -180,9 +184,9 @@
@submit.prevent="changePassword"
>
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>{{ t('header.changePasswordModal.currentUsername') }}</label
>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
t('header.changePasswordModal.currentUsername')
}}</label>
<input
class="form-input w-full cursor-not-allowed bg-gray-100 dark:bg-gray-700 dark:text-gray-300"
disabled
@@ -195,22 +199,24 @@
</div>
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>{{ t('header.changePasswordModal.newUsername') }}</label
>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
t('header.changePasswordModal.newUsername')
}}</label>
<input
v-model="changePasswordForm.newUsername"
class="form-input w-full"
:placeholder="t('header.changePasswordModal.newUsernamePlaceholder')"
type="text"
/>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">{{ t('header.changePasswordModal.newUsernameHint') }}</p>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
{{ t('header.changePasswordModal.newUsernameHint') }}
</p>
</div>
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>{{ t('header.changePasswordModal.currentPassword') }}</label
>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
t('header.changePasswordModal.currentPassword')
}}</label>
<input
v-model="changePasswordForm.currentPassword"
class="form-input w-full"
@@ -221,9 +227,9 @@
</div>
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>{{ t('header.changePasswordModal.newPassword') }}</label
>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
t('header.changePasswordModal.newPassword')
}}</label>
<input
v-model="changePasswordForm.newPassword"
class="form-input w-full"
@@ -231,13 +237,15 @@
required
type="password"
/>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">{{ t('header.changePasswordModal.newPasswordHint') }}</p>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
{{ t('header.changePasswordModal.newPasswordHint') }}
</p>
</div>
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>{{ t('header.changePasswordModal.confirmPassword') }}</label
>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
t('header.changePasswordModal.confirmPassword')
}}</label>
<input
v-model="changePasswordForm.confirmPassword"
class="form-input w-full"
@@ -262,7 +270,11 @@
>
<div v-if="changePasswordLoading" class="loading-spinner mr-2" />
<i v-else class="fas fa-save mr-2" />
{{ changePasswordLoading ? t('header.changePasswordModal.saving') : t('header.changePasswordModal.save') }}
{{
changePasswordLoading
? t('header.changePasswordModal.saving')
: t('header.changePasswordModal.save')
}}
</button>
</div>
</form>

File diff suppressed because it is too large Load Diff

View File

@@ -202,7 +202,8 @@ export default {
apiKeysPlaceholder: '请输入您的 API Keys支持以下格式\ncr_xxx\ncr_yyy\n或\ncr_xxx, cr_yyy',
clearInput: '清空输入',
securityNoticeSingle: '您的 API Key 仅用于查询自己的统计数据,不会被存储或用于其他用途',
securityNoticeMulti: '您的 API Keys 仅用于查询统计数据,不会被存储。聚合模式下部分个体化信息将不显示。',
securityNoticeMulti:
'您的 API Keys 仅用于查询统计数据,不会被存储。聚合模式下部分个体化信息将不显示。',
multiKeyTip: '提示:最多支持同时查询 30 个 API Keys。使用 Ctrl+Enter 快速查询。'
},
@@ -454,7 +455,8 @@ export default {
// Messages and confirmations
resetStatusConfirmTitle: '重置账户状态',
resetStatusConfirmMessage: '确定要重置此账户的所有异常状态吗这将清除限流状态、401错误计数等所有异常标记。',
resetStatusConfirmMessage:
'确定要重置此账户的所有异常状态吗这将清除限流状态、401错误计数等所有异常标记。',
resetStatusConfirmButton: '确定重置',
resetStatusCancelButton: '取消',
statusResetSuccess: '账户状态已重置',
@@ -464,7 +466,8 @@ export default {
deleteAccountMessage: '确定要删除账户 "{name}" 吗?\n\n此操作不可恢复。',
deleteAccountButton: '删除',
deleteAccountCancel: '取消',
cannotDeleteBoundAccount: '无法删除此账号,有 {count} 个API Key绑定到此账号请先解绑所有API Key',
cannotDeleteBoundAccount:
'无法删除此账号,有 {count} 个API Key绑定到此账号请先解绑所有API Key',
accountDeleted: '账户已删除',
deleteFailed: '删除失败',
@@ -629,8 +632,10 @@ export default {
confirmDelete: '确定要删除这个 API Key 吗?此操作不可恢复。',
confirmBatchDelete: '确定要删除选中的 {count} 个 API Key 吗?此操作不可恢复。',
confirmRestore: '确定要恢复这个 API Key 吗?恢复后可以重新使用。',
confirmPermanentDelete: '确定要彻底删除这个 API Key 吗?此操作不可恢复,所有相关数据将被永久删除。',
confirmClearAll: '确定要彻底删除全部 {count} 个已删除的 API Keys 吗?此操作不可恢复,所有相关数据将被永久删除。',
confirmPermanentDelete:
'确定要彻底删除这个 API Key 吗?此操作不可恢复,所有相关数据将被永久删除。',
confirmClearAll:
'确定要彻底删除全部 {count} 个已删除的 API Keys 吗?此操作不可恢复,所有相关数据将被永久删除。',
// Success messages
keyActivated: 'API Key 已激活',
@@ -661,7 +666,583 @@ export default {
// Batch operations
batchSuccess: '成功处理 {count} 个项目',
batchPartialFail: '{failed} 个处理失败',
batchAllFailed: '所有项目处理失败'
batchAllFailed: '所有项目处理失败',
// Batch API Key Modal
batchApiKeyModal: {
title: '批量创建成功',
successMessage: '成功创建 {count} 个 API Key',
importantReminder: '重要提醒',
warningMessage:
'这是您唯一能看到所有 API Key 的机会。关闭此窗口后,系统将不再显示完整的 API Key。请立即下载并妥善保存。',
// Statistics cards
createdCount: '创建数量',
baseName: '基础名称',
permissionScope: '权限范围',
expiryTime: '过期时间',
// Permission texts
permissions: {
all: '全部服务',
claude: '仅 Claude',
gemini: '仅 Gemini',
unknown: '未知'
},
// Expiry time texts
neverExpire: '永不过期',
daysFormat: '{days}天',
weeksFormat: '{weeks}周',
monthsFormat: '{months}个月',
yearsFormat: '{years}年',
// Preview section
previewTitle: 'API Keys 预览',
hide: '隐藏',
show: '显示',
preview: '预览',
maxDisplayNote: '最多显示前10个',
moreKeysNote: '... 还有 {count} 个 API Key',
// Action buttons
downloadAll: '下载所有 API Keys',
alreadySaved: '我已保存',
directCloseTooltip: '直接关闭(不推荐)',
// File info
fileFormatInfo:
'下载的文件格式为文本文件(.txt每行包含一个 API Key。请将文件保存在安全的位置避免泄露。',
// Confirmation dialogs
closeReminderTitle: '关闭提醒',
closeReminderMessage:
'关闭后将无法再次查看这些 API Key请确保已经下载并妥善保存。\n\n确定要关闭吗',
confirmCloseButton: '确定关闭',
goBackDownloadButton: '返回下载',
directCloseTitle: '确定要关闭吗?',
directCloseMessage: '您还没有下载 API Keys关闭后将无法再次查看。\n\n强烈建议您先下载保存。',
stillCloseButton: '仍然关闭',
directCloseFallbackMessage: '您还没有下载 API Keys关闭后将无法再次查看。\n\n确定要关闭吗',
// Success messages
downloadSuccess: 'API Keys 文件已下载'
},
// Window Countdown
windowCountdown: {
expired: '窗口已过期',
notStarted: '窗口未激活',
minutes: '分钟',
requests: '请求',
tokens: 'Token',
cost: '费用',
aboutToReset: '即将重置',
minutesUntilReset: '分钟后重置',
untilReset: '后重置',
windowLimit: '窗口限制',
hours: '小时'
},
// Expiry Edit Modal
expiryEditModal: {
title: '修改过期时间',
subtitle: '为 "{name}" 设置新的过期时间',
currentStatus: '当前状态',
notActivated: '未激活',
activationDaysHint: '(激活后 {days} 天过期)',
neverExpire: '永不过期',
expired: '已过期',
daysToExpire: '{days} 天后过期',
monthsToExpire: '{months} 个月后过期',
activateNow: '立即激活',
activateButton: '立即激活 (激活后 {days} 天过期)',
activationInfo: '点击立即激活此 API Key激活后将在 {days} 天后过期',
selectNewDuration: '选择新的期限',
neverExpireOption: '永不过期',
oneDay: '1 天',
sevenDays: '7 天',
thirtyDays: '30 天',
ninetyDays: '90 天',
oneHundredEightyDays: '180 天',
threeSixtyFiveDays: '1 年',
twoYears: '2 年',
custom: '自定义',
selectDateAndTime: '选择日期和时间',
selectFutureDateTime: '选择一个未来的日期和时间作为过期时间',
newExpiryTime: '新的过期时间',
cancel: '取消',
saving: '保存中...',
saveChanges: '保存更改',
activateConfirmTitle: '激活 API Key',
activateConfirmMessage: '确定要立即激活此 API Key 吗?激活后将在 {days} 天后自动过期。',
confirmActivate: '确定激活',
confirmCancel: '取消'
},
// Edit API Key Modal
editApiKeyModal: {
title: '编辑 API Key',
// Basic Info
name: '名称',
namePlaceholder: '请输入API Key名称',
nameHint: '用于识别此 API Key 的用途',
// Owner
owner: '所有者',
adminLabel: '- 管理员',
ownerHint: '分配此 API Key 给指定用户或管理员,管理员分配时不受用户 API Key 数量限制',
// Tags
tags: '标签',
selectedTags: '已选择的标签:',
clickToSelectTags: '点击选择已有标签:',
createNewTag: '创建新标签:',
newTagPlaceholder: '输入新标签名称',
tagsHint: '用于标记不同团队或用途,方便筛选管理',
// Rate Limit Settings
rateLimitTitle: '速率限制设置 (可选)',
rateLimitWindow: '时间窗口 (分钟)',
rateLimitRequests: '请求次数限制',
rateLimitCost: '费用限制 (美元)',
rateLimitWindowHint: '时间段单位',
rateLimitRequestsHint: '窗口内最大请求',
rateLimitCostHint: '窗口内最大费用',
noLimit: '无限制',
// Usage Examples
usageExamples: '💡 使用示例',
example1: '示例1: 时间窗口=60请求次数=1000 → 每60分钟最多1000次请求',
example2: '示例2: 时间窗口=1费用=0.1 → 每分钟最多$0.1费用',
example3: '示例3: 窗口=30请求=50费用=5 → 每30分钟50次请求且不超$5费用',
// Cost Limits
dailyCostLimit: '每日费用限制 (美元)',
dailyCostLimitPlaceholder: '0 表示无限制',
dailyCostHint: '设置此 API Key 每日的费用限制超过限制将拒绝请求0 或留空表示无限制',
weeklyOpusCostLimit: 'Opus 模型周费用限制 (美元)',
weeklyOpusHint:
'设置 Opus 模型的周费用限制(周一到周日),仅限 Claude 官方账户0 或留空表示无限制',
custom: '自定义',
// Concurrency
concurrencyLimit: '并发限制',
concurrencyLimitPlaceholder: '0 表示无限制',
concurrencyHint: '设置此 API Key 可同时处理的最大请求数',
// Active Status
activeStatus: '激活账号',
activeStatusHint: '取消勾选将禁用此 API Key暂停所有请求客户端返回 401 错误',
// Service Permissions
servicePermissions: '服务权限',
allServices: '全部服务',
claudeOnly: '仅 Claude',
geminiOnly: '仅 Gemini',
openaiOnly: '仅 OpenAI',
permissionsHint: '控制此 API Key 可以访问哪些服务',
// Account Binding
accountBinding: '专属账号绑定',
refreshAccounts: '刷新账号',
refreshing: '刷新中...',
claudeAccount: 'Claude 专属账号',
geminiAccount: 'Gemini 专属账号',
openaiAccount: 'OpenAI 专属账号',
bedrockAccount: 'Bedrock 专属账号',
useSharedPool: '使用共享账号池',
selectClaudeAccount: '请选择Claude账号',
selectGeminiAccount: '请选择Gemini账号',
selectOpenaiAccount: '请选择OpenAI账号',
selectBedrockAccount: '请选择Bedrock账号',
accountBindingHint: '修改绑定账号将影响此API Key的请求路由',
// Model Restrictions
enableModelRestriction: '启用模型限制',
restrictedModels: '限制的模型列表',
noRestrictedModels: '暂无限制的模型',
allCommonModelsRestricted: '所有常用模型已在限制列表中',
addRestrictedModelPlaceholder: '输入模型名称,按回车添加',
modelRestrictionHint: '设置此API Key无法访问的模型例如claude-opus-4-20250514',
// Client Restrictions
enableClientRestriction: '启用客户端限制',
allowedClients: '允许的客户端',
clientRestrictionHint: '勾选允许使用此API Key的客户端',
// Buttons
cancel: '取消',
save: '保存修改',
saving: '保存中...',
// Messages
costLimitConfirmTitle: '费用限制提醒',
costLimitConfirmMessage:
'您设置了时间窗口但费用限制为0这意味着不会有费用限制。\n\n是否继续',
costLimitConfirmContinue: '继续保存',
costLimitConfirmBack: '返回修改',
refreshAccountsSuccess: '账号列表已刷新',
refreshAccountsFailed: '刷新账号列表失败',
updateFailed: '更新失败'
},
// Renew API Key Modal
renewApiKeyModal: {
title: '续期 API Key',
apiKeyInfo: 'API Key 信息',
currentExpiry: '当前过期时间:',
neverExpires: '永不过期',
renewDuration: '续期时长',
extend7Days: '延长 7 天',
extend30Days: '延长 30 天',
extend90Days: '延长 90 天',
extend180Days: '延长 180 天',
extend365Days: '延长 365 天',
customDate: '自定义日期',
setPermanent: '设为永不过期',
newExpiry: '新的过期时间:',
cancel: '取消',
renewing: '续期中...',
confirmRenew: '确认续期',
renewSuccess: 'API Key 续期成功',
renewFailed: '续期失败'
},
// Batch Edit API Key Modal
batchEditApiKeyModal: {
title: '批量编辑 API Keys ({count} 个)',
// Info section
infoTitle: '批量编辑说明',
infoContent:
'以下设置将应用到所选的 {count} 个 API Key。只有填写或修改的字段才会被更新空白字段将保持原值不变。',
// Tag operations
tagLabel: '标签 (批量操作)',
tagOperations: {
replace: '替换标签',
add: '添加标签',
remove: '移除标签',
none: '不修改标签'
},
// Tag status texts
newTagsList: '新标签列表:',
tagsToAdd: '要添加的标签:',
tagsToRemove: '要移除的标签:',
clickToSelectTags: '点击选择已有标签:',
createNewTag: '创建新标签:',
inputNewTagPlaceholder: '输入新标签名称',
// Rate limit settings
rateLimitTitle: '速率限制设置',
rateLimitWindow: '时间窗口 (分钟)',
rateLimitRequests: '请求次数限制',
rateLimitCost: '费用限制 (美元)',
noModifyPlaceholder: '不修改',
// Daily cost limit
dailyCostLimit: '每日费用限制 (美元)',
dailyCostLimitPlaceholder: '不修改 (0 表示无限制)',
// Weekly Opus cost limit
weeklyOpusCostLimit: 'Opus 模型周费用限制 (美元)',
weeklyOpusCostLimitPlaceholder: '不修改 (0 表示无限制)',
opusLimitDescription: '设置 Opus 模型的周费用限制(周一到周日),仅限 Claude 官方账户',
// Concurrency limit
concurrencyLimit: '并发限制',
concurrencyLimitPlaceholder: '不修改 (0 表示无限制)',
// Active status
activeStatus: '激活状态',
statusOptions: {
active: '激活',
disabled: '禁用',
noChange: '不修改'
},
// Service permissions
servicePermissions: '服务权限',
permissionOptions: {
noChange: '不修改',
all: '全部服务',
claude: '仅 Claude',
gemini: '仅 Gemini',
openai: '仅 OpenAI'
},
// Account binding
accountBinding: '专属账号绑定',
refreshAccounts: '刷新账号',
refreshing: '刷新中...',
claudeAccount: 'Claude 专属账号',
geminiAccount: 'Gemini 专属账号',
openaiAccount: 'OpenAI 专属账号',
bedrockAccount: 'Bedrock 专属账号',
accountOptions: {
noChange: '不修改',
sharedPool: '使用共享账号池',
groupPrefix: '分组 - '
},
// Optgroup labels
optgroupLabels: {
accountGroups: '账号分组',
dedicatedAccounts: '专属账号'
},
// Buttons
cancel: '取消',
saving: '保存中...',
batchSave: '批量保存',
// Messages
refreshAccountsSuccess: '账号列表已刷新',
refreshAccountsFailed: '刷新账号列表失败',
batchEditSuccess: '成功批量编辑 {count} 个 API Keys',
batchEditPartialFail: '{failedCount} 个编辑失败:\n{errors}',
batchEditAllFailed: '所有 API Keys 编辑失败',
batchEditFailed: '批量编辑失败',
batchEditErrorLog: '批量编辑 API Keys 失败:'
},
// Usage Detail Modal
usageDetailModal: {
title: '使用统计详情',
close: '关闭',
// Statistics cards
totalRequests: '总请求数',
totalTokens: '总Token数',
totalCost: '总费用',
averageRate: '平均速率',
// Today stats
today: '今日',
todayRequests: '{count} 次',
todayTokens: '{count}',
todayCost: '${amount}',
// Usage labels
times: '次',
// Token distribution
tokenDistribution: 'Token 使用分布',
inputTokens: '输入 Token',
outputTokens: '输出 Token',
cacheCreateTokens: '缓存创建 Token',
cacheReadTokens: '缓存读取 Token',
// Limits section
limitSettings: '限制设置',
dailyCostLimit: '每日费用限制',
concurrencyLimit: '并发限制',
timeWindowLimit: '时间窗口限制',
windowStatus: '窗口状态',
used: '已使用',
remainingQuota: '剩余: ${amount}',
// Progress indicators
usedPercentage: '已使用 {percentage}%'
},
// Create API Key Modal
newApiKeyModal: {
title: 'API Key 创建成功',
subtitle: '请妥善保存您的 API Key',
directCloseTooltip: '直接关闭(不推荐)',
// 警告提示
warningTitle: '重要提醒',
warningMessage:
'这是您唯一能看到完整 API Key 的机会。关闭此窗口后,系统将不再显示完整的 API Key。请立即复制并妥善保存。',
// 字段标签
apiKeyName: 'API Key 名称',
remarks: '备注',
noDescription: '无描述',
apiKey: 'API Key',
// 可见性切换
hideApiKey: '隐藏API Key',
showFullApiKey: '显示完整API Key',
visibilityHint: '点击眼睛图标切换显示模式,使用下方按钮复制完整 API Key',
// 按钮
copyApiKey: '复制 API Key',
alreadySaved: '我已保存',
// 确认对话框
closeReminderTitle: '关闭提醒',
closeReminderMessage:
'关闭后将无法再次查看完整的API Key请确保已经妥善保存。\n\n确定要关闭吗',
confirmClose: '确定关闭',
cancel: '取消',
directCloseTitle: '确定要关闭吗?',
directCloseMessage:
'您还没有保存API Key关闭后将无法再次查看。\n\n建议您先复制API Key再关闭。',
stillClose: '仍然关闭',
goBack: '返回复制',
directCloseFallback: '您还没有保存API Key关闭后将无法再次查看。\n\n确定要关闭吗',
// 成功消息
apiKeyNotFound: 'API Key 不存在',
copySuccess: 'API Key 已复制到剪贴板',
copyFailed: '复制失败,请手动复制'
},
createApiKeyModal: {
title: '创建新的 API Key',
// Create type section
createType: '创建类型',
singleCreate: '单个创建',
batchCreate: '批量创建',
batchCount: '创建数量',
batchCountPlaceholder: '输入数量 (2-500)',
maxSupported: '最大支持 500 个',
batchHint: '批量创建时,每个 Key 的名称会自动添加序号后缀,例如:{name}_1, {name}_2 ...',
// Basic form fields
name: '名称',
nameRequired: '*',
nameError: '请输入API Key名称',
singleNamePlaceholder: '为您的 API Key 取一个名称',
batchNamePlaceholder: '输入基础名称(将自动添加序号)',
description: '备注 (可选)',
descriptionPlaceholder: '描述此 API Key 的用途...',
// Tags section
tags: '标签',
selectedTags: '已选择的标签:',
clickToSelectTags: '点击选择已有标签:',
createNewTag: '创建新标签:',
newTagPlaceholder: '输入新标签名称',
tagHint: '用于标记不同团队或用途,方便筛选管理',
// Rate limit section
rateLimitTitle: '速率限制设置 (可选)',
rateLimitWindow: '时间窗口 (分钟)',
rateLimitRequests: '请求次数限制',
rateLimitCost: '费用限制 (美元)',
rateLimitWindowPlaceholder: '无限制',
rateLimitRequestsPlaceholder: '无限制',
rateLimitCostPlaceholder: '无限制',
rateLimitWindowHint: '时间段单位',
rateLimitRequestsHint: '窗口内最大请求',
rateLimitCostHint: '窗口内最大费用',
// Rate limit examples
exampleTitle: '💡 使用示例',
example1: '示例1: 时间窗口=60请求次数=1000 → 每60分钟最多1000次请求',
example2: '示例2: 时间窗口=1费用=0.1 → 每分钟最多$0.1费用',
example3: '示例3: 窗口=30请求=50费用=5 → 每30分钟50次请求且不超$5费用',
// Cost limits
dailyCostLimit: '每日费用限制 (美元)',
dailyCostLimitPlaceholder: '0 表示无限制',
dailyCostHint: '设置此 API Key 每日的费用限制超过限制将拒绝请求0 或留空表示无限制',
weeklyOpusCostLimit: 'Opus 模型周费用限制 (美元)',
weeklyOpusCostLimitPlaceholder: '0 表示无限制',
weeklyOpusHint:
'设置 Opus 模型的周费用限制(周一到周日),仅限 Claude 官方账户0 或留空表示无限制',
custom: '自定义',
// Concurrency limit
concurrencyLimit: '并发限制 (可选)',
concurrencyLimitPlaceholder: '0 表示无限制',
concurrencyHint: '设置此 API Key 可同时处理的最大请求数0 或留空表示无限制',
// Expiration settings
expirationSettings: '过期设置',
fixedTimeExpiry: '固定时间过期',
activationExpiry: '首次使用后激活',
fixedModeHint: '固定时间模式Key 创建后立即生效,按设定时间过期',
activationModeHint: 'Key 首次使用时激活,激活后按设定天数过期(适合批量销售)',
// Expiration duration options
neverExpire: '永不过期',
'1d': '1 天',
'7d': '7 天',
'30d': '30 天',
'90d': '90 天',
'180d': '180 天',
'365d': '365 天',
customDate: '自定义日期',
// Activation mode
activationDays: '输入天数',
daysUnit: '天',
activationHint: 'Key 将在首次使用后激活,激活后 {days} 天过期',
// Expiry status
willExpireOn: '将于 {date} 过期',
// Service permissions
servicePermissions: '服务权限',
allServices: '全部服务',
claudeOnly: '仅 Claude',
geminiOnly: '仅 Gemini',
openaiOnly: '仅 OpenAI',
permissionHint: '控制此 API Key 可以访问哪些服务',
// Account binding
dedicatedAccountBinding: '专属账号绑定 (可选)',
refreshAccounts: '刷新账号',
refreshing: '刷新中...',
claudeDedicatedAccount: 'Claude 专属账号',
geminiDedicatedAccount: 'Gemini 专属账号',
openaiDedicatedAccount: 'OpenAI 专属账号',
bedrockDedicatedAccount: 'Bedrock 专属账号',
useSharedPool: '使用共享账号池',
accountBindingHint: '选择专属账号后此API Key将只使用该账号不选择则使用共享账号池',
selectClaudeAccount: '请选择Claude账号',
selectGeminiAccount: '请选择Gemini账号',
selectOpenaiAccount: '请选择OpenAI账号',
selectBedrockAccount: '请选择Bedrock账号',
// Model restrictions
enableModelRestriction: '启用模型限制',
restrictedModelsList: '限制的模型列表',
noRestrictedModels: '暂无限制的模型',
allCommonModelsRestricted: '所有常用模型已在限制列表中',
addRestrictedModelPlaceholder: '输入模型名称,按回车添加',
modelRestrictionHint: '设置此API Key无法访问的模型例如claude-opus-4-20250514',
// Client restrictions
enableClientRestriction: '启用客户端限制',
allowedClients: '允许的客户端',
// Buttons
cancel: '取消',
create: '创建',
creating: '创建中...',
// Messages
batchCountError: '批量创建数量必须在 2-500 之间',
costLimitConfirmTitle: '费用限制提醒',
costLimitConfirmMessage:
'您设置了时间窗口但费用限制为0这意味着不会有费用限制。\n\n是否继续',
costLimitConfirmContinue: '继续创建',
costLimitConfirmBack: '返回修改',
costLimitFallbackMessage:
'您设置了时间窗口但费用限制为0这意味着不会有费用限制。\n是否继续',
createSuccess: 'API Key 创建成功',
batchCreateSuccess: '成功创建 {count} 个 API Key',
createFailed: '创建失败',
batchCreateFailed: '批量创建失败',
refreshAccountsSuccess: '账号列表已刷新',
refreshAccountsFailed: '刷新账号列表失败'
}
},
// User-related translations
@@ -771,13 +1352,15 @@ export default {
// Confirmation dialogs
disableUserTitle: 'Disable User',
enableUserTitle: 'Enable User',
disableUserMessage: 'Are you sure you want to disable user "{username}"? This will prevent them from logging in.',
disableUserMessage:
'Are you sure you want to disable user "{username}"? This will prevent them from logging in.',
enableUserMessage: 'Are you sure you want to enable user "{username}"?',
disable: 'Disable',
enable: 'Enable',
disableAllKeysTitle: 'Disable All API Keys',
disableAllKeysMessage: 'Are you sure you want to disable all {count} API keys for user "{username}"? This will prevent them from using the service.',
disableAllKeysMessage:
'Are you sure you want to disable all {count} API keys for user "{username}"? This will prevent them from using the service.',
disableKeys: 'Disable Keys',
// Success messages
@@ -874,7 +1457,8 @@ export default {
// Warning messages
roleChangeWarning: {
title: '角色变更警告',
grantAdmin: '授予管理员权限将使该用户拥有系统的完整访问权限包括管理其他用户及其API密钥的能力。',
grantAdmin:
'授予管理员权限将使该用户拥有系统的完整访问权限包括管理其他用户及其API密钥的能力。',
removeAdmin: '移除管理员权限将限制该用户只能管理自己的API密钥和查看自己的使用统计。'
},
@@ -1128,7 +1712,8 @@ export default {
accountTypeShared: '共享账户',
accountTypeDedicated: '专属账户',
accountTypeGroup: '分组调度',
accountTypeDescription: '共享账户供所有API Key使用专属账户仅供特定API Key使用分组调度加入分组供分组内调度',
accountTypeDescription:
'共享账户供所有API Key使用专属账户仅供特定API Key使用分组调度加入分组供分组内调度',
// 分组选择
selectGroup: '选择分组',
@@ -1149,7 +1734,8 @@ export default {
projectIdStep3: '⚠️ 注意:要复制项目 IDProject ID不要复制项目编号Project Number',
projectIdTip: '提示:如果您的账号是普通个人账号(未绑定 Google Cloud请留空此字段。',
projectIdGoogleCloudRequired: 'Google Cloud/Workspace 账号需要提供项目 ID',
projectIdGoogleCloudDescription: '某些 Google 账号(特别是绑定了 Google Cloud 的账号)会被识别为 Workspace 账号,需要提供额外的项目 ID。',
projectIdGoogleCloudDescription:
'某些 Google 账号(特别是绑定了 Google Cloud 的账号)会被识别为 Workspace 账号,需要提供额外的项目 ID。',
// Bedrock 字段
awsAccessKeyId: 'AWS 访问密钥 ID',
@@ -1191,7 +1777,8 @@ export default {
azureEndpoint: 'Azure Endpoint',
azureEndpointRequired: 'Azure Endpoint *',
azureEndpointPlaceholder: 'https://your-resource.openai.azure.com',
azureEndpointDescription: 'Azure OpenAI 资源的终结点 URL格式https://your-resource.openai.azure.com',
azureEndpointDescription:
'Azure OpenAI 资源的终结点 URL格式https://your-resource.openai.azure.com',
apiVersion: 'API 版本',
apiVersionPlaceholder: '2024-02-01',
apiVersionDescription: 'Azure OpenAI API 版本,默认使用最新稳定版本 2024-02-01',
@@ -1224,7 +1811,8 @@ export default {
used: '已使用',
modelMapping: '模型映射表',
modelMappingOptional: '模型映射表 (可选)',
modelMappingDescription: '留空表示支持所有模型且不修改请求。配置映射后,左侧模型会被识别为支持的模型,右侧是实际发送的模型。',
modelMappingDescription:
'留空表示支持所有模型且不修改请求。配置映射后,左侧模型会被识别为支持的模型,右侧是实际发送的模型。',
originalModel: '原始模型名称',
mappedModel: '映射后的模型名称',
addModelMapping: '添加模型映射',
@@ -1246,16 +1834,20 @@ export default {
// Claude 特殊功能
autoStopOnWarning: '5小时使用量接近限制时自动停止调度',
autoStopOnWarningDescription: '当系统检测到账户接近5小时使用限制时自动暂停调度该账户。进入新的时间窗口后会自动恢复调度。',
autoStopOnWarningDescription:
'当系统检测到账户接近5小时使用限制时自动暂停调度该账户。进入新的时间窗口后会自动恢复调度。',
useUnifiedUserAgent: '使用统一 Claude Code 版本',
useUnifiedUserAgentDescription: '开启后将使用从真实 Claude Code 客户端捕获的统一 User-Agent提高兼容性',
useUnifiedUserAgentDescription:
'开启后将使用从真实 Claude Code 客户端捕获的统一 User-Agent提高兼容性',
currentUnifiedVersion: '💡 当前统一版本:',
clearCache: '清除缓存',
clearing: '清除中...',
waitingForCapture: '⏳ 等待从 Claude Code 客户端捕获 User-Agent',
captureHint: '💡 提示:如果长时间未能捕获,请确认有 Claude Code 客户端正在使用此账户,或联系开发者检查 User-Agent 格式是否发生变化',
captureHint:
'💡 提示:如果长时间未能捕获,请确认有 Claude Code 客户端正在使用此账户,或联系开发者检查 User-Agent 格式是否发生变化',
useUnifiedClientId: '使用统一的客户端标识',
useUnifiedClientIdDescription: '开启后将使用固定的客户端标识,使所有请求看起来来自同一个客户端,减少特征',
useUnifiedClientIdDescription:
'开启后将使用固定的客户端标识,使所有请求看起来来自同一个客户端,减少特征',
clientId: '客户端标识 ID',
regenerate: '重新生成',
clientIdDescription: '此ID将替换请求中的user_id客户端部分保留session部分用于粘性会话',
@@ -1268,20 +1860,28 @@ export default {
// 手动输入 Token
manualTokenTitle: '手动输入 Token',
manualTokenDescription: '请输入有效的 Access Token。如果您有 Refresh Token建议也一并填写以支持自动刷新。',
manualTokenClaudeDescription: '请输入有效的 Claude Access Token。如果您有 Refresh Token建议也一并填写以支持自动刷新。',
manualTokenGeminiDescription: '请输入有效的 Gemini Access Token。如果您有 Refresh Token建议也一并填写以支持自动刷新。',
manualTokenOpenAIDescription: '请输入有效的 OpenAI Access Token。如果您有 Refresh Token建议也一并填写以支持自动刷新。',
manualTokenDescription:
'请输入有效的 Access Token。如果您有 Refresh Token建议也一并填写以支持自动刷新。',
manualTokenClaudeDescription:
'请输入有效的 Claude Access Token。如果您有 Refresh Token建议也一并填写以支持自动刷新。',
manualTokenGeminiDescription:
'请输入有效的 Gemini Access Token。如果您有 Refresh Token建议也一并填写以支持自动刷新。',
manualTokenOpenAIDescription:
'请输入有效的 OpenAI Access Token。如果您有 Refresh Token建议也一并填写以支持自动刷新。',
obtainTokenMethods: '获取 Access Token 的方法:',
claudeTokenPath: '请从已登录 Claude Code 的机器上获取 ~/.claude/.credentials.json 文件中的凭证, 请勿使用 Claude 官网 API Keys 页面的密钥。',
geminiTokenPath: '请从已登录 Gemini CLI 的机器上获取 ~/.config/gemini/credentials.json 文件中的凭证。',
openaiTokenPath: '请从已登录 OpenAI 账户的机器上获取认证凭证, 或通过 OAuth 授权流程获取 Access Token。',
claudeTokenPath:
'请从已登录 Claude Code 的机器上获取 ~/.claude/.credentials.json 文件中的凭证, 请勿使用 Claude 官网 API Keys 页面的密钥。',
geminiTokenPath:
'请从已登录 Gemini CLI 的机器上获取 ~/.config/gemini/credentials.json 文件中的凭证。',
openaiTokenPath:
'请从已登录 OpenAI 账户的机器上获取认证凭证, 或通过 OAuth 授权流程获取 Access Token。',
accessToken: 'Access Token',
accessTokenOptional: 'Access Token (可选)',
accessTokenRequired: 'Access Token *',
accessTokenPlaceholder: '请输入 Access Token...',
accessTokenOptionalPlaceholder: '可选:如果不填写,系统会自动通过 Refresh Token 获取...',
accessTokenOptionalDescription: 'Access Token 可选填。如果不提供,系统会通过 Refresh Token 自动获取。',
accessTokenOptionalDescription:
'Access Token 可选填。如果不提供,系统会通过 Refresh Token 自动获取。',
refreshToken: 'Refresh Token',
refreshTokenOptional: 'Refresh Token (可选)',
refreshTokenRequired: 'Refresh Token *',
@@ -1295,10 +1895,12 @@ export default {
setupTokenDescription: '请按照以下步骤通过 Setup Token 完成 Claude 账户的授权:',
setupTokenStep1Title: '点击下方按钮生成授权链接',
setupTokenStep2Title: '在浏览器中打开链接并完成授权',
setupTokenStep2Description: '请在新标签页中打开授权链接,登录您的 Claude 账户并授权 Claude Code。',
setupTokenStep2Description:
'请在新标签页中打开授权链接,登录您的 Claude 账户并授权 Claude Code。',
setupTokenStep2Warning: '注意:如果您设置了代理,请确保浏览器也使用相同的代理访问授权页面。',
setupTokenStep3Title: '输入 Authorization Code',
setupTokenStep3Description: '授权完成后,从返回页面复制 Authorization Code并粘贴到下方输入框',
setupTokenStep3Description:
'授权完成后,从返回页面复制 Authorization Code并粘贴到下方输入框',
generateSetupTokenUrl: '生成 Setup Token 授权链接',
generating: '生成中...',
copyLink: '复制链接',
@@ -1311,7 +1913,8 @@ export default {
// Token 更新(编辑模式)
updateTokenTitle: '更新 Token',
updateTokenDescription: '可以更新 Access Token 和 Refresh Token。为了安全起见不会显示当前的 Token 值。',
updateTokenDescription:
'可以更新 Access Token 和 Refresh Token。为了安全起见不会显示当前的 Token 值。',
updateTokenTip: '💡 留空表示不更新该字段。',
newAccessToken: '新的 Access Token',
newRefreshToken: '新的 Refresh Token',
@@ -1361,7 +1964,8 @@ export default {
// 确认对话框
projectIdNotFilledTitle: '项目 ID 未填写',
projectIdNotFilledMessage: '您尚未填写项目 ID。\n\n如果您的Google账号绑定了Google Cloud或被识别为Workspace账号需要提供项目 ID。\n如果您使用的是普通个人账号可以继续不填写。',
projectIdNotFilledMessage:
'您尚未填写项目 ID。\n\n如果您的Google账号绑定了Google Cloud或被识别为Workspace账号需要提供项目 ID。\n如果您使用的是普通个人账号可以继续不填写。',
continueButton: '继续',
goBackToFill: '返回填写',
continueSave: '继续保存',
@@ -1379,19 +1983,24 @@ export default {
leaveBlankNoUpdateSession: '留空表示不更新',
// 通用描述文本
allModelsIfEmpty: '留空表示支持所有模型。如果指定模型,请求中的模型不在列表内将不会调度到此账号',
allModelsIfEmpty:
'留空表示支持所有模型。如果指定模型,请求中的模型不在列表内将不会调度到此账号',
systemDefaultIfEmpty: '留空将使用系统默认模型。支持 inference profile ID 或 ARN',
noUpdateIfEmpty: '留空表示不更新该字段',
// 手动 Token 输入部分
manualTokenInput: '手动输入 Token',
manualTokenClaudeDescription: '请输入有效的 Claude Access Token。如果您有 Refresh Token建议也一并填写以支持自动刷新。',
manualTokenGeminiDescription: '请输入有效的 Gemini Access Token。如果您有 Refresh Token建议也一并填写以支持自动刷新。',
manualTokenOpenAIDescription: '请输入有效的 OpenAI Access Token。如果您有 Refresh Token建议也一并填写以支持自动刷新。',
manualTokenClaudeDescription:
'请输入有效的 Claude Access Token。如果您有 Refresh Token建议也一并填写以支持自动刷新。',
manualTokenGeminiDescription:
'请输入有效的 Gemini Access Token。如果您有 Refresh Token建议也一并填写以支持自动刷新。',
manualTokenOpenAIDescription:
'请输入有效的 OpenAI Access Token。如果您有 Refresh Token建议也一并填写以支持自动刷新。',
getAccessTokenMethod: '获取 Access Token 的方法:',
claudeCredentialsPath: '请从已登录 Claude Code 的机器上获取',
geminiCredentialsPath: '请从已登录 Gemini CLI 的机器上获取',
openaiCredentialsPath: '请从已登录 OpenAI 账户的机器上获取认证凭证,或通过 OAuth 授权流程获取 Access Token。',
openaiCredentialsPath:
'请从已登录 OpenAI 账户的机器上获取认证凭证,或通过 OAuth 授权流程获取 Access Token。',
claudeCredentialsWarning: '文件中的凭证,请勿使用 Claude 官网 API Keys 页面的密钥。',
refreshTokenWarning: '💡 如果未填写 Refresh TokenToken 过期后需要手动更新。',
accessTokenOptional: 'Access Token (可选)',
@@ -1422,16 +2031,20 @@ export default {
claudeProSubscription: 'Claude Pro',
claudeProLimitation: 'Pro 账号不支持 Claude Opus 4 模型',
autoStopOnWarning: '5小时使用量接近限制时自动停止调度',
autoStopOnWarningDescription: '当系统检测到账户接近5小时使用限制时自动暂停调度该账户。进入新的时间窗口后会自动恢复调度。',
autoStopOnWarningDescription:
'当系统检测到账户接近5小时使用限制时自动暂停调度该账户。进入新的时间窗口后会自动恢复调度。',
useUnifiedUserAgent: '使用统一 Claude Code 版本',
useUnifiedUserAgentDescription: '开启后将使用从真实 Claude Code 客户端捕获的统一 User-Agent提高兼容性',
useUnifiedUserAgentDescription:
'开启后将使用从真实 Claude Code 客户端捕获的统一 User-Agent提高兼容性',
currentUnifiedVersion: '当前统一版本:',
clearCache: '清除缓存',
clearing: '清除中...',
waitingForCapture: '等待从 Claude Code 客户端捕获 User-Agent',
captureHint: '💡 提示:如果长时间未能捕获,请确认有 Claude Code 客户端正在使用此账户,或联系开发者检查 User-Agent 格式是否发生变化',
captureHint:
'💡 提示:如果长时间未能捕获,请确认有 Claude Code 客户端正在使用此账户,或联系开发者检查 User-Agent 格式是否发生变化',
useUnifiedClientId: '使用统一的客户端标识',
useUnifiedClientIdDescription: '开启后将使用固定的客户端标识,使所有请求看起来来自同一个客户端,减少特征',
useUnifiedClientIdDescription:
'开启后将使用固定的客户端标识,使所有请求看起来来自同一个客户端,减少特征',
clientIdLabel: '客户端标识 ID',
regenerateClientId: '重新生成',
clientIdDescription: '此ID将替换请求中的user_id客户端部分保留session部分用于粘性会话',
@@ -1489,7 +2102,8 @@ export default {
// Claude Console 模型映射
claudeConsoleModels: '模型映射',
claudeConsoleModelsDescription: '配置模型请求的映射关系,将客户端请求的模型名映射为实际调用的模型。',
claudeConsoleModelsDescription:
'配置模型请求的映射关系,将客户端请求的模型名映射为实际调用的模型。',
modelMappingFrom: '请求模型',
modelMappingFromPlaceholder: '例如claude-3-5-sonnet-20241022',
modelMappingTo: '实际模型',
@@ -1579,12 +2193,15 @@ export default {
// Gemini 项目 ID 详细说明
geminiProjectIdRequired: 'Google Cloud/Workspace 账号需要提供项目 ID',
geminiProjectIdDetail: '某些 Google 账号(特别是绑定了 Google Cloud 的账号)会被识别为 Workspace 账号,需要提供额外的项目 ID。',
geminiProjectIdDetail:
'某些 Google 账号(特别是绑定了 Google Cloud 的账号)会被识别为 Workspace 账号,需要提供额外的项目 ID。',
geminiHowToGetProjectId: '如何获取项目 ID',
geminiVisitConsole: '访问',
geminiCopyProjectId: '复制\u9879目 IDProject ID\uff0c通常是字符串格式',
geminiProjectIdWarning: '⚠️ 注意:要复制项目 IDProject ID不要复制项目编号Project Number',
geminiPersonalAccountTip: '\u63d0示\uff1a如果您的账号是普通个人账号未绑定 Google Cloud请留空此字段。',
geminiProjectIdWarning:
'⚠️ 注意:要复制项目 IDProject ID不要复制项目编号Project Number',
geminiPersonalAccountTip:
'\u63d0示\uff1a如果您的账号是普通个人账号未绑定 Google Cloud请留空此字段。',
// AWS 区域参考
awsRegionReference: '常用 AWS 区域参考:',
@@ -1663,7 +2280,8 @@ export default {
dailyQuotaDescription: '设置每日使用额度0 表示不限制',
quotaResetTime: '额度重置时间',
quotaResetTimeDescription: '每日自动重置额度的时间',
modelMappingDescription: '留空表示支持所有模型且不修改请求。配置映射后,左侧模型会被识别为支持的模型,右侧是实际发送的模型。',
modelMappingDescription:
'留空表示支持所有模型且不修改请求。配置映射后,左侧模型会被识别为支持的模型,右侧是实际发送的模型。',
rateLimitDurationMinutes: '限流时间 (分钟)',
rateLimitDefaultMinutes: '默认60分钟',
rateLimitPauseDesc: '账号被限流后暂停调度的时间(分钟)',
@@ -1678,7 +2296,8 @@ export default {
// 模型映射
modelMappingOptional: '模型映射表 (可选)',
modelMappingDesc: '留空表示支持所有模型且不修改请求。配置映射后,左侧模型会被识别为支持的模型,右侧是实际发送的模型。',
modelMappingDesc:
'留空表示支持所有模型且不修改请求。配置映射后,左侧模型会被识别为支持的模型,右侧是实际发送的模型。',
originalModelName: '原始模型名称',
mappedModelName: '映射后的模型名称',
addModelMappingBtn: '添加模型映射',
@@ -1692,14 +2311,17 @@ export default {
// Claude 高级选项
claudeAutoStopScheduling: '5小时使用量接近限制时自动停止调度',
claudeAutoStopDesc: '当系统检测到账户接近5小时使用限制时自动暂停调度该账户。进入新的时间窗口后会自动恢复调度。',
claudeAutoStopDesc:
'当系统检测到账户接近5小时使用限制时自动暂停调度该账户。进入新的时间窗口后会自动恢复调度。',
claudeUseUnifiedUA: '使用统一 Claude Code 版本',
claudeUnifiedUADesc: '开启后将使用从真实 Claude Code 客户端捕获的统一 User-Agent提高兼容性',
claudeCurrentUnifiedVersion: '💡 当前统一版本:',
claudeWaitingCapture: '⏳ 等待从 Claude Code 客户端捕获 User-Agent',
claudeCaptureHint: '💡 提示:如果长时间未能捕获,请确认有 Claude Code 客户端正在使用此账户, 或联系开发者检查 User-Agent 格式是否发生变化',
claudeCaptureHint:
'💡 提示:如果长时间未能捕获,请确认有 Claude Code 客户端正在使用此账户, 或联系开发者检查 User-Agent 格式是否发生变化',
claudeUseUnifiedClientId: '使用统一的客户端标识',
claudeUnifiedClientIdDesc: '开启后将使用固定的客户端标识,使所有请求看起来来自同一个客户端,减少特征',
claudeUnifiedClientIdDesc:
'开启后将使用固定的客户端标识,使所有请求看起来来自同一个客户端,减少特征',
claudeClientIdLabel: '客户端标识 ID',
claudeClientIdDesc: '此ID将替换请求中的user_id客户端部分保留session部分用于粘性会话',
@@ -1752,7 +2374,8 @@ export default {
// 描述性文本
claudeProLimitation: 'Pro 账号不支持 Claude Opus 4 模型',
claude5HourLimitDesc: '5小时使用量接近限制时自动停止调度',
claude5HourLimitExplanation: '当系统检测到账户接近5小时使用限制时自动暂停调度该账户。进入新的时间窗口后会自动恢复调度。',
claude5HourLimitExplanation:
'当系统检测到账户接近5小时使用限制时自动暂停调度该账户。进入新的时间窗口后会自动恢复调度。',
useUnifiedClaudeVersion: '使用统一 Claude Code 版本',
unifiedVersionDesc: '开启后将使用从真实 Claude Code 客户端捕获的统一 User-Agent提高兼容性',
currentUnifiedVersion: '💡 当前统一版本:',
@@ -1779,14 +2402,15 @@ export default {
// AWS 区域参考
awsRegionRef: '常用 AWS 区域参考:',
// 错误信息
apiKeyRequired: '请填写 API Key',
// Error messages
apiKeyRequiredError: '请填写 API Key',
refreshTokenRequired: '请填写 Refresh Token',
accessTokenRequired: '请填写 Access Token',
copyFailedManual: '复制失败,请手动复制',
// 表单描述
modelSupportDesc: '留空表示支持所有模型。如果指定模型,请求中的模型不在列表内将不会调度到此账号',
modelSupportDesc:
'留空表示支持所有模型。如果指定模型,请求中的模型不在列表内将不会调度到此账号',
modelTypeSelectionDesc: '选择此部署支持的模型类型',
userAgentDesc: '留空时将自动使用客户端的 User-Agent仅在需要固定特定 UA 时填写',
@@ -1829,7 +2453,8 @@ export default {
step3Description: '授权完成后,页面会显示一个',
step3DescriptionMiddle: ',请将其复制并粘贴到下方输入框:',
step3DescriptionGemini: '授权完成后,页面会显示一个 Authorization Code请将其复制并粘贴到下方输入框',
step3DescriptionGemini:
'授权完成后,页面会显示一个 Authorization Code请将其复制并粘贴到下方输入框',
step3DescriptionOpenAI: '授权完成后,当页面地址变为',
step3DescriptionOpenAIMiddle: '时:',
@@ -1844,7 +2469,8 @@ export default {
// 占位符
authCodePlaceholder: '粘贴从Claude页面获取的Authorization Code...',
authCodePlaceholderGemini: '粘贴从Gemini页面获取的Authorization Code...',
authCodePlaceholderOpenAI: '方式1复制完整的链接http://localhost:1455/auth/callback?code=...)\n方式2仅复制 code 参数的值\n系统会自动识别并提取所需信息',
authCodePlaceholderOpenAI:
'方式1复制完整的链接http://localhost:1455/auth/callback?code=...)\n方式2仅复制 code 参数的值\n系统会自动识别并提取所需信息',
// 标签
authorizationCode: 'Authorization Code',

View File

@@ -202,7 +202,8 @@ export default {
apiKeysPlaceholder: '請輸入您的 API Keys支援以下格式\ncr_xxx\ncr_yyy\n或\ncr_xxx, cr_yyy',
clearInput: '清空輸入',
securityNoticeSingle: '您的 API Key 僅用於查詢自己的統計資料,不會被儲存或用於其他用途',
securityNoticeMulti: '您的 API Keys 僅用於查詢統計資料,不會被儲存。彙整模式下部分個體化資訊將不顯示。',
securityNoticeMulti:
'您的 API Keys 僅用於查詢統計資料,不會被儲存。彙整模式下部分個體化資訊將不顯示。',
multiKeyTip: '提示:最多支援同時查詢 30 個 API Keys。使用 Ctrl+Enter 快速查詢。'
},
@@ -454,7 +455,8 @@ export default {
// Messages and confirmations
resetStatusConfirmTitle: '重置帳戶狀態',
resetStatusConfirmMessage: '確定要重置此帳戶的所有異常狀態嗎這將清除限流狀態、401錯誤計數等所有異常標記。',
resetStatusConfirmMessage:
'確定要重置此帳戶的所有異常狀態嗎這將清除限流狀態、401錯誤計數等所有異常標記。',
resetStatusConfirmButton: '確定重置',
resetStatusCancelButton: '取消',
statusResetSuccess: '帳戶狀態已重置',
@@ -464,7 +466,8 @@ export default {
deleteAccountMessage: '確定要刪除帳戶 "{name}" 嗎?\n\n此操作不可恢復。',
deleteAccountButton: '刪除',
deleteAccountCancel: '取消',
cannotDeleteBoundAccount: '無法刪除此帳號,有 {count} 個API Key綁定到此帳號請先解綁所有API Key',
cannotDeleteBoundAccount:
'無法刪除此帳號,有 {count} 個API Key綁定到此帳號請先解綁所有API Key',
accountDeleted: '帳戶已刪除',
deleteFailed: '刪除失敗',
@@ -629,8 +632,10 @@ export default {
confirmDelete: '確定要刪除這個 API Key 嗎?此操作不可恢復。',
confirmBatchDelete: '確定要刪除已選取的 {count} 個 API Key 嗎?此操作不可恢復。',
confirmRestore: '確定要恢復這個 API Key 嗎?恢復後可以重新使用。',
confirmPermanentDelete: '確定要徹底刪除這個 API Key 嗎?此操作不可恢復,所有相關資料將被永久刪除。',
confirmClearAll: '確定要徹底刪除全部 {count} 個已刪除的 API Keys 嗎?此操作不可恢復,所有相關資料將被永久刪除。',
confirmPermanentDelete:
'確定要徹底刪除這個 API Key 嗎?此操作不可恢復,所有相關資料將被永久刪除。',
confirmClearAll:
'確定要徹底刪除全部 {count} 個已刪除的 API Keys 嗎?此操作不可恢復,所有相關資料將被永久刪除。',
// Success messages
keyActivated: 'API Key 已啟用',
@@ -661,7 +666,544 @@ export default {
// Batch operations
batchSuccess: '成功處理 {count} 個項目',
batchPartialFail: '{failed} 個處理失敗',
batchAllFailed: '所有項目處理失敗'
batchAllFailed: '所有項目處理失敗',
// Batch API Key Modal
batchApiKeyModal: {
title: '批量建立成功',
successMessage: '成功建立 {count} 個 API Key',
importantReminder: '重要提醒',
warningMessage:
'這是您唯一能看到所有 API Key 的機會。關閉此視窗後,系統將不再顯示完整的 API Key。請立即下載並妥善保存。',
// Statistics cards
createdCount: '建立數量',
baseName: '基礎名稱',
permissionScope: '權限範圍',
expiryTime: '過期時間',
// Permission texts
permissions: {
all: '全部服務',
claude: '僅 Claude',
gemini: '僅 Gemini',
unknown: '未知'
},
// Expiry time texts
neverExpire: '永不過期',
daysFormat: '{days}天',
weeksFormat: '{weeks}週',
monthsFormat: '{months}個月',
yearsFormat: '{years}年',
// Preview section
previewTitle: 'API Keys 預覽',
hide: '隱藏',
show: '顯示',
preview: '預覽',
maxDisplayNote: '最多顯示前10個',
moreKeysNote: '... 還有 {count} 個 API Key',
// Action buttons
downloadAll: '下載所有 API Keys',
alreadySaved: '我已保存',
directCloseTooltip: '直接關閉(不推薦)',
// File info
fileFormatInfo:
'下載的檔案格式為文字檔案(.txt每行包含一個 API Key。請將檔案保存在安全的位置避免洩露。',
// Confirmation dialogs
closeReminderTitle: '關閉提醒',
closeReminderMessage:
'關閉後將無法再次查看這些 API Key請確保已經下載並妥善保存。\n\n確定要關閉嗎',
confirmCloseButton: '確定關閉',
goBackDownloadButton: '返回下載',
directCloseTitle: '確定要關閉嗎?',
directCloseMessage: '您還沒有下載 API Keys關閉後將無法再次查看。\n\n強烈建議您先下載保存。',
stillCloseButton: '仍然關閉',
directCloseFallbackMessage: '您還沒有下載 API Keys關閉後將無法再次查看。\n\n確定要關閉嗎',
// Success messages
downloadSuccess: 'API Keys 檔案已下載'
},
// Expiry Edit Modal
expiryEditModal: {
title: '修改過期時間',
subtitle: '為 "{name}" 設定新的過期時間',
currentStatus: '目前狀態',
notActivated: '未啟動',
activationDaysHint: '(啟動後 {days} 天過期)',
neverExpire: '永不過期',
expired: '已過期',
daysToExpire: '{days} 天後過期',
monthsToExpire: '{months} 個月後過期',
activateNow: '立即啟動',
activateButton: '立即啟動 (啟動後 {days} 天過期)',
activationInfo: '點選立即啟動此 API Key啟動後將在 {days} 天後過期',
selectNewDuration: '選擇新的期限',
neverExpireOption: '永不過期',
oneDay: '1 天',
sevenDays: '7 天',
thirtyDays: '30 天',
ninetyDays: '90 天',
oneHundredEightyDays: '180 天',
threeSixtyFiveDays: '1 年',
twoYears: '2 年',
custom: '自訂',
selectDateAndTime: '選擇日期和時間',
selectFutureDateTime: '選擇一個未來的日期和時間作為過期時間',
newExpiryTime: '新的過期時間',
cancel: '取消',
saving: '儲存中...',
saveChanges: '儲存變更',
activateConfirmTitle: '啟動 API Key',
activateConfirmMessage: '確定要立即啟動此 API Key 嗎?啟動後將在 {days} 天後自動過期。',
confirmActivate: '確定啟動',
confirmCancel: '取消'
},
// Edit API Key Modal
editApiKeyModal: {
title: '編輯 API Key',
// Basic Info
name: '名稱',
namePlaceholder: '請輸入API Key名稱',
nameHint: '用於識別此 API Key 的用途',
// Owner
owner: '所有者',
adminLabel: '- 管理員',
ownerHint: '分配此 API Key 給指定使用者或管理員,管理員分配時不受使用者 API Key 數量限制',
// Tags
tags: '標籤',
selectedTags: '已選擇的標籤:',
clickToSelectTags: '點擊選擇已有標籤:',
createNewTag: '建立新標籤:',
newTagPlaceholder: '輸入新標籤名稱',
tagsHint: '用於標記不同團隊或用途,方便篩選管理',
// Rate Limit Settings
rateLimitTitle: '速率限制設定 (可選)',
rateLimitWindow: '時間視窗 (分鐘)',
rateLimitRequests: '請求次數限制',
rateLimitCost: '費用限制 (美元)',
rateLimitWindowHint: '時間段單位',
rateLimitRequestsHint: '視窗內最大請求',
rateLimitCostHint: '視窗內最大費用',
noLimit: '無限制',
// Usage Examples
usageExamples: '💡 使用範例',
example1: '範例1: 時間視窗=60請求次數=1000 → 每60分鐘最多1000次請求',
example2: '範例2: 時間視窗=1費用=0.1 → 每分鐘最多$0.1費用',
example3: '範例3: 視窗=30請求=50費用=5 → 每30分鐘50次請求且不超$5費用',
// Cost Limits
dailyCostLimit: '每日費用限制 (美元)',
dailyCostLimitPlaceholder: '0 表示無限制',
dailyCostHint: '設定此 API Key 每日的費用限制超過限制將拒絕請求0 或留空表示無限制',
weeklyOpusCostLimit: 'Opus 模型週費用限制 (美元)',
weeklyOpusHint:
'設定 Opus 模型的週費用限制(週一到週日),僅限 Claude 官方帳戶0 或留空表示無限制',
custom: '自訂',
// Concurrency
concurrencyLimit: '並發限制',
concurrencyLimitPlaceholder: '0 表示無限制',
concurrencyHint: '設定此 API Key 可同時處理的最大請求數',
// Active Status
activeStatus: '啟動帳號',
activeStatusHint: '取消勾選將停用此 API Key暫停所有請求客戶端回傳 401 錯誤',
// Service Permissions
servicePermissions: '服務權限',
allServices: '全部服務',
claudeOnly: '僅 Claude',
geminiOnly: '僅 Gemini',
openaiOnly: '僅 OpenAI',
permissionsHint: '控制此 API Key 可以存取哪些服務',
// Account Binding
accountBinding: '專屬帳號綁定',
refreshAccounts: '重新整理帳號',
refreshing: '重新整理中...',
claudeAccount: 'Claude 專屬帳號',
geminiAccount: 'Gemini 專屬帳號',
openaiAccount: 'OpenAI 專屬帳號',
bedrockAccount: 'Bedrock 專屬帳號',
useSharedPool: '使用共享帳號池',
selectClaudeAccount: '請選擇Claude帳號',
selectGeminiAccount: '請選擇Gemini帳號',
selectOpenaiAccount: '請選擇OpenAI帳號',
selectBedrockAccount: '請選擇Bedrock帳號',
accountBindingHint: '修改綁定帳號將影響此API Key的請求路由',
// Model Restrictions
enableModelRestriction: '啟用模型限制',
restrictedModels: '限制的模型清單',
noRestrictedModels: '暫無限制的模型',
allCommonModelsRestricted: '所有常用模型已在限制清單中',
addRestrictedModelPlaceholder: '輸入模型名稱,按回車新增',
modelRestrictionHint: '設定此API Key無法存取的模型例如claude-opus-4-20250514',
// Client Restrictions
enableClientRestriction: '啟用客戶端限制',
allowedClients: '允許的客戶端',
clientRestrictionHint: '勾選允許使用此API Key的客戶端',
// Buttons
cancel: '取消',
save: '儲存修改',
saving: '儲存中...',
// Messages
costLimitConfirmTitle: '費用限制提醒',
costLimitConfirmMessage:
'您設定了時間視窗但費用限制為0這意味著不會有費用限制。\n\n是否繼續',
costLimitConfirmContinue: '繼續儲存',
costLimitConfirmBack: '返回修改',
refreshAccountsSuccess: '帳號清單已重新整理',
refreshAccountsFailed: '重新整理帳號清單失敗',
updateFailed: '更新失敗'
},
// Batch Edit API Key Modal
batchEditApiKeyModal: {
title: '批次編輯 API Keys ({count} 個)',
// Info section
infoTitle: '批次編輯說明',
infoContent:
'以下設定將套用到所選的 {count} 個 API Key。僅填寫或修改的欄位會被更新空白欄位將保持原值不變。',
// Tag operations
tagLabel: '標籤 (批次操作)',
tagOperations: {
replace: '替換標籤',
add: '新增標籤',
remove: '移除標籤',
none: '不修改標籤'
},
// Tag status texts
newTagsList: '新標籤清單:',
tagsToAdd: '要新增的標籤:',
tagsToRemove: '要移除的標籤:',
clickToSelectTags: '點擊選擇已有標籤:',
createNewTag: '建立新標籤:',
inputNewTagPlaceholder: '輸入新標籤名稱',
// Rate limit settings
rateLimitTitle: '速率限制設定',
rateLimitWindow: '時間視窗 (分鐘)',
rateLimitRequests: '請求次數限制',
rateLimitCost: '費用限制 (美元)',
noModifyPlaceholder: '不修改',
// Daily cost limit
dailyCostLimit: '每日費用限制 (美元)',
dailyCostLimitPlaceholder: '不修改 (0 表示無限制)',
// Weekly Opus cost limit
weeklyOpusCostLimit: 'Opus 模型週費用限制 (美元)',
weeklyOpusCostLimitPlaceholder: '不修改 (0 表示無限制)',
opusLimitDescription: '設定 Opus 模型的週費用限制(週一到週日),僅限 Claude 官方帳戶',
// Concurrency limit
concurrencyLimit: '並發限制',
concurrencyLimitPlaceholder: '不修改 (0 表示無限制)',
// Active status
activeStatus: '啟用狀態',
statusOptions: {
active: '啟用',
disabled: '停用',
noChange: '不修改'
},
// Service permissions
servicePermissions: '服務權限',
permissionOptions: {
noChange: '不修改',
all: '全部服務',
claude: '僅 Claude',
gemini: '僅 Gemini',
openai: '僅 OpenAI'
},
// Account binding
accountBinding: '專屬帳號綁定',
refreshAccounts: '重新整理帳號',
refreshing: '重新整理中...',
claudeAccount: 'Claude 專屬帳號',
geminiAccount: 'Gemini 專屬帳號',
openaiAccount: 'OpenAI 專屬帳號',
bedrockAccount: 'Bedrock 專屬帳號',
accountOptions: {
noChange: '不修改',
sharedPool: '使用共享帳號池',
groupPrefix: '分組 - '
},
// Optgroup labels
optgroupLabels: {
accountGroups: '帳號分組',
dedicatedAccounts: '專屬帳號'
},
// Buttons
cancel: '取消',
saving: '儲存中...',
batchSave: '批次儲存',
// Messages
refreshAccountsSuccess: '帳號清單已重新整理',
refreshAccountsFailed: '重新整理帳號清單失敗',
batchEditSuccess: '成功批次編輯 {count} 個 API Keys',
batchEditPartialFail: '{failedCount} 個編輯失敗:\n{errors}',
batchEditAllFailed: '所有 API Keys 編輯失敗',
batchEditFailed: '批次編輯失敗',
batchEditErrorLog: '批次編輯 API Keys 失敗:'
},
// Renew API Key Modal
renewApiKeyModal: {
title: '續期 API Key',
apiKeyInfo: 'API Key 資訊',
currentExpiry: '當前到期時間:',
neverExpires: '永不到期',
renewDuration: '續期時長',
extend7Days: '延長 7 天',
extend30Days: '延長 30 天',
extend90Days: '延長 90 天',
extend180Days: '延長 180 天',
extend365Days: '延長 365 天',
customDate: '自訂日期',
setPermanent: '設為永不到期',
newExpiry: '新的到期時間:',
cancel: '取消',
renewing: '續期中...',
confirmRenew: '確認續期',
renewSuccess: 'API Key 續期成功',
renewFailed: '續期失敗'
},
// New API Key Modal
newApiKeyModal: {
title: 'API Key 建立成功',
subtitle: '請妥善保存您的 API Key',
directCloseTooltip: '直接關閉(不推薦)',
// 警告提示
warningTitle: '重要提醒',
warningMessage:
'這是您唯一能看到完整 API Key 的機會。關閉此視窗後,系統將不再顯示完整的 API Key。請立即複製並妥善保存。',
// 欄位標籤
apiKeyName: 'API Key 名稱',
remarks: '備註',
noDescription: '無描述',
apiKey: 'API Key',
// 可見性切換
hideApiKey: '隱藏 API Key',
showFullApiKey: '顯示完整 API Key',
visibilityHint: '點擊眼睛圖示切換顯示模式,使用下方按鈕複製完整 API Key',
// 按鈕
copyApiKey: '複製 API Key',
alreadySaved: '我已保存',
// 確認對話框
closeReminderTitle: '關閉提醒',
closeReminderMessage:
'關閉後將無法再次查看完整的 API Key請確保已經妥善保存。\n\n確定要關閉嗎',
confirmClose: '確定關閉',
cancel: '取消',
directCloseTitle: '確定要關閉嗎?',
directCloseMessage:
'您還沒有保存 API Key關閉後將無法再次查看。\n\n建議您先複製 API Key 再關閉。',
stillClose: '仍然關閉',
goBack: '返回複製',
directCloseFallback: '您還沒有保存 API Key關閉後將無法再次查看。\n\n確定要關閉嗎',
// 成功訊息
apiKeyNotFound: 'API Key 不存在',
copySuccess: 'API Key 已複製到剪貼簿',
copyFailed: '複製失敗,請手動複製'
},
// Create API Key Modal
createApiKeyModal: {
title: '建立新的 API Key',
// Create type section
createType: '建立類型',
singleCreate: '單一建立',
batchCreate: '批次建立',
batchCount: '建立數量',
batchCountPlaceholder: '輸入數量 (2-500)',
maxSupported: '最大支援 500 個',
batchHint: '批次建立時,每個 Key 的名稱會自動添加序號後綴,例如:{name}_1, {name}_2 ...',
// Basic form fields
name: '名稱',
nameRequired: '*',
nameError: '請輸入API Key名稱',
singleNamePlaceholder: '為您的 API Key 取一個名稱',
batchNamePlaceholder: '輸入基礎名稱(將自動添加序號)',
description: '備註 (選填)',
descriptionPlaceholder: '描述此 API Key 的用途...',
// Tags section
tags: '標籤',
selectedTags: '已選擇的標籤:',
clickToSelectTags: '點擊選擇已有標籤:',
createNewTag: '建立新標籤:',
newTagPlaceholder: '輸入新標籤名稱',
tagHint: '用於標記不同團隊或用途,方便篩選管理',
// Rate limit section
rateLimitTitle: '速率限制設定 (選填)',
rateLimitWindow: '時間視窗 (分鐘)',
rateLimitRequests: '請求次數限制',
rateLimitCost: '費用限制 (美元)',
rateLimitWindowPlaceholder: '無限制',
rateLimitRequestsPlaceholder: '無限制',
rateLimitCostPlaceholder: '無限制',
rateLimitWindowHint: '時間段單位',
rateLimitRequestsHint: '視窗內最大請求',
rateLimitCostHint: '視窗內最大費用',
// Rate limit examples
exampleTitle: '💡 使用範例',
example1: '範例1: 時間視窗=60請求次數=1000 → 每60分鐘最多1000次請求',
example2: '範例2: 時間視窗=1費用=0.1 → 每分鐘最多$0.1費用',
example3: '範例3: 視窗=30請求=50費用=5 → 每30分鐘50次請求且不超$5費用',
// Cost limits
dailyCostLimit: '每日費用限制 (美元)',
dailyCostLimitPlaceholder: '0 表示無限制',
dailyCostHint: '設定此 API Key 每日的費用限制超過限制將拒絶請求0 或留空表示無限制',
weeklyOpusCostLimit: 'Opus 模型週費用限制 (美元)',
weeklyOpusCostLimitPlaceholder: '0 表示無限制',
weeklyOpusHint:
'設定 Opus 模型的週費用限制(週一到週日),僅限 Claude 官方帳戶0 或留空表示無限制',
custom: '自訂',
// Concurrency limit
concurrencyLimit: '並發限制 (選填)',
concurrencyLimitPlaceholder: '0 表示無限制',
concurrencyHint: '設定此 API Key 可同時處理的最大請求數0 或留空表示無限制',
// Expiration settings
expirationSettings: '過期設定',
fixedTimeExpiry: '固定時間過期',
activationExpiry: '首次使用後啟用',
fixedModeHint: '固定時間模式Key 建立後立即生效,按設定時間過期',
activationModeHint: 'Key 首次使用時啟用,啟用後按設定天數過期(適合批次銷售)',
// Expiration duration options
neverExpire: '永不過期',
'1d': '1 天',
'7d': '7 天',
'30d': '30 天',
'90d': '90 天',
'180d': '180 天',
'365d': '365 天',
customDate: '自訂日期',
// Activation mode
activationDays: '輸入天數',
daysUnit: '天',
activationHint: 'Key 將在首次使用後啟用,啟用後 {days} 天過期',
// Expiry status
willExpireOn: '將於 {date} 過期',
// Service permissions
servicePermissions: '服務權限',
allServices: '全部服務',
claudeOnly: '僅 Claude',
geminiOnly: '僅 Gemini',
openaiOnly: '僅 OpenAI',
permissionHint: '控制此 API Key 可以存取哪些服務',
// Account binding
dedicatedAccountBinding: '專屬帳號綁定 (選填)',
refreshAccounts: '重新整理帳號',
refreshing: '重新整理中...',
claudeDedicatedAccount: 'Claude 專屬帳號',
geminiDedicatedAccount: 'Gemini 專屬帳號',
openaiDedicatedAccount: 'OpenAI 專屬帳號',
bedrockDedicatedAccount: 'Bedrock 專屬帳號',
useSharedPool: '使用共用帳號池',
accountBindingHint: '選擇專屬帳號後此API Key將僅使用該帳號不選擇則使用共用帳號池',
selectClaudeAccount: '請選擇Claude帳號',
selectGeminiAccount: '請選擇Gemini帳號',
selectOpenaiAccount: '請選擇OpenAI帳號',
selectBedrockAccount: '請選擇Bedrock帳號',
// Model restrictions
enableModelRestriction: '啟用模型限制',
restrictedModelsList: '限制的模型清單',
noRestrictedModels: '暫無限制的模型',
allCommonModelsRestricted: '所有常用模型已在限制清單中',
addRestrictedModelPlaceholder: '輸入模型名稱,按 Enter 添加',
modelRestrictionHint: '設定此API Key無法存取的模型例如claude-opus-4-20250514',
// Client restrictions
enableClientRestriction: '啟用用戶端限制',
allowedClients: '允許的用戶端',
// Buttons
cancel: '取消',
create: '建立',
creating: '建立中...',
// Messages
batchCountError: '批次建立數量必須在 2-500 之間',
costLimitConfirmTitle: '費用限制提醒',
costLimitConfirmMessage:
'您設定了時間視窗但費用限制為0這意味著不會有費用限制。\n\n是否繼續',
costLimitConfirmContinue: '繼續建立',
costLimitConfirmBack: '返回修改',
costLimitFallbackMessage:
'您設定了時間視窗但費用限制為0這意味著不會有費用限制。\n是否繼續',
createSuccess: 'API Key 建立成功',
batchCreateSuccess: '成功建立 {count} 個 API Key',
createFailed: '建立失敗',
batchCreateFailed: '批次建立失敗',
refreshAccountsSuccess: '帳號清單已重新整理',
refreshAccountsFailed: '重新整理帳號清單失敗'
},
// Window Countdown
windowCountdown: {
expired: '視窗已過期',
notStarted: '視窗未啟動',
minutes: '分鐘',
requests: '請求',
tokens: 'Token',
cost: '費用',
aboutToReset: '即將重置',
minutesUntilReset: '分鐘後重置',
untilReset: '後重置',
windowLimit: '視窗限制',
hours: '小時'
}
},
// User-related translations
@@ -771,13 +1313,15 @@ export default {
// Confirmation dialogs
disableUserTitle: 'Disable User',
enableUserTitle: 'Enable User',
disableUserMessage: 'Are you sure you want to disable user "{username}"? This will prevent them from logging in.',
disableUserMessage:
'Are you sure you want to disable user "{username}"? This will prevent them from logging in.',
enableUserMessage: 'Are you sure you want to enable user "{username}"?',
disable: 'Disable',
enable: 'Enable',
disableAllKeysTitle: 'Disable All API Keys',
disableAllKeysMessage: 'Are you sure you want to disable all {count} API keys for user "{username}"? This will prevent them from using the service.',
disableAllKeysMessage:
'Are you sure you want to disable all {count} API keys for user "{username}"? This will prevent them from using the service.',
disableKeys: 'Disable Keys',
// Success messages
@@ -874,7 +1418,8 @@ export default {
// Warning messages
roleChangeWarning: {
title: '角色變更警告',
grantAdmin: '授予管理員權限將使該使用者擁有系統的完整存取權限包括管理其他使用者及其API密鑰的能力。',
grantAdmin:
'授予管理員權限將使該使用者擁有系統的完整存取權限包括管理其他使用者及其API密鑰的能力。',
removeAdmin: '移除管理員權限將限制該使用者只能管理自己的API密鑰和檢視自己的使用統計。'
},
@@ -885,6 +1430,46 @@ export default {
// Success message
roleUpdated: '使用者角色已更新為 {role}'
},
// Usage Detail Modal
usageDetailModal: {
title: '使用統計詳情',
close: '關閉',
// Statistics cards
totalRequests: '總請求數',
totalTokens: '總Token數',
totalCost: '總費用',
averageRate: '平均速率',
// Today stats
today: '今日',
todayRequests: '{count} 次',
todayTokens: '{count}',
todayCost: '${amount}',
// Usage labels
times: '次',
// Token distribution
tokenDistribution: 'Token 使用分佈',
inputTokens: '輸入 Token',
outputTokens: '輸出 Token',
cacheCreateTokens: '快取建立 Token',
cacheReadTokens: '快取讀取 Token',
// Limits section
limitSettings: '限制設置',
dailyCostLimit: '每日費用限制',
concurrencyLimit: '並行限制',
timeWindowLimit: '時間窗口限制',
windowStatus: '窗口狀態',
used: '已使用',
remainingQuota: '剩餘: ${amount}',
// Progress indicators
usedPercentage: '已使用 {percentage}%'
}
},
@@ -1128,7 +1713,8 @@ export default {
accountTypeShared: '共用帳戶',
accountTypeDedicated: '專屬帳戶',
accountTypeGroup: '群組排程',
accountTypeDescription: '共用帳戶供所有API Key使用專屬帳戶僅供特定API Key使用群組排程加入群組供群組內排程',
accountTypeDescription:
'共用帳戶供所有API Key使用專屬帳戶僅供特定API Key使用群組排程加入群組供群組內排程',
// 群組選擇
selectGroup: '選擇群組',
@@ -1149,7 +1735,8 @@ export default {
projectIdStep3: '⚠️ 注意:要複製專案 IDProject ID不要複製專案編號Project Number',
projectIdTip: '提示:如果您的帳號是普通個人帳號(未綁定 Google Cloud請留空此欄位。',
projectIdGoogleCloudRequired: 'Google Cloud/Workspace 帳號需要提供專案 ID',
projectIdGoogleCloudDescription: '某些 Google 帳號(特別是綁定了 Google Cloud 的帳號)會被識別為 Workspace 帳號,需要提供額外的專案 ID。',
projectIdGoogleCloudDescription:
'某些 Google 帳號(特別是綁定了 Google Cloud 的帳號)會被識別為 Workspace 帳號,需要提供額外的專案 ID。',
// Bedrock 欄位
awsAccessKeyId: 'AWS 存取金鑰 ID',
@@ -1191,7 +1778,8 @@ export default {
azureEndpoint: 'Azure Endpoint',
azureEndpointRequired: 'Azure Endpoint *',
azureEndpointPlaceholder: 'https://your-resource.openai.azure.com',
azureEndpointDescription: 'Azure OpenAI 資源的終結點 URL格式https://your-resource.openai.azure.com',
azureEndpointDescription:
'Azure OpenAI 資源的終結點 URL格式https://your-resource.openai.azure.com',
apiVersion: 'API 版本',
apiVersionPlaceholder: '2024-02-01',
apiVersionDescription: 'Azure OpenAI API 版本,預設使用最新穩定版本 2024-02-01',
@@ -1224,7 +1812,8 @@ export default {
used: '已使用',
modelMapping: '模型映射表',
modelMappingOptional: '模型映射表(可選)',
modelMappingDescription: '留空表示支援所有模型且不修改請求。配置映射後,左側模型會被識別為支援的模型,右側是實際發送的模型。',
modelMappingDescription:
'留空表示支援所有模型且不修改請求。配置映射後,左側模型會被識別為支援的模型,右側是實際發送的模型。',
originalModel: '原始模型名稱',
mappedModel: '映射後的模型名稱',
addModelMapping: '新增模型映射',
@@ -1246,16 +1835,20 @@ export default {
// Claude 特殊功能
autoStopOnWarning: '5小時使用量接近限制時自動停止排程',
autoStopOnWarningDescription: '當系統偵測到帳戶接近5小時使用限制時自動暫停排程該帳戶。進入新的時間視窗後會自動恢復排程。',
autoStopOnWarningDescription:
'當系統偵測到帳戶接近5小時使用限制時自動暫停排程該帳戶。進入新的時間視窗後會自動恢復排程。',
useUnifiedUserAgent: '使用統一 Claude Code 版本',
useUnifiedUserAgentDescription: '開啟後將使用從真實 Claude Code 用戶端擷取的統一 User-Agent提高相容性',
useUnifiedUserAgentDescription:
'開啟後將使用從真實 Claude Code 用戶端擷取的統一 User-Agent提高相容性',
currentUnifiedVersion: '💡 目前統一版本:',
clearCache: '清除快取',
clearing: '清除中...',
waitingForCapture: '⏳ 等待從 Claude Code 用戶端擷取 User-Agent',
captureHint: '💡 提示:如果長時間未能擷取,請確認有 Claude Code 用戶端正在使用此帳戶,或聯繫開發者檢查 User-Agent 格式是否發生變化',
captureHint:
'💡 提示:如果長時間未能擷取,請確認有 Claude Code 用戶端正在使用此帳戶,或聯繫開發者檢查 User-Agent 格式是否發生變化',
useUnifiedClientId: '使用統一的用戶端識別',
useUnifiedClientIdDescription: '開啟後將使用固定的用戶端識別,使所有請求看起來來自同一個用戶端,減少特徵',
useUnifiedClientIdDescription:
'開啟後將使用固定的用戶端識別,使所有請求看起來來自同一個用戶端,減少特徵',
clientId: '用戶端識別 ID',
regenerate: '重新產生',
clientIdDescription: '此ID將替換請求中的user_id用戶端部分保留session部分用於黏性會話',
@@ -1268,20 +1861,28 @@ export default {
// 手動輸入 Token
manualTokenTitle: '手動輸入 Token',
manualTokenDescription: '請輸入有效的 Access Token。如果您有 Refresh Token建議也一併填寫以支援自動重新整理。',
manualTokenClaudeDescription: '請輸入有效的 Claude Access Token。如果您有 Refresh Token建議也一併填寫以支援自動重新整理。',
manualTokenGeminiDescription: '請輸入有效的 Gemini Access Token。如果您有 Refresh Token建議也一併填寫以支援自動重新整理。',
manualTokenOpenAIDescription: '請輸入有效的 OpenAI Access Token。如果您有 Refresh Token建議也一併填寫以支援自動重新整理。',
manualTokenDescription:
'請輸入有效的 Access Token。如果您有 Refresh Token建議也一併填寫以支援自動重新整理。',
manualTokenClaudeDescription:
'請輸入有效的 Claude Access Token。如果您有 Refresh Token建議也一併填寫以支援自動重新整理。',
manualTokenGeminiDescription:
'請輸入有效的 Gemini Access Token。如果您有 Refresh Token建議也一併填寫以支援自動重新整理。',
manualTokenOpenAIDescription:
'請輸入有效的 OpenAI Access Token。如果您有 Refresh Token建議也一併填寫以支援自動重新整理。',
obtainTokenMethods: '取得 Access Token 的方法:',
claudeTokenPath: '請從已登入 Claude Code 的機器上取得 ~/.claude/.credentials.json 檔案中的憑證,請勿使用 Claude 官網 API Keys 頁面的金鑰。',
geminiTokenPath: '請從已登入 Gemini CLI 的機器上取得 ~/.config/gemini/credentials.json 檔案中的憑證。',
openaiTokenPath: '請從已登入 OpenAI 帳戶的機器上取得認證憑證,或透過 OAuth 授權流程取得 Access Token。',
claudeTokenPath:
'請從已登入 Claude Code 的機器上取得 ~/.claude/.credentials.json 檔案中的憑證,請勿使用 Claude 官網 API Keys 頁面的金鑰。',
geminiTokenPath:
'請從已登入 Gemini CLI 的機器上取得 ~/.config/gemini/credentials.json 檔案中的憑證。',
openaiTokenPath:
'請從已登入 OpenAI 帳戶的機器上取得認證憑證,或透過 OAuth 授權流程取得 Access Token。',
accessToken: 'Access Token',
accessTokenOptional: 'Access Token可選',
accessTokenRequired: 'Access Token *',
accessTokenPlaceholder: '請輸入 Access Token...',
accessTokenOptionalPlaceholder: '可選:如果不填寫,系統會自動透過 Refresh Token 取得...',
accessTokenOptionalDescription: 'Access Token 可選填。如果不提供,系統會透過 Refresh Token 自動取得。',
accessTokenOptionalDescription:
'Access Token 可選填。如果不提供,系統會透過 Refresh Token 自動取得。',
refreshToken: 'Refresh Token',
refreshTokenOptional: 'Refresh Token可選',
refreshTokenRequired: 'Refresh Token *',
@@ -1295,10 +1896,12 @@ export default {
setupTokenDescription: '請按照以下步驟透過 Setup Token 完成 Claude 帳戶的授權:',
setupTokenStep1Title: '點擊下方按鈕產生授權連結',
setupTokenStep2Title: '在瀏覽器中開啟連結並完成授權',
setupTokenStep2Description: '請在新分頁中開啟授權連結,登入您的 Claude 帳戶並授權 Claude Code。',
setupTokenStep2Description:
'請在新分頁中開啟授權連結,登入您的 Claude 帳戶並授權 Claude Code。',
setupTokenStep2Warning: '注意:如果您設置了代理,請確保瀏覽器也使用相同的代理訪問授權頁面。',
setupTokenStep3Title: '輸入 Authorization Code',
setupTokenStep3Description: '授權完成後,從返回頁面複製 Authorization Code並貼上到下方輸入框',
setupTokenStep3Description:
'授權完成後,從返回頁面複製 Authorization Code並貼上到下方輸入框',
generateSetupTokenUrl: '產生 Setup Token 授權連結',
generating: '產生中...',
copyLink: '複製連結',
@@ -1311,7 +1914,8 @@ export default {
// Token 更新(編輯模式)
updateTokenTitle: '更新 Token',
updateTokenDescription: '可以更新 Access Token 和 Refresh Token。為了安全起見不會顯示目前的 Token 值。',
updateTokenDescription:
'可以更新 Access Token 和 Refresh Token。為了安全起見不會顯示目前的 Token 值。',
updateTokenTip: '💡 留空表示不更新該欄位。',
newAccessToken: '新的 Access Token',
newRefreshToken: '新的 Refresh Token',
@@ -1361,7 +1965,8 @@ export default {
// 確認對話框
projectIdNotFilledTitle: '專案 ID 未填寫',
projectIdNotFilledMessage: '您尚未填寫專案 ID。\n\n如果您的Google帳號綁定了Google Cloud或被識別為Workspace帳號需要提供專案 ID。\n如果您使用的是普通個人帳號可以繼續不填寫。',
projectIdNotFilledMessage:
'您尚未填寫專案 ID。\n\n如果您的Google帳號綁定了Google Cloud或被識別為Workspace帳號需要提供專案 ID。\n如果您使用的是普通個人帳號可以繼續不填寫。',
continueButton: '繼續',
goBackToFill: '返回填寫',
continueSave: '繼續保存',
@@ -1379,19 +1984,24 @@ export default {
leaveBlankNoUpdateSession: '留空表示不更新',
// 通用描述文字
allModelsIfEmpty: '留空表示支援所有模型。如果指定模型,請求中的模型不在列表內將不會排程到此帳號',
allModelsIfEmpty:
'留空表示支援所有模型。如果指定模型,請求中的模型不在列表內將不會排程到此帳號',
systemDefaultIfEmpty: '留空將使用系統預設模型。支援 inference profile ID 或 ARN',
noUpdateIfEmpty: '留空表示不更新該欄位',
// 手動 Token 輸入部分
manualTokenInput: '手動輸入 Token',
manualTokenClaudeDescription: '請輸入有效的 Claude Access Token。如果您有 Refresh Token建議也一併填寫以支援自動刷新。',
manualTokenGeminiDescription: '請輸入有效的 Gemini Access Token。如果您有 Refresh Token建議也一併填寫以支援自動刷新。',
manualTokenOpenAIDescription: '請輸入有效的 OpenAI Access Token。如果您有 Refresh Token建議也一併填寫以支援自動刷新。',
manualTokenClaudeDescription:
'請輸入有效的 Claude Access Token。如果您有 Refresh Token建議也一併填寫以支援自動刷新。',
manualTokenGeminiDescription:
'請輸入有效的 Gemini Access Token。如果您有 Refresh Token建議也一併填寫以支援自動刷新。',
manualTokenOpenAIDescription:
'請輸入有效的 OpenAI Access Token。如果您有 Refresh Token建議也一併填寫以支援自動刷新。',
getAccessTokenMethod: '取得 Access Token 的方法:',
claudeCredentialsPath: '請從已登入 Claude Code 的機器上取得',
geminiCredentialsPath: '請從已登入 Gemini CLI 的機器上取得',
openaiCredentialsPath: '請從已登入 OpenAI 帳戶的機器上取得認證憑證,或透過 OAuth 授權流程取得 Access Token。',
openaiCredentialsPath:
'請從已登入 OpenAI 帳戶的機器上取得認證憑證,或透過 OAuth 授權流程取得 Access Token。',
claudeCredentialsWarning: '檔案中的憑證,請勿使用 Claude 官網 API Keys 頁面的金鑰。',
refreshTokenWarning: '💡 如果未填寫 Refresh TokenToken 過期後需要手動更新。',
accessTokenOptional: 'Access Token (可選)',
@@ -1422,16 +2032,20 @@ export default {
claudeProSubscription: 'Claude Pro',
claudeProLimitation: 'Pro 帳號不支援 Claude Opus 4 模型',
autoStopOnWarning: '5小時使用量接近限制時自動停止排程',
autoStopOnWarningDescription: '當系統檢測到帳戶接近5小時使用限制時自動暫停排程該帳戶。進入新的時間視窗後會自動恢復排程。',
autoStopOnWarningDescription:
'當系統檢測到帳戶接近5小時使用限制時自動暫停排程該帳戶。進入新的時間視窗後會自動恢復排程。',
useUnifiedUserAgent: '使用統一 Claude Code 版本',
useUnifiedUserAgentDescription: '開啟後將使用從真實 Claude Code 用戶端捕獲的統一 User-Agent提高相容性',
useUnifiedUserAgentDescription:
'開啟後將使用從真實 Claude Code 用戶端捕獲的統一 User-Agent提高相容性',
currentUnifiedVersion: '目前統一版本:',
clearCache: '清除快取',
clearing: '清除中...',
waitingForCapture: '等待從 Claude Code 用戶端捕獲 User-Agent',
captureHint: '💡 提示:如果長時間未能捕獲,請確認有 Claude Code 用戶端正在使用此帳戶,或聯絡開發者檢查 User-Agent 格式是否發生變化',
captureHint:
'💡 提示:如果長時間未能捕獲,請確認有 Claude Code 用戶端正在使用此帳戶,或聯絡開發者檢查 User-Agent 格式是否發生變化',
useUnifiedClientId: '使用統一的用戶端識別',
useUnifiedClientIdDescription: '開啟後將使用固定的用戶端識別,使所有請求看起來來自同一個用戶端,減少特徵',
useUnifiedClientIdDescription:
'開啟後將使用固定的用戶端識別,使所有請求看起來來自同一個用戶端,減少特徵',
clientIdLabel: '用戶端識別 ID',
regenerateClientId: '重新產生',
clientIdDescription: '此ID將替換請求中的user_id用戶端部分保留session部分用於黏性工作階段',
@@ -1489,7 +2103,8 @@ export default {
// Claude Console 模型映射
claudeConsoleModels: '模型映射',
claudeConsoleModelsDescription: '設定模型請求的映射關係,將客戶端請求的模型名映射為實際呼叫的模型。',
claudeConsoleModelsDescription:
'設定模型請求的映射關係,將客戶端請求的模型名映射為實際呼叫的模型。',
modelMappingFrom: '請求模型',
modelMappingFromPlaceholder: '例如claude-3-5-sonnet-20241022',
modelMappingTo: '實際模型',
@@ -1579,12 +2194,15 @@ export default {
// Gemini 專案 ID 詳細說明
geminiProjectIdRequired: 'Google Cloud/Workspace 帳戶需要提供專案 ID',
geminiProjectIdDetail: '某些 Google 帳戶(特別是綁定了 Google Cloud 的帳戶)會被識別為 Workspace 帳戶,需要提供額外的專案 ID。',
geminiProjectIdDetail:
'某些 Google 帳戶(特別是綁定了 Google Cloud 的帳戶)會被識別為 Workspace 帳戶,需要提供額外的專案 ID。',
geminiHowToGetProjectId: '如何取得專案 ID',
geminiVisitConsole: '造訪',
geminiCopyProjectId: '複製專案 IDProject ID通常是字串格式',
geminiProjectIdWarning: '⚠️ 注意:要複製專案 IDProject ID不要複製專案編號Project Number',
geminiPersonalAccountTip: '提示:如果您的帳戶是普通個人帳戶(未綁定 Google Cloud請留空此欄位。',
geminiProjectIdWarning:
'⚠️ 注意:要複製專案 IDProject ID不要複製專案編號Project Number',
geminiPersonalAccountTip:
'提示:如果您的帳戶是普通個人帳戶(未綁定 Google Cloud請留空此欄位。',
// AWS 區域參考
awsRegionReference: '常用 AWS 區域參考:',
@@ -1666,7 +2284,8 @@ export default {
dailyQuotaDescription: '設定每日使用額度0 表示不限制',
quotaResetTime: '額度重設時間',
quotaResetTimeDescription: '每日自動重設額度的時間',
modelMappingDescription: '留空表示支援所有模型且不修改請求。設定映射後,左側模型會被識別為支援的模型,右側是實際傳送的模型。',
modelMappingDescription:
'留空表示支援所有模型且不修改請求。設定映射後,左側模型會被識別為支援的模型,右側是實際傳送的模型。',
// 額度管理
quotaManagementFields: '配額管理欄位',
@@ -1678,7 +2297,8 @@ export default {
// 模型映射
modelMappingOptional: '模型映射表 (可選)',
modelMappingDesc: '留空表示支援所有模型且不修改請求。設定映射後,左側模型會被識別為支援的模型,右側是實際發送的模型。',
modelMappingDesc:
'留空表示支援所有模型且不修改請求。設定映射後,左側模型會被識別為支援的模型,右側是實際發送的模型。',
originalModelName: '原始模型名稱',
mappedModelName: '映射後的模型名稱',
addModelMappingBtn: '新增模型映射',
@@ -1692,14 +2312,17 @@ export default {
// Claude 進階選項
claudeAutoStopScheduling: '5小時使用量接近限制時自動停止調度',
claudeAutoStopDesc: '當系統檢測到帳戶接近5小時使用限制時自動暫停調度該帳戶。進入新的時間視窗後會自動恢復調度。',
claudeAutoStopDesc:
'當系統檢測到帳戶接近5小時使用限制時自動暫停調度該帳戶。進入新的時間視窗後會自動恢復調度。',
claudeUseUnifiedUA: '使用統一 Claude Code 版本',
claudeUnifiedUADesc: '開啟後將使用從真實 Claude Code 客戶端捕獲的統一 User-Agent提高相容性',
claudeCurrentUnifiedVersion: '💡 目前統一版本:',
claudeWaitingCapture: '⏳ 等待從 Claude Code 客戶端捕獲 User-Agent',
claudeCaptureHint: '💡 提示:如果長時間未能捕獲,請確認有 Claude Code 客戶端正在使用此帳戶, 或聯繫開發者檢查 User-Agent 格式是否發生變化',
claudeCaptureHint:
'💡 提示:如果長時間未能捕獲,請確認有 Claude Code 客戶端正在使用此帳戶, 或聯繫開發者檢查 User-Agent 格式是否發生變化',
claudeUseUnifiedClientId: '使用統一的客戶端標識',
claudeUnifiedClientIdDesc: '開啟後將使用固定的客戶端標識,使所有請求看起來來自同一個客戶端,減少特徵',
claudeUnifiedClientIdDesc:
'開啟後將使用固定的客戶端標識,使所有請求看起來來自同一個客戶端,減少特徵',
claudeClientIdLabel: '客戶端標識 ID',
claudeClientIdDesc: '此ID將替換請求中的user_id客戶端部分保留session部分用於黏性工作階段',
@@ -1752,7 +2375,8 @@ export default {
// 描述性文字
claudeProLimitation: 'Pro 帳戶不支援 Claude Opus 4 模型',
claude5HourLimitDesc: '5小時使用量接近限制時自動停止調度',
claude5HourLimitExplanation: '當系統檢測到帳戶接近5小時使用限制時自動暫停調度該帳戶。進入新的時間視窗後會自動恢復調度。',
claude5HourLimitExplanation:
'當系統檢測到帳戶接近5小時使用限制時自動暫停調度該帳戶。進入新的時間視窗後會自動恢復調度。',
useUnifiedClaudeVersion: '使用統一 Claude Code 版本',
unifiedVersionDesc: '開啟後將使用從真實 Claude Code 用戶端捕獲的統一 User-Agent提高相容性',
currentUnifiedVersion: '💡 目前統一版本:',
@@ -1786,7 +2410,8 @@ export default {
copyFailedManual: '複製失敗,請手動複製',
// 表單描述
modelSupportDesc: '留空表示支援所有模型。如果指定模型,請求中的模型不在列表內將不會調度到此帳戶',
modelSupportDesc:
'留空表示支援所有模型。如果指定模型,請求中的模型不在列表內將不會調度到此帳戶',
modelTypeSelectionDesc: '選擇此部署支援的模型類型',
userAgentDesc: '留空時將自動使用用戶端的 User-Agent僅在需要固定特定 UA 時填寫',
@@ -1829,7 +2454,8 @@ export default {
step3Description: '授權完成後,頁面會顯示一個',
step3DescriptionMiddle: ',請將其複製並貼上到下方輸入框:',
step3DescriptionGemini: '授權完成後,頁面會顯示一個 Authorization Code請將其複製並貼上到下方輸入框',
step3DescriptionGemini:
'授權完成後,頁面會顯示一個 Authorization Code請將其複製並貼上到下方輸入框',
step3DescriptionOpenAI: '授權完成後,當頁面地址變為',
step3DescriptionOpenAIMiddle: '時:',
@@ -1844,7 +2470,8 @@ export default {
// 占位符
authCodePlaceholder: '貼上Claude頁面獲取的Authorization Code...',
authCodePlaceholderGemini: '貼上Gemini頁面獲取的Authorization Code...',
authCodePlaceholderOpenAI: '方式1複製完整的連結http://localhost:1455/auth/callback?code=...)\n方式2僅複製 code 參數的值\n系統會自動識別並提取所需資訊',
authCodePlaceholderOpenAI:
'方式1複製完整的連結http://localhost:1455/auth/callback?code=...)\n方式2僅複製 code 參數的值\n系統會自動識別並提取所需資訊',
// 標籤
authorizationCode: 'Authorization Code',

View File

@@ -60,11 +60,7 @@
<!-- 刷新按钮 -->
<div class="relative">
<el-tooltip
:content="t('accounts.refreshTooltip')"
effect="dark"
placement="bottom"
>
<el-tooltip :content="t('accounts.refreshTooltip')" effect="dark" placement="bottom">
<button
class="group relative flex items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-all duration-200 hover:border-gray-300 hover:shadow-md disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:border-gray-500 sm:w-auto"
:disabled="accountsLoading"
@@ -110,7 +106,9 @@
<i class="fas fa-user-circle text-xl text-gray-400" />
</div>
<p class="text-lg text-gray-500 dark:text-gray-400">{{ t('accounts.noAccounts') }}</p>
<p class="mt-2 text-sm text-gray-400 dark:text-gray-500">{{ t('accounts.noAccountsHint') }}</p>
<p class="mt-2 text-sm text-gray-400 dark:text-gray-500">
{{ t('accounts.noAccountsHint') }}
</p>
</div>
<!-- 桌面端表格视图 -->
@@ -390,7 +388,9 @@
class="flex items-center gap-1.5 rounded-lg border border-gray-200 bg-gradient-to-r from-gray-100 to-gray-200 px-2.5 py-1"
>
<i class="fas fa-question text-xs text-gray-700" />
<span class="text-xs font-semibold text-gray-800">{{ t('accounts.unknown') }}</span>
<span class="text-xs font-semibold text-gray-800">{{
t('accounts.unknown')
}}</span>
</div>
</div>
</td>
@@ -451,7 +451,11 @@
typeof account.rateLimitStatus === 'object' &&
account.rateLimitStatus.minutesRemaining > 0
"
>({{ t('accounts.rateLimitTime', { time: formatRateLimitTime(account.rateLimitStatus.minutesRemaining) }) }})</span
>({{
t('accounts.rateLimitTime', {
time: formatRateLimitTime(account.rateLimitStatus.minutesRemaining)
})
}})</span
>
</span>
<span
@@ -609,7 +613,11 @@
v-if="account.sessionWindow.remainingTime > 0"
class="font-medium text-indigo-600 dark:text-indigo-400"
>
{{ t('accounts.remaining', { time: formatRemainingTime(account.sessionWindow.remainingTime) }) }}
{{
t('accounts.remaining', {
time: formatRemainingTime(account.sessionWindow.remainingTime)
})
}}
</div>
</div>
</div>
@@ -617,7 +625,9 @@
<div v-else-if="account.platform === 'claude-console'" class="space-y-2">
<div v-if="Number(account.dailyQuota) > 0">
<div class="flex items-center justify-between text-xs">
<span class="text-gray-600 dark:text-gray-300">{{ t('accounts.quotaProgress') }}</span>
<span class="text-gray-600 dark:text-gray-300">{{
t('accounts.quotaProgress')
}}</span>
<span class="font-medium text-gray-700 dark:text-gray-200">
{{ getQuotaUsagePercent(account).toFixed(1) }}%
</span>
@@ -642,9 +652,9 @@
</div>
<div class="text-xs text-gray-600 dark:text-gray-400">
{{ t('accounts.remainingQuota', { amount: formatRemainingQuota(account) }) }}
<span class="ml-2 text-gray-400"
>{{ t('accounts.reset', { time: account.quotaResetTime || '00:00' }) }}</span
>
<span class="ml-2 text-gray-400">{{
t('accounts.reset', { time: account.quotaResetTime || '00:00' })
}}</span>
</div>
</div>
<div v-else class="text-sm text-gray-400">
@@ -682,7 +692,11 @@
: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200'
]"
:disabled="account.isResetting"
:title="account.isResetting ? t('accounts.resetting') : t('accounts.resetStatusTooltip')"
:title="
account.isResetting
? t('accounts.resetting')
: t('accounts.resetStatusTooltip')
"
@click="resetAccountStatus(account)"
>
<i :class="['fas fa-redo', account.isResetting ? 'animate-spin' : '']" />
@@ -698,11 +712,17 @@
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
]"
:disabled="account.isTogglingSchedulable"
:title="account.schedulable ? t('accounts.disableTooltip') : t('accounts.enableTooltip')"
:title="
account.schedulable
? t('accounts.disableTooltip')
: t('accounts.enableTooltip')
"
@click="toggleSchedulable(account)"
>
<i :class="['fas', account.schedulable ? 'fa-toggle-on' : 'fa-toggle-off']" />
<span class="ml-1">{{ account.schedulable ? t('accounts.scheduling') : t('accounts.disabled') }}</span>
<span class="ml-1">{{
account.schedulable ? t('accounts.scheduling') : t('accounts.disabled')
}}</span>
</button>
<button
class="rounded bg-blue-100 px-2.5 py-1 text-xs font-medium text-blue-700 transition-colors hover:bg-blue-200"
@@ -799,7 +819,9 @@
<!-- 使用统计 -->
<div class="mb-3 grid grid-cols-2 gap-3">
<div>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('accounts.dailyUsageLabel') }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('accounts.dailyUsageLabel') }}
</p>
<div class="space-y-1">
<div class="flex items-center gap-1.5">
<div class="h-1.5 w-1.5 rounded-full bg-blue-500" />
@@ -822,7 +844,9 @@
</div>
</div>
<div>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('accounts.sessionWindowLabel') }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('accounts.sessionWindowLabel') }}
</p>
<div v-if="account.usage && account.usage.sessionWindow" class="space-y-1">
<div class="flex items-center gap-1.5">
<div class="h-1.5 w-1.5 rounded-full bg-purple-500" />
@@ -854,11 +878,10 @@
>
<div class="flex items-center justify-between text-xs">
<div class="flex items-center gap-1">
<span class="font-medium text-gray-600 dark:text-gray-300">{{ t('accounts.sessionWindowLabel') }}</span>
<el-tooltip
:content="t('accounts.sessionWindowTooltipMobile')"
placement="top"
>
<span class="font-medium text-gray-600 dark:text-gray-300">{{
t('accounts.sessionWindowLabel')
}}</span>
<el-tooltip :content="t('accounts.sessionWindowTooltipMobile')" placement="top">
<i
class="fas fa-question-circle cursor-help text-xs text-gray-400 hover:text-gray-600"
/>
@@ -890,7 +913,11 @@
v-if="account.sessionWindow.remainingTime > 0"
class="font-medium text-indigo-600"
>
{{ t('accounts.remaining', { time: formatRemainingTime(account.sessionWindow.remainingTime) }) }}
{{
t('accounts.remaining', {
time: formatRemainingTime(account.sessionWindow.remainingTime)
})
}}
</span>
<span v-else class="text-gray-500"> {{ t('accounts.ended') }} </span>
</div>
@@ -898,9 +925,15 @@
<!-- 最后使用时间 -->
<div class="flex items-center justify-between text-xs">
<span class="text-gray-500 dark:text-gray-400">{{ t('accounts.lastUsedLabel') }}</span>
<span class="text-gray-500 dark:text-gray-400">{{
t('accounts.lastUsedLabel')
}}</span>
<span class="text-gray-700 dark:text-gray-200">
{{ account.lastUsedAt ? formatRelativeTime(account.lastUsedAt) : t('accounts.neverUsed') }}
{{
account.lastUsedAt
? formatRelativeTime(account.lastUsedAt)
: t('accounts.neverUsed')
}}
</span>
</div>
@@ -917,7 +950,9 @@
<!-- 调度优先级 -->
<div class="flex items-center justify-between text-xs">
<span class="text-gray-500 dark:text-gray-400">{{ t('accounts.priorityLabel') }}</span>
<span class="text-gray-500 dark:text-gray-400">{{
t('accounts.priorityLabel')
}}</span>
<span class="font-medium text-gray-700 dark:text-gray-200">
{{ account.priority || 50 }}
</span>
@@ -1610,10 +1645,7 @@ const deleteAccount = async (account) => {
).length
if (boundKeysCount > 0) {
showToast(
t('accounts.cannotDeleteBoundAccount', { count: boundKeysCount }),
'error'
)
showToast(t('accounts.cannotDeleteBoundAccount', { count: boundKeysCount }), 'error')
return
}
@@ -1749,7 +1781,10 @@ const toggleSchedulable = async (account) => {
if (data.success) {
account.schedulable = data.schedulable
showToast(data.schedulable ? t('accounts.enabledScheduling') : t('accounts.disabledScheduling'), 'success')
showToast(
data.schedulable ? t('accounts.enabledScheduling') : t('accounts.disabledScheduling'),
'success'
)
} else {
showToast(data.message || t('accounts.operationFailed'), 'error')
}

View File

@@ -68,7 +68,10 @@
icon="fa-calendar-alt"
icon-color="text-blue-500"
:options="timeRangeOptions"
:placeholder="timeRangeOptions.find(o => o.value === 'today')?.label || t('apiKeys.timeRange.today')"
:placeholder="
timeRangeOptions.find((o) => o.value === 'today')?.label ||
t('apiKeys.timeRange.today')
"
@change="loadApiKeys()"
/>
</div>
@@ -84,7 +87,9 @@
icon="fa-tags"
icon-color="text-purple-500"
:options="tagOptions"
:placeholder="tagOptions.find(o => o.value === '')?.label || t('apiKeys.allTags')"
:placeholder="
tagOptions.find((o) => o.value === '')?.label || t('apiKeys.allTags')
"
@change="currentPage = 1"
/>
<span
@@ -105,7 +110,11 @@
<input
v-model="searchKeyword"
class="w-full rounded-lg border border-gray-200 bg-white px-3 py-2 pl-9 text-sm text-gray-700 placeholder-gray-400 shadow-sm transition-all duration-200 hover:border-gray-300 focus:border-cyan-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/20 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:placeholder-gray-500 dark:hover:border-gray-500"
:placeholder="isLdapEnabled ? t('apiKeys.searchPlaceholderWithOwner') : t('apiKeys.searchPlaceholder')"
:placeholder="
isLdapEnabled
? t('apiKeys.searchPlaceholderWithOwner')
: t('apiKeys.searchPlaceholder')
"
type="text"
@input="currentPage = 1"
/>
@@ -148,7 +157,9 @@
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-blue-500 to-indigo-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
></div>
<i class="fas fa-edit relative text-blue-600 dark:text-blue-400" />
<span class="relative">{{ t('apiKeys.bulkEdit') }} ({{ selectedApiKeys.length }})</span>
<span class="relative"
>{{ t('apiKeys.bulkEdit') }} ({{ selectedApiKeys.length }})</span
>
</button>
<!-- 批量删除按钮 - 移到刷新按钮旁边 -->
@@ -161,7 +172,9 @@
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-red-500 to-pink-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
></div>
<i class="fas fa-trash relative text-red-600 dark:text-red-400" />
<span class="relative">{{ t('apiKeys.bulkDelete') }} ({{ selectedApiKeys.length }})</span>
<span class="relative"
>{{ t('apiKeys.bulkDelete') }} ({{ selectedApiKeys.length }})</span
>
</button>
</div>
@@ -471,25 +484,34 @@
<!-- 今日使用统计 -->
<div class="mb-2">
<div class="mb-1 flex items-center justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400">{{ t('apiKeys.dailyRequests') }}</span>
<span class="text-gray-600 dark:text-gray-400">{{
t('apiKeys.dailyRequests')
}}</span>
<span class="font-semibold text-gray-900 dark:text-gray-100"
>{{ formatNumber(key.usage?.daily?.requests || 0) }}{{ t('apiKeys.requests') }}</span
>{{ formatNumber(key.usage?.daily?.requests || 0)
}}{{ t('apiKeys.requests') }}</span
>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400">{{ t('apiKeys.dailyCost') }}</span>
<span class="text-gray-600 dark:text-gray-400">{{
t('apiKeys.dailyCost')
}}</span>
<span class="font-semibold text-green-600"
>${{ (key.dailyCost || 0).toFixed(4) }}</span
>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400">{{ t('apiKeys.totalCost') }}</span>
<span class="text-gray-600 dark:text-gray-400">{{
t('apiKeys.totalCost')
}}</span>
<span class="font-semibold text-blue-600"
>${{ (key.totalCost || 0).toFixed(4) }}</span
>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400">{{ t('apiKeys.lastUsed') }}</span>
<span class="text-gray-600 dark:text-gray-400">{{
t('apiKeys.lastUsed')
}}</span>
<span class="font-medium text-gray-700 dark:text-gray-300">{{
formatLastUsed(key.lastUsedAt)
}}</span>
@@ -499,7 +521,9 @@
<!-- 每日费用限制进度条 -->
<div v-if="key.dailyCostLimit > 0" class="space-y-1">
<div class="flex items-center justify-between text-xs">
<span class="text-gray-500 dark:text-gray-400">{{ t('apiKeys.dailyLimit') }}</span>
<span class="text-gray-500 dark:text-gray-400">{{
t('apiKeys.dailyLimit')
}}</span>
<span class="text-gray-700 dark:text-gray-300">
${{ (key.dailyCost || 0).toFixed(2) }} / ${{
key.dailyCostLimit.toFixed(2)
@@ -518,7 +542,9 @@
<!-- Opus 周费用限制进度条 -->
<div v-if="key.weeklyOpusCostLimit > 0" class="space-y-1">
<div class="flex items-center justify-between text-xs">
<span class="text-gray-500 dark:text-gray-400">{{ t('apiKeys.weeklyOpusLimit') }}</span>
<span class="text-gray-500 dark:text-gray-400">{{
t('apiKeys.weeklyOpusLimit')
}}</span>
<span class="text-gray-700 dark:text-gray-300">
${{ (key.weeklyOpusCost || 0).toFixed(2) }} / ${{
key.weeklyOpusCostLimit.toFixed(2)
@@ -703,7 +729,9 @@
<td class="bg-gray-50 px-3 py-4 dark:bg-gray-700" colspan="8">
<div v-if="!apiKeyModelStats[key.id]" class="py-4 text-center">
<div class="loading-spinner mx-auto" />
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">{{ t('apiKeys.loadingModelStats') }}</p>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{{ t('apiKeys.loadingModelStats') }}
</p>
</div>
<div class="space-y-4">
<!-- 通用的标题和时间筛选器,无论是否有数据都显示 -->
@@ -719,7 +747,11 @@
v-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length > 0"
class="rounded-full bg-gray-100 px-2 py-1 text-xs text-gray-500 dark:bg-gray-700 dark:text-gray-400"
>
{{ t('apiKeys.modelStatsCount', { count: apiKeyModelStats[key.id].length }) }}
{{
t('apiKeys.modelStatsCount', {
count: apiKeyModelStats[key.id].length
})
}}
</span>
<!-- API Keys日期筛选器 -->
@@ -773,7 +805,9 @@
>
<div class="mb-3 flex items-center justify-center gap-2">
<i class="fas fa-chart-line text-lg text-gray-400" />
<p class="text-sm text-gray-500 dark:text-gray-400">{{ t('apiKeys.noModelData') }}</p>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t('apiKeys.noModelData') }}
</p>
<button
class="ml-2 flex items-center gap-1 text-sm text-blue-500 transition-colors hover:text-blue-700"
:title="t('apiKeys.resetFilter')"
@@ -1078,7 +1112,9 @@
<!-- 今日使用 -->
<div class="rounded-lg bg-gray-50 p-3 dark:bg-gray-700">
<div class="mb-2 flex items-center justify-between">
<span class="text-xs text-gray-600 dark:text-gray-400">{{ t('apiKeys.dailyUsage') }}</span>
<span class="text-xs text-gray-600 dark:text-gray-400">{{
t('apiKeys.dailyUsage')
}}</span>
<button
class="text-xs text-blue-600 hover:text-blue-800"
@click="showUsageDetails(key)"
@@ -1089,19 +1125,26 @@
<div class="grid grid-cols-2 gap-3">
<div>
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ formatNumber(key.usage?.daily?.requests || 0) }} {{ t('apiKeys.requests') }}
{{ formatNumber(key.usage?.daily?.requests || 0) }}
{{ t('apiKeys.requests') }}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('apiKeys.requests') }}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('apiKeys.requests') }}</p>
</div>
<div>
<p class="text-sm font-semibold text-green-600">
${{ (key.dailyCost || 0).toFixed(4) }}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('apiKeys.totalCost') }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('apiKeys.totalCost') }}
</p>
</div>
</div>
<div class="mt-2 flex items-center justify-between">
<span class="text-xs text-gray-600 dark:text-gray-400">{{ t('apiKeys.lastUsed') }}</span>
<span class="text-xs text-gray-600 dark:text-gray-400">{{
t('apiKeys.lastUsed')
}}</span>
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
formatLastUsed(key.lastUsedAt)
}}</span>
@@ -1111,7 +1154,9 @@
<!-- 限制进度 -->
<div v-if="key.dailyCostLimit > 0" class="space-y-1">
<div class="flex items-center justify-between text-xs">
<span class="text-gray-500 dark:text-gray-400">{{ t('apiKeys.dailyLimit') }}</span>
<span class="text-gray-500 dark:text-gray-400">{{
t('apiKeys.dailyLimit')
}}</span>
<span class="text-gray-700 dark:text-gray-300">
${{ (key.dailyCost || 0).toFixed(2) }} / ${{ key.dailyCostLimit.toFixed(2) }}
</span>
@@ -1250,7 +1295,9 @@
{{ t('apiKeys.totalRecords', { count: sortedApiKeys.length }) }}
</span>
<div class="flex items-center gap-2">
<span class="text-xs text-gray-600 dark:text-gray-400 sm:text-sm">{{ t('apiKeys.pageSize') }}</span>
<span class="text-xs text-gray-600 dark:text-gray-400 sm:text-sm">{{
t('apiKeys.pageSize')
}}</span>
<select
v-model="pageSize"
class="rounded-md border border-gray-200 bg-white px-2 py-1 text-xs text-gray-700 transition-colors hover:border-gray-300 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:border-gray-500 sm:text-sm"
@@ -1260,7 +1307,9 @@
{{ size }}
</option>
</select>
<span class="text-xs text-gray-600 dark:text-gray-400 sm:text-sm">{{ t('apiKeys.records') }}</span>
<span class="text-xs text-gray-600 dark:text-gray-400 sm:text-sm">{{
t('apiKeys.records')
}}</span>
</div>
</div>
@@ -1475,19 +1524,26 @@
<td class="px-3 py-4">
<div class="text-sm">
<div class="flex items-center justify-between">
<span class="text-gray-600 dark:text-gray-400">{{ t('apiKeys.requests') }}</span>
<span class="text-gray-600 dark:text-gray-400">{{
t('apiKeys.requests')
}}</span>
<span class="font-semibold text-gray-900 dark:text-gray-100">
{{ formatNumber(key.usage?.total?.requests || 0) }} {{ t('apiKeys.requests') }}
{{ formatNumber(key.usage?.total?.requests || 0) }}
{{ t('apiKeys.requests') }}
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-gray-600 dark:text-gray-400">{{ t('apiKeys.totalCost') }}</span>
<span class="text-gray-600 dark:text-gray-400">{{
t('apiKeys.totalCost')
}}</span>
<span class="font-semibold text-green-600">
${{ (key.usage?.total?.cost || 0).toFixed(4) }}
</span>
</div>
<div v-if="key.lastUsedAt" class="flex items-center justify-between">
<span class="text-gray-600 dark:text-gray-400">{{ t('apiKeys.lastUsed') }}</span>
<span class="text-gray-600 dark:text-gray-400">{{
t('apiKeys.lastUsed')
}}</span>
<span class="font-medium text-gray-700 dark:text-gray-300">
{{ formatLastUsed(key.lastUsedAt) }}
</span>
@@ -2350,9 +2406,7 @@ const toggleApiKeyStatus = async (key) => {
)
} else {
// 降级方案
confirmed = confirm(
t('apiKeys.confirmDisable', { name: key.name })
)
confirmed = confirm(t('apiKeys.confirmDisable', { name: key.name }))
}
}
@@ -2539,7 +2593,12 @@ const batchDeleteApiKeys = async () => {
const message = t('apiKeys.confirmBatchDelete', { count: selectedCount })
if (window.showConfirm) {
confirmed = await window.showConfirm(t('apiKeys.confirmBatchDelete').split(' ')[0], message, t('common.confirm'), t('common.cancel'))
confirmed = await window.showConfirm(
t('apiKeys.confirmBatchDelete').split(' ')[0],
message,
t('common.confirm'),
t('common.cancel')
)
} else {
confirmed = confirm(message)
}
@@ -2562,7 +2621,10 @@ const batchDeleteApiKeys = async () => {
// 如果有失败的,显示详细信息
if (failedCount > 0) {
const errorMessages = errors.map((e) => `${e.keyId}: ${e.error}`).join('\n')
showToast(t('apiKeys.batchPartialFail', { failed: failedCount }) + ':\n' + errorMessages, 'warning')
showToast(
t('apiKeys.batchPartialFail', { failed: failedCount }) + ':\n' + errorMessages,
'warning'
)
}
} else {
showToast(t('apiKeys.batchAllFailed'), 'error')

View File

@@ -33,7 +33,9 @@
to="/user-login"
>
<i class="fas fa-user text-sm md:text-base" />
<span class="text-xs font-semibold tracking-wide md:text-sm">{{ t('apiStats.userLogin') }}</span>
<span class="text-xs font-semibold tracking-wide md:text-sm">{{
t('apiStats.userLogin')
}}</span>
</router-link>
<!-- 管理后台按钮 -->
<router-link
@@ -42,7 +44,9 @@
to="/dashboard"
>
<i class="fas fa-shield-alt text-sm md:text-base" />
<span class="text-xs font-semibold tracking-wide md:text-sm">{{ t('apiStats.adminPanel') }}</span>
<span class="text-xs font-semibold tracking-wide md:text-sm">{{
t('apiStats.adminPanel')
}}</span>
</router-link>
</div>
</div>
@@ -97,9 +101,9 @@
>
<div class="flex items-center gap-2 md:gap-3">
<i class="fas fa-clock text-base text-blue-500 md:text-lg" />
<span class="text-base font-medium text-gray-700 dark:text-gray-200 md:text-lg"
>{{ t('apiStats.timeRange') }}</span
>
<span class="text-base font-medium text-gray-700 dark:text-gray-200 md:text-lg">{{
t('apiStats.timeRange')
}}</span>
</div>
<div class="flex w-full gap-2 md:w-auto">
<button
@@ -190,7 +194,7 @@ const currentTutorialComponent = computed(() => {
const components = {
'zh-cn': TutorialViewZhCn,
'zh-tw': TutorialViewZhTw,
'en': TutorialViewEn
en: TutorialViewEn
}
return components[locale] || TutorialViewZhCn
})

View File

@@ -42,7 +42,12 @@
dashboardData.accountsByPlatform.claude.total > 0
"
class="inline-flex items-center gap-0.5"
:title="t('dashboard.claudeAccount', { total: dashboardData.accountsByPlatform.claude.total, normal: dashboardData.accountsByPlatform.claude.normal })"
:title="
t('dashboard.claudeAccount', {
total: dashboardData.accountsByPlatform.claude.total,
normal: dashboardData.accountsByPlatform.claude.normal
})
"
>
<i class="fas fa-brain text-xs text-indigo-600" />
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
@@ -56,7 +61,12 @@
dashboardData.accountsByPlatform['claude-console'].total > 0
"
class="inline-flex items-center gap-0.5"
:title="t('dashboard.consoleAccount', { total: dashboardData.accountsByPlatform['claude-console'].total, normal: dashboardData.accountsByPlatform['claude-console'].normal })"
:title="
t('dashboard.consoleAccount', {
total: dashboardData.accountsByPlatform['claude-console'].total,
normal: dashboardData.accountsByPlatform['claude-console'].normal
})
"
>
<i class="fas fa-terminal text-xs text-purple-600" />
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
@@ -70,7 +80,12 @@
dashboardData.accountsByPlatform.gemini.total > 0
"
class="inline-flex items-center gap-0.5"
:title="t('dashboard.geminiAccount', { total: dashboardData.accountsByPlatform.gemini.total, normal: dashboardData.accountsByPlatform.gemini.normal })"
:title="
t('dashboard.geminiAccount', {
total: dashboardData.accountsByPlatform.gemini.total,
normal: dashboardData.accountsByPlatform.gemini.normal
})
"
>
<i class="fas fa-robot text-xs text-yellow-600" />
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
@@ -84,7 +99,12 @@
dashboardData.accountsByPlatform.bedrock.total > 0
"
class="inline-flex items-center gap-0.5"
:title="t('dashboard.bedrockAccount', { total: dashboardData.accountsByPlatform.bedrock.total, normal: dashboardData.accountsByPlatform.bedrock.normal })"
:title="
t('dashboard.bedrockAccount', {
total: dashboardData.accountsByPlatform.bedrock.total,
normal: dashboardData.accountsByPlatform.bedrock.normal
})
"
>
<i class="fab fa-aws text-xs text-orange-600" />
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
@@ -98,7 +118,12 @@
dashboardData.accountsByPlatform.openai.total > 0
"
class="inline-flex items-center gap-0.5"
:title="t('dashboard.openaiAccount', { total: dashboardData.accountsByPlatform.openai.total, normal: dashboardData.accountsByPlatform.openai.normal })"
:title="
t('dashboard.openaiAccount', {
total: dashboardData.accountsByPlatform.openai.total,
normal: dashboardData.accountsByPlatform.openai.normal
})
"
>
<i class="fas fa-openai text-xs text-gray-100" />
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
@@ -112,7 +137,12 @@
dashboardData.accountsByPlatform.azure_openai.total > 0
"
class="inline-flex items-center gap-0.5"
:title="t('dashboard.azureOpenaiAccount', { total: dashboardData.accountsByPlatform.azure_openai.total, normal: dashboardData.accountsByPlatform.azure_openai.normal })"
:title="
t('dashboard.azureOpenaiAccount', {
total: dashboardData.accountsByPlatform.azure_openai.total,
normal: dashboardData.accountsByPlatform.azure_openai.normal
})
"
>
<i class="fab fa-microsoft text-xs text-blue-600" />
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
@@ -153,7 +183,8 @@
{{ dashboardData.todayRequests }}
</p>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('dashboard.totalRequests') }}: {{ formatNumber(dashboardData.totalRequests || 0) }}
{{ t('dashboard.totalRequests') }}:
{{ formatNumber(dashboardData.totalRequests || 0) }}
</p>
</div>
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-purple-500 to-purple-600">
@@ -303,7 +334,9 @@
<div>
<p class="mb-1 text-xs font-semibold text-gray-600 dark:text-gray-400 sm:text-sm">
{{ t('dashboard.realtimeRPM') }}
<span class="text-xs text-gray-400">({{ dashboardData.metricsWindow }}{{ t('dashboard.minutes') }})</span>
<span class="text-xs text-gray-400"
>({{ dashboardData.metricsWindow }}{{ t('dashboard.minutes') }})</span
>
</p>
<p class="text-2xl font-bold text-orange-600 sm:text-3xl">
{{ dashboardData.realtimeRPM || 0 }}
@@ -326,7 +359,9 @@
<div>
<p class="mb-1 text-xs font-semibold text-gray-600 dark:text-gray-400 sm:text-sm">
{{ t('dashboard.realtimeTPM') }}
<span class="text-xs text-gray-400">({{ dashboardData.metricsWindow }}{{ t('dashboard.minutes') }})</span>
<span class="text-xs text-gray-400"
>({{ dashboardData.metricsWindow }}{{ t('dashboard.minutes') }})</span
>
</p>
<p class="text-2xl font-bold text-rose-600 sm:text-3xl">
{{ formatNumber(dashboardData.realtimeTPM || 0) }}
@@ -453,7 +488,9 @@
@click="refreshAllData()"
>
<i :class="['fas fa-sync-alt text-xs', { 'animate-spin': isRefreshing }]" />
<span class="hidden sm:inline">{{ isRefreshing ? t('dashboard.refreshing') : t('dashboard.refresh') }}</span>
<span class="hidden sm:inline">{{
isRefreshing ? t('dashboard.refreshing') : t('dashboard.refresh')
}}</span>
</button>
</div>
</div>
@@ -579,7 +616,9 @@
]"
@click="((apiKeysTrendMetric = 'requests'), updateApiKeysUsageTrendChart())"
>
<i class="fas fa-exchange-alt mr-1" /><span class="hidden sm:inline">{{ t('dashboard.requestsCount') }}</span
<i class="fas fa-exchange-alt mr-1" /><span class="hidden sm:inline">{{
t('dashboard.requestsCount')
}}</span
><span class="sm:hidden">{{ t('dashboard.requestsCount').split(' ')[0] }}</span>
</button>
<button
@@ -591,7 +630,9 @@
]"
@click="((apiKeysTrendMetric = 'tokens'), updateApiKeysUsageTrendChart())"
>
<i class="fas fa-coins mr-1" /><span class="hidden sm:inline">{{ t('dashboard.tokenCount') }}</span
<i class="fas fa-coins mr-1" /><span class="hidden sm:inline">{{
t('dashboard.tokenCount')
}}</span
><span class="sm:hidden">Token</span>
</button>
</div>
@@ -600,7 +641,9 @@
<span v-if="apiKeysTrendData.totalApiKeys > 10">
{{ t('dashboard.showingTop10', { count: apiKeysTrendData.totalApiKeys }) }}
</span>
<span v-else>{{ t('dashboard.totalApiKeysCount', { count: apiKeysTrendData.totalApiKeys }) }}</span>
<span v-else>{{
t('dashboard.totalApiKeysCount', { count: apiKeysTrendData.totalApiKeys })
}}</span>
</div>
<div class="sm:h-[350px]" style="height: 300px">
<canvas ref="apiKeysUsageTrendChart" />
@@ -1164,7 +1207,10 @@ function createApiKeysUsageTrendChart() {
beginAtZero: true,
title: {
display: true,
text: apiKeysTrendMetric.value === 'tokens' ? t('dashboard.tokenQuantity') : t('dashboard.requestsQuantity'),
text:
apiKeysTrendMetric.value === 'tokens'
? t('dashboard.tokenQuantity')
: t('dashboard.requestsQuantity'),
color: chartColors.value.text
},
ticks: {

View File

@@ -42,7 +42,8 @@
<form class="space-y-4 sm:space-y-6" @submit.prevent="handleLogin">
<div>
<label class="mb-2 block text-sm font-semibold text-gray-900 dark:text-gray-100 sm:mb-3"
<label
class="mb-2 block text-sm font-semibold text-gray-900 dark:text-gray-100 sm:mb-3"
>{{ t('login.username') }}</label
>
<input
@@ -55,7 +56,8 @@
</div>
<div>
<label class="mb-2 block text-sm font-semibold text-gray-900 dark:text-gray-100 sm:mb-3"
<label
class="mb-2 block text-sm font-semibold text-gray-900 dark:text-gray-100 sm:mb-3"
>{{ t('login.password') }}</label
>
<input

View File

@@ -6,7 +6,9 @@
<h3 class="mb-1 text-lg font-bold text-gray-900 dark:text-gray-100 sm:mb-2 sm:text-xl">
{{ t('settings.title') }}
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 sm:text-base">{{ t('settings.description') }}</p>
<p class="text-sm text-gray-600 dark:text-gray-400 sm:text-base">
{{ t('settings.description') }}
</p>
</div>
<!-- 设置分类导航 -->
@@ -66,7 +68,9 @@
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ t('settings.siteName') }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">{{ t('settings.siteNameDescription') }}</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ t('settings.siteNameDescription') }}
</div>
</div>
</div>
</td>
@@ -97,7 +101,9 @@
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ t('settings.siteIcon') }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">{{ t('settings.siteIconDescription') }}</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ t('settings.siteIconDescription') }}
</div>
</div>
</div>
</td>
@@ -114,7 +120,9 @@
:src="oemSettings.siteIconData || oemSettings.siteIcon"
@error="handleIconError"
/>
<span class="text-sm text-gray-600 dark:text-gray-400">{{ t('settings.currentIcon') }}</span>
<span class="text-sm text-gray-600 dark:text-gray-400">{{
t('settings.currentIcon')
}}</span>
<button
class="rounded-lg px-3 py-1 font-medium text-red-600 transition-colors hover:bg-red-50 hover:text-red-900"
@click="removeIcon"
@@ -139,9 +147,9 @@
<i class="fas fa-upload mr-2" />
{{ t('settings.uploadIcon') }}
</button>
<span class="ml-3 text-xs text-gray-500 dark:text-gray-400"
>{{ t('settings.iconFormats') }}</span
>
<span class="ml-3 text-xs text-gray-500 dark:text-gray-400">{{
t('settings.iconFormats')
}}</span>
</div>
</div>
</td>
@@ -160,7 +168,9 @@
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ t('settings.adminEntry') }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">{{ t('settings.adminEntryDescription') }}</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ t('settings.adminEntryDescription') }}
</div>
</div>
</div>
</td>
@@ -172,7 +182,9 @@
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800"
></div>
<span class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300">{{
hideAdminButton ? t('settings.hideLoginButton') : t('settings.showLoginButton')
hideAdminButton
? t('settings.hideLoginButton')
: t('settings.showLoginButton')
}}</span>
</label>
</div>
@@ -213,7 +225,9 @@
class="text-sm text-gray-500 dark:text-gray-400"
>
<i class="fas fa-clock mr-1" />
{{ t('settings.lastUpdated', { time: formatDateTime(oemSettings.updatedAt) }) }}
{{
t('settings.lastUpdated', { time: formatDateTime(oemSettings.updatedAt) })
}}
</div>
</div>
</td>
@@ -233,8 +247,12 @@
<i class="fas fa-tag"></i>
</div>
<div>
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">{{ t('settings.siteNameCard') }}</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">{{ t('settings.siteNameCardDesc') }}</p>
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">
{{ t('settings.siteNameCard') }}
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t('settings.siteNameCardDesc') }}
</p>
</div>
</div>
<input
@@ -255,7 +273,9 @@
<i class="fas fa-image"></i>
</div>
<div>
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">{{ t('settings.siteIconCard') }}</h3>
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">
{{ t('settings.siteIconCard') }}
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t('settings.siteIconCardDesc') }}
</p>
@@ -273,7 +293,9 @@
:src="oemSettings.siteIconData || oemSettings.siteIcon"
@error="handleIconError"
/>
<span class="text-sm text-gray-600 dark:text-gray-400">{{ t('settings.currentIcon') }}</span>
<span class="text-sm text-gray-600 dark:text-gray-400">{{
t('settings.currentIcon')
}}</span>
<button
class="rounded-lg px-3 py-1 font-medium text-red-600 transition-colors hover:bg-red-50 hover:text-red-900"
@click="removeIcon"
@@ -314,8 +336,12 @@
<i class="fas fa-eye-slash"></i>
</div>
<div>
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">{{ t('settings.adminEntryCard') }}</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">{{ t('settings.adminEntryCardDesc') }}</p>
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">
{{ t('settings.adminEntryCard') }}
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t('settings.adminEntryCardDesc') }}
</p>
</div>
</div>
<div class="space-y-2">
@@ -362,7 +388,9 @@
class="text-center text-sm text-gray-500 dark:text-gray-400"
>
<i class="fas fa-clock mr-1" />
{{ t('settings.lastUpdatedMobile', { time: formatDateTime(oemSettings.updatedAt) }) }}
{{
t('settings.lastUpdatedMobile', { time: formatDateTime(oemSettings.updatedAt) })
}}
</div>
</div>
</div>
@@ -402,7 +430,9 @@
<div
class="mb-6 rounded-lg bg-white/80 p-6 shadow-lg backdrop-blur-sm dark:bg-gray-800/80"
>
<h2 class="mb-4 text-lg font-semibold text-gray-800 dark:text-gray-200">{{ t('settings.notificationTypes') }}</h2>
<h2 class="mb-4 text-lg font-semibold text-gray-800 dark:text-gray-200">
{{ t('settings.notificationTypes') }}
</h2>
<div class="space-y-3">
<div
v-for="(enabled, type) in webhookConfig.notificationTypes"
@@ -1207,7 +1237,10 @@ const savePlatform = async () => {
}
if (response.success && isMounted.value) {
showToast(editingPlatform.value ? t('settings.platformUpdated') : t('settings.platformAdded'), 'success')
showToast(
editingPlatform.value ? t('settings.platformUpdated') : t('settings.platformAdded'),
'success'
)
await loadWebhookConfig()
closePlatformModal()
}

View File

@@ -17,7 +17,7 @@ const currentTutorialComponent = computed(() => {
const components = {
'zh-cn': TutorialViewZhCn,
'zh-tw': TutorialViewZhTw,
'en': TutorialViewEn
en: TutorialViewEn
}
return components[locale] || TutorialViewZhCn
})

View File

@@ -72,7 +72,8 @@
</div>
<div class="flex items-center space-x-4">
<div class="text-sm text-gray-700 dark:text-gray-300">
{{ t('user.dashboard.welcome') }}, <span class="font-medium">{{ userStore.userName }}</span>
{{ t('user.dashboard.welcome') }},
<span class="font-medium">{{ userStore.userName }}</span>
</div>
<!-- 主题切换按钮 -->
@@ -94,7 +95,9 @@
<!-- Overview Tab -->
<div v-if="activeTab === 'overview'" class="space-y-6">
<div>
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">{{ t('user.dashboard.title') }}</h1>
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">
{{ t('user.dashboard.title') }}
</h1>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{{ t('user.dashboard.welcomeMessage') }}
</p>
@@ -272,25 +275,33 @@
<div class="mt-5 border-t border-gray-200 dark:border-gray-700">
<dl class="divide-y divide-gray-200 dark:divide-gray-700">
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('user.dashboard.username') }}</dt>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
{{ t('user.dashboard.username') }}
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
{{ userProfile?.username }}
</dd>
</div>
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('user.dashboard.displayName') }}</dt>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
{{ t('user.dashboard.displayName') }}
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
{{ userProfile?.displayName || t('user.dashboard.notAvailable') }}
</dd>
</div>
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('user.dashboard.email') }}</dt>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
{{ t('user.dashboard.email') }}
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
{{ userProfile?.email || t('user.dashboard.notAvailable') }}
</dd>
</div>
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('user.dashboard.role') }}</dt>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
{{ t('user.dashboard.role') }}
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
<span
class="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-200"
@@ -300,13 +311,17 @@
</dd>
</div>
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('user.dashboard.memberSince') }}</dt>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
{{ t('user.dashboard.memberSince') }}
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
{{ formatDate(userProfile?.createdAt) }}
</dd>
</div>
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('user.dashboard.lastLogin') }}</dt>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
{{ t('user.dashboard.lastLogin') }}
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
{{ formatDate(userProfile?.lastLoginAt) || t('user.dashboard.notAvailable') }}
</dd>

View File

@@ -3,7 +3,9 @@
<!-- Header -->
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">{{ t('user.management.title') }}</h1>
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">
{{ t('user.management.title') }}
</h1>
<p class="mt-2 text-sm text-gray-700 dark:text-gray-300">
{{ t('user.management.description') }}
</p>
@@ -254,7 +256,9 @@
fill="currentColor"
></path>
</svg>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">{{ t('user.management.loadingUsers') }}</p>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{{ t('user.management.loadingUsers') }}
</p>
</div>
<!-- Users List -->
@@ -299,7 +303,9 @@
: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
]"
>
{{ user.isActive ? t('user.management.active') : t('user.management.disabled') }}
{{
user.isActive ? t('user.management.active') : t('user.management.disabled')
}}
</span>
<span
:class="[
@@ -328,8 +334,14 @@
v-if="user.totalUsage"
class="mt-1 flex items-center space-x-4 text-xs text-gray-400 dark:text-gray-500"
>
<span>{{ formatNumber(user.totalUsage.requests || 0) }} {{ t('user.management.requests') }}</span>
<span>${{ (user.totalUsage.totalCost || 0).toFixed(4) }} {{ t('user.management.totalCostLabel') }}</span>
<span
>{{ formatNumber(user.totalUsage.requests || 0) }}
{{ t('user.management.requests') }}</span
>
<span
>${{ (user.totalUsage.totalCost || 0).toFixed(4) }}
{{ t('user.management.totalCostLabel') }}</span
>
</div>
</div>
</div>
@@ -375,7 +387,9 @@
? 'text-gray-400 hover:text-red-600'
: 'text-gray-400 hover:text-green-600'
]"
:title="user.isActive ? t('user.management.disableUser') : t('user.management.enableUser')"
:title="
user.isActive ? t('user.management.disableUser') : t('user.management.enableUser')
"
@click="toggleUserStatus(user)"
>
<svg
@@ -437,7 +451,9 @@
stroke-width="2"
/>
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">{{ t('user.management.noUsersFound') }}</h3>
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">
{{ t('user.management.noUsersFound') }}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{
searchQuery ? t('user.management.noUsersMatch') : t('user.management.noUsersCreated')
@@ -597,7 +613,9 @@ const viewUserStats = (user) => {
const toggleUserStatus = (user) => {
selectedUser.value = user
confirmAction.value = {
title: user.isActive ? t('user.management.disableUserTitle') : t('user.management.enableUserTitle'),
title: user.isActive
? t('user.management.disableUserTitle')
: t('user.management.enableUserTitle'),
message: user.isActive
? t('user.management.disableUserMessage', { username: user.username })
: t('user.management.enableUserMessage', { username: user.username }),
@@ -614,7 +632,10 @@ const disableUserApiKeys = (user) => {
selectedUser.value = user
confirmAction.value = {
title: t('user.management.disableAllKeysTitle'),
message: t('user.management.disableAllKeysMessage', { count: user.apiKeyCount, username: user.username }),
message: t('user.management.disableAllKeysMessage', {
count: user.apiKeyCount,
username: user.username
}),
confirmText: t('user.management.disableKeys'),
confirmClass: 'bg-red-600 hover:bg-red-700',
action: 'disableKeys'
@@ -642,13 +663,21 @@ const handleConfirmAction = async () => {
if (userIndex !== -1) {
users.value[userIndex].isActive = !user.isActive
}
showToast(user.isActive ? t('user.management.userDisabledSuccess') : t('user.management.userEnabledSuccess'), 'success')
showToast(
user.isActive
? t('user.management.userDisabledSuccess')
: t('user.management.userEnabledSuccess'),
'success'
)
}
} else if (action === 'disableKeys') {
const response = await apiClient.post(`/users/${user.id}/disable-keys`)
if (response.success) {
showToast(t('user.management.keysDisabledSuccess', { count: response.disabledCount }), 'success')
showToast(
t('user.management.keysDisabledSuccess', { count: response.disabledCount }),
'success'
)
await loadUsers() // Refresh to get updated counts
}
}

View File

@@ -81,7 +81,9 @@
>
file
</li>
<li>Follow the installation wizard to complete installation, keep default settings</li>
<li>
Follow the installation wizard to complete installation, keep default settings
</li>
</ol>
</div>
<div class="mb-3 sm:mb-4">
@@ -105,14 +107,18 @@
<ul class="space-y-1 text-xs text-blue-700 sm:text-sm sm:text-xs">
<li> Recommend using PowerShell instead of CMD</li>
<li> If you encounter permission issues, try running as administrator</li>
<li> Some antivirus software may flag as false positive, need to add to whitelist</li>
<li>
Some antivirus software may flag as false positive, need to add to whitelist
</li>
</ul>
</div>
</div>
<!-- Verify Installation -->
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
<h6 class="mb-2 text-sm font-medium text-green-800 sm:text-base">Verify Installation Success</h6>
<h6 class="mb-2 text-sm font-medium text-green-800 sm:text-base">
Verify Installation Success
</h6>
<p class="mb-2 text-xs text-green-700 sm:mb-3 sm:text-sm">
After installation completes, open PowerShell or CMD and enter the following commands:
</p>
@@ -122,7 +128,9 @@
<div class="whitespace-nowrap text-gray-300">node --version</div>
<div class="whitespace-nowrap text-gray-300">npm --version</div>
</div>
<p class="mt-2 text-xs text-green-700 sm:text-sm">If version numbers are displayed, installation was successful!</p>
<p class="mt-2 text-xs text-green-700 sm:text-sm">
If version numbers are displayed, installation was successful!
</p>
</div>
</div>
@@ -159,7 +167,8 @@
</div>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">
This command will download and install the latest version of Claude Code from the official npm repository.
This command will download and install the latest version of Claude Code from the
official npm repository.
</p>
<div class="mt-4 rounded-lg border border-blue-200 bg-blue-50 p-3 sm:p-4">
@@ -173,15 +182,21 @@
<!-- Verify Installation -->
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">Verify Claude Code Installation</h6>
<p class="mb-3 text-sm text-green-700">After installation completes, enter the following command to check if installation was successful:</p>
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">
Verify Claude Code Installation
</h6>
<p class="mb-3 text-sm text-green-700">
After installation completes, enter the following command to check if installation was
successful:
</p>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">claude --version</div>
</div>
<p class="mt-2 text-sm text-green-700">
If version number is displayed, congratulations! Claude Code has been successfully installed.
If version number is displayed, congratulations! Claude Code has been successfully
installed.
</p>
</div>
</div>
@@ -230,7 +245,8 @@
</div>
</div>
<p class="mt-2 text-xs text-yellow-700">
💡 Remember to replace "your-api-key" with the actual key created in the "API Keys" tab above.
💡 Remember to replace "your-api-key" with the actual key created in the "API Keys"
tab above.
</p>
</div>
@@ -280,9 +296,12 @@
<!-- Verify Environment Variable Setup -->
<div class="mt-6 rounded-lg border border-blue-200 bg-blue-50 p-3 sm:p-4">
<h6 class="mb-2 font-medium text-blue-800 dark:text-blue-300">Verify Environment Variable Setup</h6>
<h6 class="mb-2 font-medium text-blue-800 dark:text-blue-300">
Verify Environment Variable Setup
</h6>
<p class="mb-3 text-sm text-blue-700">
After setting environment variables, you can verify if they were set successfully with the following commands:
After setting environment variables, you can verify if they were set successfully with
the following commands:
</p>
<div class="space-y-4">
@@ -320,7 +339,8 @@
<div>cr_xxxxxxxxxxxxxxxxxx</div>
</div>
<p class="text-xs text-blue-700">
💡 If output is empty or shows variable name itself, environment variable setup failed, please reconfigure.
💡 If output is empty or shows variable name itself, environment variable setup
failed, please reconfigure.
</p>
</div>
</div>
@@ -416,7 +436,8 @@
Configure Codex Environment Variables
</h5>
<p class="mb-3 text-sm text-gray-700 dark:text-gray-300 sm:mb-4 sm:text-base">
If you use tools that support OpenAI API (such as Codex), you need to set the following environment variables:
If you use tools that support OpenAI API (such as Codex), you need to set the following
environment variables:
</p>
<div class="space-y-4">
@@ -528,8 +549,8 @@
<ul class="list-inside list-disc space-y-1 text-sm">
<li>Run PowerShell as Administrator</li>
<li>
Or configure npm to use user directory: <code
class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
Or configure npm to use user directory:
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
>npm config set prefix %APPDATA%\npm</code
>
</li>
@@ -567,7 +588,8 @@
<li>Restart PowerShell or CMD</li>
<li>Or log out and log back into Windows</li>
<li>
Verify settings: <code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
Verify settings:
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
>echo $env:ANTHROPIC_BASE_URL</code
>
</li>
@@ -605,7 +627,8 @@
<div class="mb-4">
<p class="mb-3 text-gray-700">Method 1: Using Homebrew (Recommended)</p>
<p class="mb-2 text-xs text-gray-600 dark:text-gray-400 sm:text-sm">
If you already have Homebrew installed, using it to install Node.js will be more convenient:
If you already have Homebrew installed, using it to install Node.js will be more
convenient:
</p>
<div
class="overflow-x-auto rounded-lg bg-gray-900 p-3 font-mono text-xs text-green-400 sm:p-4 sm:text-sm"
@@ -655,15 +678,21 @@
<!-- Verify Installation -->
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">Verify Installation Success</h6>
<p class="mb-3 text-sm text-green-700">After installation, open Terminal and enter the following commands:</p>
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">
Verify Installation Success
</h6>
<p class="mb-3 text-sm text-green-700">
After installation, open Terminal and enter the following commands:
</p>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">node --version</div>
<div class="whitespace-nowrap text-gray-300">npm --version</div>
</div>
<p class="mt-2 text-sm text-green-700">If version numbers are displayed, the installation was successful!</p>
<p class="mt-2 text-sm text-green-700">
If version numbers are displayed, the installation was successful!
</p>
</div>
</div>
@@ -699,7 +728,9 @@
npm install -g @anthropic-ai/claude-code
</div>
</div>
<p class="mb-2 text-sm text-gray-600">If you encounter permission issues, you can use sudo:</p>
<p class="mb-2 text-sm text-gray-600">
If you encounter permission issues, you can use sudo:
</p>
<div
class="overflow-x-auto rounded-lg bg-gray-900 p-3 font-mono text-xs text-green-400 sm:p-4 sm:text-sm"
>
@@ -711,15 +742,21 @@
<!-- Verify Installation -->
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">Verify Claude Code Installation</h6>
<p class="mb-3 text-sm text-green-700">After installation completes, enter the following command to check if installation was successful:</p>
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">
Verify Claude Code Installation
</h6>
<p class="mb-3 text-sm text-green-700">
After installation completes, enter the following command to check if installation was
successful:
</p>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">claude --version</div>
</div>
<p class="mt-2 text-sm text-green-700">
If version number is displayed, congratulations! Claude Code has been successfully installed.
If version number is displayed, congratulations! Claude Code has been successfully
installed.
</p>
</div>
</div>
@@ -766,7 +803,8 @@
</div>
</div>
<p class="mt-2 text-xs text-yellow-700">
💡 Remember to replace "your-api-key" with the actual key created in the "API Keys" tab above.
💡 Remember to replace "your-api-key" with the actual key created in the "API Keys"
tab above.
</p>
</div>
@@ -903,7 +941,8 @@
Configure Codex Environment Variables
</h5>
<p class="mb-3 text-sm text-gray-700 dark:text-gray-300 sm:mb-4 sm:text-base">
If you use tools that support OpenAI API (such as Codex), you need to set the following environment variables:
If you use tools that support OpenAI API (such as Codex), you need to set the following
environment variables:
</p>
<div class="space-y-4">
@@ -1014,13 +1053,14 @@
<p class="mb-2">Try the following solutions:</p>
<ul class="list-inside list-disc space-y-1 text-sm">
<li>
Install with sudo: <code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
Install with sudo:
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
>sudo npm install -g @anthropic-ai/claude-code</code
>
</li>
<li>
Or configure npm to use user directory: <code
class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
Or configure npm to use user directory:
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
>npm config set prefix ~/.npm-global</code
>
</li>
@@ -1040,7 +1080,8 @@
<li>Open "System Preferences" "Security & Privacy"</li>
<li>Click "Allow Anyway" or "Open Anyway"</li>
<li>
Or run in Terminal: <code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
Or run in Terminal:
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
>sudo spctl --master-disable</code
>
</li>
@@ -1057,10 +1098,13 @@
<div class="px-3 pb-3 text-gray-600 sm:px-4 sm:pb-4">
<p class="mb-2">Check the following points:</p>
<ul class="list-inside list-disc space-y-1 text-sm">
<li>Confirm you modified the correct configuration file (.zshrc or .bash_profile)</li>
<li>
Confirm you modified the correct configuration file (.zshrc or .bash_profile)
</li>
<li>Restart Terminal</li>
<li>
Verify settings: <code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
Verify settings:
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
>echo $ANTHROPIC_BASE_URL</code
>
</li>
@@ -1127,7 +1171,10 @@
<h6 class="mb-2 text-sm font-medium text-orange-800 sm:text-base">Linux Notes</h6>
<ul class="space-y-1 text-xs text-orange-700 sm:text-sm">
<li>• Some distributions may require additional dependencies</li>
<li>• If you encounter permission issues, use <code class="rounded bg-orange-200 px-1">sudo</code></li>
<li>
• If you encounter permission issues, use
<code class="rounded bg-orange-200 px-1">sudo</code>
</li>
<li>• Ensure your user has write permissions to npm's global directory</li>
</ul>
</div>
@@ -1135,15 +1182,21 @@
<!-- Verify Installation -->
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">Verify Installation Success</h6>
<p class="mb-3 text-sm text-green-700">After installation, open terminal and enter the following commands:</p>
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">
Verify Installation Success
</h6>
<p class="mb-3 text-sm text-green-700">
After installation, open terminal and enter the following commands:
</p>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">node --version</div>
<div class="whitespace-nowrap text-gray-300">npm --version</div>
</div>
<p class="mt-2 text-sm text-green-700">If version numbers are displayed, the installation was successful!</p>
<p class="mt-2 text-sm text-green-700">
If version numbers are displayed, the installation was successful!
</p>
</div>
</div>
@@ -1179,7 +1232,9 @@
npm install -g @anthropic-ai/claude-code
</div>
</div>
<p class="mb-2 text-sm text-gray-600">If you encounter permission issues, you can use sudo:</p>
<p class="mb-2 text-sm text-gray-600">
If you encounter permission issues, you can use sudo:
</p>
<div
class="overflow-x-auto rounded-lg bg-gray-900 p-3 font-mono text-xs text-green-400 sm:p-4 sm:text-sm"
>
@@ -1191,15 +1246,21 @@
<!-- Verify Installation -->
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">Verify Claude Code Installation</h6>
<p class="mb-3 text-sm text-green-700">After installation completes, enter the following command to check if installation was successful:</p>
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">
Verify Claude Code Installation
</h6>
<p class="mb-3 text-sm text-green-700">
After installation completes, enter the following command to check if installation was
successful:
</p>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">claude --version</div>
</div>
<p class="mt-2 text-sm text-green-700">
If version number is displayed, congratulations! Claude Code has been successfully installed.
If version number is displayed, congratulations! Claude Code has been successfully
installed.
</p>
</div>
</div>
@@ -1246,7 +1307,8 @@
</div>
</div>
<p class="mt-2 text-xs text-yellow-700">
💡 Remember to replace "your-api-key" with the actual key created in the "API Keys" tab above.
💡 Remember to replace "your-api-key" with the actual key created in the "API Keys"
tab above.
</p>
</div>
@@ -1381,7 +1443,8 @@
Configure Codex Environment Variables
</h5>
<p class="mb-3 text-sm text-gray-700 dark:text-gray-300 sm:mb-4 sm:text-base">
If you use tools that support OpenAI API (such as Codex), you need to set the following environment variables:
If you use tools that support OpenAI API (such as Codex), you need to set the following
environment variables:
</p>
<div class="space-y-4">
@@ -1492,18 +1555,20 @@
<p class="mb-2">Try the following solutions:</p>
<ul class="list-inside list-disc space-y-1 text-sm">
<li>
Install with sudo: <code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
Install with sudo:
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
>sudo npm install -g @anthropic-ai/claude-code</code
>
</li>
<li>
Or configure npm to use user directory: <code
class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
Or configure npm to use user directory:
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
>npm config set prefix ~/.npm-global</code
>
</li>
<li>
Then add to PATH: <code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
Then add to PATH:
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
>export PATH=~/.npm-global/bin:$PATH</code
>
</li>
@@ -1547,7 +1612,8 @@
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm">source ~/.bashrc</code>
</li>
<li>
Verify settings: <code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
Verify settings:
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
>echo $ANTHROPIC_BASE_URL</code
>
</li>
@@ -1564,10 +1630,12 @@
>
<h5 class="mb-2 text-lg font-semibold sm:text-xl">🎉 Congratulations!</h5>
<p class="mb-3 text-sm text-blue-100 sm:mb-4 sm:text-base">
You have successfully installed and configured Claude Code. Now you can start enjoying the convenience brought by AI programming assistant.
You have successfully installed and configured Claude Code. Now you can start enjoying the
convenience brought by AI programming assistant.
</p>
<p class="text-xs text-blue-200 sm:text-sm">
If you encounter any issues during use, you can check the official documentation or community discussions for help.
If you encounter any issues during use, you can check the official documentation or
community discussions for help.
</p>
</div>
</div>