mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 完成web/admin-spa/src/components/apikeys的国际化并修复语法错误和警告
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,9 @@
|
||||
>
|
||||
<i class="fas fa-layer-group text-sm text-white sm:text-base" />
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-gray-900 sm:text-xl">{{ t('groupManagement.title') }}</h3>
|
||||
<h3 class="text-lg font-bold text-gray-900 sm:text-xl">
|
||||
{{ t('groupManagement.title') }}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
class="p-1 text-gray-400 transition-colors hover:text-gray-600"
|
||||
@@ -31,10 +33,14 @@
|
||||
|
||||
<!-- 创建分组表单 -->
|
||||
<div v-if="showCreateForm" class="mb-6 rounded-lg border border-blue-200 bg-blue-50 p-4">
|
||||
<h4 class="mb-4 text-lg font-semibold text-gray-900">{{ t('groupManagement.createGroup') }}</h4>
|
||||
<h4 class="mb-4 text-lg font-semibold text-gray-900">
|
||||
{{ t('groupManagement.createGroup') }}
|
||||
</h4>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">{{ t('groupManagement.groupNameRequired') }}</label>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">{{
|
||||
t('groupManagement.groupNameRequired')
|
||||
}}</label>
|
||||
<input
|
||||
v-model="createForm.name"
|
||||
class="form-input w-full"
|
||||
@@ -44,7 +50,9 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">{{ t('groupManagement.platformTypeRequired') }}</label>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">{{
|
||||
t('groupManagement.platformTypeRequired')
|
||||
}}</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="createForm.platform" class="mr-2" type="radio" value="claude" />
|
||||
@@ -62,7 +70,9 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">{{ t('groupManagement.descriptionOptional') }}</label>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">{{
|
||||
t('groupManagement.descriptionOptional')
|
||||
}}</label>
|
||||
<textarea
|
||||
v-model="createForm.description"
|
||||
class="form-input w-full resize-none"
|
||||
@@ -80,7 +90,9 @@
|
||||
<div v-if="creating" class="loading-spinner mr-2" />
|
||||
{{ creating ? t('groupManagement.creating') : t('groupManagement.create') }}
|
||||
</button>
|
||||
<button class="btn btn-secondary px-4 py-2" @click="cancelCreate">{{ t('groupManagement.cancel') }}</button>
|
||||
<button class="btn btn-secondary px-4 py-2" @click="cancelCreate">
|
||||
{{ t('groupManagement.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -184,7 +196,9 @@
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">{{ t('groupManagement.groupNameRequired') }}</label>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">{{
|
||||
t('groupManagement.groupNameRequired')
|
||||
}}</label>
|
||||
<input
|
||||
v-model="editForm.name"
|
||||
class="form-input w-full"
|
||||
@@ -194,7 +208,9 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">{{ t('groupManagement.platformTypeLabel') }}</label>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">{{
|
||||
t('groupManagement.platformTypeLabel')
|
||||
}}</label>
|
||||
<div class="rounded-lg bg-gray-100 px-3 py-2 text-sm text-gray-600">
|
||||
{{
|
||||
editForm.platform === 'claude'
|
||||
@@ -203,12 +219,16 @@
|
||||
? 'Gemini'
|
||||
: 'OpenAI'
|
||||
}}
|
||||
<span class="ml-2 text-xs text-gray-500">{{ t('groupManagement.cannotModify') }}</span>
|
||||
<span class="ml-2 text-xs text-gray-500">{{
|
||||
t('groupManagement.cannotModify')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">{{ t('groupManagement.descriptionOptional') }}</label>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">{{
|
||||
t('groupManagement.descriptionOptional')
|
||||
}}</label>
|
||||
<textarea
|
||||
v-model="editForm.description"
|
||||
class="form-input w-full resize-none"
|
||||
@@ -226,7 +246,9 @@
|
||||
<div v-if="updating" class="loading-spinner mr-2" />
|
||||
{{ updating ? t('groupManagement.updating') : t('groupManagement.update') }}
|
||||
</button>
|
||||
<button class="btn btn-secondary flex-1 px-4 py-2" @click="cancelEdit">{{ t('groupManagement.cancel') }}</button>
|
||||
<button class="btn btn-secondary flex-1 px-4 py-2" @click="cancelEdit">
|
||||
{{ t('groupManagement.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
<i class="fas fa-link text-white" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="mb-3 font-semibold text-blue-900 dark:text-blue-200">{{ t('oauthFlow.claudeAccountAuth') }}</h4>
|
||||
<h4 class="mb-3 font-semibold text-blue-900 dark:text-blue-200">
|
||||
{{ t('oauthFlow.claudeAccountAuth') }}
|
||||
</h4>
|
||||
<p class="mb-4 text-sm text-blue-800 dark:text-blue-300">
|
||||
{{ t('oauthFlow.claudeAuthDescription') }}
|
||||
</p>
|
||||
@@ -115,14 +117,17 @@
|
||||
</p>
|
||||
<p class="mb-3 text-sm text-blue-700 dark:text-blue-300">
|
||||
{{ t('oauthFlow.step3Description') }}
|
||||
<strong>{{ t('oauthFlow.authorizationCode') }}</strong>{{ t('oauthFlow.step3DescriptionMiddle') }}
|
||||
<strong>{{ t('oauthFlow.authorizationCode') }}</strong
|
||||
>{{ t('oauthFlow.step3DescriptionMiddle') }}
|
||||
</p>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label
|
||||
class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
<i class="fas fa-key mr-2 text-blue-500" />{{ t('oauthFlow.authorizationCode') }}
|
||||
<i class="fas fa-key mr-2 text-blue-500" />{{
|
||||
t('oauthFlow.authorizationCode')
|
||||
}}
|
||||
</label>
|
||||
<textarea
|
||||
v-model="authCode"
|
||||
@@ -157,7 +162,9 @@
|
||||
<i class="fas fa-robot text-white" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="mb-3 font-semibold text-green-900 dark:text-green-200">{{ t('oauthFlow.geminiAccountAuth') }}</h4>
|
||||
<h4 class="mb-3 font-semibold text-green-900 dark:text-green-200">
|
||||
{{ t('oauthFlow.geminiAccountAuth') }}
|
||||
</h4>
|
||||
<p class="mb-4 text-sm text-green-800 dark:text-green-300">
|
||||
{{ t('oauthFlow.geminiAuthDescription') }}
|
||||
</p>
|
||||
@@ -266,7 +273,9 @@
|
||||
<label
|
||||
class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
<i class="fas fa-key mr-2 text-green-500" />{{ t('oauthFlow.authorizationCode') }}
|
||||
<i class="fas fa-key mr-2 text-green-500" />{{
|
||||
t('oauthFlow.authorizationCode')
|
||||
}}
|
||||
</label>
|
||||
<textarea
|
||||
v-model="authCode"
|
||||
@@ -303,7 +312,9 @@
|
||||
<i class="fas fa-brain text-white" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="mb-3 font-semibold text-orange-900 dark:text-orange-200">{{ t('oauthFlow.openaiAccountAuth') }}</h4>
|
||||
<h4 class="mb-3 font-semibold text-orange-900 dark:text-orange-200">
|
||||
{{ t('oauthFlow.openaiAccountAuth') }}
|
||||
</h4>
|
||||
<p class="mb-4 text-sm text-orange-800 dark:text-orange-300">
|
||||
{{ t('oauthFlow.openaiAuthDescription') }}
|
||||
</p>
|
||||
@@ -382,7 +393,8 @@
|
||||
>
|
||||
<p class="text-xs text-amber-800 dark:text-amber-300">
|
||||
<i class="fas fa-clock mr-1" />
|
||||
<strong>{{ t('oauthFlow.openaiImportantNote') }}</strong>{{ t('oauthFlow.openaiLoadingNote') }}
|
||||
<strong>{{ t('oauthFlow.openaiImportantNote') }}</strong
|
||||
>{{ t('oauthFlow.openaiLoadingNote') }}
|
||||
</p>
|
||||
<p class="mt-2 text-xs text-amber-700 dark:text-amber-400">
|
||||
{{ t('oauthFlow.openaiAddressNote') }}
|
||||
@@ -419,14 +431,17 @@
|
||||
</p>
|
||||
<p class="mb-3 text-sm text-orange-700 dark:text-orange-300">
|
||||
{{ t('oauthFlow.step3DescriptionOpenAI') }}
|
||||
<strong class="font-mono">http://localhost:1455/...</strong> {{ t('oauthFlow.step3DescriptionOpenAIMiddle') }}
|
||||
<strong class="font-mono">http://localhost:1455/...</strong>
|
||||
{{ t('oauthFlow.step3DescriptionOpenAIMiddle') }}
|
||||
</p>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label
|
||||
class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
<i class="fas fa-link mr-2 text-orange-500" />{{ t('oauthFlow.authLinkOrCode') }}
|
||||
<i class="fas fa-link mr-2 text-orange-500" />{{
|
||||
t('oauthFlow.authLinkOrCode')
|
||||
}}
|
||||
</label>
|
||||
<textarea
|
||||
v-model="authCode"
|
||||
@@ -440,17 +455,18 @@
|
||||
>
|
||||
<p class="text-xs text-blue-700 dark:text-blue-300">
|
||||
<i class="fas fa-lightbulb mr-1" />
|
||||
<strong>{{ t('oauthFlow.openaiTip') }}</strong>{{ t('oauthFlow.openaiTipText') }}
|
||||
<strong>{{ t('oauthFlow.openaiTip') }}</strong
|
||||
>{{ t('oauthFlow.openaiTipText') }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-blue-600 dark:text-blue-400">
|
||||
{{ t('oauthFlow.openaiLinkExample') }}<span class="font-mono"
|
||||
{{ t('oauthFlow.openaiLinkExample')
|
||||
}}<span class="font-mono"
|
||||
>http://localhost:1455/auth/callback?code=ac_4hm8...</span
|
||||
>
|
||||
</p>
|
||||
<p class="text-xs text-blue-600">
|
||||
{{ t('oauthFlow.openaiCodeExample') }}<span class="font-mono"
|
||||
>ac_4hm8iqmx9A2fzMy_cwye7U3W7...</span
|
||||
>
|
||||
{{ t('oauthFlow.openaiCodeExample')
|
||||
}}<span class="font-mono">ac_4hm8iqmx9A2fzMy_cwye7U3W7...</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">{{ t('proxyConfig.title') }}</h4>
|
||||
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
{{ t('proxyConfig.title') }}
|
||||
</h4>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input
|
||||
v-model="proxy.enabled"
|
||||
class="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-blue-500"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">{{ t('proxyConfig.enableProxy') }}</span>
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">{{
|
||||
t('proxyConfig.enableProxy')
|
||||
}}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -70,9 +74,9 @@
|
||||
<div class="my-3 border-t border-gray-200 dark:border-gray-600"></div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>{{ t('proxyConfig.proxyType') }}</label
|
||||
>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">{{
|
||||
t('proxyConfig.proxyType')
|
||||
}}</label>
|
||||
<select
|
||||
v-model="proxy.type"
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||
@@ -85,9 +89,9 @@
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>{{ t('proxyConfig.hostAddress') }}</label
|
||||
>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">{{
|
||||
t('proxyConfig.hostAddress')
|
||||
}}</label>
|
||||
<input
|
||||
v-model="proxy.host"
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
@@ -96,9 +100,9 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>{{ t('proxyConfig.port') }}</label
|
||||
>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">{{
|
||||
t('proxyConfig.port')
|
||||
}}</label>
|
||||
<input
|
||||
v-model="proxy.port"
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
@@ -126,9 +130,9 @@
|
||||
|
||||
<div v-if="showAuth" class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>{{ t('proxyConfig.username') }}</label
|
||||
>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">{{
|
||||
t('proxyConfig.username')
|
||||
}}</label>
|
||||
<input
|
||||
v-model="proxy.username"
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
@@ -137,9 +141,9 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>{{ t('proxyConfig.password') }}</label
|
||||
>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">{{
|
||||
t('proxyConfig.password')
|
||||
}}</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
v-model="proxy.password"
|
||||
|
||||
@@ -64,7 +64,9 @@
|
||||
<!-- Role Selection -->
|
||||
<form class="space-y-4" @submit.prevent="handleSubmit">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700"> {{ $t('user.changeRoleModal.newRole') }} </label>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700">
|
||||
{{ $t('user.changeRoleModal.newRole') }}
|
||||
</label>
|
||||
<div class="space-y-2">
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
@@ -75,8 +77,12 @@
|
||||
value="user"
|
||||
/>
|
||||
<div class="ml-3">
|
||||
<div class="text-sm font-medium text-gray-900">{{ $t('user.changeRoleModal.roles.user') }}</div>
|
||||
<div class="text-xs text-gray-500">{{ $t('user.changeRoleModal.roles.userDesc') }}</div>
|
||||
<div class="text-sm font-medium text-gray-900">
|
||||
{{ $t('user.changeRoleModal.roles.user') }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{{ $t('user.changeRoleModal.roles.userDesc') }}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
@@ -88,8 +94,12 @@
|
||||
value="admin"
|
||||
/>
|
||||
<div class="ml-3">
|
||||
<div class="text-sm font-medium text-gray-900">{{ $t('user.changeRoleModal.roles.admin') }}</div>
|
||||
<div class="text-xs text-gray-500">{{ $t('user.changeRoleModal.roles.adminDesc') }}</div>
|
||||
<div class="text-sm font-medium text-gray-900">
|
||||
{{ $t('user.changeRoleModal.roles.admin') }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{{ $t('user.changeRoleModal.roles.adminDesc') }}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
@@ -111,7 +121,9 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-yellow-800">{{ $t('user.changeRoleModal.roleChangeWarning.title') }}</h3>
|
||||
<h3 class="text-sm font-medium text-yellow-800">
|
||||
{{ $t('user.changeRoleModal.roleChangeWarning.title') }}
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-yellow-700">
|
||||
<p v-if="selectedRole === 'admin'">
|
||||
{{ $t('user.changeRoleModal.roleChangeWarning.grantAdmin') }}
|
||||
|
||||
@@ -8,7 +8,11 @@
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900">
|
||||
{{ $t('user.usageStatsModal.titleWithUser', { displayName: user?.displayName || user?.username }) }}
|
||||
{{
|
||||
$t('user.usageStatsModal.titleWithUser', {
|
||||
displayName: user?.displayName || user?.username
|
||||
})
|
||||
}}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500">@{{ user?.username }} • {{ user?.role }}</p>
|
||||
</div>
|
||||
@@ -34,7 +38,9 @@
|
||||
<option value="day">{{ $t('user.usageStatsModal.periodSelection.day') }}</option>
|
||||
<option value="week">{{ $t('user.usageStatsModal.periodSelection.week') }}</option>
|
||||
<option value="month">{{ $t('user.usageStatsModal.periodSelection.month') }}</option>
|
||||
<option value="quarter">{{ $t('user.usageStatsModal.periodSelection.quarter') }}</option>
|
||||
<option value="quarter">
|
||||
{{ $t('user.usageStatsModal.periodSelection.quarter') }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -87,7 +93,9 @@
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="truncate text-sm font-medium text-blue-600">{{ $t('user.usageStatsModal.summaryCards.requests') }}</dt>
|
||||
<dt class="truncate text-sm font-medium text-blue-600">
|
||||
{{ $t('user.usageStatsModal.summaryCards.requests') }}
|
||||
</dt>
|
||||
<dd class="text-lg font-medium text-blue-900">
|
||||
{{ formatNumber(usageStats?.totalRequests || 0) }}
|
||||
</dd>
|
||||
@@ -117,7 +125,9 @@
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="truncate text-sm font-medium text-green-600">{{ $t('user.usageStatsModal.summaryCards.inputTokens') }}</dt>
|
||||
<dt class="truncate text-sm font-medium text-green-600">
|
||||
{{ $t('user.usageStatsModal.summaryCards.inputTokens') }}
|
||||
</dt>
|
||||
<dd class="text-lg font-medium text-green-900">
|
||||
{{ formatNumber(usageStats?.totalInputTokens || 0) }}
|
||||
</dd>
|
||||
@@ -147,7 +157,9 @@
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="truncate text-sm font-medium text-purple-600">{{ $t('user.usageStatsModal.summaryCards.outputTokens') }}</dt>
|
||||
<dt class="truncate text-sm font-medium text-purple-600">
|
||||
{{ $t('user.usageStatsModal.summaryCards.outputTokens') }}
|
||||
</dt>
|
||||
<dd class="text-lg font-medium text-purple-900">
|
||||
{{ formatNumber(usageStats?.totalOutputTokens || 0) }}
|
||||
</dd>
|
||||
@@ -177,7 +189,9 @@
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="truncate text-sm font-medium text-yellow-600">{{ $t('user.usageStatsModal.summaryCards.totalCost') }}</dt>
|
||||
<dt class="truncate text-sm font-medium text-yellow-600">
|
||||
{{ $t('user.usageStatsModal.summaryCards.totalCost') }}
|
||||
</dt>
|
||||
<dd class="text-lg font-medium text-yellow-900">
|
||||
${{ (usageStats?.totalCost || 0).toFixed(4) }}
|
||||
</dd>
|
||||
@@ -194,7 +208,9 @@
|
||||
class="rounded-lg border border-gray-200 bg-white"
|
||||
>
|
||||
<div class="border-b border-gray-200 px-4 py-5 sm:px-6">
|
||||
<h4 class="text-lg font-medium leading-6 text-gray-900">{{ $t('user.usageStatsModal.apiKeysTable.title') }}</h4>
|
||||
<h4 class="text-lg font-medium leading-6 text-gray-900">
|
||||
{{ $t('user.usageStatsModal.apiKeysTable.title') }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="overflow-hidden">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
@@ -253,21 +269,35 @@
|
||||
: 'bg-red-100 text-red-800'
|
||||
]"
|
||||
>
|
||||
{{ apiKey.isActive ? $t('user.usageStatsModal.apiKeysTable.status.active') : $t('user.usageStatsModal.apiKeysTable.status.disabled') }}
|
||||
{{
|
||||
apiKey.isActive
|
||||
? $t('user.usageStatsModal.apiKeysTable.status.active')
|
||||
: $t('user.usageStatsModal.apiKeysTable.status.disabled')
|
||||
}}
|
||||
</span>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">
|
||||
{{ formatNumber(apiKey.usage?.requests || 0) }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">
|
||||
<div>{{ $t('user.usageStatsModal.apiKeysTable.tokensFormat.input') }}: {{ formatNumber(apiKey.usage?.inputTokens || 0) }}</div>
|
||||
<div>{{ $t('user.usageStatsModal.apiKeysTable.tokensFormat.output') }}: {{ formatNumber(apiKey.usage?.outputTokens || 0) }}</div>
|
||||
<div>
|
||||
{{ $t('user.usageStatsModal.apiKeysTable.tokensFormat.input') }}:
|
||||
{{ formatNumber(apiKey.usage?.inputTokens || 0) }}
|
||||
</div>
|
||||
<div>
|
||||
{{ $t('user.usageStatsModal.apiKeysTable.tokensFormat.output') }}:
|
||||
{{ formatNumber(apiKey.usage?.outputTokens || 0) }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">
|
||||
${{ (apiKey.usage?.totalCost || 0).toFixed(4) }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
|
||||
{{ apiKey.lastUsedAt ? formatDate(apiKey.lastUsedAt) : $t('user.usageStatsModal.apiKeysTable.never') }}
|
||||
{{
|
||||
apiKey.lastUsedAt
|
||||
? formatDate(apiKey.lastUsedAt)
|
||||
: $t('user.usageStatsModal.apiKeysTable.never')
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -278,7 +308,9 @@
|
||||
<!-- Chart Placeholder -->
|
||||
<div class="rounded-lg border border-gray-200 bg-white">
|
||||
<div class="border-b border-gray-200 px-4 py-5 sm:px-6">
|
||||
<h4 class="text-lg font-medium leading-6 text-gray-900">{{ $t('user.usageStatsModal.usageTrend.title') }}</h4>
|
||||
<h4 class="text-lg font-medium leading-6 text-gray-900">
|
||||
{{ $t('user.usageStatsModal.usageTrend.title') }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div
|
||||
@@ -298,9 +330,13 @@
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">{{ $t('user.usageStatsModal.usageTrend.chartTitle') }}</h3>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">
|
||||
{{ $t('user.usageStatsModal.usageTrend.chartTitle') }}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
{{ $t('user.usageStatsModal.usageTrend.dailyTrends', { period: selectedPeriod }) }}
|
||||
{{
|
||||
$t('user.usageStatsModal.usageTrend.dailyTrends', { period: selectedPeriod })
|
||||
}}
|
||||
</p>
|
||||
<p class="mt-2 text-xs text-gray-400">
|
||||
{{ $t('user.usageStatsModal.usageTrend.chartNote') }}
|
||||
@@ -325,7 +361,9 @@
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">{{ $t('user.usageStatsModal.noData.title') }}</h3>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">
|
||||
{{ $t('user.usageStatsModal.noData.title') }}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
{{ $t('user.usageStatsModal.noData.description') }}
|
||||
</p>
|
||||
@@ -349,9 +387,7 @@
|
||||
import { ref, watch } from 'vue'
|
||||
import { apiClient } from '@/config/api'
|
||||
import { showToast } from '@/utils/toast'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
// import { useI18n } from 'vue-i18n' - using $t in template instead
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
|
||||
@@ -12,13 +12,17 @@
|
||||
<i class="fas fa-layer-group text-lg text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-gray-900">批量创建成功</h3>
|
||||
<p class="text-sm text-gray-600">成功创建 {{ apiKeys.length }} 个 API Key</p>
|
||||
<h3 class="text-xl font-bold text-gray-900">
|
||||
{{ $t('apiKeys.batchApiKeyModal.title') }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600">
|
||||
{{ $t('apiKeys.batchApiKeyModal.successMessage', { count: apiKeys.length }) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="text-gray-400 transition-colors hover:text-gray-600"
|
||||
title="直接关闭(不推荐)"
|
||||
:title="$t('apiKeys.batchApiKeyModal.directCloseTooltip')"
|
||||
@click="handleDirectClose"
|
||||
>
|
||||
<i class="fas fa-times text-xl" />
|
||||
@@ -34,10 +38,11 @@
|
||||
<i class="fas fa-exclamation-triangle text-sm text-white" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h5 class="mb-1 font-semibold text-amber-900">重要提醒</h5>
|
||||
<h5 class="mb-1 font-semibold text-amber-900">
|
||||
{{ $t('apiKeys.batchApiKeyModal.importantReminder') }}
|
||||
</h5>
|
||||
<p class="text-sm text-amber-800">
|
||||
这是您唯一能看到所有 API Key 的机会。关闭此窗口后,系统将不再显示完整的 API
|
||||
Key。请立即下载并妥善保存。
|
||||
{{ $t('apiKeys.batchApiKeyModal.warningMessage') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -50,7 +55,9 @@
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-medium text-blue-600">创建数量</p>
|
||||
<p class="text-xs font-medium text-blue-600">
|
||||
{{ $t('apiKeys.batchApiKeyModal.createdCount') }}
|
||||
</p>
|
||||
<p class="mt-1 text-2xl font-bold text-blue-900">
|
||||
{{ apiKeys.length }}
|
||||
</p>
|
||||
@@ -68,7 +75,9 @@
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-medium text-green-600">基础名称</p>
|
||||
<p class="text-xs font-medium text-green-600">
|
||||
{{ $t('apiKeys.batchApiKeyModal.baseName') }}
|
||||
</p>
|
||||
<p class="mt-1 truncate text-lg font-bold text-green-900">
|
||||
{{ baseName }}
|
||||
</p>
|
||||
@@ -86,7 +95,9 @@
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-medium text-purple-600">权限范围</p>
|
||||
<p class="text-xs font-medium text-purple-600">
|
||||
{{ $t('apiKeys.batchApiKeyModal.permissionScope') }}
|
||||
</p>
|
||||
<p class="mt-1 text-lg font-bold text-purple-900">
|
||||
{{ getPermissionText() }}
|
||||
</p>
|
||||
@@ -104,7 +115,9 @@
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-medium text-orange-600">过期时间</p>
|
||||
<p class="text-xs font-medium text-orange-600">
|
||||
{{ $t('apiKeys.batchApiKeyModal.expiryTime') }}
|
||||
</p>
|
||||
<p class="mt-1 text-lg font-bold text-orange-900">
|
||||
{{ getExpiryText() }}
|
||||
</p>
|
||||
@@ -121,7 +134,9 @@
|
||||
<!-- API Keys 预览 -->
|
||||
<div class="mb-6">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label class="text-sm font-semibold text-gray-700">API Keys 预览</label>
|
||||
<label class="text-sm font-semibold text-gray-700">{{
|
||||
$t('apiKeys.batchApiKeyModal.previewTitle')
|
||||
}}</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="flex items-center gap-1 text-xs text-blue-600 hover:text-blue-800"
|
||||
@@ -129,9 +144,15 @@
|
||||
@click="togglePreview"
|
||||
>
|
||||
<i :class="['fas', showPreview ? 'fa-eye-slash' : 'fa-eye']" />
|
||||
{{ showPreview ? '隐藏' : '显示' }}预览
|
||||
{{
|
||||
showPreview
|
||||
? $t('apiKeys.batchApiKeyModal.hide')
|
||||
: $t('apiKeys.batchApiKeyModal.show')
|
||||
}}{{ $t('apiKeys.batchApiKeyModal.preview') }}
|
||||
</button>
|
||||
<span class="text-xs text-gray-500">(最多显示前10个)</span>
|
||||
<span class="text-xs text-gray-500">{{
|
||||
$t('apiKeys.batchApiKeyModal.maxDisplayNote')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -150,13 +171,13 @@
|
||||
@click="downloadApiKeys"
|
||||
>
|
||||
<i class="fas fa-download" />
|
||||
下载所有 API Keys
|
||||
{{ $t('apiKeys.batchApiKeyModal.downloadAll') }}
|
||||
</button>
|
||||
<button
|
||||
class="rounded-xl border border-gray-300 bg-gray-200 px-6 py-3 font-semibold text-gray-800 transition-colors hover:bg-gray-300"
|
||||
@click="handleClose"
|
||||
>
|
||||
我已保存
|
||||
{{ $t('apiKeys.batchApiKeyModal.alreadySaved') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -165,8 +186,7 @@
|
||||
<p class="flex items-start text-xs text-blue-700">
|
||||
<i class="fas fa-info-circle mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span>
|
||||
下载的文件格式为文本文件(.txt),每行包含一个 API Key。
|
||||
请将文件保存在安全的位置,避免泄露。
|
||||
{{ $t('apiKeys.batchApiKeyModal.fileFormatInfo') }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -177,8 +197,11 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { showToast } from '@/utils/toast'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
apiKeys: {
|
||||
type: Array,
|
||||
@@ -203,30 +226,28 @@ const baseName = computed(() => {
|
||||
|
||||
// 获取权限文本
|
||||
const getPermissionText = () => {
|
||||
if (props.apiKeys.length === 0) return '未知'
|
||||
if (props.apiKeys.length === 0) return t('apiKeys.batchApiKeyModal.permissions.unknown')
|
||||
const permissions = props.apiKeys[0].permissions
|
||||
const permissionMap = {
|
||||
all: '全部服务',
|
||||
claude: '仅 Claude',
|
||||
gemini: '仅 Gemini'
|
||||
}
|
||||
return permissionMap[permissions] || permissions
|
||||
const permissionKey = `apiKeys.batchApiKeyModal.permissions.${permissions}`
|
||||
return t(permissionKey, t('apiKeys.batchApiKeyModal.permissions.unknown'))
|
||||
}
|
||||
|
||||
// 获取过期时间文本
|
||||
const getExpiryText = () => {
|
||||
if (props.apiKeys.length === 0) return '未知'
|
||||
if (props.apiKeys.length === 0) return t('apiKeys.batchApiKeyModal.permissions.unknown')
|
||||
const expiresAt = props.apiKeys[0].expiresAt
|
||||
if (!expiresAt) return '永不过期'
|
||||
if (!expiresAt) return t('apiKeys.batchApiKeyModal.neverExpire')
|
||||
|
||||
const expiryDate = new Date(expiresAt)
|
||||
const now = new Date()
|
||||
const diffDays = Math.ceil((expiryDate - now) / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (diffDays <= 7) return `${diffDays}天`
|
||||
if (diffDays <= 30) return `${Math.ceil(diffDays / 7)}周`
|
||||
if (diffDays <= 365) return `${Math.ceil(diffDays / 30)}个月`
|
||||
return `${Math.ceil(diffDays / 365)}年`
|
||||
if (diffDays <= 7) return t('apiKeys.batchApiKeyModal.daysFormat', { days: diffDays })
|
||||
if (diffDays <= 30)
|
||||
return t('apiKeys.batchApiKeyModal.weeksFormat', { weeks: Math.ceil(diffDays / 7) })
|
||||
if (diffDays <= 365)
|
||||
return t('apiKeys.batchApiKeyModal.monthsFormat', { months: Math.ceil(diffDays / 30) })
|
||||
return t('apiKeys.batchApiKeyModal.yearsFormat', { years: Math.ceil(diffDays / 365) })
|
||||
}
|
||||
|
||||
// 切换预览显示
|
||||
@@ -242,7 +263,7 @@ const getPreviewText = () => {
|
||||
})
|
||||
|
||||
if (props.apiKeys.length > 10) {
|
||||
lines.push(`... 还有 ${props.apiKeys.length - 10} 个 API Key`)
|
||||
lines.push(t('apiKeys.batchApiKeyModal.moreKeysNote', { count: props.apiKeys.length - 10 }))
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
@@ -277,26 +298,24 @@ const downloadApiKeys = () => {
|
||||
// 释放 URL 对象
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
showToast('API Keys 文件已下载', 'success')
|
||||
showToast(t('apiKeys.batchApiKeyModal.downloadSuccess'), 'success')
|
||||
}
|
||||
|
||||
// 关闭弹窗(带确认)
|
||||
const handleClose = async () => {
|
||||
if (window.showConfirm) {
|
||||
const confirmed = await window.showConfirm(
|
||||
'关闭提醒',
|
||||
'关闭后将无法再次查看这些 API Key,请确保已经下载并妥善保存。\n\n确定要关闭吗?',
|
||||
'确定关闭',
|
||||
'返回下载'
|
||||
t('apiKeys.batchApiKeyModal.closeReminderTitle'),
|
||||
t('apiKeys.batchApiKeyModal.closeReminderMessage'),
|
||||
t('apiKeys.batchApiKeyModal.confirmCloseButton'),
|
||||
t('apiKeys.batchApiKeyModal.goBackDownloadButton')
|
||||
)
|
||||
if (confirmed) {
|
||||
emit('close')
|
||||
}
|
||||
} else {
|
||||
// 降级方案
|
||||
const confirmed = confirm(
|
||||
'关闭后将无法再次查看这些 API Key,请确保已经下载并妥善保存。\n\n确定要关闭吗?'
|
||||
)
|
||||
const confirmed = confirm(t('apiKeys.batchApiKeyModal.closeReminderMessage'))
|
||||
if (confirmed) {
|
||||
emit('close')
|
||||
}
|
||||
@@ -307,17 +326,17 @@ const handleClose = async () => {
|
||||
const handleDirectClose = async () => {
|
||||
if (window.showConfirm) {
|
||||
const confirmed = await window.showConfirm(
|
||||
'确定要关闭吗?',
|
||||
'您还没有下载 API Keys,关闭后将无法再次查看。\n\n强烈建议您先下载保存。',
|
||||
'仍然关闭',
|
||||
'返回下载'
|
||||
t('apiKeys.batchApiKeyModal.directCloseTitle'),
|
||||
t('apiKeys.batchApiKeyModal.directCloseMessage'),
|
||||
t('apiKeys.batchApiKeyModal.stillCloseButton'),
|
||||
t('apiKeys.batchApiKeyModal.goBackDownloadButton')
|
||||
)
|
||||
if (confirmed) {
|
||||
emit('close')
|
||||
}
|
||||
} else {
|
||||
// 降级方案
|
||||
const confirmed = confirm('您还没有下载 API Keys,关闭后将无法再次查看。\n\n确定要关闭吗?')
|
||||
const confirmed = confirm(t('apiKeys.batchApiKeyModal.directCloseFallbackMessage'))
|
||||
if (confirmed) {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<i class="fas fa-edit text-sm text-white sm:text-base" />
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100 sm:text-xl">
|
||||
批量编辑 API Keys ({{ selectedCount }} 个)
|
||||
{{ $t('apiKeys.batchEditApiKeyModal.title', { count: selectedCount }) }}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
@@ -32,10 +32,11 @@
|
||||
<div class="flex items-start gap-3">
|
||||
<i class="fas fa-info-circle mt-1 text-blue-500" />
|
||||
<div>
|
||||
<p class="text-sm font-medium text-blue-800 dark:text-blue-300">批量编辑说明</p>
|
||||
<p class="text-sm font-medium text-blue-800 dark:text-blue-300">
|
||||
{{ $t('apiKeys.batchEditApiKeyModal.infoTitle') }}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-blue-700 dark:text-blue-400">
|
||||
以下设置将应用到所选的 {{ selectedCount }} 个 API
|
||||
Key。只有填写或修改的字段才会被更新,空白字段将保持原值不变。
|
||||
{{ $t('apiKeys.batchEditApiKeyModal.infoContent', { count: selectedCount }) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,26 +47,34 @@
|
||||
<label
|
||||
class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-3 sm:text-sm"
|
||||
>
|
||||
标签 (批量操作)
|
||||
{{ $t('apiKeys.batchEditApiKeyModal.tagLabel') }}
|
||||
</label>
|
||||
<div class="space-y-4">
|
||||
<!-- 标签操作模式选择 -->
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="tagOperation" class="mr-2" type="radio" value="replace" />
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">替换标签</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.batchEditApiKeyModal.tagOperations.replace')
|
||||
}}</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="tagOperation" class="mr-2" type="radio" value="add" />
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">添加标签</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.batchEditApiKeyModal.tagOperations.add')
|
||||
}}</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="tagOperation" class="mr-2" type="radio" value="remove" />
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">移除标签</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.batchEditApiKeyModal.tagOperations.remove')
|
||||
}}</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="tagOperation" class="mr-2" type="radio" value="none" />
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">不修改标签</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.batchEditApiKeyModal.tagOperations.none')
|
||||
}}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -76,10 +85,10 @@
|
||||
<div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{{
|
||||
tagOperation === 'replace'
|
||||
? '新标签列表:'
|
||||
? $t('apiKeys.batchEditApiKeyModal.newTagsList')
|
||||
: tagOperation === 'add'
|
||||
? '要添加的标签:'
|
||||
: '要移除的标签:'
|
||||
? $t('apiKeys.batchEditApiKeyModal.tagsToAdd')
|
||||
: $t('apiKeys.batchEditApiKeyModal.tagsToRemove')
|
||||
}}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@@ -103,7 +112,7 @@
|
||||
<!-- 可选择的已有标签 -->
|
||||
<div v-if="unselectedTags.length > 0">
|
||||
<div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
点击选择已有标签:
|
||||
{{ $t('apiKeys.batchEditApiKeyModal.clickToSelectTags') }}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
@@ -122,13 +131,13 @@
|
||||
<!-- 创建新标签 -->
|
||||
<div>
|
||||
<div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
创建新标签:
|
||||
{{ $t('apiKeys.batchEditApiKeyModal.createNewTag') }}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="newTag"
|
||||
class="form-input flex-1 border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
||||
placeholder="输入新标签名称"
|
||||
:placeholder="$t('apiKeys.batchEditApiKeyModal.inputNewTagPlaceholder')"
|
||||
type="text"
|
||||
@keypress.enter.prevent="addTag"
|
||||
/>
|
||||
@@ -155,46 +164,48 @@
|
||||
>
|
||||
<i class="fas fa-tachometer-alt text-xs text-white" />
|
||||
</div>
|
||||
<h4 class="text-sm font-semibold text-gray-800 dark:text-gray-200">速率限制设置</h4>
|
||||
<h4 class="text-sm font-semibold text-gray-800 dark:text-gray-200">
|
||||
{{ $t('apiKeys.batchEditApiKeyModal.rateLimitTitle') }}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||
时间窗口 (分钟)
|
||||
{{ $t('apiKeys.batchEditApiKeyModal.rateLimitWindow') }}
|
||||
</label>
|
||||
<input
|
||||
v-model="form.rateLimitWindow"
|
||||
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
||||
min="1"
|
||||
placeholder="不修改"
|
||||
:placeholder="$t('apiKeys.batchEditApiKeyModal.noModifyPlaceholder')"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300"
|
||||
>请求次数限制</label
|
||||
>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.batchEditApiKeyModal.rateLimitRequests')
|
||||
}}</label>
|
||||
<input
|
||||
v-model="form.rateLimitRequests"
|
||||
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
||||
min="1"
|
||||
placeholder="不修改"
|
||||
:placeholder="$t('apiKeys.batchEditApiKeyModal.noModifyPlaceholder')"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300"
|
||||
>费用限制 (美元)</label
|
||||
>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.batchEditApiKeyModal.rateLimitCost')
|
||||
}}</label>
|
||||
<input
|
||||
v-model="form.rateLimitCost"
|
||||
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
||||
min="0"
|
||||
placeholder="不修改"
|
||||
:placeholder="$t('apiKeys.batchEditApiKeyModal.noModifyPlaceholder')"
|
||||
step="0.01"
|
||||
type="number"
|
||||
/>
|
||||
@@ -206,13 +217,13 @@
|
||||
<!-- 每日费用限制 -->
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
每日费用限制 (美元)
|
||||
{{ $t('apiKeys.batchEditApiKeyModal.dailyCostLimit') }}
|
||||
</label>
|
||||
<input
|
||||
v-model="form.dailyCostLimit"
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
||||
min="0"
|
||||
placeholder="不修改 (0 表示无限制)"
|
||||
:placeholder="$t('apiKeys.batchEditApiKeyModal.dailyCostLimitPlaceholder')"
|
||||
step="0.01"
|
||||
type="number"
|
||||
/>
|
||||
@@ -221,31 +232,31 @@
|
||||
<!-- Opus 模型周费用限制 -->
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
Opus 模型周费用限制 (美元)
|
||||
{{ $t('apiKeys.batchEditApiKeyModal.weeklyOpusCostLimit') }}
|
||||
</label>
|
||||
<input
|
||||
v-model="form.weeklyOpusCostLimit"
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
||||
min="0"
|
||||
placeholder="不修改 (0 表示无限制)"
|
||||
:placeholder="$t('apiKeys.batchEditApiKeyModal.weeklyOpusCostLimitPlaceholder')"
|
||||
step="0.01"
|
||||
type="number"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
设置 Opus 模型的周费用限制(周一到周日),仅限 Claude 官方账户
|
||||
{{ $t('apiKeys.batchEditApiKeyModal.opusLimitDescription') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 并发限制 -->
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>并发限制</label
|
||||
>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.batchEditApiKeyModal.concurrencyLimit')
|
||||
}}</label>
|
||||
<input
|
||||
v-model="form.concurrencyLimit"
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
||||
min="0"
|
||||
placeholder="不修改 (0 表示无限制)"
|
||||
:placeholder="$t('apiKeys.batchEditApiKeyModal.concurrencyLimitPlaceholder')"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
@@ -253,19 +264,27 @@
|
||||
<!-- 激活状态 -->
|
||||
<div>
|
||||
<div class="mb-3 flex items-center gap-4">
|
||||
<label class="text-sm font-semibold text-gray-700 dark:text-gray-300">激活状态</label>
|
||||
<label class="text-sm font-semibold text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.batchEditApiKeyModal.activeStatus')
|
||||
}}</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="form.isActive" class="mr-2" type="radio" :value="true" />
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">激活</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.batchEditApiKeyModal.statusOptions.active')
|
||||
}}</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="form.isActive" class="mr-2" type="radio" :value="false" />
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">禁用</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.batchEditApiKeyModal.statusOptions.disabled')
|
||||
}}</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="form.isActive" class="mr-2" type="radio" :value="null" />
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">不修改</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.batchEditApiKeyModal.statusOptions.noChange')
|
||||
}}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -273,29 +292,39 @@
|
||||
|
||||
<!-- 服务权限 -->
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>服务权限</label
|
||||
>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.batchEditApiKeyModal.servicePermissions')
|
||||
}}</label>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="form.permissions" class="mr-2" type="radio" value="" />
|
||||
<span class="text-sm text-gray-700">不修改</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.batchEditApiKeyModal.permissionOptions.noChange')
|
||||
}}</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="form.permissions" class="mr-2" type="radio" value="all" />
|
||||
<span class="text-sm text-gray-700">全部服务</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.batchEditApiKeyModal.permissionOptions.all')
|
||||
}}</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="form.permissions" class="mr-2" type="radio" value="claude" />
|
||||
<span class="text-sm text-gray-700">仅 Claude</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.batchEditApiKeyModal.permissionOptions.claude')
|
||||
}}</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="form.permissions" class="mr-2" type="radio" value="gemini" />
|
||||
<span class="text-sm text-gray-700">仅 Gemini</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.batchEditApiKeyModal.permissionOptions.gemini')
|
||||
}}</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="form.permissions" class="mr-2" type="radio" value="openai" />
|
||||
<span class="text-sm text-gray-700">仅 OpenAI</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.batchEditApiKeyModal.permissionOptions.openai')
|
||||
}}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -303,13 +332,13 @@
|
||||
<!-- 专属账号绑定 -->
|
||||
<div>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label class="text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>专属账号绑定</label
|
||||
>
|
||||
<label class="text-sm font-semibold text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.batchEditApiKeyModal.accountBinding')
|
||||
}}</label>
|
||||
<button
|
||||
class="flex items-center gap-1 text-sm text-blue-600 transition-colors hover:text-blue-800 disabled:cursor-not-allowed disabled:opacity-50 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
:disabled="accountsLoading"
|
||||
title="刷新账号列表"
|
||||
:title="$t('apiKeys.batchEditApiKeyModal.refreshAccounts')"
|
||||
type="button"
|
||||
@click="refreshAccounts"
|
||||
>
|
||||
@@ -320,31 +349,46 @@
|
||||
'text-xs'
|
||||
]"
|
||||
/>
|
||||
<span>{{ accountsLoading ? '刷新中...' : '刷新账号' }}</span>
|
||||
<span>{{
|
||||
accountsLoading
|
||||
? $t('apiKeys.batchEditApiKeyModal.refreshing')
|
||||
: $t('apiKeys.batchEditApiKeyModal.refreshAccounts')
|
||||
}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
>Claude 专属账号</label
|
||||
>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
|
||||
$t('apiKeys.batchEditApiKeyModal.claudeAccount')
|
||||
}}</label>
|
||||
<select
|
||||
v-model="form.claudeAccountId"
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
||||
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
|
||||
>
|
||||
<option value="">不修改</option>
|
||||
<option value="SHARED_POOL">使用共享账号池</option>
|
||||
<optgroup v-if="localAccounts.claudeGroups.length > 0" label="账号分组">
|
||||
<option value="">
|
||||
{{ $t('apiKeys.batchEditApiKeyModal.accountOptions.noChange') }}
|
||||
</option>
|
||||
<option value="SHARED_POOL">
|
||||
{{ $t('apiKeys.batchEditApiKeyModal.accountOptions.sharedPool') }}
|
||||
</option>
|
||||
<optgroup
|
||||
v-if="localAccounts.claudeGroups.length > 0"
|
||||
:label="$t('apiKeys.batchEditApiKeyModal.optgroupLabels.accountGroups')"
|
||||
>
|
||||
<option
|
||||
v-for="group in localAccounts.claudeGroups"
|
||||
:key="group.id"
|
||||
:value="`group:${group.id}`"
|
||||
>
|
||||
分组 - {{ group.name }}
|
||||
{{ $t('apiKeys.batchEditApiKeyModal.accountOptions.groupPrefix')
|
||||
}}{{ group.name }}
|
||||
</option>
|
||||
</optgroup>
|
||||
<optgroup v-if="localAccounts.claude.length > 0" label="专属账号">
|
||||
<optgroup
|
||||
v-if="localAccounts.claude.length > 0"
|
||||
:label="$t('apiKeys.batchEditApiKeyModal.optgroupLabels.dedicatedAccounts')"
|
||||
>
|
||||
<option
|
||||
v-for="account in localAccounts.claude"
|
||||
:key="account.id"
|
||||
@@ -360,26 +404,37 @@
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
>Gemini 专属账号</label
|
||||
>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
|
||||
$t('apiKeys.batchEditApiKeyModal.geminiAccount')
|
||||
}}</label>
|
||||
<select
|
||||
v-model="form.geminiAccountId"
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
||||
:disabled="form.permissions === 'claude' || form.permissions === 'openai'"
|
||||
>
|
||||
<option value="">不修改</option>
|
||||
<option value="SHARED_POOL">使用共享账号池</option>
|
||||
<optgroup v-if="localAccounts.geminiGroups.length > 0" label="账号分组">
|
||||
<option value="">
|
||||
{{ $t('apiKeys.batchEditApiKeyModal.accountOptions.noChange') }}
|
||||
</option>
|
||||
<option value="SHARED_POOL">
|
||||
{{ $t('apiKeys.batchEditApiKeyModal.accountOptions.sharedPool') }}
|
||||
</option>
|
||||
<optgroup
|
||||
v-if="localAccounts.geminiGroups.length > 0"
|
||||
:label="$t('apiKeys.batchEditApiKeyModal.optgroupLabels.accountGroups')"
|
||||
>
|
||||
<option
|
||||
v-for="group in localAccounts.geminiGroups"
|
||||
:key="group.id"
|
||||
:value="`group:${group.id}`"
|
||||
>
|
||||
分组 - {{ group.name }}
|
||||
{{ $t('apiKeys.batchEditApiKeyModal.accountOptions.groupPrefix')
|
||||
}}{{ group.name }}
|
||||
</option>
|
||||
</optgroup>
|
||||
<optgroup v-if="localAccounts.gemini.length > 0" label="专属账号">
|
||||
<optgroup
|
||||
v-if="localAccounts.gemini.length > 0"
|
||||
:label="$t('apiKeys.batchEditApiKeyModal.optgroupLabels.dedicatedAccounts')"
|
||||
>
|
||||
<option
|
||||
v-for="account in localAccounts.gemini"
|
||||
:key="account.id"
|
||||
@@ -391,26 +446,37 @@
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
>OpenAI 专属账号</label
|
||||
>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
|
||||
$t('apiKeys.batchEditApiKeyModal.openaiAccount')
|
||||
}}</label>
|
||||
<select
|
||||
v-model="form.openaiAccountId"
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
||||
:disabled="form.permissions === 'claude' || form.permissions === 'gemini'"
|
||||
>
|
||||
<option value="">不修改</option>
|
||||
<option value="SHARED_POOL">使用共享账号池</option>
|
||||
<optgroup v-if="localAccounts.openaiGroups.length > 0" label="账号分组">
|
||||
<option value="">
|
||||
{{ $t('apiKeys.batchEditApiKeyModal.accountOptions.noChange') }}
|
||||
</option>
|
||||
<option value="SHARED_POOL">
|
||||
{{ $t('apiKeys.batchEditApiKeyModal.accountOptions.sharedPool') }}
|
||||
</option>
|
||||
<optgroup
|
||||
v-if="localAccounts.openaiGroups.length > 0"
|
||||
:label="$t('apiKeys.batchEditApiKeyModal.optgroupLabels.accountGroups')"
|
||||
>
|
||||
<option
|
||||
v-for="group in localAccounts.openaiGroups"
|
||||
:key="group.id"
|
||||
:value="`group:${group.id}`"
|
||||
>
|
||||
分组 - {{ group.name }}
|
||||
{{ $t('apiKeys.batchEditApiKeyModal.accountOptions.groupPrefix')
|
||||
}}{{ group.name }}
|
||||
</option>
|
||||
</optgroup>
|
||||
<optgroup v-if="localAccounts.openai.length > 0" label="专属账号">
|
||||
<optgroup
|
||||
v-if="localAccounts.openai.length > 0"
|
||||
:label="$t('apiKeys.batchEditApiKeyModal.optgroupLabels.dedicatedAccounts')"
|
||||
>
|
||||
<option
|
||||
v-for="account in localAccounts.openai"
|
||||
:key="account.id"
|
||||
@@ -422,17 +488,24 @@
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
>Bedrock 专属账号</label
|
||||
>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
|
||||
$t('apiKeys.batchEditApiKeyModal.bedrockAccount')
|
||||
}}</label>
|
||||
<select
|
||||
v-model="form.bedrockAccountId"
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
||||
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
|
||||
>
|
||||
<option value="">不修改</option>
|
||||
<option value="SHARED_POOL">使用共享账号池</option>
|
||||
<optgroup v-if="localAccounts.bedrock.length > 0" label="专属账号">
|
||||
<option value="">
|
||||
{{ $t('apiKeys.batchEditApiKeyModal.accountOptions.noChange') }}
|
||||
</option>
|
||||
<option value="SHARED_POOL">
|
||||
{{ $t('apiKeys.batchEditApiKeyModal.accountOptions.sharedPool') }}
|
||||
</option>
|
||||
<optgroup
|
||||
v-if="localAccounts.bedrock.length > 0"
|
||||
:label="$t('apiKeys.batchEditApiKeyModal.optgroupLabels.dedicatedAccounts')"
|
||||
>
|
||||
<option
|
||||
v-for="account in localAccounts.bedrock"
|
||||
:key="account.id"
|
||||
@@ -452,7 +525,7 @@
|
||||
type="button"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
取消
|
||||
{{ $t('apiKeys.batchEditApiKeyModal.cancel') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary flex-1 px-6 py-3 font-semibold"
|
||||
@@ -461,7 +534,11 @@
|
||||
>
|
||||
<div v-if="loading" class="loading-spinner mr-2" />
|
||||
<i v-else class="fas fa-save mr-2" />
|
||||
{{ loading ? '保存中...' : '批量保存' }}
|
||||
{{
|
||||
loading
|
||||
? $t('apiKeys.batchEditApiKeyModal.saving')
|
||||
: $t('apiKeys.batchEditApiKeyModal.batchSave')
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -472,10 +549,13 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { showToast } from '@/utils/toast'
|
||||
import { useApiKeysStore } from '@/stores/apiKeys'
|
||||
import { apiClient } from '@/config/api'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
selectedKeys: {
|
||||
type: Array,
|
||||
@@ -620,9 +700,9 @@ const refreshAccounts = async () => {
|
||||
localAccounts.value.openaiGroups = allGroups.filter((g) => g.platform === 'openai')
|
||||
}
|
||||
|
||||
showToast('账号列表已刷新', 'success')
|
||||
showToast(t('apiKeys.batchEditApiKeyModal.refreshAccountsSuccess'), 'success')
|
||||
} catch (error) {
|
||||
showToast('刷新账号列表失败', 'error')
|
||||
showToast(t('apiKeys.batchEditApiKeyModal.refreshAccountsFailed'), 'error')
|
||||
} finally {
|
||||
accountsLoading.value = false
|
||||
}
|
||||
@@ -722,24 +802,33 @@ const batchUpdateApiKeys = async () => {
|
||||
const { successCount, failedCount, errors } = result.data
|
||||
|
||||
if (successCount > 0) {
|
||||
showToast(`成功批量编辑 ${successCount} 个 API Keys`, 'success')
|
||||
showToast(
|
||||
t('apiKeys.batchEditApiKeyModal.batchEditSuccess', { count: successCount }),
|
||||
'success'
|
||||
)
|
||||
|
||||
if (failedCount > 0) {
|
||||
const errorMessages = errors.map((e) => `${e.keyId}: ${e.error}`).join('\n')
|
||||
showToast(`${failedCount} 个编辑失败:\n${errorMessages}`, 'warning')
|
||||
showToast(
|
||||
t('apiKeys.batchEditApiKeyModal.batchEditPartialFail', {
|
||||
failedCount,
|
||||
errors: errorMessages
|
||||
}),
|
||||
'warning'
|
||||
)
|
||||
}
|
||||
} else {
|
||||
showToast('所有 API Keys 编辑失败', 'error')
|
||||
showToast(t('apiKeys.batchEditApiKeyModal.batchEditAllFailed'), 'error')
|
||||
}
|
||||
|
||||
emit('success')
|
||||
emit('close')
|
||||
} else {
|
||||
showToast(result.message || '批量编辑失败', 'error')
|
||||
showToast(result.message || t('apiKeys.batchEditApiKeyModal.batchEditFailed'), 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('批量编辑失败', 'error')
|
||||
console.error('批量编辑 API Keys 失败:', error)
|
||||
showToast(t('apiKeys.batchEditApiKeyModal.batchEditFailed'), 'error')
|
||||
console.error(t('apiKeys.batchEditApiKeyModal.batchEditErrorLog'), error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<i class="fas fa-key text-sm text-white sm:text-base" />
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100 sm:text-xl">
|
||||
创建新的 API Key
|
||||
{{ $t('apiKeys.createApiKeyModal.title') }}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
@@ -37,7 +37,7 @@
|
||||
>
|
||||
<label
|
||||
class="flex h-full items-center text-xs font-semibold text-gray-700 dark:text-gray-300 sm:text-sm"
|
||||
>创建类型</label
|
||||
>{{ $t('apiKeys.createApiKeyModal.createType') }}</label
|
||||
>
|
||||
<div class="flex items-center gap-3 sm:gap-4">
|
||||
<label class="flex cursor-pointer items-center">
|
||||
@@ -51,7 +51,7 @@
|
||||
class="flex items-center text-xs text-gray-700 dark:text-gray-300 sm:text-sm"
|
||||
>
|
||||
<i class="fas fa-key mr-1 text-xs" />
|
||||
单个创建
|
||||
{{ $t('apiKeys.createApiKeyModal.singleCreate') }}
|
||||
</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
@@ -65,7 +65,7 @@
|
||||
class="flex items-center text-xs text-gray-700 dark:text-gray-300 sm:text-sm"
|
||||
>
|
||||
<i class="fas fa-layer-group mr-1 text-xs" />
|
||||
批量创建
|
||||
{{ $t('apiKeys.createApiKeyModal.batchCreate') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -75,32 +75,30 @@
|
||||
<div v-if="form.createType === 'batch'" class="mt-3">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex-1">
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400"
|
||||
>创建数量</label
|
||||
>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">{{
|
||||
$t('apiKeys.createApiKeyModal.batchCount')
|
||||
}}</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
v-model.number="form.batchCount"
|
||||
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
max="500"
|
||||
min="2"
|
||||
placeholder="输入数量 (2-500)"
|
||||
:placeholder="$t('apiKeys.createApiKeyModal.batchCountPlaceholder')"
|
||||
required
|
||||
type="number"
|
||||
/>
|
||||
<div class="whitespace-nowrap text-xs text-gray-500 dark:text-gray-400">
|
||||
最大支持 500 个
|
||||
{{ $t('apiKeys.createApiKeyModal.maxSupported') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 flex items-start text-xs text-amber-600 dark:text-amber-400">
|
||||
<i class="fas fa-info-circle mr-1 mt-0.5 flex-shrink-0" />
|
||||
<span
|
||||
>批量创建时,每个 Key 的名称会自动添加序号后缀,例如:{{
|
||||
form.name || 'MyKey'
|
||||
}}_1, {{ form.name || 'MyKey' }}_2 ...</span
|
||||
>
|
||||
<span>{{
|
||||
$t('apiKeys.createApiKeyModal.batchHint', { name: form.name || 'MyKey' })
|
||||
}}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -108,23 +106,24 @@
|
||||
<div>
|
||||
<label
|
||||
class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-2 sm:text-sm"
|
||||
>名称 <span class="text-red-500">*</span></label
|
||||
>{{ $t('apiKeys.createApiKeyModal.name') }}
|
||||
<span class="text-red-500">{{
|
||||
$t('apiKeys.createApiKeyModal.nameRequired')
|
||||
}}</span></label
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
v-model="form.name"
|
||||
class="form-input flex-1 border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
:class="{ 'border-red-500': errors.name }"
|
||||
:placeholder="
|
||||
form.createType === 'batch'
|
||||
? '输入基础名称(将自动添加序号)'
|
||||
: '为您的 API Key 取一个名称'
|
||||
"
|
||||
required
|
||||
type="text"
|
||||
@input="errors.name = ''"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
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="{ 'border-red-500': errors.name }"
|
||||
:placeholder="
|
||||
form.createType === 'batch'
|
||||
? $t('apiKeys.createApiKeyModal.batchNamePlaceholder')
|
||||
: $t('apiKeys.createApiKeyModal.singleNamePlaceholder')
|
||||
"
|
||||
required
|
||||
type="text"
|
||||
@input="errors.name = ''"
|
||||
/>
|
||||
<p v-if="errors.name" class="mt-1 text-xs text-red-500 dark:text-red-400">
|
||||
{{ errors.name }}
|
||||
</p>
|
||||
@@ -132,14 +131,14 @@
|
||||
|
||||
<!-- 标签 -->
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>标签</label
|
||||
>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.createApiKeyModal.tags')
|
||||
}}</label>
|
||||
<div class="space-y-4">
|
||||
<!-- 已选择的标签 -->
|
||||
<div v-if="form.tags.length > 0">
|
||||
<div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
已选择的标签:
|
||||
{{ $t('apiKeys.createApiKeyModal.selectedTags') }}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span
|
||||
@@ -162,7 +161,7 @@
|
||||
<!-- 可选择的已有标签 -->
|
||||
<div v-if="unselectedTags.length > 0">
|
||||
<div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
点击选择已有标签:
|
||||
{{ $t('apiKeys.createApiKeyModal.clickToSelectTags') }}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
@@ -181,13 +180,13 @@
|
||||
<!-- 创建新标签 -->
|
||||
<div>
|
||||
<div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
创建新标签:
|
||||
{{ $t('apiKeys.createApiKeyModal.createNewTag') }}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="newTag"
|
||||
class="form-input flex-1 border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
placeholder="输入新标签名称"
|
||||
:placeholder="$t('apiKeys.createApiKeyModal.newTagPlaceholder')"
|
||||
type="text"
|
||||
@keypress.enter.prevent="addTag"
|
||||
/>
|
||||
@@ -202,7 +201,7 @@
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
用于标记不同团队或用途,方便筛选管理
|
||||
{{ $t('apiKeys.createApiKeyModal.tagHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -218,68 +217,76 @@
|
||||
<i class="fas fa-tachometer-alt text-xs text-white" />
|
||||
</div>
|
||||
<h4 class="text-sm font-semibold text-gray-800 dark:text-gray-200">
|
||||
速率限制设置 (可选)
|
||||
{{ $t('apiKeys.createApiKeyModal.rateLimitTitle') }}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300"
|
||||
>时间窗口 (分钟)</label
|
||||
>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.createApiKeyModal.rateLimitWindow')
|
||||
}}</label>
|
||||
<input
|
||||
v-model="form.rateLimitWindow"
|
||||
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
min="1"
|
||||
placeholder="无限制"
|
||||
:placeholder="$t('apiKeys.createApiKeyModal.rateLimitWindowPlaceholder')"
|
||||
type="number"
|
||||
/>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">时间段单位</p>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $t('apiKeys.createApiKeyModal.rateLimitWindowHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300"
|
||||
>请求次数限制</label
|
||||
>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.createApiKeyModal.rateLimitRequests')
|
||||
}}</label>
|
||||
<input
|
||||
v-model="form.rateLimitRequests"
|
||||
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
min="1"
|
||||
placeholder="无限制"
|
||||
:placeholder="$t('apiKeys.createApiKeyModal.rateLimitRequestsPlaceholder')"
|
||||
type="number"
|
||||
/>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">窗口内最大请求</p>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $t('apiKeys.createApiKeyModal.rateLimitRequestsHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300"
|
||||
>费用限制 (美元)</label
|
||||
>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.createApiKeyModal.rateLimitCost')
|
||||
}}</label>
|
||||
<input
|
||||
v-model="form.rateLimitCost"
|
||||
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
min="0"
|
||||
placeholder="无限制"
|
||||
:placeholder="$t('apiKeys.createApiKeyModal.rateLimitCostPlaceholder')"
|
||||
step="0.01"
|
||||
type="number"
|
||||
/>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">窗口内最大费用</p>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $t('apiKeys.createApiKeyModal.rateLimitCostHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 示例说明 -->
|
||||
<div class="rounded-lg bg-blue-100 p-2 dark:bg-blue-900/30">
|
||||
<h5 class="mb-1 text-xs font-semibold text-blue-800 dark:text-blue-400">
|
||||
💡 使用示例
|
||||
{{ $t('apiKeys.createApiKeyModal.exampleTitle') }}
|
||||
</h5>
|
||||
<div class="space-y-0.5 text-xs text-blue-700 dark:text-blue-300">
|
||||
<div>
|
||||
<strong>示例1:</strong> 时间窗口=60,请求次数=1000 → 每60分钟最多1000次请求
|
||||
<strong>{{ $t('apiKeys.createApiKeyModal.example1') }}</strong>
|
||||
</div>
|
||||
<div><strong>示例2:</strong> 时间窗口=1,费用=0.1 → 每分钟最多$0.1费用</div>
|
||||
<div>
|
||||
<strong>示例3:</strong> 窗口=30,请求=50,费用=5 → 每30分钟50次请求且不超$5费用
|
||||
<strong>{{ $t('apiKeys.createApiKeyModal.example2') }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<strong>{{ $t('apiKeys.createApiKeyModal.example3') }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -287,9 +294,9 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>每日费用限制 (美元)</label
|
||||
>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.createApiKeyModal.dailyCostLimit')
|
||||
}}</label>
|
||||
<div class="space-y-2">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@@ -318,27 +325,27 @@
|
||||
type="button"
|
||||
@click="form.dailyCostLimit = ''"
|
||||
>
|
||||
自定义
|
||||
{{ $t('apiKeys.createApiKeyModal.custom') }}
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
v-model="form.dailyCostLimit"
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
min="0"
|
||||
placeholder="0 表示无限制"
|
||||
:placeholder="$t('apiKeys.createApiKeyModal.dailyCostLimitPlaceholder')"
|
||||
step="0.01"
|
||||
type="number"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
设置此 API Key 每日的费用限制,超过限制将拒绝请求,0 或留空表示无限制
|
||||
{{ $t('apiKeys.createApiKeyModal.dailyCostHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>Opus 模型周费用限制 (美元)</label
|
||||
>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.createApiKeyModal.weeklyOpusCostLimit')
|
||||
}}</label>
|
||||
<div class="space-y-2">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@@ -367,55 +374,55 @@
|
||||
type="button"
|
||||
@click="form.weeklyOpusCostLimit = ''"
|
||||
>
|
||||
自定义
|
||||
{{ $t('apiKeys.createApiKeyModal.custom') }}
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
v-model="form.weeklyOpusCostLimit"
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
min="0"
|
||||
placeholder="0 表示无限制"
|
||||
:placeholder="$t('apiKeys.createApiKeyModal.weeklyOpusCostLimitPlaceholder')"
|
||||
step="0.01"
|
||||
type="number"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
设置 Opus 模型的周费用限制(周一到周日),仅限 Claude 官方账户,0 或留空表示无限制
|
||||
{{ $t('apiKeys.createApiKeyModal.weeklyOpusHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>并发限制 (可选)</label
|
||||
>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.createApiKeyModal.concurrencyLimit')
|
||||
}}</label>
|
||||
<input
|
||||
v-model="form.concurrencyLimit"
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
min="0"
|
||||
placeholder="0 表示无限制"
|
||||
:placeholder="$t('apiKeys.createApiKeyModal.concurrencyLimitPlaceholder')"
|
||||
type="number"
|
||||
/>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
设置此 API Key 可同时处理的最大请求数,0 或留空表示无限制
|
||||
{{ $t('apiKeys.createApiKeyModal.concurrencyHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>备注 (可选)</label
|
||||
>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.createApiKeyModal.description')
|
||||
}}</label>
|
||||
<textarea
|
||||
v-model="form.description"
|
||||
class="form-input w-full resize-none border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
placeholder="描述此 API Key 的用途..."
|
||||
:placeholder="$t('apiKeys.createApiKeyModal.descriptionPlaceholder')"
|
||||
rows="2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>过期设置</label
|
||||
>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.createApiKeyModal.expirationSettings')
|
||||
}}</label>
|
||||
<!-- 过期模式选择 -->
|
||||
<div
|
||||
class="mb-3 rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800"
|
||||
@@ -428,7 +435,9 @@
|
||||
type="radio"
|
||||
value="fixed"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">固定时间过期</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.createApiKeyModal.fixedTimeExpiry')
|
||||
}}</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input
|
||||
@@ -437,17 +446,19 @@
|
||||
type="radio"
|
||||
value="activation"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">首次使用后激活</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.createApiKeyModal.activationExpiry')
|
||||
}}</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span v-if="form.expirationMode === 'fixed'">
|
||||
<i class="fas fa-info-circle mr-1" />
|
||||
固定时间模式:Key 创建后立即生效,按设定时间过期
|
||||
{{ $t('apiKeys.createApiKeyModal.fixedModeHint') }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<i class="fas fa-info-circle mr-1" />
|
||||
激活模式:Key 首次使用时激活,激活后按设定天数过期(适合批量销售)
|
||||
{{ $t('apiKeys.createApiKeyModal.activationModeHint') }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -459,14 +470,14 @@
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||
@change="updateExpireAt"
|
||||
>
|
||||
<option value="">永不过期</option>
|
||||
<option value="1d">1 天</option>
|
||||
<option value="7d">7 天</option>
|
||||
<option value="30d">30 天</option>
|
||||
<option value="90d">90 天</option>
|
||||
<option value="180d">180 天</option>
|
||||
<option value="365d">365 天</option>
|
||||
<option value="custom">自定义日期</option>
|
||||
<option value="">{{ $t('apiKeys.createApiKeyModal.neverExpire') }}</option>
|
||||
<option value="1d">{{ $t('apiKeys.createApiKeyModal.1d') }}</option>
|
||||
<option value="7d">{{ $t('apiKeys.createApiKeyModal.7d') }}</option>
|
||||
<option value="30d">{{ $t('apiKeys.createApiKeyModal.30d') }}</option>
|
||||
<option value="90d">{{ $t('apiKeys.createApiKeyModal.90d') }}</option>
|
||||
<option value="180d">{{ $t('apiKeys.createApiKeyModal.180d') }}</option>
|
||||
<option value="365d">{{ $t('apiKeys.createApiKeyModal.365d') }}</option>
|
||||
<option value="custom">{{ $t('apiKeys.createApiKeyModal.customDate') }}</option>
|
||||
</select>
|
||||
<div v-if="form.expireDuration === 'custom'" class="mt-3">
|
||||
<input
|
||||
@@ -478,7 +489,11 @@
|
||||
/>
|
||||
</div>
|
||||
<p v-if="form.expiresAt" class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
将于 {{ formatExpireDate(form.expiresAt) }} 过期
|
||||
{{
|
||||
$t('apiKeys.createApiKeyModal.willExpireOn', {
|
||||
date: formatExpireDate(form.expiresAt)
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -490,10 +505,12 @@
|
||||
class="form-input flex-1 border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||
max="3650"
|
||||
min="1"
|
||||
placeholder="输入天数"
|
||||
:placeholder="$t('apiKeys.createApiKeyModal.activationDays')"
|
||||
type="number"
|
||||
/>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">天</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">{{
|
||||
$t('apiKeys.createApiKeyModal.daysUnit')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<button
|
||||
@@ -503,20 +520,24 @@
|
||||
type="button"
|
||||
@click="form.activationDays = days"
|
||||
>
|
||||
{{ days }}天
|
||||
{{ days }}{{ $t('apiKeys.createApiKeyModal.daysUnit') }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
<i class="fas fa-clock mr-1" />
|
||||
Key 将在首次使用后激活,激活后 {{ form.activationDays || 30 }} 天过期
|
||||
{{
|
||||
$t('apiKeys.createApiKeyModal.activationHint', {
|
||||
days: form.activationDays || 30
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>服务权限</label
|
||||
>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.createApiKeyModal.servicePermissions')
|
||||
}}</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input
|
||||
@@ -525,7 +546,9 @@
|
||||
type="radio"
|
||||
value="all"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">全部服务</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.createApiKeyModal.allServices')
|
||||
}}</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input
|
||||
@@ -534,7 +557,9 @@
|
||||
type="radio"
|
||||
value="claude"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">仅 Claude</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.createApiKeyModal.claudeOnly')
|
||||
}}</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input
|
||||
@@ -543,7 +568,9 @@
|
||||
type="radio"
|
||||
value="gemini"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">仅 Gemini</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.createApiKeyModal.geminiOnly')
|
||||
}}</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input
|
||||
@@ -552,23 +579,25 @@
|
||||
type="radio"
|
||||
value="openai"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">仅 OpenAI</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.createApiKeyModal.openaiOnly')
|
||||
}}</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
控制此 API Key 可以访问哪些服务
|
||||
{{ $t('apiKeys.createApiKeyModal.permissionHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<label class="text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>专属账号绑定 (可选)</label
|
||||
>
|
||||
<label class="text-sm font-semibold text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.createApiKeyModal.dedicatedAccountBinding')
|
||||
}}</label>
|
||||
<button
|
||||
class="flex items-center gap-1 text-sm text-blue-600 transition-colors hover:text-blue-800 disabled:cursor-not-allowed disabled:opacity-50 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
:disabled="accountsLoading"
|
||||
title="刷新账号列表"
|
||||
title="{{ $t('apiKeys.createApiKeyModal.refreshAccounts') }}"
|
||||
type="button"
|
||||
@click="refreshAccounts"
|
||||
>
|
||||
@@ -579,69 +608,73 @@
|
||||
'text-xs'
|
||||
]"
|
||||
/>
|
||||
<span>{{ accountsLoading ? '刷新中...' : '刷新账号' }}</span>
|
||||
<span>{{
|
||||
accountsLoading
|
||||
? $t('apiKeys.createApiKeyModal.refreshing')
|
||||
: $t('apiKeys.createApiKeyModal.refreshAccounts')
|
||||
}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
>Claude 专属账号</label
|
||||
>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
|
||||
$t('apiKeys.createApiKeyModal.claudeDedicatedAccount')
|
||||
}}</label>
|
||||
<AccountSelector
|
||||
v-model="form.claudeAccountId"
|
||||
:accounts="localAccounts.claude"
|
||||
default-option-text="使用共享账号池"
|
||||
:default-option-text="$t('apiKeys.createApiKeyModal.useSharedPool')"
|
||||
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
|
||||
:groups="localAccounts.claudeGroups"
|
||||
placeholder="请选择Claude账号"
|
||||
:placeholder="$t('apiKeys.createApiKeyModal.selectClaudeAccount')"
|
||||
platform="claude"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
>Gemini 专属账号</label
|
||||
>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
|
||||
$t('apiKeys.createApiKeyModal.geminiDedicatedAccount')
|
||||
}}</label>
|
||||
<AccountSelector
|
||||
v-model="form.geminiAccountId"
|
||||
:accounts="localAccounts.gemini"
|
||||
default-option-text="使用共享账号池"
|
||||
:default-option-text="$t('apiKeys.createApiKeyModal.useSharedPool')"
|
||||
:disabled="form.permissions === 'claude' || form.permissions === 'openai'"
|
||||
:groups="localAccounts.geminiGroups"
|
||||
placeholder="请选择Gemini账号"
|
||||
:placeholder="$t('apiKeys.createApiKeyModal.selectGeminiAccount')"
|
||||
platform="gemini"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
>OpenAI 专属账号</label
|
||||
>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
|
||||
$t('apiKeys.createApiKeyModal.openaiDedicatedAccount')
|
||||
}}</label>
|
||||
<AccountSelector
|
||||
v-model="form.openaiAccountId"
|
||||
:accounts="localAccounts.openai"
|
||||
default-option-text="使用共享账号池"
|
||||
:default-option-text="$t('apiKeys.createApiKeyModal.useSharedPool')"
|
||||
:disabled="form.permissions === 'claude' || form.permissions === 'gemini'"
|
||||
:groups="localAccounts.openaiGroups"
|
||||
placeholder="请选择OpenAI账号"
|
||||
:placeholder="$t('apiKeys.createApiKeyModal.selectOpenaiAccount')"
|
||||
platform="openai"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
>Bedrock 专属账号</label
|
||||
>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
|
||||
$t('apiKeys.createApiKeyModal.bedrockDedicatedAccount')
|
||||
}}</label>
|
||||
<AccountSelector
|
||||
v-model="form.bedrockAccountId"
|
||||
:accounts="localAccounts.bedrock"
|
||||
default-option-text="使用共享账号池"
|
||||
:default-option-text="$t('apiKeys.createApiKeyModal.useSharedPool')"
|
||||
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
|
||||
:groups="[]"
|
||||
placeholder="请选择Bedrock账号"
|
||||
:placeholder="$t('apiKeys.createApiKeyModal.selectBedrockAccount')"
|
||||
platform="bedrock"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
选择专属账号后,此API Key将只使用该账号,不选择则使用共享账号池
|
||||
{{ $t('apiKeys.createApiKeyModal.accountBindingHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -657,13 +690,15 @@
|
||||
class="ml-2 cursor-pointer text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
for="enableModelRestriction"
|
||||
>
|
||||
启用模型限制
|
||||
{{ $t('apiKeys.createApiKeyModal.enableModelRestriction') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="form.enableModelRestriction" class="space-y-3">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-600">限制的模型列表</label>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-600">{{
|
||||
$t('apiKeys.createApiKeyModal.restrictedModelsList')
|
||||
}}</label>
|
||||
<div
|
||||
class="mb-3 flex min-h-[32px] flex-wrap gap-2 rounded-lg border border-gray-200 bg-gray-50 p-2"
|
||||
>
|
||||
@@ -682,7 +717,7 @@
|
||||
</button>
|
||||
</span>
|
||||
<span v-if="form.restrictedModels.length === 0" class="text-sm text-gray-400">
|
||||
暂无限制的模型
|
||||
{{ $t('apiKeys.createApiKeyModal.noRestrictedModels') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
@@ -701,7 +736,7 @@
|
||||
v-if="availableQuickModels.length === 0"
|
||||
class="text-sm italic text-gray-400"
|
||||
>
|
||||
所有常用模型已在限制列表中
|
||||
{{ $t('apiKeys.createApiKeyModal.allCommonModelsRestricted') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -710,7 +745,7 @@
|
||||
<input
|
||||
v-model="form.modelInput"
|
||||
class="form-input flex-1"
|
||||
placeholder="输入模型名称,按回车添加"
|
||||
:placeholder="$t('apiKeys.createApiKeyModal.addRestrictedModelPlaceholder')"
|
||||
type="text"
|
||||
@keydown.enter.prevent="addRestrictedModel"
|
||||
/>
|
||||
@@ -724,7 +759,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500">
|
||||
设置此API Key无法访问的模型,例如:claude-opus-4-20250514
|
||||
{{ $t('apiKeys.createApiKeyModal.modelRestrictionHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -743,7 +778,7 @@
|
||||
class="ml-2 cursor-pointer text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
for="enableClientRestriction"
|
||||
>
|
||||
启用客户端限制
|
||||
{{ $t('apiKeys.createApiKeyModal.enableClientRestriction') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -752,9 +787,9 @@
|
||||
class="rounded-lg border border-green-200 bg-green-50 p-3 dark:border-green-700 dark:bg-green-900/20"
|
||||
>
|
||||
<div>
|
||||
<label class="mb-2 block text-xs font-medium text-gray-700 dark:text-gray-300"
|
||||
>允许的客户端</label
|
||||
>
|
||||
<label class="mb-2 block text-xs font-medium text-gray-700 dark:text-gray-300">{{
|
||||
$t('apiKeys.createApiKeyModal.allowedClients')
|
||||
}}</label>
|
||||
<div class="space-y-1">
|
||||
<div v-for="client in supportedClients" :key="client.id" class="flex items-start">
|
||||
<input
|
||||
@@ -784,7 +819,7 @@
|
||||
type="button"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
取消
|
||||
{{ $t('apiKeys.createApiKeyModal.cancel') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary flex-1 px-4 py-2.5 text-sm font-semibold"
|
||||
@@ -793,7 +828,11 @@
|
||||
>
|
||||
<div v-if="loading" class="loading-spinner mr-2" />
|
||||
<i v-else class="fas fa-plus mr-2" />
|
||||
{{ loading ? '创建中...' : '创建' }}
|
||||
{{
|
||||
loading
|
||||
? $t('apiKeys.createApiKeyModal.creating')
|
||||
: $t('apiKeys.createApiKeyModal.create')
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -804,12 +843,15 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { showToast } from '@/utils/toast'
|
||||
import { useClientsStore } from '@/stores/clients'
|
||||
import { useApiKeysStore } from '@/stores/apiKeys'
|
||||
import { apiClient } from '@/config/api'
|
||||
import AccountSelector from '@/components/common/AccountSelector.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
accounts: {
|
||||
type: Object,
|
||||
@@ -886,61 +928,31 @@ onMounted(async () => {
|
||||
availableTags.value = await apiKeysStore.fetchTags()
|
||||
// 初始化账号数据
|
||||
if (props.accounts) {
|
||||
// 合并 OpenAI 和 OpenAI-Responses 账号
|
||||
const openaiAccounts = []
|
||||
if (props.accounts.openai) {
|
||||
props.accounts.openai.forEach((account) => {
|
||||
openaiAccounts.push({
|
||||
...account,
|
||||
platform: 'openai'
|
||||
})
|
||||
})
|
||||
}
|
||||
if (props.accounts.openaiResponses) {
|
||||
props.accounts.openaiResponses.forEach((account) => {
|
||||
openaiAccounts.push({
|
||||
...account,
|
||||
platform: 'openai-responses'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
localAccounts.value = {
|
||||
claude: props.accounts.claude || [],
|
||||
gemini: props.accounts.gemini || [],
|
||||
openai: openaiAccounts,
|
||||
openai: props.accounts.openai || [],
|
||||
bedrock: props.accounts.bedrock || [], // 添加 Bedrock 账号
|
||||
claudeGroups: props.accounts.claudeGroups || [],
|
||||
geminiGroups: props.accounts.geminiGroups || [],
|
||||
openaiGroups: props.accounts.openaiGroups || []
|
||||
}
|
||||
}
|
||||
|
||||
// 自动加载账号数据
|
||||
await refreshAccounts()
|
||||
})
|
||||
|
||||
// 刷新账号列表
|
||||
const refreshAccounts = async () => {
|
||||
accountsLoading.value = true
|
||||
try {
|
||||
const [
|
||||
claudeData,
|
||||
claudeConsoleData,
|
||||
geminiData,
|
||||
openaiData,
|
||||
openaiResponsesData,
|
||||
bedrockData,
|
||||
groupsData
|
||||
] = await Promise.all([
|
||||
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')
|
||||
])
|
||||
const [claudeData, claudeConsoleData, geminiData, openaiData, bedrockData, groupsData] =
|
||||
await Promise.all([
|
||||
apiClient.get('/admin/claude-accounts'),
|
||||
apiClient.get('/admin/claude-console-accounts'),
|
||||
apiClient.get('/admin/gemini-accounts'),
|
||||
apiClient.get('/admin/openai-accounts'),
|
||||
apiClient.get('/admin/bedrock-accounts'), // 添加 Bedrock 账号获取
|
||||
apiClient.get('/admin/account-groups')
|
||||
])
|
||||
|
||||
// 合并Claude OAuth账户和Claude Console账户
|
||||
const claudeAccounts = []
|
||||
@@ -974,31 +986,13 @@ const refreshAccounts = async () => {
|
||||
}))
|
||||
}
|
||||
|
||||
// 合并 OpenAI 和 OpenAI-Responses 账号
|
||||
const openaiAccounts = []
|
||||
|
||||
if (openaiData.success) {
|
||||
;(openaiData.data || []).forEach((account) => {
|
||||
openaiAccounts.push({
|
||||
...account,
|
||||
platform: 'openai',
|
||||
isDedicated: account.accountType === 'dedicated' // 保留以便向后兼容
|
||||
})
|
||||
})
|
||||
localAccounts.value.openai = (openaiData.data || []).map((account) => ({
|
||||
...account,
|
||||
isDedicated: account.accountType === 'dedicated' // 保留以便向后兼容
|
||||
}))
|
||||
}
|
||||
|
||||
if (openaiResponsesData.success) {
|
||||
;(openaiResponsesData.data || []).forEach((account) => {
|
||||
openaiAccounts.push({
|
||||
...account,
|
||||
platform: 'openai-responses',
|
||||
isDedicated: account.accountType === 'dedicated' // 保留以便向后兼容
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
localAccounts.value.openai = openaiAccounts
|
||||
|
||||
if (bedrockData.success) {
|
||||
localAccounts.value.bedrock = (bedrockData.data || []).map((account) => ({
|
||||
...account,
|
||||
@@ -1014,9 +1008,9 @@ const refreshAccounts = async () => {
|
||||
localAccounts.value.openaiGroups = allGroups.filter((g) => g.platform === 'openai')
|
||||
}
|
||||
|
||||
showToast('账号列表已刷新', 'success')
|
||||
showToast(t('apiKeys.createApiKeyModal.refreshAccountsSuccess'), 'success')
|
||||
} catch (error) {
|
||||
showToast('刷新账号列表失败', 'error')
|
||||
showToast(t('apiKeys.createApiKeyModal.refreshAccountsFailed'), 'error')
|
||||
} finally {
|
||||
accountsLoading.value = false
|
||||
}
|
||||
@@ -1077,13 +1071,17 @@ const updateCustomExpireAt = () => {
|
||||
// 格式化过期日期
|
||||
const formatExpireDate = (dateString) => {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
const { locale } = useI18n()
|
||||
return date.toLocaleString(
|
||||
locale.value === 'zh-cn' ? 'zh-CN' : locale.value === 'zh-tw' ? 'zh-TW' : 'en-US',
|
||||
{
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 添加限制的模型
|
||||
@@ -1141,14 +1139,14 @@ const createApiKey = async () => {
|
||||
errors.value.name = ''
|
||||
|
||||
if (!form.name || !form.name.trim()) {
|
||||
errors.value.name = '请输入API Key名称'
|
||||
errors.value.name = t('apiKeys.createApiKeyModal.nameError')
|
||||
return
|
||||
}
|
||||
|
||||
// 批量创建时验证数量
|
||||
if (form.createType === 'batch') {
|
||||
if (!form.batchCount || form.batchCount < 2 || form.batchCount > 500) {
|
||||
showToast('批量创建数量必须在 2-500 之间', 'error')
|
||||
showToast(t('apiKeys.createApiKeyModal.batchCountError'), 'error')
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1158,14 +1156,14 @@ const createApiKey = async () => {
|
||||
let confirmed = false
|
||||
if (window.showConfirm) {
|
||||
confirmed = await window.showConfirm(
|
||||
'费用限制提醒',
|
||||
'您设置了时间窗口但费用限制为0,这意味着不会有费用限制。\n\n是否继续?',
|
||||
'继续创建',
|
||||
'返回修改'
|
||||
t('apiKeys.createApiKeyModal.costLimitConfirmTitle'),
|
||||
t('apiKeys.createApiKeyModal.costLimitConfirmMessage'),
|
||||
t('apiKeys.createApiKeyModal.costLimitConfirmContinue'),
|
||||
t('apiKeys.createApiKeyModal.costLimitConfirmBack')
|
||||
)
|
||||
} else {
|
||||
// 降级方案
|
||||
confirmed = confirm('您设置了时间窗口但费用限制为0,这意味着不会有费用限制。\n是否继续?')
|
||||
confirmed = confirm(t('apiKeys.createApiKeyModal.costLimitFallbackMessage'))
|
||||
}
|
||||
if (!confirmed) {
|
||||
return
|
||||
@@ -1254,11 +1252,11 @@ const createApiKey = async () => {
|
||||
const result = await apiClient.post('/admin/api-keys', data)
|
||||
|
||||
if (result.success) {
|
||||
showToast('API Key 创建成功', 'success')
|
||||
showToast(t('apiKeys.createApiKeyModal.createSuccess'), 'success')
|
||||
emit('success', result.data)
|
||||
emit('close')
|
||||
} else {
|
||||
showToast(result.message || '创建失败', 'error')
|
||||
showToast(result.message || t('apiKeys.createApiKeyModal.createFailed'), 'error')
|
||||
}
|
||||
} else {
|
||||
// 批量创建
|
||||
@@ -1272,15 +1270,18 @@ const createApiKey = async () => {
|
||||
const result = await apiClient.post('/admin/api-keys/batch', data)
|
||||
|
||||
if (result.success) {
|
||||
showToast(`成功创建 ${result.data.length} 个 API Key`, 'success')
|
||||
showToast(
|
||||
t('apiKeys.createApiKeyModal.batchCreateSuccess', { count: result.data.length }),
|
||||
'success'
|
||||
)
|
||||
emit('batch-success', result.data)
|
||||
emit('close')
|
||||
} else {
|
||||
showToast(result.message || '批量创建失败', 'error')
|
||||
showToast(result.message || t('apiKeys.createApiKeyModal.batchCreateFailed'), 'error')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('创建失败', 'error')
|
||||
showToast(t('apiKeys.createApiKeyModal.createFailed'), 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<i class="fas fa-edit text-sm text-white sm:text-base" />
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100 sm:text-xl">
|
||||
编辑 API Key
|
||||
{{ t('apiKeys.editApiKeyModal.title') }}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
@@ -30,20 +30,18 @@
|
||||
<div>
|
||||
<label
|
||||
class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-3 sm:text-sm"
|
||||
>名称</label
|
||||
>{{ t('apiKeys.editApiKeyModal.name') }}</label
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
v-model="form.name"
|
||||
class="form-input flex-1 border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
maxlength="100"
|
||||
placeholder="请输入API Key名称"
|
||||
required
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
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"
|
||||
maxlength="100"
|
||||
:placeholder="t('apiKeys.editApiKeyModal.namePlaceholder')"
|
||||
required
|
||||
type="text"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 sm:mt-2">
|
||||
用于识别此 API Key 的用途
|
||||
{{ t('apiKeys.editApiKeyModal.nameHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +49,7 @@
|
||||
<div>
|
||||
<label
|
||||
class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-3 sm:text-sm"
|
||||
>所有者</label
|
||||
>{{ t('apiKeys.editApiKeyModal.owner') }}</label
|
||||
>
|
||||
<select
|
||||
v-model="form.ownerId"
|
||||
@@ -59,11 +57,13 @@
|
||||
>
|
||||
<option v-for="user in availableUsers" :key="user.id" :value="user.id">
|
||||
{{ user.displayName }} ({{ user.username }})
|
||||
<span v-if="user.role === 'admin'" class="text-gray-500">- 管理员</span>
|
||||
<span v-if="user.role === 'admin'" class="text-gray-500">{{
|
||||
t('apiKeys.editApiKeyModal.adminLabel')
|
||||
}}</span>
|
||||
</option>
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 sm:mt-2">
|
||||
分配此 API Key 给指定用户或管理员,管理员分配时不受用户 API Key 数量限制
|
||||
{{ t('apiKeys.editApiKeyModal.ownerHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -71,13 +71,13 @@
|
||||
<div>
|
||||
<label
|
||||
class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-3 sm:text-sm"
|
||||
>标签</label
|
||||
>{{ t('apiKeys.editApiKeyModal.tags') }}</label
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<!-- 已选择的标签 -->
|
||||
<div v-if="form.tags.length > 0">
|
||||
<div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
已选择的标签:
|
||||
{{ t('apiKeys.editApiKeyModal.selectedTags') }}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span
|
||||
@@ -100,7 +100,7 @@
|
||||
<!-- 可选择的已有标签 -->
|
||||
<div v-if="unselectedTags.length > 0">
|
||||
<div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
点击选择已有标签:
|
||||
{{ t('apiKeys.editApiKeyModal.clickToSelectTags') }}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
@@ -119,13 +119,13 @@
|
||||
<!-- 创建新标签 -->
|
||||
<div>
|
||||
<div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
创建新标签:
|
||||
{{ t('apiKeys.editApiKeyModal.createNewTag') }}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="newTag"
|
||||
class="form-input flex-1 border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
placeholder="输入新标签名称"
|
||||
:placeholder="t('apiKeys.editApiKeyModal.newTagPlaceholder')"
|
||||
type="text"
|
||||
@keypress.enter.prevent="addTag"
|
||||
/>
|
||||
@@ -140,7 +140,7 @@
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
用于标记不同团队或用途,方便筛选管理
|
||||
{{ t('apiKeys.editApiKeyModal.tagsHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,68 +156,76 @@
|
||||
<i class="fas fa-tachometer-alt text-xs text-white" />
|
||||
</div>
|
||||
<h4 class="text-sm font-semibold text-gray-800 dark:text-gray-200">
|
||||
速率限制设置 (可选)
|
||||
{{ t('apiKeys.editApiKeyModal.rateLimitTitle') }}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300"
|
||||
>时间窗口 (分钟)</label
|
||||
>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">{{
|
||||
t('apiKeys.editApiKeyModal.rateLimitWindow')
|
||||
}}</label>
|
||||
<input
|
||||
v-model="form.rateLimitWindow"
|
||||
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
min="1"
|
||||
placeholder="无限制"
|
||||
:placeholder="t('apiKeys.editApiKeyModal.noLimit')"
|
||||
type="number"
|
||||
/>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">时间段单位</p>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('apiKeys.editApiKeyModal.rateLimitWindowHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300"
|
||||
>请求次数限制</label
|
||||
>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">{{
|
||||
t('apiKeys.editApiKeyModal.rateLimitRequests')
|
||||
}}</label>
|
||||
<input
|
||||
v-model="form.rateLimitRequests"
|
||||
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
min="1"
|
||||
placeholder="无限制"
|
||||
:placeholder="t('apiKeys.editApiKeyModal.noLimit')"
|
||||
type="number"
|
||||
/>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">窗口内最大请求</p>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('apiKeys.editApiKeyModal.rateLimitRequestsHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300"
|
||||
>费用限制 (美元)</label
|
||||
>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">{{
|
||||
t('apiKeys.editApiKeyModal.rateLimitCost')
|
||||
}}</label>
|
||||
<input
|
||||
v-model="form.rateLimitCost"
|
||||
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
min="0"
|
||||
placeholder="无限制"
|
||||
:placeholder="t('apiKeys.editApiKeyModal.noLimit')"
|
||||
step="0.01"
|
||||
type="number"
|
||||
/>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">窗口内最大费用</p>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('apiKeys.editApiKeyModal.rateLimitCostHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 示例说明 -->
|
||||
<div class="rounded-lg bg-blue-100 p-2 dark:bg-blue-900/30">
|
||||
<h5 class="mb-1 text-xs font-semibold text-blue-800 dark:text-blue-400">
|
||||
💡 使用示例
|
||||
{{ t('apiKeys.editApiKeyModal.usageExamples') }}
|
||||
</h5>
|
||||
<div class="space-y-0.5 text-xs text-blue-700 dark:text-blue-300">
|
||||
<div>
|
||||
<strong>示例1:</strong> 时间窗口=60,请求次数=1000 → 每60分钟最多1000次请求
|
||||
<strong>{{ t('apiKeys.editApiKeyModal.example1') }}</strong>
|
||||
</div>
|
||||
<div><strong>示例2:</strong> 时间窗口=1,费用=0.1 → 每分钟最多$0.1费用</div>
|
||||
<div>
|
||||
<strong>示例3:</strong> 窗口=30,请求=50,费用=5 → 每30分钟50次请求且不超$5费用
|
||||
<strong>{{ t('apiKeys.editApiKeyModal.example2') }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<strong>{{ t('apiKeys.editApiKeyModal.example3') }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -225,9 +233,9 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>每日费用限制 (美元)</label
|
||||
>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
|
||||
t('apiKeys.editApiKeyModal.dailyCostLimit')
|
||||
}}</label>
|
||||
<div class="space-y-3">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@@ -256,27 +264,27 @@
|
||||
type="button"
|
||||
@click="form.dailyCostLimit = ''"
|
||||
>
|
||||
自定义
|
||||
{{ t('apiKeys.editApiKeyModal.custom') }}
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
v-model="form.dailyCostLimit"
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
min="0"
|
||||
placeholder="0 表示无限制"
|
||||
:placeholder="t('apiKeys.editApiKeyModal.dailyCostLimitPlaceholder')"
|
||||
step="0.01"
|
||||
type="number"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
设置此 API Key 每日的费用限制,超过限制将拒绝请求,0 或留空表示无限制
|
||||
{{ t('apiKeys.editApiKeyModal.dailyCostHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>Opus 模型周费用限制 (美元)</label
|
||||
>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
|
||||
t('apiKeys.editApiKeyModal.weeklyOpusCostLimit')
|
||||
}}</label>
|
||||
<div class="space-y-3">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@@ -305,36 +313,36 @@
|
||||
type="button"
|
||||
@click="form.weeklyOpusCostLimit = ''"
|
||||
>
|
||||
自定义
|
||||
{{ t('apiKeys.editApiKeyModal.custom') }}
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
v-model="form.weeklyOpusCostLimit"
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
min="0"
|
||||
placeholder="0 表示无限制"
|
||||
:placeholder="t('apiKeys.editApiKeyModal.weeklyOpusCostLimitPlaceholder')"
|
||||
step="0.01"
|
||||
type="number"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
设置 Opus 模型的周费用限制(周一到周日),仅限 Claude 官方账户,0 或留空表示无限制
|
||||
{{ t('apiKeys.editApiKeyModal.weeklyOpusHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>并发限制</label
|
||||
>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
|
||||
t('apiKeys.editApiKeyModal.concurrencyLimit')
|
||||
}}</label>
|
||||
<input
|
||||
v-model="form.concurrencyLimit"
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
min="0"
|
||||
placeholder="0 表示无限制"
|
||||
:placeholder="t('apiKeys.editApiKeyModal.concurrencyLimitPlaceholder')"
|
||||
type="number"
|
||||
/>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
设置此 API Key 可同时处理的最大请求数
|
||||
{{ t('apiKeys.editApiKeyModal.concurrencyHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -351,18 +359,18 @@
|
||||
class="ml-2 cursor-pointer text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
for="editIsActive"
|
||||
>
|
||||
激活账号
|
||||
{{ t('apiKeys.editApiKeyModal.activeStatus') }}
|
||||
</label>
|
||||
</div>
|
||||
<p class="mb-4 text-xs text-gray-500 dark:text-gray-400">
|
||||
取消勾选将禁用此 API Key,暂停所有请求,客户端返回 401 错误
|
||||
{{ t('apiKeys.editApiKeyModal.activeStatusHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>服务权限</label
|
||||
>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
|
||||
t('apiKeys.editApiKeyModal.servicePermissions')
|
||||
}}</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input
|
||||
@@ -371,7 +379,9 @@
|
||||
type="radio"
|
||||
value="all"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">全部服务</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{
|
||||
t('apiKeys.editApiKeyModal.allServices')
|
||||
}}</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input
|
||||
@@ -380,7 +390,9 @@
|
||||
type="radio"
|
||||
value="claude"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">仅 Claude</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{
|
||||
t('apiKeys.editApiKeyModal.claudeOnly')
|
||||
}}</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input
|
||||
@@ -389,7 +401,9 @@
|
||||
type="radio"
|
||||
value="gemini"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">仅 Gemini</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{
|
||||
t('apiKeys.editApiKeyModal.geminiOnly')
|
||||
}}</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input
|
||||
@@ -398,23 +412,25 @@
|
||||
type="radio"
|
||||
value="openai"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">仅 OpenAI</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{
|
||||
t('apiKeys.editApiKeyModal.openaiOnly')
|
||||
}}</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
控制此 API Key 可以访问哪些服务
|
||||
{{ t('apiKeys.editApiKeyModal.permissionsHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label class="text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>专属账号绑定</label
|
||||
>
|
||||
<label class="text-sm font-semibold text-gray-700 dark:text-gray-300">{{
|
||||
t('apiKeys.editApiKeyModal.accountBinding')
|
||||
}}</label>
|
||||
<button
|
||||
class="flex items-center gap-1 text-sm text-blue-600 transition-colors hover:text-blue-800 disabled:cursor-not-allowed disabled:opacity-50 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
:disabled="accountsLoading"
|
||||
title="刷新账号列表"
|
||||
:title="t('apiKeys.editApiKeyModal.refreshAccounts')"
|
||||
type="button"
|
||||
@click="refreshAccounts"
|
||||
>
|
||||
@@ -425,69 +441,73 @@
|
||||
'text-xs'
|
||||
]"
|
||||
/>
|
||||
<span>{{ accountsLoading ? '刷新中...' : '刷新账号' }}</span>
|
||||
<span>{{
|
||||
accountsLoading
|
||||
? t('apiKeys.editApiKeyModal.refreshing')
|
||||
: t('apiKeys.editApiKeyModal.refreshAccounts')
|
||||
}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
>Claude 专属账号</label
|
||||
>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
|
||||
t('apiKeys.editApiKeyModal.claudeAccount')
|
||||
}}</label>
|
||||
<AccountSelector
|
||||
v-model="form.claudeAccountId"
|
||||
:accounts="localAccounts.claude"
|
||||
default-option-text="使用共享账号池"
|
||||
:default-option-text="t('apiKeys.editApiKeyModal.useSharedPool')"
|
||||
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
|
||||
:groups="localAccounts.claudeGroups"
|
||||
placeholder="请选择Claude账号"
|
||||
:placeholder="t('apiKeys.editApiKeyModal.selectClaudeAccount')"
|
||||
platform="claude"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
>Gemini 专属账号</label
|
||||
>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
|
||||
t('apiKeys.editApiKeyModal.geminiAccount')
|
||||
}}</label>
|
||||
<AccountSelector
|
||||
v-model="form.geminiAccountId"
|
||||
:accounts="localAccounts.gemini"
|
||||
default-option-text="使用共享账号池"
|
||||
:default-option-text="t('apiKeys.editApiKeyModal.useSharedPool')"
|
||||
:disabled="form.permissions === 'claude' || form.permissions === 'openai'"
|
||||
:groups="localAccounts.geminiGroups"
|
||||
placeholder="请选择Gemini账号"
|
||||
:placeholder="t('apiKeys.editApiKeyModal.selectGeminiAccount')"
|
||||
platform="gemini"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
>OpenAI 专属账号</label
|
||||
>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
|
||||
t('apiKeys.editApiKeyModal.openaiAccount')
|
||||
}}</label>
|
||||
<AccountSelector
|
||||
v-model="form.openaiAccountId"
|
||||
:accounts="localAccounts.openai"
|
||||
default-option-text="使用共享账号池"
|
||||
:default-option-text="t('apiKeys.editApiKeyModal.useSharedPool')"
|
||||
:disabled="form.permissions === 'claude' || form.permissions === 'gemini'"
|
||||
:groups="localAccounts.openaiGroups"
|
||||
placeholder="请选择OpenAI账号"
|
||||
:placeholder="t('apiKeys.editApiKeyModal.selectOpenaiAccount')"
|
||||
platform="openai"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
>Bedrock 专属账号</label
|
||||
>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
|
||||
t('apiKeys.editApiKeyModal.bedrockAccount')
|
||||
}}</label>
|
||||
<AccountSelector
|
||||
v-model="form.bedrockAccountId"
|
||||
:accounts="localAccounts.bedrock"
|
||||
default-option-text="使用共享账号池"
|
||||
:default-option-text="t('apiKeys.editApiKeyModal.useSharedPool')"
|
||||
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
|
||||
:groups="[]"
|
||||
placeholder="请选择Bedrock账号"
|
||||
:placeholder="t('apiKeys.editApiKeyModal.selectBedrockAccount')"
|
||||
platform="bedrock"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
修改绑定账号将影响此API Key的请求路由
|
||||
{{ t('apiKeys.editApiKeyModal.accountBindingHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -503,15 +523,15 @@
|
||||
class="ml-2 cursor-pointer text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
for="editEnableModelRestriction"
|
||||
>
|
||||
启用模型限制
|
||||
{{ t('apiKeys.editApiKeyModal.enableModelRestriction') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="form.enableModelRestriction" class="space-y-3">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
>限制的模型列表</label
|
||||
>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
|
||||
t('apiKeys.editApiKeyModal.restrictedModels')
|
||||
}}</label>
|
||||
<div
|
||||
class="mb-3 flex min-h-[32px] flex-wrap gap-2 rounded-lg border border-gray-200 bg-gray-50 p-2 dark:border-gray-600 dark:bg-gray-700"
|
||||
>
|
||||
@@ -533,7 +553,7 @@
|
||||
v-if="form.restrictedModels.length === 0"
|
||||
class="text-sm text-gray-400 dark:text-gray-500"
|
||||
>
|
||||
暂无限制的模型
|
||||
{{ t('apiKeys.editApiKeyModal.noRestrictedModels') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
@@ -552,7 +572,7 @@
|
||||
v-if="availableQuickModels.length === 0"
|
||||
class="text-sm italic text-gray-400 dark:text-gray-500"
|
||||
>
|
||||
所有常用模型已在限制列表中
|
||||
{{ t('apiKeys.editApiKeyModal.allCommonModelsRestricted') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -561,7 +581,7 @@
|
||||
<input
|
||||
v-model="form.modelInput"
|
||||
class="form-input flex-1 border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
placeholder="输入模型名称,按回车添加"
|
||||
:placeholder="t('apiKeys.editApiKeyModal.addRestrictedModelPlaceholder')"
|
||||
type="text"
|
||||
@keydown.enter.prevent="addRestrictedModel"
|
||||
/>
|
||||
@@ -575,7 +595,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
设置此API Key无法访问的模型,例如:claude-opus-4-20250514
|
||||
{{ t('apiKeys.editApiKeyModal.modelRestrictionHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -594,17 +614,17 @@
|
||||
class="ml-2 cursor-pointer text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
for="editEnableClientRestriction"
|
||||
>
|
||||
启用客户端限制
|
||||
{{ t('apiKeys.editApiKeyModal.enableClientRestriction') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="form.enableClientRestriction" class="space-y-3">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
>允许的客户端</label
|
||||
>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-600 dark:text-gray-400">{{
|
||||
t('apiKeys.editApiKeyModal.allowedClients')
|
||||
}}</label>
|
||||
<p class="mb-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
勾选允许使用此API Key的客户端
|
||||
{{ t('apiKeys.editApiKeyModal.clientRestrictionHint') }}
|
||||
</p>
|
||||
<div class="space-y-2">
|
||||
<div v-for="client in supportedClients" :key="client.id" class="flex items-start">
|
||||
@@ -635,7 +655,7 @@
|
||||
type="button"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
取消
|
||||
{{ t('apiKeys.editApiKeyModal.cancel') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary flex-1 px-6 py-3 font-semibold"
|
||||
@@ -644,7 +664,9 @@
|
||||
>
|
||||
<div v-if="loading" class="loading-spinner mr-2" />
|
||||
<i v-else class="fas fa-save mr-2" />
|
||||
{{ loading ? '保存中...' : '保存修改' }}
|
||||
{{
|
||||
loading ? t('apiKeys.editApiKeyModal.saving') : t('apiKeys.editApiKeyModal.save')
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -655,6 +677,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { showToast } from '@/utils/toast'
|
||||
import { useClientsStore } from '@/stores/clients'
|
||||
import { useApiKeysStore } from '@/stores/apiKeys'
|
||||
@@ -674,6 +697,9 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['close', 'success'])
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
// const authStore = useAuthStore()
|
||||
const clientsStore = useClientsStore()
|
||||
const apiKeysStore = useApiKeysStore()
|
||||
@@ -785,14 +811,14 @@ const updateApiKey = async () => {
|
||||
let confirmed = false
|
||||
if (window.showConfirm) {
|
||||
confirmed = await window.showConfirm(
|
||||
'费用限制提醒',
|
||||
'您设置了时间窗口但费用限制为0,这意味着不会有费用限制。\n\n是否继续?',
|
||||
'继续保存',
|
||||
'返回修改'
|
||||
t('apiKeys.editApiKeyModal.costLimitConfirmTitle'),
|
||||
t('apiKeys.editApiKeyModal.costLimitConfirmMessage'),
|
||||
t('apiKeys.editApiKeyModal.costLimitConfirmContinue'),
|
||||
t('apiKeys.editApiKeyModal.costLimitConfirmBack')
|
||||
)
|
||||
} else {
|
||||
// 降级方案
|
||||
confirmed = confirm('您设置了时间窗口但费用限制为0,这意味着不会有费用限制。\n是否继续?')
|
||||
confirmed = confirm(t('apiKeys.editApiKeyModal.costLimitConfirmMessage'))
|
||||
}
|
||||
if (!confirmed) {
|
||||
return
|
||||
@@ -898,10 +924,10 @@ const updateApiKey = async () => {
|
||||
emit('success')
|
||||
emit('close')
|
||||
} else {
|
||||
showToast(result.message || '更新失败', 'error')
|
||||
showToast(result.message || t('apiKeys.editApiKeyModal.updateFailed'), 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('更新失败', 'error')
|
||||
showToast(t('apiKeys.editApiKeyModal.updateFailed'), 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -911,23 +937,15 @@ const updateApiKey = async () => {
|
||||
const refreshAccounts = async () => {
|
||||
accountsLoading.value = true
|
||||
try {
|
||||
const [
|
||||
claudeData,
|
||||
claudeConsoleData,
|
||||
geminiData,
|
||||
openaiData,
|
||||
openaiResponsesData,
|
||||
bedrockData,
|
||||
groupsData
|
||||
] = await Promise.all([
|
||||
apiClient.get('/admin/claude-accounts'),
|
||||
apiClient.get('/admin/claude-console-accounts'),
|
||||
apiClient.get('/admin/gemini-accounts'),
|
||||
apiClient.get('/admin/openai-accounts'),
|
||||
apiClient.get('/admin/openai-responses-accounts'), // 获取 OpenAI-Responses 账号
|
||||
apiClient.get('/admin/bedrock-accounts'), // 添加 Bedrock 账号获取
|
||||
apiClient.get('/admin/account-groups')
|
||||
])
|
||||
const [claudeData, claudeConsoleData, geminiData, openaiData, bedrockData, groupsData] =
|
||||
await Promise.all([
|
||||
apiClient.get('/admin/claude-accounts'),
|
||||
apiClient.get('/admin/claude-console-accounts'),
|
||||
apiClient.get('/admin/gemini-accounts'),
|
||||
apiClient.get('/admin/openai-accounts'),
|
||||
apiClient.get('/admin/bedrock-accounts'), // 添加 Bedrock 账号获取
|
||||
apiClient.get('/admin/account-groups')
|
||||
])
|
||||
|
||||
// 合并Claude OAuth账户和Claude Console账户
|
||||
const claudeAccounts = []
|
||||
@@ -961,31 +979,13 @@ const refreshAccounts = async () => {
|
||||
}))
|
||||
}
|
||||
|
||||
// 合并 OpenAI 和 OpenAI-Responses 账号
|
||||
const openaiAccounts = []
|
||||
|
||||
if (openaiData.success) {
|
||||
;(openaiData.data || []).forEach((account) => {
|
||||
openaiAccounts.push({
|
||||
...account,
|
||||
platform: 'openai',
|
||||
isDedicated: account.accountType === 'dedicated'
|
||||
})
|
||||
})
|
||||
localAccounts.value.openai = (openaiData.data || []).map((account) => ({
|
||||
...account,
|
||||
isDedicated: account.accountType === 'dedicated'
|
||||
}))
|
||||
}
|
||||
|
||||
if (openaiResponsesData.success) {
|
||||
;(openaiResponsesData.data || []).forEach((account) => {
|
||||
openaiAccounts.push({
|
||||
...account,
|
||||
platform: 'openai-responses',
|
||||
isDedicated: account.accountType === 'dedicated'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
localAccounts.value.openai = openaiAccounts
|
||||
|
||||
if (bedrockData.success) {
|
||||
localAccounts.value.bedrock = (bedrockData.data || []).map((account) => ({
|
||||
...account,
|
||||
@@ -1001,9 +1001,9 @@ const refreshAccounts = async () => {
|
||||
localAccounts.value.openaiGroups = allGroups.filter((g) => g.platform === 'openai')
|
||||
}
|
||||
|
||||
showToast('账号列表已刷新', 'success')
|
||||
showToast(t('apiKeys.editApiKeyModal.refreshAccountsSuccess'), 'success')
|
||||
} catch (error) {
|
||||
showToast('刷新账号列表失败', 'error')
|
||||
showToast(t('apiKeys.editApiKeyModal.refreshAccountsFailed'), 'error')
|
||||
} finally {
|
||||
accountsLoading.value = false
|
||||
}
|
||||
@@ -1017,7 +1017,7 @@ const loadUsers = async () => {
|
||||
availableUsers.value = response.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error('Failed to load users:', error)
|
||||
console.error('Failed to load users:', error)
|
||||
availableUsers.value = [
|
||||
{
|
||||
id: 'admin',
|
||||
@@ -1043,7 +1043,7 @@ onMounted(async () => {
|
||||
supportedClients.value = clients || []
|
||||
availableTags.value = tags || []
|
||||
} catch (error) {
|
||||
// console.error('Error loading initial data:', error)
|
||||
console.error('Error loading initial data:', error)
|
||||
// Fallback to empty arrays if loading fails
|
||||
supportedClients.value = []
|
||||
availableTags.value = []
|
||||
@@ -1051,29 +1051,10 @@ onMounted(async () => {
|
||||
|
||||
// 初始化账号数据
|
||||
if (props.accounts) {
|
||||
// 合并 OpenAI 和 OpenAI-Responses 账号
|
||||
const openaiAccounts = []
|
||||
if (props.accounts.openai) {
|
||||
props.accounts.openai.forEach((account) => {
|
||||
openaiAccounts.push({
|
||||
...account,
|
||||
platform: 'openai'
|
||||
})
|
||||
})
|
||||
}
|
||||
if (props.accounts.openaiResponses) {
|
||||
props.accounts.openaiResponses.forEach((account) => {
|
||||
openaiAccounts.push({
|
||||
...account,
|
||||
platform: 'openai-responses'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
localAccounts.value = {
|
||||
claude: props.accounts.claude || [],
|
||||
gemini: props.accounts.gemini || [],
|
||||
openai: openaiAccounts,
|
||||
openai: props.accounts.openai || [],
|
||||
bedrock: props.accounts.bedrock || [], // 添加 Bedrock 账号
|
||||
claudeGroups: props.accounts.claudeGroups || [],
|
||||
geminiGroups: props.accounts.geminiGroups || [],
|
||||
@@ -1081,9 +1062,6 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 自动加载账号数据
|
||||
await refreshAccounts()
|
||||
|
||||
form.name = props.apiKey.name
|
||||
|
||||
// 处理速率限制迁移:如果有tokenLimit且没有rateLimitCost,提示用户
|
||||
@@ -1093,7 +1071,7 @@ onMounted(async () => {
|
||||
// 如果有历史tokenLimit但没有rateLimitCost,提示用户需要重新设置
|
||||
if (props.apiKey.tokenLimit > 0 && !props.apiKey.rateLimitCost) {
|
||||
// 可以根据需要添加提示,或者自动迁移(这里选择让用户手动设置)
|
||||
// console.log('检测到历史Token限制,请考虑设置费用限制')
|
||||
console.log('Token limit migration detected, consider setting cost limit')
|
||||
}
|
||||
|
||||
form.rateLimitWindow = props.apiKey.rateLimitWindow || ''
|
||||
@@ -1109,10 +1087,7 @@ onMounted(async () => {
|
||||
form.claudeAccountId = props.apiKey.claudeAccountId || ''
|
||||
}
|
||||
form.geminiAccountId = props.apiKey.geminiAccountId || ''
|
||||
|
||||
// 处理 OpenAI 账号 - 直接使用后端传来的值(已包含 responses: 前缀)
|
||||
form.openaiAccountId = props.apiKey.openaiAccountId || ''
|
||||
|
||||
form.bedrockAccountId = props.apiKey.bedrockAccountId || '' // 添加 Bedrock 账号ID初始化
|
||||
form.restrictedModels = props.apiKey.restrictedModels || []
|
||||
form.allowedClients = props.apiKey.allowedClients || []
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
<i class="fas fa-clock text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-gray-100">修改过期时间</h3>
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{{ $t('apiKeys.expiryEditModal.title') }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
为 "{{ apiKey.name || 'API Key' }}" 设置新的过期时间
|
||||
{{ $t('apiKeys.expiryEditModal.subtitle', { name: apiKey.name || 'API Key' }) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,14 +41,20 @@
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="mb-1 text-xs font-medium text-gray-600 dark:text-gray-400">当前状态</p>
|
||||
<p class="mb-1 text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ $t('apiKeys.expiryEditModal.currentStatus') }}
|
||||
</p>
|
||||
<p class="text-sm font-semibold text-gray-800 dark:text-gray-200">
|
||||
<!-- 未激活状态 -->
|
||||
<template v-if="apiKey.expirationMode === 'activation' && !apiKey.isActivated">
|
||||
<i class="fas fa-pause-circle mr-1 text-blue-500" />
|
||||
未激活
|
||||
{{ $t('apiKeys.expiryEditModal.notActivated') }}
|
||||
<span class="ml-2 text-xs font-normal text-gray-600">
|
||||
(激活后 {{ apiKey.activationDays || 30 }} 天过期)
|
||||
{{
|
||||
$t('apiKeys.expiryEditModal.activationDaysHint', {
|
||||
days: apiKey.activationDays || 30
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
<!-- 已设置过期时间 -->
|
||||
@@ -63,7 +71,7 @@
|
||||
<!-- 永不过期 -->
|
||||
<template v-else>
|
||||
<i class="fas fa-infinity mr-1 text-gray-500" />
|
||||
永不过期
|
||||
{{ $t('apiKeys.expiryEditModal.neverExpire') }}
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
@@ -89,19 +97,23 @@
|
||||
@click="handleActivateNow"
|
||||
>
|
||||
<i class="fas fa-rocket mr-2" />
|
||||
立即激活 (激活后 {{ apiKey.activationDays || 30 }} 天过期)
|
||||
{{
|
||||
$t('apiKeys.expiryEditModal.activateButton', { days: apiKey.activationDays || 30 })
|
||||
}}
|
||||
</button>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
<i class="fas fa-info-circle mr-1" />
|
||||
点击立即激活此 API Key,激活后将在 {{ apiKey.activationDays || 30 }} 天后过期
|
||||
{{
|
||||
$t('apiKeys.expiryEditModal.activationInfo', { days: apiKey.activationDays || 30 })
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 快捷选项 -->
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>选择新的期限</label
|
||||
>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
{{ $t('apiKeys.expiryEditModal.selectNewDuration') }}
|
||||
</label>
|
||||
<div class="mb-3 grid grid-cols-3 gap-2">
|
||||
<button
|
||||
v-for="option in quickOptions"
|
||||
@@ -126,16 +138,16 @@
|
||||
@click="selectQuickOption('custom')"
|
||||
>
|
||||
<i class="fas fa-calendar-alt mr-1" />
|
||||
自定义
|
||||
{{ $t('apiKeys.expiryEditModal.custom') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 自定义日期选择 -->
|
||||
<div v-if="localForm.expireDuration === 'custom'" class="animate-fadeIn">
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>选择日期和时间</label
|
||||
>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
{{ $t('apiKeys.expiryEditModal.selectDateAndTime') }}
|
||||
</label>
|
||||
<input
|
||||
v-model="localForm.customExpireDate"
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||
@@ -144,7 +156,7 @@
|
||||
@change="updateCustomExpiryPreview"
|
||||
/>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
选择一个未来的日期和时间作为过期时间
|
||||
{{ $t('apiKeys.expiryEditModal.selectFutureDateTime') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -157,7 +169,7 @@
|
||||
<div>
|
||||
<p class="mb-1 text-xs font-medium text-blue-700 dark:text-blue-400">
|
||||
<i class="fas fa-arrow-right mr-1" />
|
||||
新的过期时间
|
||||
{{ $t('apiKeys.expiryEditModal.newExpiryTime') }}
|
||||
</p>
|
||||
<p class="text-sm font-semibold text-blue-900 dark:text-blue-200">
|
||||
<template v-if="localForm.expiresAt">
|
||||
@@ -172,7 +184,7 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<i class="fas fa-infinity mr-1" />
|
||||
永不过期
|
||||
{{ $t('apiKeys.expiryEditModal.neverExpire') }}
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
@@ -190,7 +202,7 @@
|
||||
class="flex-1 rounded-lg bg-gray-100 px-4 py-2.5 font-semibold text-gray-700 transition-colors hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
取消
|
||||
{{ $t('apiKeys.expiryEditModal.cancel') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary flex-1 px-4 py-2.5 font-semibold"
|
||||
@@ -199,7 +211,11 @@
|
||||
>
|
||||
<div v-if="saving" class="loading-spinner mr-2" />
|
||||
<i v-else class="fas fa-save mr-2" />
|
||||
{{ saving ? '保存中...' : '保存更改' }}
|
||||
{{
|
||||
saving
|
||||
? $t('apiKeys.expiryEditModal.saving')
|
||||
: $t('apiKeys.expiryEditModal.saveChanges')
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,6 +226,9 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
@@ -235,14 +254,14 @@ const localForm = reactive({
|
||||
|
||||
// 快捷选项
|
||||
const quickOptions = [
|
||||
{ value: '', label: '永不过期' },
|
||||
{ value: '1d', label: '1 天' },
|
||||
{ value: '7d', label: '7 天' },
|
||||
{ value: '30d', label: '30 天' },
|
||||
{ value: '90d', label: '90 天' },
|
||||
{ value: '180d', label: '180 天' },
|
||||
{ value: '365d', label: '1 年' },
|
||||
{ value: '730d', label: '2 年' }
|
||||
{ value: '', label: t('apiKeys.expiryEditModal.neverExpireOption') },
|
||||
{ value: '1d', label: t('apiKeys.expiryEditModal.oneDay') },
|
||||
{ value: '7d', label: t('apiKeys.expiryEditModal.sevenDays') },
|
||||
{ value: '30d', label: t('apiKeys.expiryEditModal.thirtyDays') },
|
||||
{ value: '90d', label: t('apiKeys.expiryEditModal.ninetyDays') },
|
||||
{ value: '180d', label: t('apiKeys.expiryEditModal.oneHundredEightyDays') },
|
||||
{ value: '365d', label: t('apiKeys.expiryEditModal.threeSixtyFiveDays') },
|
||||
{ value: '730d', label: t('apiKeys.expiryEditModal.twoYears') }
|
||||
]
|
||||
|
||||
// 计算最小日期时间
|
||||
@@ -337,13 +356,17 @@ const updateCustomExpiryPreview = () => {
|
||||
const formatExpireDate = (dateString) => {
|
||||
if (!dateString) return ''
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
const { locale } = useI18n()
|
||||
return date.toLocaleString(
|
||||
locale.value === 'zh-cn' ? 'zh-CN' : locale.value === 'zh-tw' ? 'zh-TW' : 'en-US',
|
||||
{
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 检查是否已过期
|
||||
@@ -363,22 +386,22 @@ const getExpiryStatus = (expiresAt) => {
|
||||
|
||||
if (diffMs < 0) {
|
||||
return {
|
||||
text: '已过期',
|
||||
text: t('apiKeys.expiryEditModal.expired'),
|
||||
class: 'text-red-600'
|
||||
}
|
||||
} else if (diffDays <= 7) {
|
||||
return {
|
||||
text: `${diffDays} 天后过期`,
|
||||
text: t('apiKeys.expiryEditModal.daysToExpire', { days: diffDays }),
|
||||
class: 'text-orange-600'
|
||||
}
|
||||
} else if (diffDays <= 30) {
|
||||
return {
|
||||
text: `${diffDays} 天后过期`,
|
||||
text: t('apiKeys.expiryEditModal.daysToExpire', { days: diffDays }),
|
||||
class: 'text-yellow-600'
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
text: `${Math.ceil(diffDays / 30)} 个月后过期`,
|
||||
text: t('apiKeys.expiryEditModal.monthsToExpire', { months: Math.ceil(diffDays / 30) }),
|
||||
class: 'text-green-600'
|
||||
}
|
||||
}
|
||||
@@ -399,15 +422,19 @@ const handleActivateNow = async () => {
|
||||
let confirmed = true
|
||||
if (window.showConfirm) {
|
||||
confirmed = await window.showConfirm(
|
||||
'激活 API Key',
|
||||
`确定要立即激活此 API Key 吗?激活后将在 ${props.apiKey.activationDays || 30} 天后自动过期。`,
|
||||
'确定激活',
|
||||
'取消'
|
||||
t('apiKeys.expiryEditModal.activateConfirmTitle'),
|
||||
t('apiKeys.expiryEditModal.activateConfirmMessage', {
|
||||
days: props.apiKey.activationDays || 30
|
||||
}),
|
||||
t('apiKeys.expiryEditModal.confirmActivate'),
|
||||
t('apiKeys.expiryEditModal.confirmCancel')
|
||||
)
|
||||
} else {
|
||||
// 降级方案
|
||||
confirmed = confirm(
|
||||
`确定要立即激活此 API Key 吗?激活后将在 ${props.apiKey.activationDays || 30} 天后自动过期。`
|
||||
t('apiKeys.expiryEditModal.activateConfirmMessage', {
|
||||
days: props.apiKey.activationDays || 30
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,13 +12,17 @@
|
||||
<i class="fas fa-check text-lg text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-gray-100">API Key 创建成功</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">请妥善保存您的 API Key</p>
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{{ t('apiKeys.newApiKeyModal.title') }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ t('apiKeys.newApiKeyModal.subtitle') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="text-gray-400 transition-colors hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
|
||||
title="直接关闭(不推荐)"
|
||||
:title="t('apiKeys.newApiKeyModal.directCloseTooltip')"
|
||||
@click="handleDirectClose"
|
||||
>
|
||||
<i class="fas fa-times text-xl" />
|
||||
@@ -36,10 +40,11 @@
|
||||
<i class="fas fa-exclamation-triangle text-sm text-white" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h5 class="mb-1 font-semibold text-amber-900 dark:text-amber-400">重要提醒</h5>
|
||||
<h5 class="mb-1 font-semibold text-amber-900 dark:text-amber-400">
|
||||
{{ t('apiKeys.newApiKeyModal.warningTitle') }}
|
||||
</h5>
|
||||
<p class="text-sm text-amber-800 dark:text-amber-300">
|
||||
这是您唯一能看到完整 API Key 的机会。关闭此窗口后,系统将不再显示完整的 API
|
||||
Key。请立即复制并妥善保存。
|
||||
{{ t('apiKeys.newApiKeyModal.warningMessage') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -48,9 +53,9 @@
|
||||
<!-- API Key 信息 -->
|
||||
<div class="mb-6 space-y-4">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>API Key 名称</label
|
||||
>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
|
||||
t('apiKeys.newApiKeyModal.apiKeyName')
|
||||
}}</label>
|
||||
<div
|
||||
class="rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-600 dark:bg-gray-800"
|
||||
>
|
||||
@@ -59,22 +64,22 @@
|
||||
</div>
|
||||
|
||||
<div v-if="apiKey.description">
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>备注</label
|
||||
>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
|
||||
t('apiKeys.newApiKeyModal.remarks')
|
||||
}}</label>
|
||||
<div
|
||||
class="rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-600 dark:bg-gray-800"
|
||||
>
|
||||
<span class="text-gray-700 dark:text-gray-300">{{
|
||||
apiKey.description || '无描述'
|
||||
apiKey.description || t('apiKeys.newApiKeyModal.noDescription')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>API Key</label
|
||||
>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
|
||||
t('apiKeys.newApiKeyModal.apiKey')
|
||||
}}</label>
|
||||
<div class="relative">
|
||||
<div
|
||||
class="flex min-h-[60px] items-center break-all rounded-lg border border-gray-700 bg-gray-900 p-4 pr-14 font-mono text-sm text-white dark:border-gray-600 dark:bg-gray-900"
|
||||
@@ -84,7 +89,11 @@
|
||||
<div class="absolute right-3 top-3">
|
||||
<button
|
||||
class="btn-icon-sm bg-gray-700 hover:bg-gray-800 dark:bg-gray-700 dark:hover:bg-gray-600"
|
||||
:title="showFullKey ? '隐藏API Key' : '显示完整API Key'"
|
||||
:title="
|
||||
showFullKey
|
||||
? t('apiKeys.newApiKeyModal.hideApiKey')
|
||||
: t('apiKeys.newApiKeyModal.showFullApiKey')
|
||||
"
|
||||
type="button"
|
||||
@click="toggleKeyVisibility"
|
||||
>
|
||||
@@ -93,7 +102,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
点击眼睛图标切换显示模式,使用下方按钮复制完整 API Key
|
||||
{{ t('apiKeys.newApiKeyModal.visibilityHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -105,13 +114,13 @@
|
||||
@click="copyApiKey"
|
||||
>
|
||||
<i class="fas fa-copy" />
|
||||
复制 API Key
|
||||
{{ t('apiKeys.newApiKeyModal.copyApiKey') }}
|
||||
</button>
|
||||
<button
|
||||
class="rounded-xl border border-gray-300 bg-gray-200 px-6 py-3 font-semibold text-gray-800 transition-colors hover:bg-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600"
|
||||
@click="handleClose"
|
||||
>
|
||||
我已保存
|
||||
{{ t('apiKeys.newApiKeyModal.alreadySaved') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -121,8 +130,11 @@
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { showToast } from '@/utils/toast'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
apiKey: {
|
||||
type: Object,
|
||||
@@ -159,13 +171,13 @@ const getDisplayedApiKey = () => {
|
||||
const copyApiKey = async () => {
|
||||
const key = props.apiKey.apiKey || props.apiKey.key || ''
|
||||
if (!key) {
|
||||
showToast('API Key 不存在', 'error')
|
||||
showToast(t('apiKeys.newApiKeyModal.apiKeyNotFound'), 'error')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(key)
|
||||
showToast('API Key 已复制到剪贴板', 'success')
|
||||
showToast(t('apiKeys.newApiKeyModal.copySuccess'), 'success')
|
||||
} catch (error) {
|
||||
// console.error('Failed to copy:', error)
|
||||
// 降级方案:创建一个临时文本区域
|
||||
@@ -175,9 +187,9 @@ const copyApiKey = async () => {
|
||||
textArea.select()
|
||||
try {
|
||||
document.execCommand('copy')
|
||||
showToast('API Key 已复制到剪贴板', 'success')
|
||||
showToast(t('apiKeys.newApiKeyModal.copySuccess'), 'success')
|
||||
} catch (fallbackError) {
|
||||
showToast('复制失败,请手动复制', 'error')
|
||||
showToast(t('apiKeys.newApiKeyModal.copyFailed'), 'error')
|
||||
} finally {
|
||||
document.body.removeChild(textArea)
|
||||
}
|
||||
@@ -188,19 +200,17 @@ const copyApiKey = async () => {
|
||||
const handleClose = async () => {
|
||||
if (window.showConfirm) {
|
||||
const confirmed = await window.showConfirm(
|
||||
'关闭提醒',
|
||||
'关闭后将无法再次查看完整的API Key,请确保已经妥善保存。\n\n确定要关闭吗?',
|
||||
'确定关闭',
|
||||
'取消'
|
||||
t('apiKeys.newApiKeyModal.closeReminderTitle'),
|
||||
t('apiKeys.newApiKeyModal.closeReminderMessage'),
|
||||
t('apiKeys.newApiKeyModal.confirmClose'),
|
||||
t('apiKeys.newApiKeyModal.cancel')
|
||||
)
|
||||
if (confirmed) {
|
||||
emit('close')
|
||||
}
|
||||
} else {
|
||||
// 降级方案
|
||||
const confirmed = confirm(
|
||||
'关闭后将无法再次查看完整的API Key,请确保已经妥善保存。\n\n确定要关闭吗?'
|
||||
)
|
||||
const confirmed = confirm(t('apiKeys.newApiKeyModal.closeReminderMessage'))
|
||||
if (confirmed) {
|
||||
emit('close')
|
||||
}
|
||||
@@ -211,17 +221,17 @@ const handleClose = async () => {
|
||||
const handleDirectClose = async () => {
|
||||
if (window.showConfirm) {
|
||||
const confirmed = await window.showConfirm(
|
||||
'确定要关闭吗?',
|
||||
'您还没有保存API Key,关闭后将无法再次查看。\n\n建议您先复制API Key再关闭。',
|
||||
'仍然关闭',
|
||||
'返回复制'
|
||||
t('apiKeys.newApiKeyModal.directCloseTitle'),
|
||||
t('apiKeys.newApiKeyModal.directCloseMessage'),
|
||||
t('apiKeys.newApiKeyModal.stillClose'),
|
||||
t('apiKeys.newApiKeyModal.goBack')
|
||||
)
|
||||
if (confirmed) {
|
||||
emit('close')
|
||||
}
|
||||
} else {
|
||||
// 降级方案
|
||||
const confirmed = confirm('您还没有保存API Key,关闭后将无法再次查看。\n\n确定要关闭吗?')
|
||||
const confirmed = confirm(t('apiKeys.newApiKeyModal.directCloseFallback'))
|
||||
if (confirmed) {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
>
|
||||
<i class="fas fa-clock text-white" />
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900">续期 API Key</h3>
|
||||
<h3 class="text-xl font-bold text-gray-900">
|
||||
{{ $t('apiKeys.renewApiKeyModal.title') }}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
class="text-gray-400 transition-colors hover:text-gray-600"
|
||||
@@ -28,13 +30,18 @@
|
||||
<i class="fas fa-info text-sm text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="mb-1 font-semibold text-gray-800">API Key 信息</h4>
|
||||
<h4 class="mb-1 font-semibold text-gray-800">
|
||||
{{ $t('apiKeys.renewApiKeyModal.apiKeyInfo') }}
|
||||
</h4>
|
||||
<p class="text-sm text-gray-700">
|
||||
{{ apiKey.name }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-600">
|
||||
当前过期时间:{{
|
||||
apiKey.expiresAt ? formatExpireDate(apiKey.expiresAt) : '永不过期'
|
||||
{{ $t('apiKeys.renewApiKeyModal.currentExpiry')
|
||||
}}{{
|
||||
apiKey.expiresAt
|
||||
? formatExpireDate(apiKey.expiresAt)
|
||||
: $t('apiKeys.renewApiKeyModal.neverExpires')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
@@ -42,19 +49,21 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700">续期时长</label>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700">{{
|
||||
$t('apiKeys.renewApiKeyModal.renewDuration')
|
||||
}}</label>
|
||||
<select
|
||||
v-model="form.renewDuration"
|
||||
class="form-input w-full"
|
||||
@change="updateRenewExpireAt"
|
||||
>
|
||||
<option value="7d">延长 7 天</option>
|
||||
<option value="30d">延长 30 天</option>
|
||||
<option value="90d">延长 90 天</option>
|
||||
<option value="180d">延长 180 天</option>
|
||||
<option value="365d">延长 365 天</option>
|
||||
<option value="custom">自定义日期</option>
|
||||
<option value="permanent">设为永不过期</option>
|
||||
<option value="7d">{{ $t('apiKeys.renewApiKeyModal.extend7Days') }}</option>
|
||||
<option value="30d">{{ $t('apiKeys.renewApiKeyModal.extend30Days') }}</option>
|
||||
<option value="90d">{{ $t('apiKeys.renewApiKeyModal.extend90Days') }}</option>
|
||||
<option value="180d">{{ $t('apiKeys.renewApiKeyModal.extend180Days') }}</option>
|
||||
<option value="365d">{{ $t('apiKeys.renewApiKeyModal.extend365Days') }}</option>
|
||||
<option value="custom">{{ $t('apiKeys.renewApiKeyModal.customDate') }}</option>
|
||||
<option value="permanent">{{ $t('apiKeys.renewApiKeyModal.setPermanent') }}</option>
|
||||
</select>
|
||||
<div v-if="form.renewDuration === 'custom'" class="mt-3">
|
||||
<input
|
||||
@@ -66,7 +75,8 @@
|
||||
/>
|
||||
</div>
|
||||
<p v-if="form.newExpiresAt" class="mt-2 text-xs text-gray-500">
|
||||
新的过期时间:{{ formatExpireDate(form.newExpiresAt) }}
|
||||
{{ $t('apiKeys.renewApiKeyModal.newExpiry')
|
||||
}}{{ formatExpireDate(form.newExpiresAt) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -77,7 +87,7 @@
|
||||
type="button"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
取消
|
||||
{{ $t('apiKeys.renewApiKeyModal.cancel') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary flex-1 px-6 py-3 font-semibold"
|
||||
@@ -87,7 +97,11 @@
|
||||
>
|
||||
<div v-if="loading" class="loading-spinner mr-2" />
|
||||
<i v-else class="fas fa-clock mr-2" />
|
||||
{{ loading ? '续期中...' : '确认续期' }}
|
||||
{{
|
||||
loading
|
||||
? $t('apiKeys.renewApiKeyModal.renewing')
|
||||
: $t('apiKeys.renewApiKeyModal.confirmRenew')
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,9 +111,12 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { showToast } from '@/utils/toast'
|
||||
import { apiClient } from '@/config/api'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
apiKey: {
|
||||
type: Object,
|
||||
@@ -133,7 +150,10 @@ const minDateTime = computed(() => {
|
||||
// 格式化过期日期
|
||||
const formatExpireDate = (dateString) => {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
// 根据当前语言设置选择合适的locale
|
||||
const locale =
|
||||
t('common.locale') === 'en' ? 'en-US' : t('common.locale') === 'zh-TW' ? 'zh-TW' : 'zh-CN'
|
||||
return date.toLocaleString(locale, {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
@@ -209,14 +229,14 @@ const renewApiKey = async () => {
|
||||
const result = await apiClient.put(`/admin/api-keys/${props.apiKey.id}`, data)
|
||||
|
||||
if (result.success) {
|
||||
showToast('API Key 续期成功', 'success')
|
||||
showToast(t('apiKeys.renewApiKeyModal.renewSuccess'), 'success')
|
||||
emit('success')
|
||||
emit('close')
|
||||
} else {
|
||||
showToast(result.message || '续期失败', 'error')
|
||||
showToast(result.message || t('apiKeys.renewApiKeyModal.renewFailed'), 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('续期失败', 'error')
|
||||
showToast(t('apiKeys.renewApiKeyModal.renewFailed'), 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<i class="fas fa-chart-line text-sm text-white sm:text-base" />
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100 sm:text-xl">
|
||||
使用统计详情 - {{ apiKey.name }}
|
||||
{{ t('apiKeys.usageDetailModal.title') }} - {{ apiKey.name }}
|
||||
</h3>
|
||||
</div>
|
||||
<button class="p-1 text-gray-400 transition-colors hover:text-gray-600" @click="close">
|
||||
@@ -34,14 +34,17 @@
|
||||
class="rounded-lg border border-blue-200 bg-gradient-to-br from-blue-50 to-blue-100 p-4 dark:border-blue-700 dark:from-blue-900/20 dark:to-blue-800/20"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">总请求数</span>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{
|
||||
t('apiKeys.usageDetailModal.totalRequests')
|
||||
}}</span>
|
||||
<i class="fas fa-paper-plane text-blue-500" />
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{{ formatNumber(totalRequests) }}
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-600 dark:text-gray-400">
|
||||
今日: {{ formatNumber(dailyRequests) }} 次
|
||||
{{ t('apiKeys.usageDetailModal.today') }}: {{ formatNumber(dailyRequests) }}
|
||||
{{ t('apiKeys.usageDetailModal.times') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -50,14 +53,16 @@
|
||||
class="rounded-lg border border-green-200 bg-gradient-to-br from-green-50 to-green-100 p-4 dark:border-green-700 dark:from-green-900/20 dark:to-green-800/20"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">总Token数</span>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{
|
||||
t('apiKeys.usageDetailModal.totalTokens')
|
||||
}}</span>
|
||||
<i class="fas fa-coins text-green-500" />
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{{ formatTokenCount(totalTokens) }}
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-600 dark:text-gray-400">
|
||||
今日: {{ formatTokenCount(dailyTokens) }}
|
||||
{{ t('apiKeys.usageDetailModal.today') }}: {{ formatTokenCount(dailyTokens) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -66,14 +71,16 @@
|
||||
class="rounded-lg border border-yellow-200 bg-gradient-to-br from-yellow-50 to-yellow-100 p-4 dark:border-yellow-700 dark:from-yellow-900/20 dark:to-yellow-800/20"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">总费用</span>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{
|
||||
t('apiKeys.usageDetailModal.totalCost')
|
||||
}}</span>
|
||||
<i class="fas fa-dollar-sign text-yellow-600" />
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
${{ totalCost.toFixed(4) }}
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-600 dark:text-gray-400">
|
||||
今日: ${{ dailyCost.toFixed(4) }}
|
||||
{{ t('apiKeys.usageDetailModal.today') }}: ${{ dailyCost.toFixed(4) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -82,7 +89,9 @@
|
||||
class="rounded-lg border border-purple-200 bg-gradient-to-br from-purple-50 to-purple-100 p-4 dark:border-purple-700 dark:from-purple-900/20 dark:to-purple-800/20"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">平均速率</span>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{
|
||||
t('apiKeys.usageDetailModal.averageRate')
|
||||
}}</span>
|
||||
<i class="fas fa-tachometer-alt text-purple-500" />
|
||||
</div>
|
||||
<div class="space-y-1 text-sm">
|
||||
@@ -104,13 +113,15 @@
|
||||
class="mb-3 flex items-center text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
<i class="fas fa-chart-pie mr-2 text-indigo-500" />
|
||||
Token 使用分布
|
||||
{{ t('apiKeys.usageDetailModal.tokenDistribution') }}
|
||||
</h4>
|
||||
<div class="space-y-3 rounded-lg bg-gray-50 p-4 dark:bg-gray-700/50">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-arrow-down mr-2 text-green-500" />
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">输入 Token</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">{{
|
||||
t('apiKeys.usageDetailModal.inputTokens')
|
||||
}}</span>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ formatTokenCount(inputTokens) }}
|
||||
@@ -119,7 +130,9 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-arrow-up mr-2 text-blue-500" />
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">输出 Token</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">{{
|
||||
t('apiKeys.usageDetailModal.outputTokens')
|
||||
}}</span>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ formatTokenCount(outputTokens) }}
|
||||
@@ -128,7 +141,9 @@
|
||||
<div v-if="cacheCreateTokens > 0" class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-save mr-2 text-purple-500" />
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">缓存创建 Token</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">{{
|
||||
t('apiKeys.usageDetailModal.cacheCreateTokens')
|
||||
}}</span>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-purple-600">
|
||||
{{ formatTokenCount(cacheCreateTokens) }}
|
||||
@@ -137,7 +152,9 @@
|
||||
<div v-if="cacheReadTokens > 0" class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-download mr-2 text-purple-500" />
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">缓存读取 Token</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">{{
|
||||
t('apiKeys.usageDetailModal.cacheReadTokens')
|
||||
}}</span>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-purple-600">
|
||||
{{ formatTokenCount(cacheReadTokens) }}
|
||||
@@ -152,12 +169,14 @@
|
||||
class="mb-3 flex items-center text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
<i class="fas fa-shield-alt mr-2 text-red-500" />
|
||||
限制设置
|
||||
{{ t('apiKeys.usageDetailModal.limitSettings') }}
|
||||
</h4>
|
||||
<div class="space-y-3 rounded-lg bg-gray-50 p-4 dark:bg-gray-700/50">
|
||||
<div v-if="apiKey.dailyCostLimit > 0" class="space-y-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-600 dark:text-gray-400">每日费用限制</span>
|
||||
<span class="text-gray-600 dark:text-gray-400">{{
|
||||
t('apiKeys.usageDetailModal.dailyCostLimit')
|
||||
}}</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-gray-100">
|
||||
${{ apiKey.dailyCostLimit.toFixed(2) }}
|
||||
</span>
|
||||
@@ -176,7 +195,11 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="text-right text-xs text-gray-500 dark:text-gray-400">
|
||||
已使用 {{ dailyCostPercentage.toFixed(1) }}%
|
||||
{{
|
||||
t('apiKeys.usageDetailModal.usedPercentage', {
|
||||
percentage: dailyCostPercentage.toFixed(1)
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -184,7 +207,9 @@
|
||||
v-if="apiKey.concurrencyLimit > 0"
|
||||
class="flex items-center justify-between text-sm"
|
||||
>
|
||||
<span class="text-gray-600 dark:text-gray-400">并发限制</span>
|
||||
<span class="text-gray-600 dark:text-gray-400">{{
|
||||
t('apiKeys.usageDetailModal.concurrencyLimit')
|
||||
}}</span>
|
||||
<span class="font-semibold text-purple-600">
|
||||
{{ apiKey.currentConcurrency || 0 }} / {{ apiKey.concurrencyLimit }}
|
||||
</span>
|
||||
@@ -193,14 +218,14 @@
|
||||
<div v-if="apiKey.rateLimitWindow > 0" class="space-y-2">
|
||||
<h5 class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<i class="fas fa-clock mr-1 text-blue-500" />
|
||||
时间窗口限制
|
||||
{{ t('apiKeys.usageDetailModal.timeWindowLimit') }}
|
||||
</h5>
|
||||
<WindowCountdown
|
||||
:cost-limit="apiKey.rateLimitCost"
|
||||
:current-cost="apiKey.currentWindowCost"
|
||||
:current-requests="apiKey.currentWindowRequests"
|
||||
:current-tokens="apiKey.currentWindowTokens"
|
||||
label="窗口状态"
|
||||
:label="t('apiKeys.usageDetailModal.windowStatus')"
|
||||
:rate-limit-window="apiKey.rateLimitWindow"
|
||||
:request-limit="apiKey.rateLimitRequests"
|
||||
:show-progress="true"
|
||||
@@ -218,7 +243,7 @@
|
||||
<!-- 底部按钮 -->
|
||||
<div class="mt-4 flex justify-end gap-2 sm:mt-6 sm:gap-3">
|
||||
<button class="btn btn-secondary px-4 py-2 text-sm" type="button" @click="close">
|
||||
关闭
|
||||
{{ t('apiKeys.usageDetailModal.close') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -228,8 +253,11 @@
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import WindowCountdown from './WindowCountdown.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
@@ -274,7 +302,9 @@ const dailyCostPercentage = computed(() => {
|
||||
// 方法
|
||||
const formatNumber = (num) => {
|
||||
if (!num && num !== 0) return '0'
|
||||
return num.toLocaleString('zh-CN')
|
||||
// 根据当前语言环境自动选择合适的地区设置
|
||||
const currentLocale = t('common.locale')
|
||||
return num.toLocaleString(currentLocale)
|
||||
}
|
||||
|
||||
// 格式化Token数量(使用K/M单位)
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
<template>
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="text-gray-500">{{ label }}</span>
|
||||
<span class="text-gray-500">{{ displayLabel }}</span>
|
||||
<span v-if="windowState === 'active'" class="font-medium text-gray-700">
|
||||
<i class="fas fa-clock mr-1 text-blue-500" />
|
||||
{{ formatTime(remainingSeconds) }}
|
||||
</span>
|
||||
<span v-else-if="windowState === 'expired'" class="font-medium text-orange-600">
|
||||
<i class="fas fa-sync-alt mr-1" />
|
||||
窗口已过期
|
||||
{{ t('apiKeys.windowCountdown.expired') }}
|
||||
</span>
|
||||
<span v-else-if="windowState === 'notStarted'" class="font-medium text-gray-500">
|
||||
<i class="fas fa-pause-circle mr-1" />
|
||||
窗口未激活
|
||||
{{ t('apiKeys.windowCountdown.notStarted') }}
|
||||
</span>
|
||||
<span v-else class="font-medium text-gray-400">
|
||||
{{ rateLimitWindow }} {{ t('apiKeys.windowCountdown.minutes') }}
|
||||
</span>
|
||||
<span v-else class="font-medium text-gray-400"> {{ rateLimitWindow }} 分钟 </span>
|
||||
</div>
|
||||
|
||||
<!-- 进度条(仅在有限制时显示) -->
|
||||
<div v-if="showProgress" class="space-y-0.5">
|
||||
<div v-if="hasRequestLimit" class="space-y-0.5">
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="text-gray-400">请求</span>
|
||||
<span class="text-gray-400">{{ t('apiKeys.windowCountdown.requests') }}</span>
|
||||
<span class="text-gray-600"> {{ currentRequests || 0 }}/{{ requestLimit }} </span>
|
||||
</div>
|
||||
<div class="h-1 w-full rounded-full bg-gray-200">
|
||||
@@ -36,7 +38,7 @@
|
||||
<!-- Token限制(向后兼容) -->
|
||||
<div v-if="hasTokenLimit" class="space-y-0.5">
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="text-gray-400">Token</span>
|
||||
<span class="text-gray-400">{{ t('apiKeys.windowCountdown.tokens') }}</span>
|
||||
<span class="text-gray-600">
|
||||
{{ formatTokenCount(currentTokens || 0) }}/{{ formatTokenCount(tokenLimit) }}
|
||||
</span>
|
||||
@@ -53,7 +55,7 @@
|
||||
<!-- 费用限制(新功能) -->
|
||||
<div v-if="hasCostLimit" class="space-y-0.5">
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="text-gray-400">费用</span>
|
||||
<span class="text-gray-400">{{ t('apiKeys.windowCountdown.cost') }}</span>
|
||||
<span class="text-gray-600">
|
||||
${{ (currentCost || 0).toFixed(2) }}/${{ costLimit.toFixed(2) }}
|
||||
</span>
|
||||
@@ -71,22 +73,29 @@
|
||||
<!-- 额外提示信息 -->
|
||||
<div v-if="windowState === 'active' && showTooltip" class="text-xs text-gray-500">
|
||||
<i class="fas fa-info-circle mr-1" />
|
||||
<span v-if="remainingSeconds < 60">即将重置</span>
|
||||
<span v-if="remainingSeconds < 60">{{ t('apiKeys.windowCountdown.aboutToReset') }}</span>
|
||||
<span v-else-if="remainingSeconds < 300"
|
||||
>{{ Math.ceil(remainingSeconds / 60) }} 分钟后重置</span
|
||||
>{{ Math.ceil(remainingSeconds / 60) }}
|
||||
{{ t('apiKeys.windowCountdown.minutesUntilReset') }}</span
|
||||
>
|
||||
<span v-else
|
||||
>{{ formatDetailedTime(remainingSeconds)
|
||||
}}{{ t('apiKeys.windowCountdown.untilReset') }}</span
|
||||
>
|
||||
<span v-else>{{ formatDetailedTime(remainingSeconds) }}后重置</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '窗口限制'
|
||||
default: ''
|
||||
},
|
||||
rateLimitWindow: {
|
||||
type: Number,
|
||||
@@ -143,6 +152,10 @@ const remainingSeconds = ref(props.windowRemainingSeconds)
|
||||
let intervalId = null
|
||||
|
||||
// 计算属性
|
||||
const displayLabel = computed(() => {
|
||||
return props.label || t('apiKeys.windowCountdown.windowLimit')
|
||||
})
|
||||
|
||||
const windowState = computed(() => {
|
||||
if (props.windowStartTime === null) {
|
||||
return 'notStarted' // 窗口未开始
|
||||
@@ -182,9 +195,9 @@ const formatDetailedTime = (seconds) => {
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}小时${minutes}分钟`
|
||||
return `${hours}${t('apiKeys.windowCountdown.hours')}${minutes}${t('apiKeys.windowCountdown.minutes')}`
|
||||
} else {
|
||||
return `${minutes}分钟`
|
||||
return `${minutes}${t('apiKeys.windowCountdown.minutes')}`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
<div
|
||||
class="mt-1 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<span>{{ formatNumber(getStatUsage(stat)?.requests || 0) }}{{ t('apiStats.requests') }}</span>
|
||||
<span
|
||||
>{{ formatNumber(getStatUsage(stat)?.requests || 0) }}{{ t('apiStats.requests') }}</span
|
||||
>
|
||||
<span>{{ getStatUsage(stat)?.formattedCost || '$0.00' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,7 +43,10 @@
|
||||
<!-- 其他Keys汇总 -->
|
||||
<div v-if="otherKeysCount > 0" class="border-t border-gray-200 pt-2 dark:border-gray-700">
|
||||
<div class="flex items-center justify-between text-sm text-gray-600 dark:text-gray-400">
|
||||
<span>{{ t('apiStats.otherKeys') }} {{ otherKeysCount }} {{ t('apiStats.individual') }}{{ t('apiStats.keys') }}</span>
|
||||
<span
|
||||
>{{ t('apiStats.otherKeys') }} {{ otherKeysCount }} {{ t('apiStats.individual')
|
||||
}}{{ t('apiStats.keys') }}</span
|
||||
>
|
||||
<span>{{ otherPercentage }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
<i class="fas fa-chart-line mr-3" />
|
||||
{{ t('apiStats.usageStatsQuery') }}
|
||||
</h2>
|
||||
<p class="text-base text-gray-600 dark:text-gray-400">{{ t('apiStats.apiKeyDescription') }}</p>
|
||||
<p class="text-base text-gray-600 dark:text-gray-400">
|
||||
{{ t('apiStats.apiKeyDescription') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
|
||||
@@ -31,13 +31,17 @@
|
||||
<div class="text-lg font-bold text-gray-900 dark:text-gray-100">
|
||||
{{ aggregatedStats.totalKeys }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">{{ t('apiStats.totalKeys') }}</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">
|
||||
{{ t('apiStats.totalKeys') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-lg font-bold text-green-600">
|
||||
{{ aggregatedStats.activeKeys }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">{{ t('apiStats.activeKeys') }}</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">
|
||||
{{ t('apiStats.activeKeys') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -48,7 +52,9 @@
|
||||
>
|
||||
<div class="mb-3 flex items-center">
|
||||
<i class="fas fa-chart-pie mr-2 text-purple-500" />
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('apiStats.aggregateStatsSummary') }}</span>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{
|
||||
t('apiStats.aggregateStatsSummary')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -106,9 +112,9 @@
|
||||
<!-- 每日费用限制 -->
|
||||
<div>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-600 dark:text-gray-400 md:text-base"
|
||||
>{{ t('apiStats.dailyCostLimit') }}</span
|
||||
>
|
||||
<span class="text-sm font-medium text-gray-600 dark:text-gray-400 md:text-base">{{
|
||||
t('apiStats.dailyCostLimit')
|
||||
}}</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400 md:text-sm">
|
||||
<span v-if="statsData.limits.dailyCostLimit > 0">
|
||||
${{ statsData.limits.currentDailyCost.toFixed(4) }} / ${{
|
||||
@@ -175,7 +181,9 @@
|
||||
<!-- 其他限制信息 -->
|
||||
<div class="space-y-2 border-t border-gray-100 pt-2 dark:border-gray-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.concurrencyLimit') }}</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
|
||||
t('apiStats.concurrencyLimit')
|
||||
}}</span>
|
||||
<span class="text-sm font-medium text-gray-900 md:text-base">
|
||||
<span v-if="statsData.limits.concurrencyLimit > 0">
|
||||
{{ statsData.limits.concurrencyLimit }}
|
||||
@@ -186,7 +194,9 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.modelLimit') }}</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
|
||||
t('apiStats.modelLimit')
|
||||
}}</span>
|
||||
<span class="text-sm font-medium text-gray-900 md:text-base">
|
||||
<span
|
||||
v-if="
|
||||
@@ -196,7 +206,11 @@
|
||||
class="text-orange-600"
|
||||
>
|
||||
<i class="fas fa-exclamation-triangle mr-1 text-xs md:text-sm" />
|
||||
{{ t('apiStats.restrictedModelsCount', { count: statsData.restrictions.restrictedModels.length }) }}
|
||||
{{
|
||||
t('apiStats.restrictedModelsCount', {
|
||||
count: statsData.restrictions.restrictedModels.length
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span v-else class="text-green-600">
|
||||
<i class="fas fa-check-circle mr-1 text-xs md:text-sm" />
|
||||
@@ -205,7 +219,9 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.clientLimit') }}</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
|
||||
t('apiStats.clientLimit')
|
||||
}}</span>
|
||||
<span class="text-sm font-medium text-gray-900 md:text-base">
|
||||
<span
|
||||
v-if="
|
||||
@@ -215,7 +231,11 @@
|
||||
class="text-orange-600"
|
||||
>
|
||||
<i class="fas fa-exclamation-triangle mr-1 text-xs md:text-sm" />
|
||||
{{ t('apiStats.restrictedClientsCount', { count: statsData.restrictions.allowedClients.length }) }}
|
||||
{{
|
||||
t('apiStats.restrictedClientsCount', {
|
||||
count: statsData.restrictions.allowedClients.length
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span v-else class="text-green-600">
|
||||
<i class="fas fa-check-circle mr-1 text-xs md:text-sm" />
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
<i
|
||||
class="fas fa-spinner loading-spinner mb-2 text-xl text-gray-600 dark:text-gray-400 md:text-2xl"
|
||||
/>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.loadingModelStats') }}</p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 md:text-base">
|
||||
{{ t('apiStats.loadingModelStats') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 模型统计数据 -->
|
||||
@@ -38,7 +40,9 @@
|
||||
<div class="text-base font-bold text-green-600 md:text-lg">
|
||||
{{ model.formatted?.total || '$0.000000' }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400 md:text-sm">{{ t('apiStats.totalCost') }}</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400 md:text-sm">
|
||||
{{ t('apiStats.totalCost') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -56,7 +60,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded bg-gray-50 p-2 dark:bg-gray-700">
|
||||
<div class="text-gray-600 dark:text-gray-400">{{ t('apiStats.cacheCreateTokens') }}</div>
|
||||
<div class="text-gray-600 dark:text-gray-400">
|
||||
{{ t('apiStats.cacheCreateTokens') }}
|
||||
</div>
|
||||
<div class="font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ formatNumber(model.cacheCreateTokens) }}
|
||||
</div>
|
||||
@@ -75,7 +81,11 @@
|
||||
<div v-else class="py-6 text-center text-gray-500 dark:text-gray-400 md:py-8">
|
||||
<i class="fas fa-chart-pie mb-3 text-2xl md:text-3xl" />
|
||||
<p class="text-sm md:text-base">
|
||||
{{ t('apiStats.noModelData', { period: statsPeriod === 'daily' ? t('apiStats.today') : t('apiStats.thisMonth') }) }}
|
||||
{{
|
||||
t('apiStats.noModelData', {
|
||||
period: statsPeriod === 'daily' ? t('apiStats.today') : t('apiStats.thisMonth')
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,39 +17,51 @@
|
||||
<!-- 多 Key 模式下的概要信息 -->
|
||||
<div v-if="multiKeyMode && aggregatedStats" class="space-y-2 md:space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.queryKeysCount') }}</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
|
||||
t('apiStats.queryKeysCount')
|
||||
}}</span>
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-gray-100 md:text-base">
|
||||
{{ aggregatedStats.totalKeys }} {{ t('apiStats.individual') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.activeKeysCount') }}</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
|
||||
t('apiStats.activeKeysCount')
|
||||
}}</span>
|
||||
<span class="text-sm font-medium text-green-600 md:text-base">
|
||||
<i class="fas fa-check-circle mr-1 text-xs md:text-sm" />
|
||||
{{ aggregatedStats.activeKeys }} {{ t('apiStats.individual') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="invalidKeys.length > 0" class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.invalidKeysCount') }}</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
|
||||
t('apiStats.invalidKeysCount')
|
||||
}}</span>
|
||||
<span class="text-sm font-medium text-red-600 md:text-base">
|
||||
<i class="fas fa-times-circle mr-1 text-xs md:text-sm" />
|
||||
{{ invalidKeys.length }} {{ t('apiStats.individual') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.totalRequests') }}</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
|
||||
t('apiStats.totalRequests')
|
||||
}}</span>
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-gray-100 md:text-base">
|
||||
{{ formatNumber(aggregatedStats.usage.requests) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.totalTokens') }}</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
|
||||
t('apiStats.totalTokens')
|
||||
}}</span>
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-gray-100 md:text-base">
|
||||
{{ formatNumber(aggregatedStats.usage.allTokens) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.totalCost') }}</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
|
||||
t('apiStats.totalCost')
|
||||
}}</span>
|
||||
<span class="text-sm font-medium text-indigo-600 md:text-base">
|
||||
{{ aggregatedStats.usage.formattedCost }}
|
||||
</span>
|
||||
@@ -60,7 +72,9 @@
|
||||
v-if="individualStats.length > 1"
|
||||
class="border-t border-gray-200 pt-2 dark:border-gray-700"
|
||||
>
|
||||
<div class="mb-2 text-xs text-gray-500 dark:text-gray-400">{{ t('apiStats.keyContribution') }}</div>
|
||||
<div class="mb-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('apiStats.keyContribution') }}
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div
|
||||
v-for="stat in topContributors"
|
||||
@@ -79,14 +93,18 @@
|
||||
<!-- 单 Key 模式下的详细信息 -->
|
||||
<div v-else class="space-y-2 md:space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.name') }}</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
|
||||
t('apiStats.name')
|
||||
}}</span>
|
||||
<span
|
||||
class="break-all text-sm font-medium text-gray-900 dark:text-gray-100 md:text-base"
|
||||
>{{ statsData.name }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.status') }}</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
|
||||
t('apiStats.status')
|
||||
}}</span>
|
||||
<span
|
||||
class="text-sm font-medium md:text-base"
|
||||
:class="statsData.isActive ? 'text-green-600' : 'text-red-600'"
|
||||
@@ -99,22 +117,26 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.permissions') }}</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
|
||||
t('apiStats.permissions')
|
||||
}}</span>
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-gray-100 md:text-base">{{
|
||||
formatPermissions(statsData.permissions)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{ t('apiStats.createdAt') }}</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
|
||||
t('apiStats.createdAt')
|
||||
}}</span>
|
||||
<span
|
||||
class="break-all text-xs font-medium text-gray-900 dark:text-gray-100 md:text-base"
|
||||
>{{ formatDate(statsData.createdAt) }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-start justify-between">
|
||||
<span class="mt-1 flex-shrink-0 text-sm text-gray-600 dark:text-gray-400 md:text-base"
|
||||
>{{ t('apiStats.expiresAt') }}</span
|
||||
>
|
||||
<span class="mt-1 flex-shrink-0 text-sm text-gray-600 dark:text-gray-400 md:text-base">{{
|
||||
t('apiStats.expiresAt')
|
||||
}}</span>
|
||||
<!-- 未激活状态 -->
|
||||
<div
|
||||
v-if="statsData.expirationMode === 'activation' && !statsData.isActivated"
|
||||
@@ -177,7 +199,9 @@
|
||||
{{ formatNumber(currentPeriodData.requests) }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400 md:text-sm">
|
||||
{{ statsPeriod === 'daily' ? t('apiStats.todayRequests') : t('apiStats.monthlyRequests') }}
|
||||
{{
|
||||
statsPeriod === 'daily' ? t('apiStats.todayRequests') : t('apiStats.monthlyRequests')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card text-center">
|
||||
@@ -201,7 +225,11 @@
|
||||
{{ formatNumber(currentPeriodData.inputTokens) }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400 md:text-sm">
|
||||
{{ statsPeriod === 'daily' ? t('apiStats.todayInputTokens') : t('apiStats.monthlyInputTokens') }}
|
||||
{{
|
||||
statsPeriod === 'daily'
|
||||
? t('apiStats.todayInputTokens')
|
||||
: t('apiStats.monthlyInputTokens')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -51,9 +51,9 @@
|
||||
</div>
|
||||
<div class="mt-3 border-t border-gray-200 pt-3 dark:border-gray-700 md:mt-4 md:pt-4">
|
||||
<div class="flex items-center justify-between font-bold text-gray-900 dark:text-gray-100">
|
||||
<span class="text-sm md:text-base"
|
||||
>{{ statsPeriod === 'daily' ? t('apiStats.todayTotal') : t('apiStats.monthlyTotal') }}</span
|
||||
>
|
||||
<span class="text-sm md:text-base">{{
|
||||
statsPeriod === 'daily' ? t('apiStats.todayTotal') : t('apiStats.monthlyTotal')
|
||||
}}</span>
|
||||
<span class="text-lg md:text-xl">{{ formatNumber(currentPeriodData.allTokens) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -77,7 +77,9 @@
|
||||
<!-- 版本信息 -->
|
||||
<div class="border-b border-gray-100 px-4 py-3 dark:border-gray-700">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ t('header.currentVersion') }}</span>
|
||||
<span class="text-gray-500 dark:text-gray-400">{{
|
||||
t('header.currentVersion')
|
||||
}}</span>
|
||||
<span class="font-mono text-gray-700 dark:text-gray-300"
|
||||
>v{{ versionInfo.current || '...' }}</span
|
||||
>
|
||||
@@ -165,7 +167,9 @@
|
||||
>
|
||||
<i class="fas fa-key text-white" />
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-gray-100">{{ t('header.changePasswordModal.title') }}</h3>
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{{ t('header.changePasswordModal.title') }}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
class="text-gray-400 transition-colors hover:text-gray-600 dark:hover:text-gray-300"
|
||||
@@ -180,9 +184,9 @@
|
||||
@submit.prevent="changePassword"
|
||||
>
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>{{ t('header.changePasswordModal.currentUsername') }}</label
|
||||
>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
|
||||
t('header.changePasswordModal.currentUsername')
|
||||
}}</label>
|
||||
<input
|
||||
class="form-input w-full cursor-not-allowed bg-gray-100 dark:bg-gray-700 dark:text-gray-300"
|
||||
disabled
|
||||
@@ -195,22 +199,24 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>{{ t('header.changePasswordModal.newUsername') }}</label
|
||||
>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
|
||||
t('header.changePasswordModal.newUsername')
|
||||
}}</label>
|
||||
<input
|
||||
v-model="changePasswordForm.newUsername"
|
||||
class="form-input w-full"
|
||||
:placeholder="t('header.changePasswordModal.newUsernamePlaceholder')"
|
||||
type="text"
|
||||
/>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">{{ t('header.changePasswordModal.newUsernameHint') }}</p>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('header.changePasswordModal.newUsernameHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>{{ t('header.changePasswordModal.currentPassword') }}</label
|
||||
>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
|
||||
t('header.changePasswordModal.currentPassword')
|
||||
}}</label>
|
||||
<input
|
||||
v-model="changePasswordForm.currentPassword"
|
||||
class="form-input w-full"
|
||||
@@ -221,9 +227,9 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>{{ t('header.changePasswordModal.newPassword') }}</label
|
||||
>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
|
||||
t('header.changePasswordModal.newPassword')
|
||||
}}</label>
|
||||
<input
|
||||
v-model="changePasswordForm.newPassword"
|
||||
class="form-input w-full"
|
||||
@@ -231,13 +237,15 @@
|
||||
required
|
||||
type="password"
|
||||
/>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">{{ t('header.changePasswordModal.newPasswordHint') }}</p>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('header.changePasswordModal.newPasswordHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>{{ t('header.changePasswordModal.confirmPassword') }}</label
|
||||
>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">{{
|
||||
t('header.changePasswordModal.confirmPassword')
|
||||
}}</label>
|
||||
<input
|
||||
v-model="changePasswordForm.confirmPassword"
|
||||
class="form-input w-full"
|
||||
@@ -262,7 +270,11 @@
|
||||
>
|
||||
<div v-if="changePasswordLoading" class="loading-spinner mr-2" />
|
||||
<i v-else class="fas fa-save mr-2" />
|
||||
{{ changePasswordLoading ? t('header.changePasswordModal.saving') : t('header.changePasswordModal.save') }}
|
||||
{{
|
||||
changePasswordLoading
|
||||
? t('header.changePasswordModal.saving')
|
||||
: t('header.changePasswordModal.save')
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -60,11 +60,7 @@
|
||||
|
||||
<!-- 刷新按钮 -->
|
||||
<div class="relative">
|
||||
<el-tooltip
|
||||
:content="t('accounts.refreshTooltip')"
|
||||
effect="dark"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-tooltip :content="t('accounts.refreshTooltip')" effect="dark" placement="bottom">
|
||||
<button
|
||||
class="group relative flex items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-all duration-200 hover:border-gray-300 hover:shadow-md disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:border-gray-500 sm:w-auto"
|
||||
:disabled="accountsLoading"
|
||||
@@ -110,7 +106,9 @@
|
||||
<i class="fas fa-user-circle text-xl text-gray-400" />
|
||||
</div>
|
||||
<p class="text-lg text-gray-500 dark:text-gray-400">{{ t('accounts.noAccounts') }}</p>
|
||||
<p class="mt-2 text-sm text-gray-400 dark:text-gray-500">{{ t('accounts.noAccountsHint') }}</p>
|
||||
<p class="mt-2 text-sm text-gray-400 dark:text-gray-500">
|
||||
{{ t('accounts.noAccountsHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 桌面端表格视图 -->
|
||||
@@ -390,7 +388,9 @@
|
||||
class="flex items-center gap-1.5 rounded-lg border border-gray-200 bg-gradient-to-r from-gray-100 to-gray-200 px-2.5 py-1"
|
||||
>
|
||||
<i class="fas fa-question text-xs text-gray-700" />
|
||||
<span class="text-xs font-semibold text-gray-800">{{ t('accounts.unknown') }}</span>
|
||||
<span class="text-xs font-semibold text-gray-800">{{
|
||||
t('accounts.unknown')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -451,7 +451,11 @@
|
||||
typeof account.rateLimitStatus === 'object' &&
|
||||
account.rateLimitStatus.minutesRemaining > 0
|
||||
"
|
||||
>({{ t('accounts.rateLimitTime', { time: formatRateLimitTime(account.rateLimitStatus.minutesRemaining) }) }})</span
|
||||
>({{
|
||||
t('accounts.rateLimitTime', {
|
||||
time: formatRateLimitTime(account.rateLimitStatus.minutesRemaining)
|
||||
})
|
||||
}})</span
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
@@ -609,7 +613,11 @@
|
||||
v-if="account.sessionWindow.remainingTime > 0"
|
||||
class="font-medium text-indigo-600 dark:text-indigo-400"
|
||||
>
|
||||
{{ t('accounts.remaining', { time: formatRemainingTime(account.sessionWindow.remainingTime) }) }}
|
||||
{{
|
||||
t('accounts.remaining', {
|
||||
time: formatRemainingTime(account.sessionWindow.remainingTime)
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -617,7 +625,9 @@
|
||||
<div v-else-if="account.platform === 'claude-console'" class="space-y-2">
|
||||
<div v-if="Number(account.dailyQuota) > 0">
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="text-gray-600 dark:text-gray-300">{{ t('accounts.quotaProgress') }}</span>
|
||||
<span class="text-gray-600 dark:text-gray-300">{{
|
||||
t('accounts.quotaProgress')
|
||||
}}</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-200">
|
||||
{{ getQuotaUsagePercent(account).toFixed(1) }}%
|
||||
</span>
|
||||
@@ -642,9 +652,9 @@
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">
|
||||
{{ t('accounts.remainingQuota', { amount: formatRemainingQuota(account) }) }}
|
||||
<span class="ml-2 text-gray-400"
|
||||
>{{ t('accounts.reset', { time: account.quotaResetTime || '00:00' }) }}</span
|
||||
>
|
||||
<span class="ml-2 text-gray-400">{{
|
||||
t('accounts.reset', { time: account.quotaResetTime || '00:00' })
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-sm text-gray-400">
|
||||
@@ -682,7 +692,11 @@
|
||||
: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200'
|
||||
]"
|
||||
:disabled="account.isResetting"
|
||||
:title="account.isResetting ? t('accounts.resetting') : t('accounts.resetStatusTooltip')"
|
||||
:title="
|
||||
account.isResetting
|
||||
? t('accounts.resetting')
|
||||
: t('accounts.resetStatusTooltip')
|
||||
"
|
||||
@click="resetAccountStatus(account)"
|
||||
>
|
||||
<i :class="['fas fa-redo', account.isResetting ? 'animate-spin' : '']" />
|
||||
@@ -698,11 +712,17 @@
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
]"
|
||||
:disabled="account.isTogglingSchedulable"
|
||||
:title="account.schedulable ? t('accounts.disableTooltip') : t('accounts.enableTooltip')"
|
||||
:title="
|
||||
account.schedulable
|
||||
? t('accounts.disableTooltip')
|
||||
: t('accounts.enableTooltip')
|
||||
"
|
||||
@click="toggleSchedulable(account)"
|
||||
>
|
||||
<i :class="['fas', account.schedulable ? 'fa-toggle-on' : 'fa-toggle-off']" />
|
||||
<span class="ml-1">{{ account.schedulable ? t('accounts.scheduling') : t('accounts.disabled') }}</span>
|
||||
<span class="ml-1">{{
|
||||
account.schedulable ? t('accounts.scheduling') : t('accounts.disabled')
|
||||
}}</span>
|
||||
</button>
|
||||
<button
|
||||
class="rounded bg-blue-100 px-2.5 py-1 text-xs font-medium text-blue-700 transition-colors hover:bg-blue-200"
|
||||
@@ -799,7 +819,9 @@
|
||||
<!-- 使用统计 -->
|
||||
<div class="mb-3 grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('accounts.dailyUsageLabel') }}</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('accounts.dailyUsageLabel') }}
|
||||
</p>
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="h-1.5 w-1.5 rounded-full bg-blue-500" />
|
||||
@@ -822,7 +844,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('accounts.sessionWindowLabel') }}</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('accounts.sessionWindowLabel') }}
|
||||
</p>
|
||||
<div v-if="account.usage && account.usage.sessionWindow" class="space-y-1">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="h-1.5 w-1.5 rounded-full bg-purple-500" />
|
||||
@@ -854,11 +878,10 @@
|
||||
>
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="font-medium text-gray-600 dark:text-gray-300">{{ t('accounts.sessionWindowLabel') }}</span>
|
||||
<el-tooltip
|
||||
:content="t('accounts.sessionWindowTooltipMobile')"
|
||||
placement="top"
|
||||
>
|
||||
<span class="font-medium text-gray-600 dark:text-gray-300">{{
|
||||
t('accounts.sessionWindowLabel')
|
||||
}}</span>
|
||||
<el-tooltip :content="t('accounts.sessionWindowTooltipMobile')" placement="top">
|
||||
<i
|
||||
class="fas fa-question-circle cursor-help text-xs text-gray-400 hover:text-gray-600"
|
||||
/>
|
||||
@@ -890,7 +913,11 @@
|
||||
v-if="account.sessionWindow.remainingTime > 0"
|
||||
class="font-medium text-indigo-600"
|
||||
>
|
||||
{{ t('accounts.remaining', { time: formatRemainingTime(account.sessionWindow.remainingTime) }) }}
|
||||
{{
|
||||
t('accounts.remaining', {
|
||||
time: formatRemainingTime(account.sessionWindow.remainingTime)
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span v-else class="text-gray-500"> {{ t('accounts.ended') }} </span>
|
||||
</div>
|
||||
@@ -898,9 +925,15 @@
|
||||
|
||||
<!-- 最后使用时间 -->
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ t('accounts.lastUsedLabel') }}</span>
|
||||
<span class="text-gray-500 dark:text-gray-400">{{
|
||||
t('accounts.lastUsedLabel')
|
||||
}}</span>
|
||||
<span class="text-gray-700 dark:text-gray-200">
|
||||
{{ account.lastUsedAt ? formatRelativeTime(account.lastUsedAt) : t('accounts.neverUsed') }}
|
||||
{{
|
||||
account.lastUsedAt
|
||||
? formatRelativeTime(account.lastUsedAt)
|
||||
: t('accounts.neverUsed')
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -917,7 +950,9 @@
|
||||
|
||||
<!-- 调度优先级 -->
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ t('accounts.priorityLabel') }}</span>
|
||||
<span class="text-gray-500 dark:text-gray-400">{{
|
||||
t('accounts.priorityLabel')
|
||||
}}</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-200">
|
||||
{{ account.priority || 50 }}
|
||||
</span>
|
||||
@@ -1610,10 +1645,7 @@ const deleteAccount = async (account) => {
|
||||
).length
|
||||
|
||||
if (boundKeysCount > 0) {
|
||||
showToast(
|
||||
t('accounts.cannotDeleteBoundAccount', { count: boundKeysCount }),
|
||||
'error'
|
||||
)
|
||||
showToast(t('accounts.cannotDeleteBoundAccount', { count: boundKeysCount }), 'error')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1749,7 +1781,10 @@ const toggleSchedulable = async (account) => {
|
||||
|
||||
if (data.success) {
|
||||
account.schedulable = data.schedulable
|
||||
showToast(data.schedulable ? t('accounts.enabledScheduling') : t('accounts.disabledScheduling'), 'success')
|
||||
showToast(
|
||||
data.schedulable ? t('accounts.enabledScheduling') : t('accounts.disabledScheduling'),
|
||||
'success'
|
||||
)
|
||||
} else {
|
||||
showToast(data.message || t('accounts.operationFailed'), 'error')
|
||||
}
|
||||
|
||||
@@ -68,7 +68,10 @@
|
||||
icon="fa-calendar-alt"
|
||||
icon-color="text-blue-500"
|
||||
:options="timeRangeOptions"
|
||||
:placeholder="timeRangeOptions.find(o => o.value === 'today')?.label || t('apiKeys.timeRange.today')"
|
||||
:placeholder="
|
||||
timeRangeOptions.find((o) => o.value === 'today')?.label ||
|
||||
t('apiKeys.timeRange.today')
|
||||
"
|
||||
@change="loadApiKeys()"
|
||||
/>
|
||||
</div>
|
||||
@@ -84,7 +87,9 @@
|
||||
icon="fa-tags"
|
||||
icon-color="text-purple-500"
|
||||
:options="tagOptions"
|
||||
:placeholder="tagOptions.find(o => o.value === '')?.label || t('apiKeys.allTags')"
|
||||
:placeholder="
|
||||
tagOptions.find((o) => o.value === '')?.label || t('apiKeys.allTags')
|
||||
"
|
||||
@change="currentPage = 1"
|
||||
/>
|
||||
<span
|
||||
@@ -105,7 +110,11 @@
|
||||
<input
|
||||
v-model="searchKeyword"
|
||||
class="w-full rounded-lg border border-gray-200 bg-white px-3 py-2 pl-9 text-sm text-gray-700 placeholder-gray-400 shadow-sm transition-all duration-200 hover:border-gray-300 focus:border-cyan-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/20 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:placeholder-gray-500 dark:hover:border-gray-500"
|
||||
:placeholder="isLdapEnabled ? t('apiKeys.searchPlaceholderWithOwner') : t('apiKeys.searchPlaceholder')"
|
||||
:placeholder="
|
||||
isLdapEnabled
|
||||
? t('apiKeys.searchPlaceholderWithOwner')
|
||||
: t('apiKeys.searchPlaceholder')
|
||||
"
|
||||
type="text"
|
||||
@input="currentPage = 1"
|
||||
/>
|
||||
@@ -148,7 +157,9 @@
|
||||
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-blue-500 to-indigo-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
|
||||
></div>
|
||||
<i class="fas fa-edit relative text-blue-600 dark:text-blue-400" />
|
||||
<span class="relative">{{ t('apiKeys.bulkEdit') }} ({{ selectedApiKeys.length }})</span>
|
||||
<span class="relative"
|
||||
>{{ t('apiKeys.bulkEdit') }} ({{ selectedApiKeys.length }})</span
|
||||
>
|
||||
</button>
|
||||
|
||||
<!-- 批量删除按钮 - 移到刷新按钮旁边 -->
|
||||
@@ -161,7 +172,9 @@
|
||||
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-red-500 to-pink-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
|
||||
></div>
|
||||
<i class="fas fa-trash relative text-red-600 dark:text-red-400" />
|
||||
<span class="relative">{{ t('apiKeys.bulkDelete') }} ({{ selectedApiKeys.length }})</span>
|
||||
<span class="relative"
|
||||
>{{ t('apiKeys.bulkDelete') }} ({{ selectedApiKeys.length }})</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -471,25 +484,34 @@
|
||||
<!-- 今日使用统计 -->
|
||||
<div class="mb-2">
|
||||
<div class="mb-1 flex items-center justify-between text-sm">
|
||||
<span class="text-gray-600 dark:text-gray-400">{{ t('apiKeys.dailyRequests') }}</span>
|
||||
<span class="text-gray-600 dark:text-gray-400">{{
|
||||
t('apiKeys.dailyRequests')
|
||||
}}</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-gray-100"
|
||||
>{{ formatNumber(key.usage?.daily?.requests || 0) }}{{ t('apiKeys.requests') }}</span
|
||||
>{{ formatNumber(key.usage?.daily?.requests || 0)
|
||||
}}{{ t('apiKeys.requests') }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-600 dark:text-gray-400">{{ t('apiKeys.dailyCost') }}</span>
|
||||
<span class="text-gray-600 dark:text-gray-400">{{
|
||||
t('apiKeys.dailyCost')
|
||||
}}</span>
|
||||
<span class="font-semibold text-green-600"
|
||||
>${{ (key.dailyCost || 0).toFixed(4) }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-600 dark:text-gray-400">{{ t('apiKeys.totalCost') }}</span>
|
||||
<span class="text-gray-600 dark:text-gray-400">{{
|
||||
t('apiKeys.totalCost')
|
||||
}}</span>
|
||||
<span class="font-semibold text-blue-600"
|
||||
>${{ (key.totalCost || 0).toFixed(4) }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-600 dark:text-gray-400">{{ t('apiKeys.lastUsed') }}</span>
|
||||
<span class="text-gray-600 dark:text-gray-400">{{
|
||||
t('apiKeys.lastUsed')
|
||||
}}</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300">{{
|
||||
formatLastUsed(key.lastUsedAt)
|
||||
}}</span>
|
||||
@@ -499,7 +521,9 @@
|
||||
<!-- 每日费用限制进度条 -->
|
||||
<div v-if="key.dailyCostLimit > 0" class="space-y-1">
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ t('apiKeys.dailyLimit') }}</span>
|
||||
<span class="text-gray-500 dark:text-gray-400">{{
|
||||
t('apiKeys.dailyLimit')
|
||||
}}</span>
|
||||
<span class="text-gray-700 dark:text-gray-300">
|
||||
${{ (key.dailyCost || 0).toFixed(2) }} / ${{
|
||||
key.dailyCostLimit.toFixed(2)
|
||||
@@ -518,7 +542,9 @@
|
||||
<!-- Opus 周费用限制进度条 -->
|
||||
<div v-if="key.weeklyOpusCostLimit > 0" class="space-y-1">
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ t('apiKeys.weeklyOpusLimit') }}</span>
|
||||
<span class="text-gray-500 dark:text-gray-400">{{
|
||||
t('apiKeys.weeklyOpusLimit')
|
||||
}}</span>
|
||||
<span class="text-gray-700 dark:text-gray-300">
|
||||
${{ (key.weeklyOpusCost || 0).toFixed(2) }} / ${{
|
||||
key.weeklyOpusCostLimit.toFixed(2)
|
||||
@@ -703,7 +729,9 @@
|
||||
<td class="bg-gray-50 px-3 py-4 dark:bg-gray-700" colspan="8">
|
||||
<div v-if="!apiKeyModelStats[key.id]" class="py-4 text-center">
|
||||
<div class="loading-spinner mx-auto" />
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">{{ t('apiKeys.loadingModelStats') }}</p>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('apiKeys.loadingModelStats') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<!-- 通用的标题和时间筛选器,无论是否有数据都显示 -->
|
||||
@@ -719,7 +747,11 @@
|
||||
v-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length > 0"
|
||||
class="rounded-full bg-gray-100 px-2 py-1 text-xs text-gray-500 dark:bg-gray-700 dark:text-gray-400"
|
||||
>
|
||||
{{ t('apiKeys.modelStatsCount', { count: apiKeyModelStats[key.id].length }) }}
|
||||
{{
|
||||
t('apiKeys.modelStatsCount', {
|
||||
count: apiKeyModelStats[key.id].length
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
|
||||
<!-- API Keys日期筛选器 -->
|
||||
@@ -773,7 +805,9 @@
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-center gap-2">
|
||||
<i class="fas fa-chart-line text-lg text-gray-400" />
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ t('apiKeys.noModelData') }}</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('apiKeys.noModelData') }}
|
||||
</p>
|
||||
<button
|
||||
class="ml-2 flex items-center gap-1 text-sm text-blue-500 transition-colors hover:text-blue-700"
|
||||
:title="t('apiKeys.resetFilter')"
|
||||
@@ -1078,7 +1112,9 @@
|
||||
<!-- 今日使用 -->
|
||||
<div class="rounded-lg bg-gray-50 p-3 dark:bg-gray-700">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400">{{ t('apiKeys.dailyUsage') }}</span>
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400">{{
|
||||
t('apiKeys.dailyUsage')
|
||||
}}</span>
|
||||
<button
|
||||
class="text-xs text-blue-600 hover:text-blue-800"
|
||||
@click="showUsageDetails(key)"
|
||||
@@ -1089,19 +1125,26 @@
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ formatNumber(key.usage?.daily?.requests || 0) }} {{ t('apiKeys.requests') }}
|
||||
{{ formatNumber(key.usage?.daily?.requests || 0) }}
|
||||
{{ t('apiKeys.requests') }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('apiKeys.requests') }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('apiKeys.requests') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-green-600">
|
||||
${{ (key.dailyCost || 0).toFixed(4) }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('apiKeys.totalCost') }}</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('apiKeys.totalCost') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center justify-between">
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400">{{ t('apiKeys.lastUsed') }}</span>
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400">{{
|
||||
t('apiKeys.lastUsed')
|
||||
}}</span>
|
||||
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
|
||||
formatLastUsed(key.lastUsedAt)
|
||||
}}</span>
|
||||
@@ -1111,7 +1154,9 @@
|
||||
<!-- 限制进度 -->
|
||||
<div v-if="key.dailyCostLimit > 0" class="space-y-1">
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ t('apiKeys.dailyLimit') }}</span>
|
||||
<span class="text-gray-500 dark:text-gray-400">{{
|
||||
t('apiKeys.dailyLimit')
|
||||
}}</span>
|
||||
<span class="text-gray-700 dark:text-gray-300">
|
||||
${{ (key.dailyCost || 0).toFixed(2) }} / ${{ key.dailyCostLimit.toFixed(2) }}
|
||||
</span>
|
||||
@@ -1250,7 +1295,9 @@
|
||||
{{ t('apiKeys.totalRecords', { count: sortedApiKeys.length }) }}
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400 sm:text-sm">{{ t('apiKeys.pageSize') }}</span>
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400 sm:text-sm">{{
|
||||
t('apiKeys.pageSize')
|
||||
}}</span>
|
||||
<select
|
||||
v-model="pageSize"
|
||||
class="rounded-md border border-gray-200 bg-white px-2 py-1 text-xs text-gray-700 transition-colors hover:border-gray-300 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:border-gray-500 sm:text-sm"
|
||||
@@ -1260,7 +1307,9 @@
|
||||
{{ size }}
|
||||
</option>
|
||||
</select>
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400 sm:text-sm">{{ t('apiKeys.records') }}</span>
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400 sm:text-sm">{{
|
||||
t('apiKeys.records')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1475,19 +1524,26 @@
|
||||
<td class="px-3 py-4">
|
||||
<div class="text-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">{{ t('apiKeys.requests') }}</span>
|
||||
<span class="text-gray-600 dark:text-gray-400">{{
|
||||
t('apiKeys.requests')
|
||||
}}</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ formatNumber(key.usage?.total?.requests || 0) }} {{ t('apiKeys.requests') }}
|
||||
{{ formatNumber(key.usage?.total?.requests || 0) }}
|
||||
{{ t('apiKeys.requests') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">{{ t('apiKeys.totalCost') }}</span>
|
||||
<span class="text-gray-600 dark:text-gray-400">{{
|
||||
t('apiKeys.totalCost')
|
||||
}}</span>
|
||||
<span class="font-semibold text-green-600">
|
||||
${{ (key.usage?.total?.cost || 0).toFixed(4) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="key.lastUsedAt" class="flex items-center justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">{{ t('apiKeys.lastUsed') }}</span>
|
||||
<span class="text-gray-600 dark:text-gray-400">{{
|
||||
t('apiKeys.lastUsed')
|
||||
}}</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ formatLastUsed(key.lastUsedAt) }}
|
||||
</span>
|
||||
@@ -2350,9 +2406,7 @@ const toggleApiKeyStatus = async (key) => {
|
||||
)
|
||||
} else {
|
||||
// 降级方案
|
||||
confirmed = confirm(
|
||||
t('apiKeys.confirmDisable', { name: key.name })
|
||||
)
|
||||
confirmed = confirm(t('apiKeys.confirmDisable', { name: key.name }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2539,7 +2593,12 @@ const batchDeleteApiKeys = async () => {
|
||||
const message = t('apiKeys.confirmBatchDelete', { count: selectedCount })
|
||||
|
||||
if (window.showConfirm) {
|
||||
confirmed = await window.showConfirm(t('apiKeys.confirmBatchDelete').split(' ')[0], message, t('common.confirm'), t('common.cancel'))
|
||||
confirmed = await window.showConfirm(
|
||||
t('apiKeys.confirmBatchDelete').split(' ')[0],
|
||||
message,
|
||||
t('common.confirm'),
|
||||
t('common.cancel')
|
||||
)
|
||||
} else {
|
||||
confirmed = confirm(message)
|
||||
}
|
||||
@@ -2562,7 +2621,10 @@ const batchDeleteApiKeys = async () => {
|
||||
// 如果有失败的,显示详细信息
|
||||
if (failedCount > 0) {
|
||||
const errorMessages = errors.map((e) => `${e.keyId}: ${e.error}`).join('\n')
|
||||
showToast(t('apiKeys.batchPartialFail', { failed: failedCount }) + ':\n' + errorMessages, 'warning')
|
||||
showToast(
|
||||
t('apiKeys.batchPartialFail', { failed: failedCount }) + ':\n' + errorMessages,
|
||||
'warning'
|
||||
)
|
||||
}
|
||||
} else {
|
||||
showToast(t('apiKeys.batchAllFailed'), 'error')
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
to="/user-login"
|
||||
>
|
||||
<i class="fas fa-user text-sm md:text-base" />
|
||||
<span class="text-xs font-semibold tracking-wide md:text-sm">{{ t('apiStats.userLogin') }}</span>
|
||||
<span class="text-xs font-semibold tracking-wide md:text-sm">{{
|
||||
t('apiStats.userLogin')
|
||||
}}</span>
|
||||
</router-link>
|
||||
<!-- 管理后台按钮 -->
|
||||
<router-link
|
||||
@@ -42,7 +44,9 @@
|
||||
to="/dashboard"
|
||||
>
|
||||
<i class="fas fa-shield-alt text-sm md:text-base" />
|
||||
<span class="text-xs font-semibold tracking-wide md:text-sm">{{ t('apiStats.adminPanel') }}</span>
|
||||
<span class="text-xs font-semibold tracking-wide md:text-sm">{{
|
||||
t('apiStats.adminPanel')
|
||||
}}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,9 +101,9 @@
|
||||
>
|
||||
<div class="flex items-center gap-2 md:gap-3">
|
||||
<i class="fas fa-clock text-base text-blue-500 md:text-lg" />
|
||||
<span class="text-base font-medium text-gray-700 dark:text-gray-200 md:text-lg"
|
||||
>{{ t('apiStats.timeRange') }}</span
|
||||
>
|
||||
<span class="text-base font-medium text-gray-700 dark:text-gray-200 md:text-lg">{{
|
||||
t('apiStats.timeRange')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex w-full gap-2 md:w-auto">
|
||||
<button
|
||||
@@ -190,7 +194,7 @@ const currentTutorialComponent = computed(() => {
|
||||
const components = {
|
||||
'zh-cn': TutorialViewZhCn,
|
||||
'zh-tw': TutorialViewZhTw,
|
||||
'en': TutorialViewEn
|
||||
en: TutorialViewEn
|
||||
}
|
||||
return components[locale] || TutorialViewZhCn
|
||||
})
|
||||
|
||||
@@ -42,7 +42,12 @@
|
||||
dashboardData.accountsByPlatform.claude.total > 0
|
||||
"
|
||||
class="inline-flex items-center gap-0.5"
|
||||
:title="t('dashboard.claudeAccount', { total: dashboardData.accountsByPlatform.claude.total, normal: dashboardData.accountsByPlatform.claude.normal })"
|
||||
:title="
|
||||
t('dashboard.claudeAccount', {
|
||||
total: dashboardData.accountsByPlatform.claude.total,
|
||||
normal: dashboardData.accountsByPlatform.claude.normal
|
||||
})
|
||||
"
|
||||
>
|
||||
<i class="fas fa-brain text-xs text-indigo-600" />
|
||||
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
|
||||
@@ -56,7 +61,12 @@
|
||||
dashboardData.accountsByPlatform['claude-console'].total > 0
|
||||
"
|
||||
class="inline-flex items-center gap-0.5"
|
||||
:title="t('dashboard.consoleAccount', { total: dashboardData.accountsByPlatform['claude-console'].total, normal: dashboardData.accountsByPlatform['claude-console'].normal })"
|
||||
:title="
|
||||
t('dashboard.consoleAccount', {
|
||||
total: dashboardData.accountsByPlatform['claude-console'].total,
|
||||
normal: dashboardData.accountsByPlatform['claude-console'].normal
|
||||
})
|
||||
"
|
||||
>
|
||||
<i class="fas fa-terminal text-xs text-purple-600" />
|
||||
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
|
||||
@@ -70,7 +80,12 @@
|
||||
dashboardData.accountsByPlatform.gemini.total > 0
|
||||
"
|
||||
class="inline-flex items-center gap-0.5"
|
||||
:title="t('dashboard.geminiAccount', { total: dashboardData.accountsByPlatform.gemini.total, normal: dashboardData.accountsByPlatform.gemini.normal })"
|
||||
:title="
|
||||
t('dashboard.geminiAccount', {
|
||||
total: dashboardData.accountsByPlatform.gemini.total,
|
||||
normal: dashboardData.accountsByPlatform.gemini.normal
|
||||
})
|
||||
"
|
||||
>
|
||||
<i class="fas fa-robot text-xs text-yellow-600" />
|
||||
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
|
||||
@@ -84,7 +99,12 @@
|
||||
dashboardData.accountsByPlatform.bedrock.total > 0
|
||||
"
|
||||
class="inline-flex items-center gap-0.5"
|
||||
:title="t('dashboard.bedrockAccount', { total: dashboardData.accountsByPlatform.bedrock.total, normal: dashboardData.accountsByPlatform.bedrock.normal })"
|
||||
:title="
|
||||
t('dashboard.bedrockAccount', {
|
||||
total: dashboardData.accountsByPlatform.bedrock.total,
|
||||
normal: dashboardData.accountsByPlatform.bedrock.normal
|
||||
})
|
||||
"
|
||||
>
|
||||
<i class="fab fa-aws text-xs text-orange-600" />
|
||||
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
|
||||
@@ -98,7 +118,12 @@
|
||||
dashboardData.accountsByPlatform.openai.total > 0
|
||||
"
|
||||
class="inline-flex items-center gap-0.5"
|
||||
:title="t('dashboard.openaiAccount', { total: dashboardData.accountsByPlatform.openai.total, normal: dashboardData.accountsByPlatform.openai.normal })"
|
||||
:title="
|
||||
t('dashboard.openaiAccount', {
|
||||
total: dashboardData.accountsByPlatform.openai.total,
|
||||
normal: dashboardData.accountsByPlatform.openai.normal
|
||||
})
|
||||
"
|
||||
>
|
||||
<i class="fas fa-openai text-xs text-gray-100" />
|
||||
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
|
||||
@@ -112,7 +137,12 @@
|
||||
dashboardData.accountsByPlatform.azure_openai.total > 0
|
||||
"
|
||||
class="inline-flex items-center gap-0.5"
|
||||
:title="t('dashboard.azureOpenaiAccount', { total: dashboardData.accountsByPlatform.azure_openai.total, normal: dashboardData.accountsByPlatform.azure_openai.normal })"
|
||||
:title="
|
||||
t('dashboard.azureOpenaiAccount', {
|
||||
total: dashboardData.accountsByPlatform.azure_openai.total,
|
||||
normal: dashboardData.accountsByPlatform.azure_openai.normal
|
||||
})
|
||||
"
|
||||
>
|
||||
<i class="fab fa-microsoft text-xs text-blue-600" />
|
||||
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
|
||||
@@ -153,7 +183,8 @@
|
||||
{{ dashboardData.todayRequests }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('dashboard.totalRequests') }}: {{ formatNumber(dashboardData.totalRequests || 0) }}
|
||||
{{ t('dashboard.totalRequests') }}:
|
||||
{{ formatNumber(dashboardData.totalRequests || 0) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-purple-500 to-purple-600">
|
||||
@@ -303,7 +334,9 @@
|
||||
<div>
|
||||
<p class="mb-1 text-xs font-semibold text-gray-600 dark:text-gray-400 sm:text-sm">
|
||||
{{ t('dashboard.realtimeRPM') }}
|
||||
<span class="text-xs text-gray-400">({{ dashboardData.metricsWindow }}{{ t('dashboard.minutes') }})</span>
|
||||
<span class="text-xs text-gray-400"
|
||||
>({{ dashboardData.metricsWindow }}{{ t('dashboard.minutes') }})</span
|
||||
>
|
||||
</p>
|
||||
<p class="text-2xl font-bold text-orange-600 sm:text-3xl">
|
||||
{{ dashboardData.realtimeRPM || 0 }}
|
||||
@@ -326,7 +359,9 @@
|
||||
<div>
|
||||
<p class="mb-1 text-xs font-semibold text-gray-600 dark:text-gray-400 sm:text-sm">
|
||||
{{ t('dashboard.realtimeTPM') }}
|
||||
<span class="text-xs text-gray-400">({{ dashboardData.metricsWindow }}{{ t('dashboard.minutes') }})</span>
|
||||
<span class="text-xs text-gray-400"
|
||||
>({{ dashboardData.metricsWindow }}{{ t('dashboard.minutes') }})</span
|
||||
>
|
||||
</p>
|
||||
<p class="text-2xl font-bold text-rose-600 sm:text-3xl">
|
||||
{{ formatNumber(dashboardData.realtimeTPM || 0) }}
|
||||
@@ -453,7 +488,9 @@
|
||||
@click="refreshAllData()"
|
||||
>
|
||||
<i :class="['fas fa-sync-alt text-xs', { 'animate-spin': isRefreshing }]" />
|
||||
<span class="hidden sm:inline">{{ isRefreshing ? t('dashboard.refreshing') : t('dashboard.refresh') }}</span>
|
||||
<span class="hidden sm:inline">{{
|
||||
isRefreshing ? t('dashboard.refreshing') : t('dashboard.refresh')
|
||||
}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -579,7 +616,9 @@
|
||||
]"
|
||||
@click="((apiKeysTrendMetric = 'requests'), updateApiKeysUsageTrendChart())"
|
||||
>
|
||||
<i class="fas fa-exchange-alt mr-1" /><span class="hidden sm:inline">{{ t('dashboard.requestsCount') }}</span
|
||||
<i class="fas fa-exchange-alt mr-1" /><span class="hidden sm:inline">{{
|
||||
t('dashboard.requestsCount')
|
||||
}}</span
|
||||
><span class="sm:hidden">{{ t('dashboard.requestsCount').split(' ')[0] }}</span>
|
||||
</button>
|
||||
<button
|
||||
@@ -591,7 +630,9 @@
|
||||
]"
|
||||
@click="((apiKeysTrendMetric = 'tokens'), updateApiKeysUsageTrendChart())"
|
||||
>
|
||||
<i class="fas fa-coins mr-1" /><span class="hidden sm:inline">{{ t('dashboard.tokenCount') }}</span
|
||||
<i class="fas fa-coins mr-1" /><span class="hidden sm:inline">{{
|
||||
t('dashboard.tokenCount')
|
||||
}}</span
|
||||
><span class="sm:hidden">Token</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -600,7 +641,9 @@
|
||||
<span v-if="apiKeysTrendData.totalApiKeys > 10">
|
||||
{{ t('dashboard.showingTop10', { count: apiKeysTrendData.totalApiKeys }) }}
|
||||
</span>
|
||||
<span v-else>{{ t('dashboard.totalApiKeysCount', { count: apiKeysTrendData.totalApiKeys }) }}</span>
|
||||
<span v-else>{{
|
||||
t('dashboard.totalApiKeysCount', { count: apiKeysTrendData.totalApiKeys })
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="sm:h-[350px]" style="height: 300px">
|
||||
<canvas ref="apiKeysUsageTrendChart" />
|
||||
@@ -1164,7 +1207,10 @@ function createApiKeysUsageTrendChart() {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: apiKeysTrendMetric.value === 'tokens' ? t('dashboard.tokenQuantity') : t('dashboard.requestsQuantity'),
|
||||
text:
|
||||
apiKeysTrendMetric.value === 'tokens'
|
||||
? t('dashboard.tokenQuantity')
|
||||
: t('dashboard.requestsQuantity'),
|
||||
color: chartColors.value.text
|
||||
},
|
||||
ticks: {
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
|
||||
<form class="space-y-4 sm:space-y-6" @submit.prevent="handleLogin">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-900 dark:text-gray-100 sm:mb-3"
|
||||
<label
|
||||
class="mb-2 block text-sm font-semibold text-gray-900 dark:text-gray-100 sm:mb-3"
|
||||
>{{ t('login.username') }}</label
|
||||
>
|
||||
<input
|
||||
@@ -55,7 +56,8 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-900 dark:text-gray-100 sm:mb-3"
|
||||
<label
|
||||
class="mb-2 block text-sm font-semibold text-gray-900 dark:text-gray-100 sm:mb-3"
|
||||
>{{ t('login.password') }}</label
|
||||
>
|
||||
<input
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
<h3 class="mb-1 text-lg font-bold text-gray-900 dark:text-gray-100 sm:mb-2 sm:text-xl">
|
||||
{{ t('settings.title') }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 sm:text-base">{{ t('settings.description') }}</p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 sm:text-base">
|
||||
{{ t('settings.description') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 设置分类导航 -->
|
||||
@@ -66,7 +68,9 @@
|
||||
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ t('settings.siteName') }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ t('settings.siteNameDescription') }}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.siteNameDescription') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -75,7 +79,7 @@
|
||||
v-model="oemSettings.siteName"
|
||||
class="form-input w-full max-w-md dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||
maxlength="100"
|
||||
:placeholder="t('settings.siteNamePlaceholder')"
|
||||
:placeholder="t('settings.siteNamePlaceholder')"
|
||||
type="text"
|
||||
/>
|
||||
<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">
|
||||
{{ t('settings.siteIcon') }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ t('settings.siteIconDescription') }}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.siteIconDescription') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -109,12 +115,14 @@
|
||||
class="inline-flex items-center gap-3 rounded-lg bg-gray-50 p-3 dark:bg-gray-700"
|
||||
>
|
||||
<img
|
||||
:alt="t('settings.iconPreview')"
|
||||
:alt="t('settings.iconPreview')"
|
||||
class="h-8 w-8"
|
||||
:src="oemSettings.siteIconData || oemSettings.siteIcon"
|
||||
@error="handleIconError"
|
||||
/>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">{{ t('settings.currentIcon') }}</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">{{
|
||||
t('settings.currentIcon')
|
||||
}}</span>
|
||||
<button
|
||||
class="rounded-lg px-3 py-1 font-medium text-red-600 transition-colors hover:bg-red-50 hover:text-red-900"
|
||||
@click="removeIcon"
|
||||
@@ -139,9 +147,9 @@
|
||||
<i class="fas fa-upload mr-2" />
|
||||
{{ t('settings.uploadIcon') }}
|
||||
</button>
|
||||
<span class="ml-3 text-xs text-gray-500 dark:text-gray-400"
|
||||
>{{ t('settings.iconFormats') }}</span
|
||||
>
|
||||
<span class="ml-3 text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('settings.iconFormats')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -160,7 +168,9 @@
|
||||
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ t('settings.adminEntry') }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ t('settings.adminEntryDescription') }}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.adminEntryDescription') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -172,7 +182,9 @@
|
||||
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800"
|
||||
></div>
|
||||
<span class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300">{{
|
||||
hideAdminButton ? t('settings.hideLoginButton') : t('settings.showLoginButton')
|
||||
hideAdminButton
|
||||
? t('settings.hideLoginButton')
|
||||
: t('settings.showLoginButton')
|
||||
}}</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -213,7 +225,9 @@
|
||||
class="text-sm text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<i class="fas fa-clock mr-1" />
|
||||
{{ t('settings.lastUpdated', { time: formatDateTime(oemSettings.updatedAt) }) }}
|
||||
{{
|
||||
t('settings.lastUpdated', { time: formatDateTime(oemSettings.updatedAt) })
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -233,15 +247,19 @@
|
||||
<i class="fas fa-tag"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">{{ t('settings.siteNameCard') }}</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ t('settings.siteNameCardDesc') }}</p>
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ t('settings.siteNameCard') }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.siteNameCardDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
v-model="oemSettings.siteName"
|
||||
class="form-input w-full dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||
maxlength="100"
|
||||
:placeholder="t('settings.siteNamePlaceholder')"
|
||||
:placeholder="t('settings.siteNamePlaceholder')"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
@@ -255,7 +273,9 @@
|
||||
<i class="fas fa-image"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">{{ t('settings.siteIconCard') }}</h3>
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ t('settings.siteIconCard') }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.siteIconCardDesc') }}
|
||||
</p>
|
||||
@@ -268,12 +288,14 @@
|
||||
class="inline-flex items-center gap-3 rounded-lg bg-gray-50 p-3 dark:bg-gray-700"
|
||||
>
|
||||
<img
|
||||
:alt="t('settings.iconPreview')"
|
||||
:alt="t('settings.iconPreview')"
|
||||
class="h-8 w-8"
|
||||
:src="oemSettings.siteIconData || oemSettings.siteIcon"
|
||||
@error="handleIconError"
|
||||
/>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">{{ t('settings.currentIcon') }}</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">{{
|
||||
t('settings.currentIcon')
|
||||
}}</span>
|
||||
<button
|
||||
class="rounded-lg px-3 py-1 font-medium text-red-600 transition-colors hover:bg-red-50 hover:text-red-900"
|
||||
@click="removeIcon"
|
||||
@@ -314,8 +336,12 @@
|
||||
<i class="fas fa-eye-slash"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">{{ t('settings.adminEntryCard') }}</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ t('settings.adminEntryCardDesc') }}</p>
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ t('settings.adminEntryCard') }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.adminEntryCardDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
@@ -362,7 +388,9 @@
|
||||
class="text-center text-sm text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<i class="fas fa-clock mr-1" />
|
||||
{{ t('settings.lastUpdatedMobile', { time: formatDateTime(oemSettings.updatedAt) }) }}
|
||||
{{
|
||||
t('settings.lastUpdatedMobile', { time: formatDateTime(oemSettings.updatedAt) })
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -402,7 +430,9 @@
|
||||
<div
|
||||
class="mb-6 rounded-lg bg-white/80 p-6 shadow-lg backdrop-blur-sm dark:bg-gray-800/80"
|
||||
>
|
||||
<h2 class="mb-4 text-lg font-semibold text-gray-800 dark:text-gray-200">{{ t('settings.notificationTypes') }}</h2>
|
||||
<h2 class="mb-4 text-lg font-semibold text-gray-800 dark:text-gray-200">
|
||||
{{ t('settings.notificationTypes') }}
|
||||
</h2>
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="(enabled, type) in webhookConfig.notificationTypes"
|
||||
@@ -1207,7 +1237,10 @@ const savePlatform = async () => {
|
||||
}
|
||||
|
||||
if (response.success && isMounted.value) {
|
||||
showToast(editingPlatform.value ? t('settings.platformUpdated') : t('settings.platformAdded'), 'success')
|
||||
showToast(
|
||||
editingPlatform.value ? t('settings.platformUpdated') : t('settings.platformAdded'),
|
||||
'success'
|
||||
)
|
||||
await loadWebhookConfig()
|
||||
closePlatformModal()
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ const currentTutorialComponent = computed(() => {
|
||||
const components = {
|
||||
'zh-cn': TutorialViewZhCn,
|
||||
'zh-tw': TutorialViewZhTw,
|
||||
'en': TutorialViewEn
|
||||
en: TutorialViewEn
|
||||
}
|
||||
return components[locale] || TutorialViewZhCn
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -72,7 +72,8 @@
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="text-sm text-gray-700 dark:text-gray-300">
|
||||
{{ t('user.dashboard.welcome') }}, <span class="font-medium">{{ userStore.userName }}</span>
|
||||
{{ t('user.dashboard.welcome') }},
|
||||
<span class="font-medium">{{ userStore.userName }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 主题切换按钮 -->
|
||||
@@ -94,7 +95,9 @@
|
||||
<!-- Overview Tab -->
|
||||
<div v-if="activeTab === 'overview'" class="space-y-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">{{ t('user.dashboard.title') }}</h1>
|
||||
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('user.dashboard.title') }}
|
||||
</h1>
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ t('user.dashboard.welcomeMessage') }}
|
||||
</p>
|
||||
@@ -272,25 +275,33 @@
|
||||
<div class="mt-5 border-t border-gray-200 dark:border-gray-700">
|
||||
<dl class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('user.dashboard.username') }}</dt>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
{{ t('user.dashboard.username') }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
|
||||
{{ userProfile?.username }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('user.dashboard.displayName') }}</dt>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
{{ t('user.dashboard.displayName') }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
|
||||
{{ userProfile?.displayName || t('user.dashboard.notAvailable') }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('user.dashboard.email') }}</dt>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
{{ t('user.dashboard.email') }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
|
||||
{{ userProfile?.email || t('user.dashboard.notAvailable') }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('user.dashboard.role') }}</dt>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
{{ t('user.dashboard.role') }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
|
||||
<span
|
||||
class="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-200"
|
||||
@@ -300,13 +311,17 @@
|
||||
</dd>
|
||||
</div>
|
||||
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('user.dashboard.memberSince') }}</dt>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
{{ t('user.dashboard.memberSince') }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
|
||||
{{ formatDate(userProfile?.createdAt) }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('user.dashboard.lastLogin') }}</dt>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
{{ t('user.dashboard.lastLogin') }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
|
||||
{{ formatDate(userProfile?.lastLoginAt) || t('user.dashboard.notAvailable') }}
|
||||
</dd>
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
<!-- Header -->
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto">
|
||||
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">{{ t('user.management.title') }}</h1>
|
||||
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('user.management.title') }}
|
||||
</h1>
|
||||
<p class="mt-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
{{ t('user.management.description') }}
|
||||
</p>
|
||||
@@ -254,7 +256,9 @@
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">{{ t('user.management.loadingUsers') }}</p>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('user.management.loadingUsers') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Users List -->
|
||||
@@ -299,7 +303,9 @@
|
||||
: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
|
||||
]"
|
||||
>
|
||||
{{ user.isActive ? t('user.management.active') : t('user.management.disabled') }}
|
||||
{{
|
||||
user.isActive ? t('user.management.active') : t('user.management.disabled')
|
||||
}}
|
||||
</span>
|
||||
<span
|
||||
:class="[
|
||||
@@ -328,8 +334,14 @@
|
||||
v-if="user.totalUsage"
|
||||
class="mt-1 flex items-center space-x-4 text-xs text-gray-400 dark:text-gray-500"
|
||||
>
|
||||
<span>{{ formatNumber(user.totalUsage.requests || 0) }} {{ t('user.management.requests') }}</span>
|
||||
<span>${{ (user.totalUsage.totalCost || 0).toFixed(4) }} {{ t('user.management.totalCostLabel') }}</span>
|
||||
<span
|
||||
>{{ formatNumber(user.totalUsage.requests || 0) }}
|
||||
{{ t('user.management.requests') }}</span
|
||||
>
|
||||
<span
|
||||
>${{ (user.totalUsage.totalCost || 0).toFixed(4) }}
|
||||
{{ t('user.management.totalCostLabel') }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -337,7 +349,7 @@
|
||||
<!-- View Usage Stats -->
|
||||
<button
|
||||
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)"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -354,7 +366,7 @@
|
||||
<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"
|
||||
:disabled="user.apiKeyCount === 0"
|
||||
:title="t('user.management.disableAllApiKeys')"
|
||||
:title="t('user.management.disableAllApiKeys')"
|
||||
@click="disableUserApiKeys(user)"
|
||||
>
|
||||
<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-green-600'
|
||||
]"
|
||||
:title="user.isActive ? t('user.management.disableUser') : t('user.management.enableUser')"
|
||||
:title="
|
||||
user.isActive ? t('user.management.disableUser') : t('user.management.enableUser')
|
||||
"
|
||||
@click="toggleUserStatus(user)"
|
||||
>
|
||||
<svg
|
||||
@@ -405,7 +419,7 @@
|
||||
<!-- Change Role -->
|
||||
<button
|
||||
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)"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -437,7 +451,9 @@
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">{{ t('user.management.noUsersFound') }}</h3>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('user.management.noUsersFound') }}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{
|
||||
searchQuery ? t('user.management.noUsersMatch') : t('user.management.noUsersCreated')
|
||||
@@ -597,7 +613,9 @@ const viewUserStats = (user) => {
|
||||
const toggleUserStatus = (user) => {
|
||||
selectedUser.value = user
|
||||
confirmAction.value = {
|
||||
title: user.isActive ? t('user.management.disableUserTitle') : t('user.management.enableUserTitle'),
|
||||
title: user.isActive
|
||||
? t('user.management.disableUserTitle')
|
||||
: t('user.management.enableUserTitle'),
|
||||
message: user.isActive
|
||||
? t('user.management.disableUserMessage', { username: user.username })
|
||||
: t('user.management.enableUserMessage', { username: user.username }),
|
||||
@@ -614,7 +632,10 @@ const disableUserApiKeys = (user) => {
|
||||
selectedUser.value = user
|
||||
confirmAction.value = {
|
||||
title: t('user.management.disableAllKeysTitle'),
|
||||
message: t('user.management.disableAllKeysMessage', { count: user.apiKeyCount, username: user.username }),
|
||||
message: t('user.management.disableAllKeysMessage', {
|
||||
count: user.apiKeyCount,
|
||||
username: user.username
|
||||
}),
|
||||
confirmText: t('user.management.disableKeys'),
|
||||
confirmClass: 'bg-red-600 hover:bg-red-700',
|
||||
action: 'disableKeys'
|
||||
@@ -642,13 +663,21 @@ const handleConfirmAction = async () => {
|
||||
if (userIndex !== -1) {
|
||||
users.value[userIndex].isActive = !user.isActive
|
||||
}
|
||||
showToast(user.isActive ? t('user.management.userDisabledSuccess') : t('user.management.userEnabledSuccess'), 'success')
|
||||
showToast(
|
||||
user.isActive
|
||||
? t('user.management.userDisabledSuccess')
|
||||
: t('user.management.userEnabledSuccess'),
|
||||
'success'
|
||||
)
|
||||
}
|
||||
} else if (action === 'disableKeys') {
|
||||
const response = await apiClient.post(`/users/${user.id}/disable-keys`)
|
||||
|
||||
if (response.success) {
|
||||
showToast(t('user.management.keysDisabledSuccess', { count: response.disabledCount }), 'success')
|
||||
showToast(
|
||||
t('user.management.keysDisabledSuccess', { count: response.disabledCount }),
|
||||
'success'
|
||||
)
|
||||
await loadUsers() // Refresh to get updated counts
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,9 @@
|
||||
>
|
||||
file
|
||||
</li>
|
||||
<li>Follow the installation wizard to complete installation, keep default settings</li>
|
||||
<li>
|
||||
Follow the installation wizard to complete installation, keep default settings
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="mb-3 sm:mb-4">
|
||||
@@ -105,14 +107,18 @@
|
||||
<ul class="space-y-1 text-xs text-blue-700 sm:text-sm sm:text-xs">
|
||||
<li>• Recommend using PowerShell instead of CMD</li>
|
||||
<li>• If you encounter permission issues, try running as administrator</li>
|
||||
<li>• Some antivirus software may flag as false positive, need to add to whitelist</li>
|
||||
<li>
|
||||
• Some antivirus software may flag as false positive, need to add to whitelist
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Verify Installation -->
|
||||
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
|
||||
<h6 class="mb-2 text-sm font-medium text-green-800 sm:text-base">Verify Installation Success</h6>
|
||||
<h6 class="mb-2 text-sm font-medium text-green-800 sm:text-base">
|
||||
Verify Installation Success
|
||||
</h6>
|
||||
<p class="mb-2 text-xs text-green-700 sm:mb-3 sm:text-sm">
|
||||
After installation completes, open PowerShell or CMD and enter the following commands:
|
||||
</p>
|
||||
@@ -122,7 +128,9 @@
|
||||
<div class="whitespace-nowrap text-gray-300">node --version</div>
|
||||
<div class="whitespace-nowrap text-gray-300">npm --version</div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-green-700 sm:text-sm">If version numbers are displayed, installation was successful!</p>
|
||||
<p class="mt-2 text-xs text-green-700 sm:text-sm">
|
||||
If version numbers are displayed, installation was successful!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -159,7 +167,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
This command will download and install the latest version of Claude Code from the official npm repository.
|
||||
This command will download and install the latest version of Claude Code from the
|
||||
official npm repository.
|
||||
</p>
|
||||
|
||||
<div class="mt-4 rounded-lg border border-blue-200 bg-blue-50 p-3 sm:p-4">
|
||||
@@ -173,15 +182,21 @@
|
||||
|
||||
<!-- Verify Installation -->
|
||||
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
|
||||
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">Verify Claude Code Installation</h6>
|
||||
<p class="mb-3 text-sm text-green-700">After installation completes, enter the following command to check if installation was successful:</p>
|
||||
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">
|
||||
Verify Claude Code Installation
|
||||
</h6>
|
||||
<p class="mb-3 text-sm text-green-700">
|
||||
After installation completes, enter the following command to check if installation was
|
||||
successful:
|
||||
</p>
|
||||
<div
|
||||
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
|
||||
>
|
||||
<div class="whitespace-nowrap text-gray-300">claude --version</div>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-green-700">
|
||||
If version number is displayed, congratulations! Claude Code has been successfully installed.
|
||||
If version number is displayed, congratulations! Claude Code has been successfully
|
||||
installed.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -230,7 +245,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-yellow-700">
|
||||
💡 Remember to replace "your-api-key" with the actual key created in the "API Keys" tab above.
|
||||
💡 Remember to replace "your-api-key" with the actual key created in the "API Keys"
|
||||
tab above.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -280,9 +296,12 @@
|
||||
|
||||
<!-- Verify Environment Variable Setup -->
|
||||
<div class="mt-6 rounded-lg border border-blue-200 bg-blue-50 p-3 sm:p-4">
|
||||
<h6 class="mb-2 font-medium text-blue-800 dark:text-blue-300">Verify Environment Variable Setup</h6>
|
||||
<h6 class="mb-2 font-medium text-blue-800 dark:text-blue-300">
|
||||
Verify Environment Variable Setup
|
||||
</h6>
|
||||
<p class="mb-3 text-sm text-blue-700">
|
||||
After setting environment variables, you can verify if they were set successfully with the following commands:
|
||||
After setting environment variables, you can verify if they were set successfully with
|
||||
the following commands:
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
@@ -320,7 +339,8 @@
|
||||
<div>cr_xxxxxxxxxxxxxxxxxx</div>
|
||||
</div>
|
||||
<p class="text-xs text-blue-700">
|
||||
💡 If output is empty or shows variable name itself, environment variable setup failed, please reconfigure.
|
||||
💡 If output is empty or shows variable name itself, environment variable setup
|
||||
failed, please reconfigure.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -416,7 +436,8 @@
|
||||
Configure Codex Environment Variables
|
||||
</h5>
|
||||
<p class="mb-3 text-sm text-gray-700 dark:text-gray-300 sm:mb-4 sm:text-base">
|
||||
If you use tools that support OpenAI API (such as Codex), you need to set the following environment variables:
|
||||
If you use tools that support OpenAI API (such as Codex), you need to set the following
|
||||
environment variables:
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
@@ -528,8 +549,8 @@
|
||||
<ul class="list-inside list-disc space-y-1 text-sm">
|
||||
<li>Run PowerShell as Administrator</li>
|
||||
<li>
|
||||
Or configure npm to use user directory: <code
|
||||
class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
|
||||
Or configure npm to use user directory:
|
||||
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
|
||||
>npm config set prefix %APPDATA%\npm</code
|
||||
>
|
||||
</li>
|
||||
@@ -567,7 +588,8 @@
|
||||
<li>Restart PowerShell or CMD</li>
|
||||
<li>Or log out and log back into Windows</li>
|
||||
<li>
|
||||
Verify settings: <code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
|
||||
Verify settings:
|
||||
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
|
||||
>echo $env:ANTHROPIC_BASE_URL</code
|
||||
>
|
||||
</li>
|
||||
@@ -605,7 +627,8 @@
|
||||
<div class="mb-4">
|
||||
<p class="mb-3 text-gray-700">Method 1: Using Homebrew (Recommended)</p>
|
||||
<p class="mb-2 text-xs text-gray-600 dark:text-gray-400 sm:text-sm">
|
||||
If you already have Homebrew installed, using it to install Node.js will be more convenient:
|
||||
If you already have Homebrew installed, using it to install Node.js will be more
|
||||
convenient:
|
||||
</p>
|
||||
<div
|
||||
class="overflow-x-auto rounded-lg bg-gray-900 p-3 font-mono text-xs text-green-400 sm:p-4 sm:text-sm"
|
||||
@@ -655,15 +678,21 @@
|
||||
|
||||
<!-- Verify Installation -->
|
||||
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
|
||||
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">Verify Installation Success</h6>
|
||||
<p class="mb-3 text-sm text-green-700">After installation, open Terminal and enter the following commands:</p>
|
||||
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">
|
||||
Verify Installation Success
|
||||
</h6>
|
||||
<p class="mb-3 text-sm text-green-700">
|
||||
After installation, open Terminal and enter the following commands:
|
||||
</p>
|
||||
<div
|
||||
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
|
||||
>
|
||||
<div class="whitespace-nowrap text-gray-300">node --version</div>
|
||||
<div class="whitespace-nowrap text-gray-300">npm --version</div>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-green-700">If version numbers are displayed, the installation was successful!</p>
|
||||
<p class="mt-2 text-sm text-green-700">
|
||||
If version numbers are displayed, the installation was successful!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -699,7 +728,9 @@
|
||||
npm install -g @anthropic-ai/claude-code
|
||||
</div>
|
||||
</div>
|
||||
<p class="mb-2 text-sm text-gray-600">If you encounter permission issues, you can use sudo:</p>
|
||||
<p class="mb-2 text-sm text-gray-600">
|
||||
If you encounter permission issues, you can use sudo:
|
||||
</p>
|
||||
<div
|
||||
class="overflow-x-auto rounded-lg bg-gray-900 p-3 font-mono text-xs text-green-400 sm:p-4 sm:text-sm"
|
||||
>
|
||||
@@ -711,15 +742,21 @@
|
||||
|
||||
<!-- Verify Installation -->
|
||||
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
|
||||
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">Verify Claude Code Installation</h6>
|
||||
<p class="mb-3 text-sm text-green-700">After installation completes, enter the following command to check if installation was successful:</p>
|
||||
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">
|
||||
Verify Claude Code Installation
|
||||
</h6>
|
||||
<p class="mb-3 text-sm text-green-700">
|
||||
After installation completes, enter the following command to check if installation was
|
||||
successful:
|
||||
</p>
|
||||
<div
|
||||
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
|
||||
>
|
||||
<div class="whitespace-nowrap text-gray-300">claude --version</div>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-green-700">
|
||||
If version number is displayed, congratulations! Claude Code has been successfully installed.
|
||||
If version number is displayed, congratulations! Claude Code has been successfully
|
||||
installed.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -766,7 +803,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-yellow-700">
|
||||
💡 Remember to replace "your-api-key" with the actual key created in the "API Keys" tab above.
|
||||
💡 Remember to replace "your-api-key" with the actual key created in the "API Keys"
|
||||
tab above.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -903,7 +941,8 @@
|
||||
Configure Codex Environment Variables
|
||||
</h5>
|
||||
<p class="mb-3 text-sm text-gray-700 dark:text-gray-300 sm:mb-4 sm:text-base">
|
||||
If you use tools that support OpenAI API (such as Codex), you need to set the following environment variables:
|
||||
If you use tools that support OpenAI API (such as Codex), you need to set the following
|
||||
environment variables:
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
@@ -1014,13 +1053,14 @@
|
||||
<p class="mb-2">Try the following solutions:</p>
|
||||
<ul class="list-inside list-disc space-y-1 text-sm">
|
||||
<li>
|
||||
Install with sudo: <code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
|
||||
Install with sudo:
|
||||
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
|
||||
>sudo npm install -g @anthropic-ai/claude-code</code
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
Or configure npm to use user directory: <code
|
||||
class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
|
||||
Or configure npm to use user directory:
|
||||
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
|
||||
>npm config set prefix ~/.npm-global</code
|
||||
>
|
||||
</li>
|
||||
@@ -1040,7 +1080,8 @@
|
||||
<li>Open "System Preferences" → "Security & Privacy"</li>
|
||||
<li>Click "Allow Anyway" or "Open Anyway"</li>
|
||||
<li>
|
||||
Or run in Terminal: <code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
|
||||
Or run in Terminal:
|
||||
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
|
||||
>sudo spctl --master-disable</code
|
||||
>
|
||||
</li>
|
||||
@@ -1057,10 +1098,13 @@
|
||||
<div class="px-3 pb-3 text-gray-600 sm:px-4 sm:pb-4">
|
||||
<p class="mb-2">Check the following points:</p>
|
||||
<ul class="list-inside list-disc space-y-1 text-sm">
|
||||
<li>Confirm you modified the correct configuration file (.zshrc or .bash_profile)</li>
|
||||
<li>
|
||||
Confirm you modified the correct configuration file (.zshrc or .bash_profile)
|
||||
</li>
|
||||
<li>Restart Terminal</li>
|
||||
<li>
|
||||
Verify settings: <code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
|
||||
Verify settings:
|
||||
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
|
||||
>echo $ANTHROPIC_BASE_URL</code
|
||||
>
|
||||
</li>
|
||||
@@ -1127,7 +1171,10 @@
|
||||
<h6 class="mb-2 text-sm font-medium text-orange-800 sm:text-base">Linux Notes</h6>
|
||||
<ul class="space-y-1 text-xs text-orange-700 sm:text-sm">
|
||||
<li>• Some distributions may require additional dependencies</li>
|
||||
<li>• If you encounter permission issues, use <code class="rounded bg-orange-200 px-1">sudo</code></li>
|
||||
<li>
|
||||
• If you encounter permission issues, use
|
||||
<code class="rounded bg-orange-200 px-1">sudo</code>
|
||||
</li>
|
||||
<li>• Ensure your user has write permissions to npm's global directory</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -1135,15 +1182,21 @@
|
||||
|
||||
<!-- Verify Installation -->
|
||||
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
|
||||
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">Verify Installation Success</h6>
|
||||
<p class="mb-3 text-sm text-green-700">After installation, open terminal and enter the following commands:</p>
|
||||
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">
|
||||
Verify Installation Success
|
||||
</h6>
|
||||
<p class="mb-3 text-sm text-green-700">
|
||||
After installation, open terminal and enter the following commands:
|
||||
</p>
|
||||
<div
|
||||
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
|
||||
>
|
||||
<div class="whitespace-nowrap text-gray-300">node --version</div>
|
||||
<div class="whitespace-nowrap text-gray-300">npm --version</div>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-green-700">If version numbers are displayed, the installation was successful!</p>
|
||||
<p class="mt-2 text-sm text-green-700">
|
||||
If version numbers are displayed, the installation was successful!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1179,7 +1232,9 @@
|
||||
npm install -g @anthropic-ai/claude-code
|
||||
</div>
|
||||
</div>
|
||||
<p class="mb-2 text-sm text-gray-600">If you encounter permission issues, you can use sudo:</p>
|
||||
<p class="mb-2 text-sm text-gray-600">
|
||||
If you encounter permission issues, you can use sudo:
|
||||
</p>
|
||||
<div
|
||||
class="overflow-x-auto rounded-lg bg-gray-900 p-3 font-mono text-xs text-green-400 sm:p-4 sm:text-sm"
|
||||
>
|
||||
@@ -1191,15 +1246,21 @@
|
||||
|
||||
<!-- Verify Installation -->
|
||||
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
|
||||
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">Verify Claude Code Installation</h6>
|
||||
<p class="mb-3 text-sm text-green-700">After installation completes, enter the following command to check if installation was successful:</p>
|
||||
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">
|
||||
Verify Claude Code Installation
|
||||
</h6>
|
||||
<p class="mb-3 text-sm text-green-700">
|
||||
After installation completes, enter the following command to check if installation was
|
||||
successful:
|
||||
</p>
|
||||
<div
|
||||
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
|
||||
>
|
||||
<div class="whitespace-nowrap text-gray-300">claude --version</div>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-green-700">
|
||||
If version number is displayed, congratulations! Claude Code has been successfully installed.
|
||||
If version number is displayed, congratulations! Claude Code has been successfully
|
||||
installed.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1246,7 +1307,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-yellow-700">
|
||||
💡 Remember to replace "your-api-key" with the actual key created in the "API Keys" tab above.
|
||||
💡 Remember to replace "your-api-key" with the actual key created in the "API Keys"
|
||||
tab above.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1381,7 +1443,8 @@
|
||||
Configure Codex Environment Variables
|
||||
</h5>
|
||||
<p class="mb-3 text-sm text-gray-700 dark:text-gray-300 sm:mb-4 sm:text-base">
|
||||
If you use tools that support OpenAI API (such as Codex), you need to set the following environment variables:
|
||||
If you use tools that support OpenAI API (such as Codex), you need to set the following
|
||||
environment variables:
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
@@ -1492,18 +1555,20 @@
|
||||
<p class="mb-2">Try the following solutions:</p>
|
||||
<ul class="list-inside list-disc space-y-1 text-sm">
|
||||
<li>
|
||||
Install with sudo: <code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
|
||||
Install with sudo:
|
||||
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
|
||||
>sudo npm install -g @anthropic-ai/claude-code</code
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
Or configure npm to use user directory: <code
|
||||
class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
|
||||
Or configure npm to use user directory:
|
||||
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
|
||||
>npm config set prefix ~/.npm-global</code
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
Then add to PATH: <code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
|
||||
Then add to PATH:
|
||||
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
|
||||
>export PATH=~/.npm-global/bin:$PATH</code
|
||||
>
|
||||
</li>
|
||||
@@ -1547,7 +1612,8 @@
|
||||
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm">source ~/.bashrc</code>
|
||||
</li>
|
||||
<li>
|
||||
Verify settings: <code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
|
||||
Verify settings:
|
||||
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
|
||||
>echo $ANTHROPIC_BASE_URL</code
|
||||
>
|
||||
</li>
|
||||
@@ -1564,10 +1630,12 @@
|
||||
>
|
||||
<h5 class="mb-2 text-lg font-semibold sm:text-xl">🎉 Congratulations!</h5>
|
||||
<p class="mb-3 text-sm text-blue-100 sm:mb-4 sm:text-base">
|
||||
You have successfully installed and configured Claude Code. Now you can start enjoying the convenience brought by AI programming assistant.
|
||||
You have successfully installed and configured Claude Code. Now you can start enjoying the
|
||||
convenience brought by AI programming assistant.
|
||||
</p>
|
||||
<p class="text-xs text-blue-200 sm:text-sm">
|
||||
If you encounter any issues during use, you can check the official documentation or community discussions for help.
|
||||
If you encounter any issues during use, you can check the official documentation or
|
||||
community discussions for help.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user