Revert "Merge pull request #424 from Wangnov/feat/i18n"

This reverts commit 1d915d8327, reversing
changes made to 009f7c84f6.
This commit is contained in:
shaw
2025-09-12 09:21:53 +08:00
parent 1d915d8327
commit 9c4dc714f8
80 changed files with 7026 additions and 19087 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,7 @@
<i class="fas fa-chart-line text-sm text-white sm:text-base" />
</div>
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100 sm:text-xl">
{{ t('apiKeys.usageDetailModal.title') }} - {{ apiKey.name }}
使用统计详情 - {{ apiKey.name }}
</h3>
</div>
<button class="p-1 text-gray-400 transition-colors hover:text-gray-600" @click="close">
@@ -34,17 +34,14 @@
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">{{
t('apiKeys.usageDetailModal.totalRequests')
}}</span>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">总请求数</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">
{{ t('apiKeys.usageDetailModal.today') }}: {{ formatNumber(dailyRequests) }}
{{ t('apiKeys.usageDetailModal.times') }}
今日: {{ formatNumber(dailyRequests) }}
</div>
</div>
@@ -53,16 +50,14 @@
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">{{
t('apiKeys.usageDetailModal.totalTokens')
}}</span>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">总Token数</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">
{{ t('apiKeys.usageDetailModal.today') }}: {{ formatTokenCount(dailyTokens) }}
今日: {{ formatTokenCount(dailyTokens) }}
</div>
</div>
@@ -71,16 +66,14 @@
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">{{
t('apiKeys.usageDetailModal.totalCost')
}}</span>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">总费用</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">
{{ t('apiKeys.usageDetailModal.today') }}: ${{ dailyCost.toFixed(4) }}
今日: ${{ dailyCost.toFixed(4) }}
</div>
</div>
@@ -89,9 +82,7 @@
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">{{
t('apiKeys.usageDetailModal.averageRate')
}}</span>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">平均速率</span>
<i class="fas fa-tachometer-alt text-purple-500" />
</div>
<div class="space-y-1 text-sm">
@@ -113,15 +104,13 @@
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" />
{{ t('apiKeys.usageDetailModal.tokenDistribution') }}
Token 使用分布
</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">{{
t('apiKeys.usageDetailModal.inputTokens')
}}</span>
<span class="text-sm text-gray-600 dark:text-gray-400">输入 Token</span>
</div>
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ formatTokenCount(inputTokens) }}
@@ -130,9 +119,7 @@
<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">{{
t('apiKeys.usageDetailModal.outputTokens')
}}</span>
<span class="text-sm text-gray-600 dark:text-gray-400">输出 Token</span>
</div>
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ formatTokenCount(outputTokens) }}
@@ -141,9 +128,7 @@
<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">{{
t('apiKeys.usageDetailModal.cacheCreateTokens')
}}</span>
<span class="text-sm text-gray-600 dark:text-gray-400">缓存创建 Token</span>
</div>
<span class="text-sm font-semibold text-purple-600">
{{ formatTokenCount(cacheCreateTokens) }}
@@ -152,9 +137,7 @@
<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">{{
t('apiKeys.usageDetailModal.cacheReadTokens')
}}</span>
<span class="text-sm text-gray-600 dark:text-gray-400">缓存读取 Token</span>
</div>
<span class="text-sm font-semibold text-purple-600">
{{ formatTokenCount(cacheReadTokens) }}
@@ -169,14 +152,12 @@
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">{{
t('apiKeys.usageDetailModal.dailyCostLimit')
}}</span>
<span class="text-gray-600 dark:text-gray-400">每日费用限制</span>
<span class="font-semibold text-gray-900 dark:text-gray-100">
${{ apiKey.dailyCostLimit.toFixed(2) }}
</span>
@@ -195,11 +176,7 @@
/>
</div>
<div class="text-right text-xs text-gray-500 dark:text-gray-400">
{{
t('apiKeys.usageDetailModal.usedPercentage', {
percentage: dailyCostPercentage.toFixed(1)
})
}}
已使用 {{ dailyCostPercentage.toFixed(1) }}%
</div>
</div>
@@ -207,9 +184,7 @@
v-if="apiKey.concurrencyLimit > 0"
class="flex items-center justify-between text-sm"
>
<span class="text-gray-600 dark:text-gray-400">{{
t('apiKeys.usageDetailModal.concurrencyLimit')
}}</span>
<span class="text-gray-600 dark:text-gray-400">并发限制</span>
<span class="font-semibold text-purple-600">
{{ apiKey.currentConcurrency || 0 }} / {{ apiKey.concurrencyLimit }}
</span>
@@ -218,14 +193,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="t('apiKeys.usageDetailModal.windowStatus')"
label="窗口状态"
:rate-limit-window="apiKey.rateLimitWindow"
:request-limit="apiKey.rateLimitRequests"
:show-progress="true"
@@ -243,7 +218,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>
@@ -253,11 +228,8 @@
<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,
@@ -302,9 +274,7 @@ const dailyCostPercentage = computed(() => {
// 方法
const formatNumber = (num) => {
if (!num && num !== 0) return '0'
// 根据当前语言环境自动选择合适的地区设置
const currentLocale = t('common.locale')
return num.toLocaleString(currentLocale)
return num.toLocaleString('zh-CN')
}
// 格式化Token数量使用K/M单位

View File

@@ -1,29 +1,27 @@
<template>
<div class="space-y-1">
<div class="flex items-center justify-between text-xs">
<span class="text-gray-500">{{ displayLabel }}</span>
<span class="text-gray-500">{{ label }}</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">{{ t('apiKeys.windowCountdown.requests') }}</span>
<span class="text-gray-400">请求</span>
<span class="text-gray-600"> {{ currentRequests || 0 }}/{{ requestLimit }} </span>
</div>
<div class="h-1 w-full rounded-full bg-gray-200">
@@ -38,7 +36,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">{{ t('apiKeys.windowCountdown.tokens') }}</span>
<span class="text-gray-400">Token</span>
<span class="text-gray-600">
{{ formatTokenCount(currentTokens || 0) }}/{{ formatTokenCount(tokenLimit) }}
</span>
@@ -55,7 +53,7 @@
<!-- 费用限制新功能 -->
<div v-if="hasCostLimit" class="space-y-0.5">
<div class="flex items-center justify-between text-xs">
<span class="text-gray-400">{{ t('apiKeys.windowCountdown.cost') }}</span>
<span class="text-gray-400">费用</span>
<span class="text-gray-600">
${{ (currentCost || 0).toFixed(2) }}/${{ costLimit.toFixed(2) }}
</span>
@@ -73,29 +71,22 @@
<!-- 额外提示信息 -->
<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">{{ t('apiKeys.windowCountdown.aboutToReset') }}</span>
<span v-if="remainingSeconds < 60">即将重置</span>
<span v-else-if="remainingSeconds < 300"
>{{ Math.ceil(remainingSeconds / 60) }}
{{ t('apiKeys.windowCountdown.minutesUntilReset') }}</span
>
<span v-else
>{{ formatDetailedTime(remainingSeconds)
}}{{ t('apiKeys.windowCountdown.untilReset') }}</span
>{{ Math.ceil(remainingSeconds / 60) }} 分钟后重置</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,
@@ -152,10 +143,6 @@ 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' // 窗口未开始
@@ -195,9 +182,9 @@ const formatDetailedTime = (seconds) => {
const minutes = Math.floor((seconds % 3600) / 60)
if (hours > 0) {
return `${hours}${t('apiKeys.windowCountdown.hours')}${minutes}${t('apiKeys.windowCountdown.minutes')}`
return `${hours}小时${minutes}分钟`
} else {
return `${minutes}${t('apiKeys.windowCountdown.minutes')}`
return `${minutes}分钟`
}
}