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" /> <i class="fas fa-layer-group text-sm text-white sm:text-base" />
</div> </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> </div>
<button <button
class="p-1 text-gray-400 transition-colors hover:text-gray-600" 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"> <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 class="space-y-4">
<div> <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 <input
v-model="createForm.name" v-model="createForm.name"
class="form-input w-full" class="form-input w-full"
@@ -44,7 +50,9 @@
</div> </div>
<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"> <div class="flex gap-4">
<label class="flex cursor-pointer items-center"> <label class="flex cursor-pointer items-center">
<input v-model="createForm.platform" class="mr-2" type="radio" value="claude" /> <input v-model="createForm.platform" class="mr-2" type="radio" value="claude" />
@@ -62,7 +70,9 @@
</div> </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 <textarea
v-model="createForm.description" v-model="createForm.description"
class="form-input w-full resize-none" class="form-input w-full resize-none"
@@ -80,7 +90,9 @@
<div v-if="creating" class="loading-spinner mr-2" /> <div v-if="creating" class="loading-spinner mr-2" />
{{ creating ? t('groupManagement.creating') : t('groupManagement.create') }} {{ creating ? t('groupManagement.creating') : t('groupManagement.create') }}
</button> </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> </div>
</div> </div>
@@ -184,7 +196,9 @@
<div class="space-y-4"> <div class="space-y-4">
<div> <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 <input
v-model="editForm.name" v-model="editForm.name"
class="form-input w-full" class="form-input w-full"
@@ -194,7 +208,9 @@
</div> </div>
<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"> <div class="rounded-lg bg-gray-100 px-3 py-2 text-sm text-gray-600">
{{ {{
editForm.platform === 'claude' editForm.platform === 'claude'
@@ -203,12 +219,16 @@
? 'Gemini' ? 'Gemini'
: 'OpenAI' : '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> </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 <textarea
v-model="editForm.description" v-model="editForm.description"
class="form-input w-full resize-none" class="form-input w-full resize-none"
@@ -226,7 +246,9 @@
<div v-if="updating" class="loading-spinner mr-2" /> <div v-if="updating" class="loading-spinner mr-2" />
{{ updating ? t('groupManagement.updating') : t('groupManagement.update') }} {{ updating ? t('groupManagement.updating') : t('groupManagement.update') }}
</button> </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> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,9 +18,11 @@
<i class="fas fa-clock text-white" /> <i class="fas fa-clock text-white" />
</div> </div>
<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"> <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> </p>
</div> </div>
</div> </div>
@@ -39,14 +41,20 @@
> >
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <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"> <p class="text-sm font-semibold text-gray-800 dark:text-gray-200">
<!-- 未激活状态 --> <!-- 未激活状态 -->
<template v-if="apiKey.expirationMode === 'activation' && !apiKey.isActivated"> <template v-if="apiKey.expirationMode === 'activation' && !apiKey.isActivated">
<i class="fas fa-pause-circle mr-1 text-blue-500" /> <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"> <span class="ml-2 text-xs font-normal text-gray-600">
(激活后 {{ apiKey.activationDays || 30 }} 天过期) {{
$t('apiKeys.expiryEditModal.activationDaysHint', {
days: apiKey.activationDays || 30
})
}}
</span> </span>
</template> </template>
<!-- 已设置过期时间 --> <!-- 已设置过期时间 -->
@@ -63,7 +71,7 @@
<!-- 永不过期 --> <!-- 永不过期 -->
<template v-else> <template v-else>
<i class="fas fa-infinity mr-1 text-gray-500" /> <i class="fas fa-infinity mr-1 text-gray-500" />
永不过期 {{ $t('apiKeys.expiryEditModal.neverExpire') }}
</template> </template>
</p> </p>
</div> </div>
@@ -89,19 +97,23 @@
@click="handleActivateNow" @click="handleActivateNow"
> >
<i class="fas fa-rocket mr-2" /> <i class="fas fa-rocket mr-2" />
立即激活 (激活后 {{ apiKey.activationDays || 30 }} 天过期) {{
$t('apiKeys.expiryEditModal.activateButton', { days: apiKey.activationDays || 30 })
}}
</button> </button>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400"> <p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
<i class="fas fa-info-circle mr-1" /> <i class="fas fa-info-circle mr-1" />
点击立即激活此 API Key激活后将在 {{ apiKey.activationDays || 30 }} 天后过期 {{
$t('apiKeys.expiryEditModal.activationInfo', { days: apiKey.activationDays || 30 })
}}
</p> </p>
</div> </div>
<!-- 快捷选项 --> <!-- 快捷选项 -->
<div> <div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300" <label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
>选择新的期限</label {{ $t('apiKeys.expiryEditModal.selectNewDuration') }}
> </label>
<div class="mb-3 grid grid-cols-3 gap-2"> <div class="mb-3 grid grid-cols-3 gap-2">
<button <button
v-for="option in quickOptions" v-for="option in quickOptions"
@@ -126,16 +138,16 @@
@click="selectQuickOption('custom')" @click="selectQuickOption('custom')"
> >
<i class="fas fa-calendar-alt mr-1" /> <i class="fas fa-calendar-alt mr-1" />
自定义 {{ $t('apiKeys.expiryEditModal.custom') }}
</button> </button>
</div> </div>
</div> </div>
<!-- 自定义日期选择 --> <!-- 自定义日期选择 -->
<div v-if="localForm.expireDuration === 'custom'" class="animate-fadeIn"> <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 class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300">
>选择日期和时间</label {{ $t('apiKeys.expiryEditModal.selectDateAndTime') }}
> </label>
<input <input
v-model="localForm.customExpireDate" v-model="localForm.customExpireDate"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200" 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" @change="updateCustomExpiryPreview"
/> />
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400"> <p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
选择一个未来的日期和时间作为过期时间 {{ $t('apiKeys.expiryEditModal.selectFutureDateTime') }}
</p> </p>
</div> </div>
@@ -157,7 +169,7 @@
<div> <div>
<p class="mb-1 text-xs font-medium text-blue-700 dark:text-blue-400"> <p class="mb-1 text-xs font-medium text-blue-700 dark:text-blue-400">
<i class="fas fa-arrow-right mr-1" /> <i class="fas fa-arrow-right mr-1" />
新的过期时间 {{ $t('apiKeys.expiryEditModal.newExpiryTime') }}
</p> </p>
<p class="text-sm font-semibold text-blue-900 dark:text-blue-200"> <p class="text-sm font-semibold text-blue-900 dark:text-blue-200">
<template v-if="localForm.expiresAt"> <template v-if="localForm.expiresAt">
@@ -172,7 +184,7 @@
</template> </template>
<template v-else> <template v-else>
<i class="fas fa-infinity mr-1" /> <i class="fas fa-infinity mr-1" />
永不过期 {{ $t('apiKeys.expiryEditModal.neverExpire') }}
</template> </template>
</p> </p>
</div> </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" 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')" @click="$emit('close')"
> >
取消 {{ $t('apiKeys.expiryEditModal.cancel') }}
</button> </button>
<button <button
class="btn btn-primary flex-1 px-4 py-2.5 font-semibold" 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" /> <div v-if="saving" class="loading-spinner mr-2" />
<i v-else class="fas fa-save mr-2" /> <i v-else class="fas fa-save mr-2" />
{{ saving ? '保存中...' : '保存更改' }} {{
saving
? $t('apiKeys.expiryEditModal.saving')
: $t('apiKeys.expiryEditModal.saveChanges')
}}
</button> </button>
</div> </div>
</div> </div>
@@ -210,6 +226,9 @@
<script setup> <script setup>
import { ref, reactive, computed, watch } from 'vue' import { ref, reactive, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const props = defineProps({ const props = defineProps({
show: { show: {
@@ -235,14 +254,14 @@ const localForm = reactive({
// 快捷选项 // 快捷选项
const quickOptions = [ const quickOptions = [
{ value: '', label: '永不过期' }, { value: '', label: t('apiKeys.expiryEditModal.neverExpireOption') },
{ value: '1d', label: '1 天' }, { value: '1d', label: t('apiKeys.expiryEditModal.oneDay') },
{ value: '7d', label: '7 天' }, { value: '7d', label: t('apiKeys.expiryEditModal.sevenDays') },
{ value: '30d', label: '30 天' }, { value: '30d', label: t('apiKeys.expiryEditModal.thirtyDays') },
{ value: '90d', label: '90 天' }, { value: '90d', label: t('apiKeys.expiryEditModal.ninetyDays') },
{ value: '180d', label: '180 天' }, { value: '180d', label: t('apiKeys.expiryEditModal.oneHundredEightyDays') },
{ value: '365d', label: '1 年' }, { value: '365d', label: t('apiKeys.expiryEditModal.threeSixtyFiveDays') },
{ value: '730d', label: '2 年' } { value: '730d', label: t('apiKeys.expiryEditModal.twoYears') }
] ]
// 计算最小日期时间 // 计算最小日期时间
@@ -337,13 +356,17 @@ const updateCustomExpiryPreview = () => {
const formatExpireDate = (dateString) => { const formatExpireDate = (dateString) => {
if (!dateString) return '' if (!dateString) return ''
const date = new Date(dateString) const date = new Date(dateString)
return date.toLocaleString('zh-CN', { const { locale } = useI18n()
year: 'numeric', return date.toLocaleString(
month: '2-digit', locale.value === 'zh-cn' ? 'zh-CN' : locale.value === 'zh-tw' ? 'zh-TW' : 'en-US',
day: '2-digit', {
hour: '2-digit', year: 'numeric',
minute: '2-digit' month: '2-digit',
}) day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}
)
} }
// 检查是否已过期 // 检查是否已过期
@@ -363,22 +386,22 @@ const getExpiryStatus = (expiresAt) => {
if (diffMs < 0) { if (diffMs < 0) {
return { return {
text: '已过期', text: t('apiKeys.expiryEditModal.expired'),
class: 'text-red-600' class: 'text-red-600'
} }
} else if (diffDays <= 7) { } else if (diffDays <= 7) {
return { return {
text: `${diffDays} 天后过期`, text: t('apiKeys.expiryEditModal.daysToExpire', { days: diffDays }),
class: 'text-orange-600' class: 'text-orange-600'
} }
} else if (diffDays <= 30) { } else if (diffDays <= 30) {
return { return {
text: `${diffDays} 天后过期`, text: t('apiKeys.expiryEditModal.daysToExpire', { days: diffDays }),
class: 'text-yellow-600' class: 'text-yellow-600'
} }
} else { } else {
return { return {
text: `${Math.ceil(diffDays / 30)} 个月后过期`, text: t('apiKeys.expiryEditModal.monthsToExpire', { months: Math.ceil(diffDays / 30) }),
class: 'text-green-600' class: 'text-green-600'
} }
} }
@@ -399,15 +422,19 @@ const handleActivateNow = async () => {
let confirmed = true let confirmed = true
if (window.showConfirm) { if (window.showConfirm) {
confirmed = await window.showConfirm( confirmed = await window.showConfirm(
'激活 API Key', t('apiKeys.expiryEditModal.activateConfirmTitle'),
`确定要立即激活此 API Key 吗?激活后将在 ${props.apiKey.activationDays || 30} 天后自动过期。`, t('apiKeys.expiryEditModal.activateConfirmMessage', {
'确定激活', days: props.apiKey.activationDays || 30
'取消' }),
t('apiKeys.expiryEditModal.confirmActivate'),
t('apiKeys.expiryEditModal.confirmCancel')
) )
} else { } else {
// 降级方案 // 降级方案
confirmed = confirm( 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" /> <i class="fas fa-check text-lg text-white" />
</div> </div>
<div> <div>
<h3 class="text-xl font-bold text-gray-900 dark:text-gray-100">API Key 创建成功</h3> <h3 class="text-xl font-bold text-gray-900 dark:text-gray-100">
<p class="text-sm text-gray-600 dark:text-gray-400">请妥善保存您的 API Key</p> {{ t('apiKeys.newApiKeyModal.title') }}
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ t('apiKeys.newApiKeyModal.subtitle') }}
</p>
</div> </div>
</div> </div>
<button <button
class="text-gray-400 transition-colors hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300" 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" @click="handleDirectClose"
> >
<i class="fas fa-times text-xl" /> <i class="fas fa-times text-xl" />
@@ -36,10 +40,11 @@
<i class="fas fa-exclamation-triangle text-sm text-white" /> <i class="fas fa-exclamation-triangle text-sm text-white" />
</div> </div>
<div class="ml-3"> <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"> <p class="text-sm text-amber-800 dark:text-amber-300">
这是您唯一能看到完整 API Key 的机会关闭此窗口后系统将不再显示完整的 API {{ t('apiKeys.newApiKeyModal.warningMessage') }}
Key请立即复制并妥善保存
</p> </p>
</div> </div>
</div> </div>
@@ -48,9 +53,9 @@
<!-- API Key 信息 --> <!-- API Key 信息 -->
<div class="mb-6 space-y-4"> <div class="mb-6 space-y-4">
<div> <div>
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300" <label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
>API Key 名称</label t('apiKeys.newApiKeyModal.apiKeyName')
> }}</label>
<div <div
class="rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-600 dark:bg-gray-800" 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>
<div v-if="apiKey.description"> <div v-if="apiKey.description">
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300" <label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
>备注</label t('apiKeys.newApiKeyModal.remarks')
> }}</label>
<div <div
class="rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-600 dark:bg-gray-800" 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">{{ <span class="text-gray-700 dark:text-gray-300">{{
apiKey.description || '无描述' apiKey.description || t('apiKeys.newApiKeyModal.noDescription')
}}</span> }}</span>
</div> </div>
</div> </div>
<div> <div>
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300" <label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
>API Key</label t('apiKeys.newApiKeyModal.apiKey')
> }}</label>
<div class="relative"> <div class="relative">
<div <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" 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"> <div class="absolute right-3 top-3">
<button <button
class="btn-icon-sm bg-gray-700 hover:bg-gray-800 dark:bg-gray-700 dark:hover:bg-gray-600" 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" type="button"
@click="toggleKeyVisibility" @click="toggleKeyVisibility"
> >
@@ -93,7 +102,7 @@
</div> </div>
</div> </div>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400"> <p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
点击眼睛图标切换显示模式使用下方按钮复制完整 API Key {{ t('apiKeys.newApiKeyModal.visibilityHint') }}
</p> </p>
</div> </div>
</div> </div>
@@ -105,13 +114,13 @@
@click="copyApiKey" @click="copyApiKey"
> >
<i class="fas fa-copy" /> <i class="fas fa-copy" />
复制 API Key {{ t('apiKeys.newApiKeyModal.copyApiKey') }}
</button> </button>
<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" 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" @click="handleClose"
> >
我已保存 {{ t('apiKeys.newApiKeyModal.alreadySaved') }}
</button> </button>
</div> </div>
</div> </div>
@@ -121,8 +130,11 @@
<script setup> <script setup>
import { ref } from 'vue' import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { showToast } from '@/utils/toast' import { showToast } from '@/utils/toast'
const { t } = useI18n()
const props = defineProps({ const props = defineProps({
apiKey: { apiKey: {
type: Object, type: Object,
@@ -159,13 +171,13 @@ const getDisplayedApiKey = () => {
const copyApiKey = async () => { const copyApiKey = async () => {
const key = props.apiKey.apiKey || props.apiKey.key || '' const key = props.apiKey.apiKey || props.apiKey.key || ''
if (!key) { if (!key) {
showToast('API Key 不存在', 'error') showToast(t('apiKeys.newApiKeyModal.apiKeyNotFound'), 'error')
return return
} }
try { try {
await navigator.clipboard.writeText(key) await navigator.clipboard.writeText(key)
showToast('API Key 已复制到剪贴板', 'success') showToast(t('apiKeys.newApiKeyModal.copySuccess'), 'success')
} catch (error) { } catch (error) {
// console.error('Failed to copy:', error) // console.error('Failed to copy:', error)
// 降级方案:创建一个临时文本区域 // 降级方案:创建一个临时文本区域
@@ -175,9 +187,9 @@ const copyApiKey = async () => {
textArea.select() textArea.select()
try { try {
document.execCommand('copy') document.execCommand('copy')
showToast('API Key 已复制到剪贴板', 'success') showToast(t('apiKeys.newApiKeyModal.copySuccess'), 'success')
} catch (fallbackError) { } catch (fallbackError) {
showToast('复制失败,请手动复制', 'error') showToast(t('apiKeys.newApiKeyModal.copyFailed'), 'error')
} finally { } finally {
document.body.removeChild(textArea) document.body.removeChild(textArea)
} }
@@ -188,19 +200,17 @@ const copyApiKey = async () => {
const handleClose = async () => { const handleClose = async () => {
if (window.showConfirm) { if (window.showConfirm) {
const confirmed = await window.showConfirm( const confirmed = await window.showConfirm(
'关闭提醒', t('apiKeys.newApiKeyModal.closeReminderTitle'),
'关闭后将无法再次查看完整的API Key请确保已经妥善保存。\n\n确定要关闭吗', t('apiKeys.newApiKeyModal.closeReminderMessage'),
'确定关闭', t('apiKeys.newApiKeyModal.confirmClose'),
'取消' t('apiKeys.newApiKeyModal.cancel')
) )
if (confirmed) { if (confirmed) {
emit('close') emit('close')
} }
} else { } else {
// 降级方案 // 降级方案
const confirmed = confirm( const confirmed = confirm(t('apiKeys.newApiKeyModal.closeReminderMessage'))
'关闭后将无法再次查看完整的API Key请确保已经妥善保存。\n\n确定要关闭吗'
)
if (confirmed) { if (confirmed) {
emit('close') emit('close')
} }
@@ -211,17 +221,17 @@ const handleClose = async () => {
const handleDirectClose = async () => { const handleDirectClose = async () => {
if (window.showConfirm) { if (window.showConfirm) {
const confirmed = await window.showConfirm( const confirmed = await window.showConfirm(
'确定要关闭吗?', t('apiKeys.newApiKeyModal.directCloseTitle'),
'您还没有保存API Key关闭后将无法再次查看。\n\n建议您先复制API Key再关闭。', t('apiKeys.newApiKeyModal.directCloseMessage'),
'仍然关闭', t('apiKeys.newApiKeyModal.stillClose'),
'返回复制' t('apiKeys.newApiKeyModal.goBack')
) )
if (confirmed) { if (confirmed) {
emit('close') emit('close')
} }
} else { } else {
// 降级方案 // 降级方案
const confirmed = confirm('您还没有保存API Key关闭后将无法再次查看。\n\n确定要关闭吗') const confirmed = confirm(t('apiKeys.newApiKeyModal.directCloseFallback'))
if (confirmed) { if (confirmed) {
emit('close') emit('close')
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,9 @@
<i class="fas fa-chart-line mr-3" /> <i class="fas fa-chart-line mr-3" />
{{ t('apiStats.usageStatsQuery') }} {{ t('apiStats.usageStatsQuery') }}
</h2> </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> </div>
<!-- 输入区域 --> <!-- 输入区域 -->

View File

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

View File

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

View File

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

View File

@@ -51,9 +51,9 @@
</div> </div>
<div class="mt-3 border-t border-gray-200 pt-3 dark:border-gray-700 md:mt-4 md:pt-4"> <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"> <div class="flex items-center justify-between font-bold text-gray-900 dark:text-gray-100">
<span class="text-sm md:text-base" <span class="text-sm md:text-base">{{
>{{ statsPeriod === 'daily' ? t('apiStats.todayTotal') : t('apiStats.monthlyTotal') }}</span statsPeriod === 'daily' ? t('apiStats.todayTotal') : t('apiStats.monthlyTotal')
> }}</span>
<span class="text-lg md:text-xl">{{ formatNumber(currentPeriodData.allTokens) }}</span> <span class="text-lg md:text-xl">{{ formatNumber(currentPeriodData.allTokens) }}</span>
</div> </div>
</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="border-b border-gray-100 px-4 py-3 dark:border-gray-700">
<div class="flex items-center justify-between text-sm"> <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" <span class="font-mono text-gray-700 dark:text-gray-300"
>v{{ versionInfo.current || '...' }}</span >v{{ versionInfo.current || '...' }}</span
> >
@@ -165,7 +167,9 @@
> >
<i class="fas fa-key text-white" /> <i class="fas fa-key text-white" />
</div> </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> </div>
<button <button
class="text-gray-400 transition-colors hover:text-gray-600 dark:hover:text-gray-300" class="text-gray-400 transition-colors hover:text-gray-600 dark:hover:text-gray-300"
@@ -180,9 +184,9 @@
@submit.prevent="changePassword" @submit.prevent="changePassword"
> >
<div> <div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300" <label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
>{{ t('header.changePasswordModal.currentUsername') }}</label t('header.changePasswordModal.currentUsername')
> }}</label>
<input <input
class="form-input w-full cursor-not-allowed bg-gray-100 dark:bg-gray-700 dark:text-gray-300" class="form-input w-full cursor-not-allowed bg-gray-100 dark:bg-gray-700 dark:text-gray-300"
disabled disabled
@@ -195,22 +199,24 @@
</div> </div>
<div> <div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300" <label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
>{{ t('header.changePasswordModal.newUsername') }}</label t('header.changePasswordModal.newUsername')
> }}</label>
<input <input
v-model="changePasswordForm.newUsername" v-model="changePasswordForm.newUsername"
class="form-input w-full" class="form-input w-full"
:placeholder="t('header.changePasswordModal.newUsernamePlaceholder')" :placeholder="t('header.changePasswordModal.newUsernamePlaceholder')"
type="text" 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>
<div> <div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300" <label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
>{{ t('header.changePasswordModal.currentPassword') }}</label t('header.changePasswordModal.currentPassword')
> }}</label>
<input <input
v-model="changePasswordForm.currentPassword" v-model="changePasswordForm.currentPassword"
class="form-input w-full" class="form-input w-full"
@@ -221,9 +227,9 @@
</div> </div>
<div> <div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300" <label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
>{{ t('header.changePasswordModal.newPassword') }}</label t('header.changePasswordModal.newPassword')
> }}</label>
<input <input
v-model="changePasswordForm.newPassword" v-model="changePasswordForm.newPassword"
class="form-input w-full" class="form-input w-full"
@@ -231,13 +237,15 @@
required required
type="password" 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>
<div> <div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300" <label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
>{{ t('header.changePasswordModal.confirmPassword') }}</label t('header.changePasswordModal.confirmPassword')
> }}</label>
<input <input
v-model="changePasswordForm.confirmPassword" v-model="changePasswordForm.confirmPassword"
class="form-input w-full" class="form-input w-full"
@@ -262,7 +270,11 @@
> >
<div v-if="changePasswordLoading" class="loading-spinner mr-2" /> <div v-if="changePasswordLoading" class="loading-spinner mr-2" />
<i v-else class="fas fa-save 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> </button>
</div> </div>
</form> </form>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -60,11 +60,7 @@
<!-- 刷新按钮 --> <!-- 刷新按钮 -->
<div class="relative"> <div class="relative">
<el-tooltip <el-tooltip :content="t('accounts.refreshTooltip')" effect="dark" placement="bottom">
:content="t('accounts.refreshTooltip')"
effect="dark"
placement="bottom"
>
<button <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" 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" :disabled="accountsLoading"
@@ -110,7 +106,9 @@
<i class="fas fa-user-circle text-xl text-gray-400" /> <i class="fas fa-user-circle text-xl text-gray-400" />
</div> </div>
<p class="text-lg text-gray-500 dark:text-gray-400">{{ t('accounts.noAccounts') }}</p> <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> </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" 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" /> <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>
</div> </div>
</td> </td>
@@ -451,7 +451,11 @@
typeof account.rateLimitStatus === 'object' && typeof account.rateLimitStatus === 'object' &&
account.rateLimitStatus.minutesRemaining > 0 account.rateLimitStatus.minutesRemaining > 0
" "
>({{ t('accounts.rateLimitTime', { time: formatRateLimitTime(account.rateLimitStatus.minutesRemaining) }) }})</span >({{
t('accounts.rateLimitTime', {
time: formatRateLimitTime(account.rateLimitStatus.minutesRemaining)
})
}})</span
> >
</span> </span>
<span <span
@@ -609,7 +613,11 @@
v-if="account.sessionWindow.remainingTime > 0" v-if="account.sessionWindow.remainingTime > 0"
class="font-medium text-indigo-600 dark:text-indigo-400" 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> </div>
</div> </div>
@@ -617,7 +625,9 @@
<div v-else-if="account.platform === 'claude-console'" class="space-y-2"> <div v-else-if="account.platform === 'claude-console'" class="space-y-2">
<div v-if="Number(account.dailyQuota) > 0"> <div v-if="Number(account.dailyQuota) > 0">
<div class="flex items-center justify-between text-xs"> <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"> <span class="font-medium text-gray-700 dark:text-gray-200">
{{ getQuotaUsagePercent(account).toFixed(1) }}% {{ getQuotaUsagePercent(account).toFixed(1) }}%
</span> </span>
@@ -642,9 +652,9 @@
</div> </div>
<div class="text-xs text-gray-600 dark:text-gray-400"> <div class="text-xs text-gray-600 dark:text-gray-400">
{{ t('accounts.remainingQuota', { amount: formatRemainingQuota(account) }) }} {{ t('accounts.remainingQuota', { amount: formatRemainingQuota(account) }) }}
<span class="ml-2 text-gray-400" <span class="ml-2 text-gray-400">{{
>{{ t('accounts.reset', { time: account.quotaResetTime || '00:00' }) }}</span t('accounts.reset', { time: account.quotaResetTime || '00:00' })
> }}</span>
</div> </div>
</div> </div>
<div v-else class="text-sm text-gray-400"> <div v-else class="text-sm text-gray-400">
@@ -682,7 +692,11 @@
: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200' : 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200'
]" ]"
:disabled="account.isResetting" :disabled="account.isResetting"
:title="account.isResetting ? t('accounts.resetting') : t('accounts.resetStatusTooltip')" :title="
account.isResetting
? t('accounts.resetting')
: t('accounts.resetStatusTooltip')
"
@click="resetAccountStatus(account)" @click="resetAccountStatus(account)"
> >
<i :class="['fas fa-redo', account.isResetting ? 'animate-spin' : '']" /> <i :class="['fas fa-redo', account.isResetting ? 'animate-spin' : '']" />
@@ -698,11 +712,17 @@
: 'bg-gray-100 text-gray-700 hover:bg-gray-200' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
]" ]"
:disabled="account.isTogglingSchedulable" :disabled="account.isTogglingSchedulable"
:title="account.schedulable ? t('accounts.disableTooltip') : t('accounts.enableTooltip')" :title="
account.schedulable
? t('accounts.disableTooltip')
: t('accounts.enableTooltip')
"
@click="toggleSchedulable(account)" @click="toggleSchedulable(account)"
> >
<i :class="['fas', account.schedulable ? 'fa-toggle-on' : 'fa-toggle-off']" /> <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>
<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" 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 class="mb-3 grid grid-cols-2 gap-3">
<div> <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="space-y-1">
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<div class="h-1.5 w-1.5 rounded-full bg-blue-500" /> <div class="h-1.5 w-1.5 rounded-full bg-blue-500" />
@@ -822,7 +844,9 @@
</div> </div>
</div> </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 v-if="account.usage && account.usage.sessionWindow" class="space-y-1">
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<div class="h-1.5 w-1.5 rounded-full bg-purple-500" /> <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 justify-between text-xs">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<span class="font-medium text-gray-600 dark:text-gray-300">{{ t('accounts.sessionWindowLabel') }}</span> <span class="font-medium text-gray-600 dark:text-gray-300">{{
<el-tooltip t('accounts.sessionWindowLabel')
:content="t('accounts.sessionWindowTooltipMobile')" }}</span>
placement="top" <el-tooltip :content="t('accounts.sessionWindowTooltipMobile')" placement="top">
>
<i <i
class="fas fa-question-circle cursor-help text-xs text-gray-400 hover:text-gray-600" 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" v-if="account.sessionWindow.remainingTime > 0"
class="font-medium text-indigo-600" class="font-medium text-indigo-600"
> >
{{ t('accounts.remaining', { time: formatRemainingTime(account.sessionWindow.remainingTime) }) }} {{
t('accounts.remaining', {
time: formatRemainingTime(account.sessionWindow.remainingTime)
})
}}
</span> </span>
<span v-else class="text-gray-500"> {{ t('accounts.ended') }} </span> <span v-else class="text-gray-500"> {{ t('accounts.ended') }} </span>
</div> </div>
@@ -898,9 +925,15 @@
<!-- 最后使用时间 --> <!-- 最后使用时间 -->
<div class="flex items-center justify-between text-xs"> <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"> <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> </span>
</div> </div>
@@ -917,7 +950,9 @@
<!-- 调度优先级 --> <!-- 调度优先级 -->
<div class="flex items-center justify-between text-xs"> <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"> <span class="font-medium text-gray-700 dark:text-gray-200">
{{ account.priority || 50 }} {{ account.priority || 50 }}
</span> </span>
@@ -1610,10 +1645,7 @@ const deleteAccount = async (account) => {
).length ).length
if (boundKeysCount > 0) { if (boundKeysCount > 0) {
showToast( showToast(t('accounts.cannotDeleteBoundAccount', { count: boundKeysCount }), 'error')
t('accounts.cannotDeleteBoundAccount', { count: boundKeysCount }),
'error'
)
return return
} }
@@ -1749,7 +1781,10 @@ const toggleSchedulable = async (account) => {
if (data.success) { if (data.success) {
account.schedulable = data.schedulable 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 { } else {
showToast(data.message || t('accounts.operationFailed'), 'error') showToast(data.message || t('accounts.operationFailed'), 'error')
} }

View File

@@ -68,7 +68,10 @@
icon="fa-calendar-alt" icon="fa-calendar-alt"
icon-color="text-blue-500" icon-color="text-blue-500"
:options="timeRangeOptions" :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()" @change="loadApiKeys()"
/> />
</div> </div>
@@ -84,7 +87,9 @@
icon="fa-tags" icon="fa-tags"
icon-color="text-purple-500" icon-color="text-purple-500"
:options="tagOptions" :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" @change="currentPage = 1"
/> />
<span <span
@@ -105,7 +110,11 @@
<input <input
v-model="searchKeyword" 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" 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" type="text"
@input="currentPage = 1" @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" 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> ></div>
<i class="fas fa-edit relative text-blue-600 dark:text-blue-400" /> <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> </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" 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> ></div>
<i class="fas fa-trash relative text-red-600 dark:text-red-400" /> <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> </button>
</div> </div>
@@ -471,25 +484,34 @@
<!-- 今日使用统计 --> <!-- 今日使用统计 -->
<div class="mb-2"> <div class="mb-2">
<div class="mb-1 flex items-center justify-between text-sm"> <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" <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>
<div class="flex items-center justify-between text-sm"> <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" <span class="font-semibold text-green-600"
>${{ (key.dailyCost || 0).toFixed(4) }}</span >${{ (key.dailyCost || 0).toFixed(4) }}</span
> >
</div> </div>
<div class="flex items-center justify-between text-sm"> <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" <span class="font-semibold text-blue-600"
>${{ (key.totalCost || 0).toFixed(4) }}</span >${{ (key.totalCost || 0).toFixed(4) }}</span
> >
</div> </div>
<div class="flex items-center justify-between text-sm"> <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">{{ <span class="font-medium text-gray-700 dark:text-gray-300">{{
formatLastUsed(key.lastUsedAt) formatLastUsed(key.lastUsedAt)
}}</span> }}</span>
@@ -499,7 +521,9 @@
<!-- 每日费用限制进度条 --> <!-- 每日费用限制进度条 -->
<div v-if="key.dailyCostLimit > 0" class="space-y-1"> <div v-if="key.dailyCostLimit > 0" class="space-y-1">
<div class="flex items-center justify-between text-xs"> <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"> <span class="text-gray-700 dark:text-gray-300">
${{ (key.dailyCost || 0).toFixed(2) }} / ${{ ${{ (key.dailyCost || 0).toFixed(2) }} / ${{
key.dailyCostLimit.toFixed(2) key.dailyCostLimit.toFixed(2)
@@ -518,7 +542,9 @@
<!-- Opus 周费用限制进度条 --> <!-- Opus 周费用限制进度条 -->
<div v-if="key.weeklyOpusCostLimit > 0" class="space-y-1"> <div v-if="key.weeklyOpusCostLimit > 0" class="space-y-1">
<div class="flex items-center justify-between text-xs"> <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"> <span class="text-gray-700 dark:text-gray-300">
${{ (key.weeklyOpusCost || 0).toFixed(2) }} / ${{ ${{ (key.weeklyOpusCost || 0).toFixed(2) }} / ${{
key.weeklyOpusCostLimit.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"> <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 v-if="!apiKeyModelStats[key.id]" class="py-4 text-center">
<div class="loading-spinner mx-auto" /> <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>
<div class="space-y-4"> <div class="space-y-4">
<!-- 通用的标题和时间筛选器,无论是否有数据都显示 --> <!-- 通用的标题和时间筛选器,无论是否有数据都显示 -->
@@ -719,7 +747,11 @@
v-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length > 0" 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" 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> </span>
<!-- API Keys日期筛选器 --> <!-- API Keys日期筛选器 -->
@@ -773,7 +805,9 @@
> >
<div class="mb-3 flex items-center justify-center gap-2"> <div class="mb-3 flex items-center justify-center gap-2">
<i class="fas fa-chart-line text-lg text-gray-400" /> <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 <button
class="ml-2 flex items-center gap-1 text-sm text-blue-500 transition-colors hover:text-blue-700" class="ml-2 flex items-center gap-1 text-sm text-blue-500 transition-colors hover:text-blue-700"
:title="t('apiKeys.resetFilter')" :title="t('apiKeys.resetFilter')"
@@ -1078,7 +1112,9 @@
<!-- 今日使用 --> <!-- 今日使用 -->
<div class="rounded-lg bg-gray-50 p-3 dark:bg-gray-700"> <div class="rounded-lg bg-gray-50 p-3 dark:bg-gray-700">
<div class="mb-2 flex items-center justify-between"> <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 <button
class="text-xs text-blue-600 hover:text-blue-800" class="text-xs text-blue-600 hover:text-blue-800"
@click="showUsageDetails(key)" @click="showUsageDetails(key)"
@@ -1089,19 +1125,26 @@
<div class="grid grid-cols-2 gap-3"> <div class="grid grid-cols-2 gap-3">
<div> <div>
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100"> <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>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('apiKeys.requests') }}</p>
</div> </div>
<div> <div>
<p class="text-sm font-semibold text-green-600"> <p class="text-sm font-semibold text-green-600">
${{ (key.dailyCost || 0).toFixed(4) }} ${{ (key.dailyCost || 0).toFixed(4) }}
</p> </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> </div>
<div class="mt-2 flex items-center justify-between"> <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">{{ <span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
formatLastUsed(key.lastUsedAt) formatLastUsed(key.lastUsedAt)
}}</span> }}</span>
@@ -1111,7 +1154,9 @@
<!-- 限制进度 --> <!-- 限制进度 -->
<div v-if="key.dailyCostLimit > 0" class="space-y-1"> <div v-if="key.dailyCostLimit > 0" class="space-y-1">
<div class="flex items-center justify-between text-xs"> <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"> <span class="text-gray-700 dark:text-gray-300">
${{ (key.dailyCost || 0).toFixed(2) }} / ${{ key.dailyCostLimit.toFixed(2) }} ${{ (key.dailyCost || 0).toFixed(2) }} / ${{ key.dailyCostLimit.toFixed(2) }}
</span> </span>
@@ -1250,7 +1295,9 @@
{{ t('apiKeys.totalRecords', { count: sortedApiKeys.length }) }} {{ t('apiKeys.totalRecords', { count: sortedApiKeys.length }) }}
</span> </span>
<div class="flex items-center gap-2"> <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 <select
v-model="pageSize" 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" 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 }} {{ size }}
</option> </option>
</select> </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>
</div> </div>
@@ -1475,19 +1524,26 @@
<td class="px-3 py-4"> <td class="px-3 py-4">
<div class="text-sm"> <div class="text-sm">
<div class="flex items-center justify-between"> <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"> <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> </span>
</div> </div>
<div class="flex items-center justify-between"> <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"> <span class="font-semibold text-green-600">
${{ (key.usage?.total?.cost || 0).toFixed(4) }} ${{ (key.usage?.total?.cost || 0).toFixed(4) }}
</span> </span>
</div> </div>
<div v-if="key.lastUsedAt" class="flex items-center justify-between"> <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"> <span class="font-medium text-gray-700 dark:text-gray-300">
{{ formatLastUsed(key.lastUsedAt) }} {{ formatLastUsed(key.lastUsedAt) }}
</span> </span>
@@ -2350,9 +2406,7 @@ const toggleApiKeyStatus = async (key) => {
) )
} else { } else {
// 降级方案 // 降级方案
confirmed = confirm( confirmed = confirm(t('apiKeys.confirmDisable', { name: key.name }))
t('apiKeys.confirmDisable', { name: key.name })
)
} }
} }
@@ -2539,7 +2593,12 @@ const batchDeleteApiKeys = async () => {
const message = t('apiKeys.confirmBatchDelete', { count: selectedCount }) const message = t('apiKeys.confirmBatchDelete', { count: selectedCount })
if (window.showConfirm) { 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 { } else {
confirmed = confirm(message) confirmed = confirm(message)
} }
@@ -2562,7 +2621,10 @@ const batchDeleteApiKeys = async () => {
// 如果有失败的,显示详细信息 // 如果有失败的,显示详细信息
if (failedCount > 0) { if (failedCount > 0) {
const errorMessages = errors.map((e) => `${e.keyId}: ${e.error}`).join('\n') 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 { } else {
showToast(t('apiKeys.batchAllFailed'), 'error') showToast(t('apiKeys.batchAllFailed'), 'error')

View File

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

View File

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

View File

@@ -42,7 +42,8 @@
<form class="space-y-4 sm:space-y-6" @submit.prevent="handleLogin"> <form class="space-y-4 sm:space-y-6" @submit.prevent="handleLogin">
<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.username') }}</label >{{ t('login.username') }}</label
> >
<input <input
@@ -55,7 +56,8 @@
</div> </div>
<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 >{{ t('login.password') }}</label
> >
<input <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"> <h3 class="mb-1 text-lg font-bold text-gray-900 dark:text-gray-100 sm:mb-2 sm:text-xl">
{{ t('settings.title') }} {{ t('settings.title') }}
</h3> </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> </div>
<!-- 设置分类导航 --> <!-- 设置分类导航 -->
@@ -66,7 +68,9 @@
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100"> <div class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ t('settings.siteName') }} {{ t('settings.siteName') }}
</div> </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>
</div> </div>
</td> </td>
@@ -75,7 +79,7 @@
v-model="oemSettings.siteName" v-model="oemSettings.siteName"
class="form-input w-full max-w-md dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200" class="form-input w-full max-w-md dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
maxlength="100" maxlength="100"
:placeholder="t('settings.siteNamePlaceholder')" :placeholder="t('settings.siteNamePlaceholder')"
type="text" type="text"
/> />
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400"> <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
@@ -97,7 +101,9 @@
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100"> <div class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ t('settings.siteIcon') }} {{ t('settings.siteIcon') }}
</div> </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>
</div> </div>
</td> </td>
@@ -109,12 +115,14 @@
class="inline-flex items-center gap-3 rounded-lg bg-gray-50 p-3 dark:bg-gray-700" class="inline-flex items-center gap-3 rounded-lg bg-gray-50 p-3 dark:bg-gray-700"
> >
<img <img
:alt="t('settings.iconPreview')" :alt="t('settings.iconPreview')"
class="h-8 w-8" class="h-8 w-8"
:src="oemSettings.siteIconData || oemSettings.siteIcon" :src="oemSettings.siteIconData || oemSettings.siteIcon"
@error="handleIconError" @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 <button
class="rounded-lg px-3 py-1 font-medium text-red-600 transition-colors hover:bg-red-50 hover:text-red-900" class="rounded-lg px-3 py-1 font-medium text-red-600 transition-colors hover:bg-red-50 hover:text-red-900"
@click="removeIcon" @click="removeIcon"
@@ -139,9 +147,9 @@
<i class="fas fa-upload mr-2" /> <i class="fas fa-upload mr-2" />
{{ t('settings.uploadIcon') }} {{ t('settings.uploadIcon') }}
</button> </button>
<span class="ml-3 text-xs text-gray-500 dark:text-gray-400" <span class="ml-3 text-xs text-gray-500 dark:text-gray-400">{{
>{{ t('settings.iconFormats') }}</span t('settings.iconFormats')
> }}</span>
</div> </div>
</div> </div>
</td> </td>
@@ -160,7 +168,9 @@
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100"> <div class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ t('settings.adminEntry') }} {{ t('settings.adminEntry') }}
</div> </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>
</div> </div>
</td> </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" 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> ></div>
<span class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300">{{ <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> }}</span>
</label> </label>
</div> </div>
@@ -213,7 +225,9 @@
class="text-sm text-gray-500 dark:text-gray-400" class="text-sm text-gray-500 dark:text-gray-400"
> >
<i class="fas fa-clock mr-1" /> <i class="fas fa-clock mr-1" />
{{ t('settings.lastUpdated', { time: formatDateTime(oemSettings.updatedAt) }) }} {{
t('settings.lastUpdated', { time: formatDateTime(oemSettings.updatedAt) })
}}
</div> </div>
</div> </div>
</td> </td>
@@ -233,15 +247,19 @@
<i class="fas fa-tag"></i> <i class="fas fa-tag"></i>
</div> </div>
<div> <div>
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">{{ t('settings.siteNameCard') }}</h3> <h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">
<p class="text-sm text-gray-500 dark:text-gray-400">{{ t('settings.siteNameCardDesc') }}</p> {{ t('settings.siteNameCard') }}
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t('settings.siteNameCardDesc') }}
</p>
</div> </div>
</div> </div>
<input <input
v-model="oemSettings.siteName" v-model="oemSettings.siteName"
class="form-input w-full dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200" class="form-input w-full dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
maxlength="100" maxlength="100"
:placeholder="t('settings.siteNamePlaceholder')" :placeholder="t('settings.siteNamePlaceholder')"
type="text" type="text"
/> />
</div> </div>
@@ -255,7 +273,9 @@
<i class="fas fa-image"></i> <i class="fas fa-image"></i>
</div> </div>
<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"> <p class="text-sm text-gray-500 dark:text-gray-400">
{{ t('settings.siteIconCardDesc') }} {{ t('settings.siteIconCardDesc') }}
</p> </p>
@@ -268,12 +288,14 @@
class="inline-flex items-center gap-3 rounded-lg bg-gray-50 p-3 dark:bg-gray-700" class="inline-flex items-center gap-3 rounded-lg bg-gray-50 p-3 dark:bg-gray-700"
> >
<img <img
:alt="t('settings.iconPreview')" :alt="t('settings.iconPreview')"
class="h-8 w-8" class="h-8 w-8"
:src="oemSettings.siteIconData || oemSettings.siteIcon" :src="oemSettings.siteIconData || oemSettings.siteIcon"
@error="handleIconError" @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 <button
class="rounded-lg px-3 py-1 font-medium text-red-600 transition-colors hover:bg-red-50 hover:text-red-900" class="rounded-lg px-3 py-1 font-medium text-red-600 transition-colors hover:bg-red-50 hover:text-red-900"
@click="removeIcon" @click="removeIcon"
@@ -314,8 +336,12 @@
<i class="fas fa-eye-slash"></i> <i class="fas fa-eye-slash"></i>
</div> </div>
<div> <div>
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">{{ t('settings.adminEntryCard') }}</h3> <h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">
<p class="text-sm text-gray-500 dark:text-gray-400">{{ t('settings.adminEntryCardDesc') }}</p> {{ t('settings.adminEntryCard') }}
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t('settings.adminEntryCardDesc') }}
</p>
</div> </div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
@@ -362,7 +388,9 @@
class="text-center text-sm text-gray-500 dark:text-gray-400" class="text-center text-sm text-gray-500 dark:text-gray-400"
> >
<i class="fas fa-clock mr-1" /> <i class="fas fa-clock mr-1" />
{{ t('settings.lastUpdatedMobile', { time: formatDateTime(oemSettings.updatedAt) }) }} {{
t('settings.lastUpdatedMobile', { time: formatDateTime(oemSettings.updatedAt) })
}}
</div> </div>
</div> </div>
</div> </div>
@@ -402,7 +430,9 @@
<div <div
class="mb-6 rounded-lg bg-white/80 p-6 shadow-lg backdrop-blur-sm dark:bg-gray-800/80" 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 class="space-y-3">
<div <div
v-for="(enabled, type) in webhookConfig.notificationTypes" v-for="(enabled, type) in webhookConfig.notificationTypes"
@@ -1207,7 +1237,10 @@ const savePlatform = async () => {
} }
if (response.success && isMounted.value) { 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() await loadWebhookConfig()
closePlatformModal() closePlatformModal()
} }

View File

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

View File

@@ -72,7 +72,8 @@
</div> </div>
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<div class="text-sm text-gray-700 dark:text-gray-300"> <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> </div>
<!-- 主题切换按钮 --> <!-- 主题切换按钮 -->
@@ -94,7 +95,9 @@
<!-- Overview Tab --> <!-- Overview Tab -->
<div v-if="activeTab === 'overview'" class="space-y-6"> <div v-if="activeTab === 'overview'" class="space-y-6">
<div> <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"> <p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{{ t('user.dashboard.welcomeMessage') }} {{ t('user.dashboard.welcomeMessage') }}
</p> </p>
@@ -272,25 +275,33 @@
<div class="mt-5 border-t border-gray-200 dark:border-gray-700"> <div class="mt-5 border-t border-gray-200 dark:border-gray-700">
<dl class="divide-y divide-gray-200 dark:divide-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"> <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"> <dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
{{ userProfile?.username }} {{ userProfile?.username }}
</dd> </dd>
</div> </div>
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5"> <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"> <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') }} {{ userProfile?.displayName || t('user.dashboard.notAvailable') }}
</dd> </dd>
</div> </div>
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5"> <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"> <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') }} {{ userProfile?.email || t('user.dashboard.notAvailable') }}
</dd> </dd>
</div> </div>
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5"> <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"> <dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
<span <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" 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> </dd>
</div> </div>
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5"> <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"> <dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
{{ formatDate(userProfile?.createdAt) }} {{ formatDate(userProfile?.createdAt) }}
</dd> </dd>
</div> </div>
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5"> <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"> <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') }} {{ formatDate(userProfile?.lastLoginAt) || t('user.dashboard.notAvailable') }}
</dd> </dd>

View File

@@ -3,7 +3,9 @@
<!-- Header --> <!-- Header -->
<div class="sm:flex sm:items-center"> <div class="sm:flex sm:items-center">
<div class="sm:flex-auto"> <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"> <p class="mt-2 text-sm text-gray-700 dark:text-gray-300">
{{ t('user.management.description') }} {{ t('user.management.description') }}
</p> </p>
@@ -254,7 +256,9 @@
fill="currentColor" fill="currentColor"
></path> ></path>
</svg> </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> </div>
<!-- Users List --> <!-- Users List -->
@@ -299,7 +303,9 @@
: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' : '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>
<span <span
:class="[ :class="[
@@ -328,8 +334,14 @@
v-if="user.totalUsage" v-if="user.totalUsage"
class="mt-1 flex items-center space-x-4 text-xs text-gray-400 dark:text-gray-500" 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
<span>${{ (user.totalUsage.totalCost || 0).toFixed(4) }} {{ t('user.management.totalCostLabel') }}</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> </div>
</div> </div>
@@ -337,7 +349,7 @@
<!-- View Usage Stats --> <!-- View Usage Stats -->
<button <button
class="inline-flex items-center rounded border border-transparent p-1 text-gray-400 hover:text-blue-600" class="inline-flex items-center rounded border border-transparent p-1 text-gray-400 hover:text-blue-600"
:title="t('user.management.viewUsageStats')" :title="t('user.management.viewUsageStats')"
@click="viewUserStats(user)" @click="viewUserStats(user)"
> >
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -354,7 +366,7 @@
<button <button
class="inline-flex items-center rounded border border-transparent p-1 text-gray-400 hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-50" class="inline-flex items-center rounded border border-transparent p-1 text-gray-400 hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="user.apiKeyCount === 0" :disabled="user.apiKeyCount === 0"
:title="t('user.management.disableAllApiKeys')" :title="t('user.management.disableAllApiKeys')"
@click="disableUserApiKeys(user)" @click="disableUserApiKeys(user)"
> >
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -375,7 +387,9 @@
? 'text-gray-400 hover:text-red-600' ? 'text-gray-400 hover:text-red-600'
: 'text-gray-400 hover:text-green-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)" @click="toggleUserStatus(user)"
> >
<svg <svg
@@ -405,7 +419,7 @@
<!-- Change Role --> <!-- Change Role -->
<button <button
class="inline-flex items-center rounded border border-transparent p-1 text-gray-400 hover:text-purple-600" class="inline-flex items-center rounded border border-transparent p-1 text-gray-400 hover:text-purple-600"
:title="t('user.management.changeRole')" :title="t('user.management.changeRole')"
@click="changeUserRole(user)" @click="changeUserRole(user)"
> >
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -437,7 +451,9 @@
stroke-width="2" stroke-width="2"
/> />
</svg> </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"> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ {{
searchQuery ? t('user.management.noUsersMatch') : t('user.management.noUsersCreated') searchQuery ? t('user.management.noUsersMatch') : t('user.management.noUsersCreated')
@@ -597,7 +613,9 @@ const viewUserStats = (user) => {
const toggleUserStatus = (user) => { const toggleUserStatus = (user) => {
selectedUser.value = user selectedUser.value = user
confirmAction.value = { 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 message: user.isActive
? t('user.management.disableUserMessage', { username: user.username }) ? t('user.management.disableUserMessage', { username: user.username })
: t('user.management.enableUserMessage', { username: user.username }), : t('user.management.enableUserMessage', { username: user.username }),
@@ -614,7 +632,10 @@ const disableUserApiKeys = (user) => {
selectedUser.value = user selectedUser.value = user
confirmAction.value = { confirmAction.value = {
title: t('user.management.disableAllKeysTitle'), 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'), confirmText: t('user.management.disableKeys'),
confirmClass: 'bg-red-600 hover:bg-red-700', confirmClass: 'bg-red-600 hover:bg-red-700',
action: 'disableKeys' action: 'disableKeys'
@@ -642,13 +663,21 @@ const handleConfirmAction = async () => {
if (userIndex !== -1) { if (userIndex !== -1) {
users.value[userIndex].isActive = !user.isActive 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') { } else if (action === 'disableKeys') {
const response = await apiClient.post(`/users/${user.id}/disable-keys`) const response = await apiClient.post(`/users/${user.id}/disable-keys`)
if (response.success) { 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 await loadUsers() // Refresh to get updated counts
} }
} }

View File

@@ -81,7 +81,9 @@
> >
file file
</li> </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> </ol>
</div> </div>
<div class="mb-3 sm:mb-4"> <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"> <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> Recommend using PowerShell instead of CMD</li>
<li> If you encounter permission issues, try running as administrator</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> </ul>
</div> </div>
</div> </div>
<!-- Verify Installation --> <!-- Verify Installation -->
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4"> <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"> <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: After installation completes, open PowerShell or CMD and enter the following commands:
</p> </p>
@@ -122,7 +128,9 @@
<div class="whitespace-nowrap text-gray-300">node --version</div> <div class="whitespace-nowrap text-gray-300">node --version</div>
<div class="whitespace-nowrap text-gray-300">npm --version</div> <div class="whitespace-nowrap text-gray-300">npm --version</div>
</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>
</div> </div>
@@ -159,7 +167,8 @@
</div> </div>
</div> </div>
<p class="text-sm text-gray-600 dark:text-gray-400"> <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> </p>
<div class="mt-4 rounded-lg border border-blue-200 bg-blue-50 p-3 sm:p-4"> <div class="mt-4 rounded-lg border border-blue-200 bg-blue-50 p-3 sm:p-4">
@@ -173,15 +182,21 @@
<!-- Verify Installation --> <!-- Verify Installation -->
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4"> <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> <h6 class="mb-2 font-medium text-green-800 dark:text-green-300">
<p class="mb-3 text-sm text-green-700">After installation completes, enter the following command to check if installation was successful:</p> 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 <div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" 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 class="whitespace-nowrap text-gray-300">claude --version</div>
</div> </div>
<p class="mt-2 text-sm text-green-700"> <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> </p>
</div> </div>
</div> </div>
@@ -230,7 +245,8 @@
</div> </div>
</div> </div>
<p class="mt-2 text-xs text-yellow-700"> <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> </p>
</div> </div>
@@ -280,9 +296,12 @@
<!-- Verify Environment Variable Setup --> <!-- Verify Environment Variable Setup -->
<div class="mt-6 rounded-lg border border-blue-200 bg-blue-50 p-3 sm:p-4"> <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"> <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> </p>
<div class="space-y-4"> <div class="space-y-4">
@@ -320,7 +339,8 @@
<div>cr_xxxxxxxxxxxxxxxxxx</div> <div>cr_xxxxxxxxxxxxxxxxxx</div>
</div> </div>
<p class="text-xs text-blue-700"> <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> </p>
</div> </div>
</div> </div>
@@ -416,7 +436,8 @@
Configure Codex Environment Variables Configure Codex Environment Variables
</h5> </h5>
<p class="mb-3 text-sm text-gray-700 dark:text-gray-300 sm:mb-4 sm:text-base"> <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> </p>
<div class="space-y-4"> <div class="space-y-4">
@@ -528,8 +549,8 @@
<ul class="list-inside list-disc space-y-1 text-sm"> <ul class="list-inside list-disc space-y-1 text-sm">
<li>Run PowerShell as Administrator</li> <li>Run PowerShell as Administrator</li>
<li> <li>
Or configure npm to use user directory: <code Or configure npm to use user directory:
class="rounded bg-gray-200 px-1 text-xs sm:text-sm" <code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
>npm config set prefix %APPDATA%\npm</code >npm config set prefix %APPDATA%\npm</code
> >
</li> </li>
@@ -567,7 +588,8 @@
<li>Restart PowerShell or CMD</li> <li>Restart PowerShell or CMD</li>
<li>Or log out and log back into Windows</li> <li>Or log out and log back into Windows</li>
<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 >echo $env:ANTHROPIC_BASE_URL</code
> >
</li> </li>
@@ -605,7 +627,8 @@
<div class="mb-4"> <div class="mb-4">
<p class="mb-3 text-gray-700">Method 1: Using Homebrew (Recommended)</p> <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"> <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> </p>
<div <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" 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 --> <!-- Verify Installation -->
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4"> <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> <h6 class="mb-2 font-medium text-green-800 dark:text-green-300">
<p class="mb-3 text-sm text-green-700">After installation, open Terminal and enter the following commands:</p> Verify Installation Success
</h6>
<p class="mb-3 text-sm text-green-700">
After installation, open Terminal and enter the following commands:
</p>
<div <div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" 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">node --version</div>
<div class="whitespace-nowrap text-gray-300">npm --version</div> <div class="whitespace-nowrap text-gray-300">npm --version</div>
</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>
</div> </div>
@@ -699,7 +728,9 @@
npm install -g @anthropic-ai/claude-code npm install -g @anthropic-ai/claude-code
</div> </div>
</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 <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" 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 --> <!-- Verify Installation -->
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4"> <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> <h6 class="mb-2 font-medium text-green-800 dark:text-green-300">
<p class="mb-3 text-sm text-green-700">After installation completes, enter the following command to check if installation was successful:</p> 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 <div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" 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 class="whitespace-nowrap text-gray-300">claude --version</div>
</div> </div>
<p class="mt-2 text-sm text-green-700"> <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> </p>
</div> </div>
</div> </div>
@@ -766,7 +803,8 @@
</div> </div>
</div> </div>
<p class="mt-2 text-xs text-yellow-700"> <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> </p>
</div> </div>
@@ -903,7 +941,8 @@
Configure Codex Environment Variables Configure Codex Environment Variables
</h5> </h5>
<p class="mb-3 text-sm text-gray-700 dark:text-gray-300 sm:mb-4 sm:text-base"> <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> </p>
<div class="space-y-4"> <div class="space-y-4">
@@ -1014,13 +1053,14 @@
<p class="mb-2">Try the following solutions:</p> <p class="mb-2">Try the following solutions:</p>
<ul class="list-inside list-disc space-y-1 text-sm"> <ul class="list-inside list-disc space-y-1 text-sm">
<li> <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 >sudo npm install -g @anthropic-ai/claude-code</code
> >
</li> </li>
<li> <li>
Or configure npm to use user directory: <code Or configure npm to use user directory:
class="rounded bg-gray-200 px-1 text-xs sm:text-sm" <code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
>npm config set prefix ~/.npm-global</code >npm config set prefix ~/.npm-global</code
> >
</li> </li>
@@ -1040,7 +1080,8 @@
<li>Open "System Preferences" "Security & Privacy"</li> <li>Open "System Preferences" "Security & Privacy"</li>
<li>Click "Allow Anyway" or "Open Anyway"</li> <li>Click "Allow Anyway" or "Open Anyway"</li>
<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 >sudo spctl --master-disable</code
> >
</li> </li>
@@ -1057,10 +1098,13 @@
<div class="px-3 pb-3 text-gray-600 sm:px-4 sm:pb-4"> <div class="px-3 pb-3 text-gray-600 sm:px-4 sm:pb-4">
<p class="mb-2">Check the following points:</p> <p class="mb-2">Check the following points:</p>
<ul class="list-inside list-disc space-y-1 text-sm"> <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>Restart Terminal</li>
<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 >echo $ANTHROPIC_BASE_URL</code
> >
</li> </li>
@@ -1127,7 +1171,10 @@
<h6 class="mb-2 text-sm font-medium text-orange-800 sm:text-base">Linux Notes</h6> <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"> <ul class="space-y-1 text-xs text-orange-700 sm:text-sm">
<li>• Some distributions may require additional dependencies</li> <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> <li>• Ensure your user has write permissions to npm's global directory</li>
</ul> </ul>
</div> </div>
@@ -1135,15 +1182,21 @@
<!-- Verify Installation --> <!-- Verify Installation -->
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4"> <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> <h6 class="mb-2 font-medium text-green-800 dark:text-green-300">
<p class="mb-3 text-sm text-green-700">After installation, open terminal and enter the following commands:</p> Verify Installation Success
</h6>
<p class="mb-3 text-sm text-green-700">
After installation, open terminal and enter the following commands:
</p>
<div <div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" 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">node --version</div>
<div class="whitespace-nowrap text-gray-300">npm --version</div> <div class="whitespace-nowrap text-gray-300">npm --version</div>
</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>
</div> </div>
@@ -1179,7 +1232,9 @@
npm install -g @anthropic-ai/claude-code npm install -g @anthropic-ai/claude-code
</div> </div>
</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 <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" 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 --> <!-- Verify Installation -->
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4"> <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> <h6 class="mb-2 font-medium text-green-800 dark:text-green-300">
<p class="mb-3 text-sm text-green-700">After installation completes, enter the following command to check if installation was successful:</p> 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 <div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" 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 class="whitespace-nowrap text-gray-300">claude --version</div>
</div> </div>
<p class="mt-2 text-sm text-green-700"> <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> </p>
</div> </div>
</div> </div>
@@ -1246,7 +1307,8 @@
</div> </div>
</div> </div>
<p class="mt-2 text-xs text-yellow-700"> <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> </p>
</div> </div>
@@ -1381,7 +1443,8 @@
Configure Codex Environment Variables Configure Codex Environment Variables
</h5> </h5>
<p class="mb-3 text-sm text-gray-700 dark:text-gray-300 sm:mb-4 sm:text-base"> <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> </p>
<div class="space-y-4"> <div class="space-y-4">
@@ -1492,18 +1555,20 @@
<p class="mb-2">Try the following solutions:</p> <p class="mb-2">Try the following solutions:</p>
<ul class="list-inside list-disc space-y-1 text-sm"> <ul class="list-inside list-disc space-y-1 text-sm">
<li> <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 >sudo npm install -g @anthropic-ai/claude-code</code
> >
</li> </li>
<li> <li>
Or configure npm to use user directory: <code Or configure npm to use user directory:
class="rounded bg-gray-200 px-1 text-xs sm:text-sm" <code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
>npm config set prefix ~/.npm-global</code >npm config set prefix ~/.npm-global</code
> >
</li> </li>
<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 >export PATH=~/.npm-global/bin:$PATH</code
> >
</li> </li>
@@ -1547,7 +1612,8 @@
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm">source ~/.bashrc</code> <code class="rounded bg-gray-200 px-1 text-xs sm:text-sm">source ~/.bashrc</code>
</li> </li>
<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 >echo $ANTHROPIC_BASE_URL</code
> >
</li> </li>
@@ -1564,10 +1630,12 @@
> >
<h5 class="mb-2 text-lg font-semibold sm:text-xl">🎉 Congratulations!</h5> <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"> <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>
<p class="text-xs text-blue-200 sm:text-sm"> <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> </p>
</div> </div>
</div> </div>