Files
claude-relay-service/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue
shaw 7e1a9daa6b feat: 完善管理界面功能和用户体验
- 添加 API Key 窗口倒计时组件 (WindowCountdown)
- 添加自定义下拉菜单组件 (CustomDropdown)
- 优化账户和 API Key 管理界面交互
- 改进教程页面布局和说明文字
- 完善账户状态显示和错误处理

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-08 14:14:46 +08:00

948 lines
33 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<Teleport to="body">
<div class="modal fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-4">
<div class="modal-content mx-auto flex max-h-[90vh] w-full max-w-4xl flex-col p-4 sm:p-6">
<div class="mb-4 flex items-center justify-between">
<div class="flex items-center gap-2 sm:gap-3">
<div
class="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-blue-600 sm:h-10 sm:w-10 sm:rounded-xl"
>
<i class="fas fa-key text-sm text-white sm:text-base" />
</div>
<h3 class="text-lg font-bold text-gray-900 sm:text-xl">创建新的 API Key</h3>
</div>
<button
class="p-1 text-gray-400 transition-colors hover:text-gray-600"
@click="$emit('close')"
>
<i class="fas fa-times text-lg sm:text-xl" />
</button>
</div>
<form
class="modal-scroll-content custom-scrollbar flex-1 space-y-4"
@submit.prevent="createApiKey"
>
<!-- 创建类型选择 -->
<div
class="rounded-lg border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-3 sm:p-4"
>
<div
:class="[
'flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between',
form.createType === 'batch' ? 'mb-3' : ''
]"
>
<label class="flex h-full items-center text-xs font-semibold text-gray-700 sm:text-sm"
>创建类型</label
>
<div class="flex items-center gap-3 sm:gap-4">
<label class="flex cursor-pointer items-center">
<input
v-model="form.createType"
class="mr-1.5 text-blue-600 sm:mr-2"
type="radio"
value="single"
/>
<span class="flex items-center text-xs text-gray-700 sm:text-sm">
<i class="fas fa-key mr-1 text-xs" />
单个创建
</span>
</label>
<label class="flex cursor-pointer items-center">
<input
v-model="form.createType"
class="mr-1.5 text-blue-600 sm:mr-2"
type="radio"
value="batch"
/>
<span class="flex items-center text-xs text-gray-700 sm:text-sm">
<i class="fas fa-layer-group mr-1 text-xs" />
批量创建
</span>
</label>
</div>
</div>
<!-- 批量创建数量输入 -->
<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">创建数量</label>
<div class="flex items-center gap-2">
<input
v-model.number="form.batchCount"
class="form-input w-full text-sm"
max="500"
min="2"
placeholder="输入数量 (2-500)"
required
type="number"
/>
<div class="whitespace-nowrap text-xs text-gray-500">最大支持 500 </div>
</div>
</div>
</div>
<p class="mt-2 flex items-start text-xs text-amber-600">
<i class="fas fa-info-circle mr-1 mt-0.5 flex-shrink-0" />
<span
>批量创建时每个 Key 的名称会自动添加序号后缀例如{{
form.name || 'MyKey'
}}_1, {{ form.name || 'MyKey' }}_2 ...</span
>
</p>
</div>
</div>
<div>
<label class="mb-1.5 block text-xs font-semibold text-gray-700 sm:mb-2 sm:text-sm"
>名称 <span class="text-red-500">*</span></label
>
<input
v-model="form.name"
class="form-input w-full text-sm"
:class="{ 'border-red-500': errors.name }"
:placeholder="
form.createType === 'batch'
? '输入基础名称(将自动添加序号)'
: '为您的 API Key 取一个名称'
"
required
type="text"
@input="errors.name = ''"
/>
<p v-if="errors.name" class="mt-1 text-xs text-red-500">
{{ errors.name }}
</p>
</div>
<!-- 标签 -->
<div>
<label class="mb-2 block text-sm font-semibold text-gray-700">标签</label>
<div class="space-y-4">
<!-- 已选择的标签 -->
<div v-if="form.tags.length > 0">
<div class="mb-2 text-xs font-medium text-gray-600">已选择的标签:</div>
<div class="flex flex-wrap gap-2">
<span
v-for="(tag, index) in form.tags"
:key="'selected-' + index"
class="inline-flex items-center gap-1 rounded-full bg-blue-100 px-3 py-1 text-sm text-blue-800"
>
{{ tag }}
<button
class="ml-1 hover:text-blue-900"
type="button"
@click="removeTag(index)"
>
<i class="fas fa-times text-xs" />
</button>
</span>
</div>
</div>
<!-- 可选择的已有标签 -->
<div v-if="unselectedTags.length > 0">
<div class="mb-2 text-xs font-medium text-gray-600">点击选择已有标签:</div>
<div class="flex flex-wrap gap-2">
<button
v-for="tag in unselectedTags"
:key="'available-' + tag"
class="inline-flex items-center gap-1 rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700 transition-colors hover:bg-blue-100 hover:text-blue-700"
type="button"
@click="selectTag(tag)"
>
<i class="fas fa-tag text-xs text-gray-500" />
{{ tag }}
</button>
</div>
</div>
<!-- 创建新标签 -->
<div>
<div class="mb-2 text-xs font-medium text-gray-600">创建新标签:</div>
<div class="flex gap-2">
<input
v-model="newTag"
class="form-input flex-1"
placeholder="输入新标签名称"
type="text"
@keypress.enter.prevent="addTag"
/>
<button
class="rounded-lg bg-green-500 px-4 py-2 text-white transition-colors hover:bg-green-600"
type="button"
@click="addTag"
>
<i class="fas fa-plus" />
</button>
</div>
</div>
<p class="text-xs text-gray-500">用于标记不同团队或用途方便筛选管理</p>
</div>
</div>
<!-- 速率限制设置 -->
<div class="rounded-lg border border-blue-200 bg-blue-50 p-3">
<div class="mb-2 flex items-center gap-2">
<div
class="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded bg-blue-500"
>
<i class="fas fa-tachometer-alt text-xs text-white" />
</div>
<h4 class="text-sm font-semibold text-gray-800">速率限制设置 (可选)</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"
>时间窗口 (分钟)</label
>
<input
v-model="form.rateLimitWindow"
class="form-input w-full text-sm"
min="1"
placeholder="无限制"
type="number"
/>
<p class="ml-2 mt-0.5 text-xs text-gray-500">时间段单位</p>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-700">请求次数限制</label>
<input
v-model="form.rateLimitRequests"
class="form-input w-full text-sm"
min="1"
placeholder="无限制"
type="number"
/>
<p class="ml-2 mt-0.5 text-xs text-gray-500">窗口内最大请求</p>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-700">Token 限制</label>
<input
v-model="form.tokenLimit"
class="form-input w-full text-sm"
placeholder="无限制"
type="number"
/>
<p class="ml-2 mt-0.5 text-xs text-gray-500">窗口内最大Token</p>
</div>
</div>
<!-- 示例说明 -->
<div class="rounded-lg bg-blue-100 p-2">
<h5 class="mb-1 text-xs font-semibold text-blue-800">💡 使用示例</h5>
<div class="space-y-0.5 text-xs text-blue-700">
<div>
<strong>示例1:</strong> 时间窗口=60请求次数=1000 每60分钟最多1000次请求
</div>
<div>
<strong>示例2:</strong> 时间窗口=1Token=10000 每分钟最多10,000个Token
</div>
<div>
<strong>示例3:</strong> 窗口=30请求=50Token=100000
每30分钟50次请求且不超10万Token
</div>
</div>
</div>
</div>
</div>
<div>
<label class="mb-2 block text-sm font-semibold text-gray-700"
>每日费用限制 (美元)</label
>
<div class="space-y-2">
<div class="flex gap-2">
<button
class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200"
type="button"
@click="form.dailyCostLimit = '50'"
>
$50
</button>
<button
class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200"
type="button"
@click="form.dailyCostLimit = '100'"
>
$100
</button>
<button
class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200"
type="button"
@click="form.dailyCostLimit = '200'"
>
$200
</button>
<button
class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200"
type="button"
@click="form.dailyCostLimit = ''"
>
自定义
</button>
</div>
<input
v-model="form.dailyCostLimit"
class="form-input w-full"
min="0"
placeholder="0 表示无限制"
step="0.01"
type="number"
/>
<p class="text-xs text-gray-500">
设置此 API Key 每日的费用限制超过限制将拒绝请求0 或留空表示无限制
</p>
</div>
</div>
<div>
<label class="mb-2 block text-sm font-semibold text-gray-700">并发限制 (可选)</label>
<input
v-model="form.concurrencyLimit"
class="form-input w-full"
min="0"
placeholder="0 表示无限制"
type="number"
/>
<p class="mt-2 text-xs text-gray-500">
设置此 API Key 可同时处理的最大请求数0 或留空表示无限制
</p>
</div>
<div>
<label class="mb-2 block text-sm font-semibold text-gray-700">备注 (可选)</label>
<textarea
v-model="form.description"
class="form-input w-full resize-none text-sm"
placeholder="描述此 API Key 的用途..."
rows="2"
/>
</div>
<div>
<label class="mb-2 block text-sm font-semibold text-gray-700">有效期限</label>
<select
v-model="form.expireDuration"
class="form-input w-full"
@change="updateExpireAt"
>
<option value="">永不过期</option>
<option value="1d">1 </option>
<option value="7d">7 </option>
<option value="30d">30 </option>
<option value="90d">90 </option>
<option value="180d">180 </option>
<option value="365d">365 </option>
<option value="custom">自定义日期</option>
</select>
<div v-if="form.expireDuration === 'custom'" class="mt-3">
<input
v-model="form.customExpireDate"
class="form-input w-full"
:min="minDateTime"
type="datetime-local"
@change="updateCustomExpireAt"
/>
</div>
<p v-if="form.expiresAt" class="mt-2 text-xs text-gray-500">
将于 {{ formatExpireDate(form.expiresAt) }} 过期
</p>
</div>
<div>
<label class="mb-2 block text-sm font-semibold text-gray-700">服务权限</label>
<div class="flex gap-4">
<label class="flex cursor-pointer items-center">
<input v-model="form.permissions" class="mr-2" type="radio" value="all" />
<span class="text-sm text-gray-700">全部服务</span>
</label>
<label class="flex cursor-pointer items-center">
<input v-model="form.permissions" class="mr-2" type="radio" value="claude" />
<span class="text-sm text-gray-700"> Claude</span>
</label>
<label class="flex cursor-pointer items-center">
<input v-model="form.permissions" class="mr-2" type="radio" value="gemini" />
<span class="text-sm text-gray-700"> Gemini</span>
</label>
</div>
<p class="mt-2 text-xs text-gray-500">控制此 API Key 可以访问哪些服务</p>
</div>
<div>
<div class="mb-2 flex items-center justify-between">
<label class="text-sm font-semibold text-gray-700">专属账号绑定 (可选)</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"
:disabled="accountsLoading"
title="刷新账号列表"
type="button"
@click="refreshAccounts"
>
<i
:class="[
'fas',
accountsLoading ? 'fa-spinner fa-spin' : 'fa-sync-alt',
'text-xs'
]"
/>
<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">Claude 专属账号</label>
<AccountSelector
v-model="form.claudeAccountId"
:accounts="localAccounts.claude"
default-option-text="使用共享账号池"
:disabled="form.permissions === 'gemini'"
:groups="localAccounts.claudeGroups"
placeholder="请选择Claude账号"
platform="claude"
/>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-gray-600">Gemini 专属账号</label>
<AccountSelector
v-model="form.geminiAccountId"
:accounts="localAccounts.gemini"
default-option-text="使用共享账号池"
:disabled="form.permissions === 'claude'"
:groups="localAccounts.geminiGroups"
placeholder="请选择Gemini账号"
platform="gemini"
/>
</div>
</div>
<p class="mt-2 text-xs text-gray-500">
选择专属账号后此API Key将只使用该账号不选择则使用共享账号池
</p>
</div>
<div>
<div class="mb-2 flex items-center">
<input
id="enableModelRestriction"
v-model="form.enableModelRestriction"
class="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-blue-500"
type="checkbox"
/>
<label
class="ml-2 cursor-pointer text-sm font-semibold text-gray-700"
for="enableModelRestriction"
>
启用模型限制
</label>
</div>
<div v-if="form.enableModelRestriction" class="space-y-3">
<div>
<label class="mb-2 block text-sm font-medium text-gray-600">限制的模型列表</label>
<div
class="mb-3 flex min-h-[32px] flex-wrap gap-2 rounded-lg border border-gray-200 bg-gray-50 p-2"
>
<span
v-for="(model, index) in form.restrictedModels"
:key="index"
class="inline-flex items-center rounded-full bg-red-100 px-3 py-1 text-sm text-red-800"
>
{{ model }}
<button
class="ml-2 text-red-600 hover:text-red-800"
type="button"
@click="removeRestrictedModel(index)"
>
<i class="fas fa-times text-xs" />
</button>
</span>
<span v-if="form.restrictedModels.length === 0" class="text-sm text-gray-400">
暂无限制的模型
</span>
</div>
<div class="space-y-3">
<!-- 快速添加按钮 -->
<div class="flex flex-wrap gap-2">
<button
v-for="model in availableQuickModels"
:key="model"
class="flex-shrink-0 rounded-lg bg-gray-100 px-3 py-1 text-xs text-gray-700 transition-colors hover:bg-gray-200 sm:text-sm"
type="button"
@click="quickAddRestrictedModel(model)"
>
{{ model }}
</button>
<span
v-if="availableQuickModels.length === 0"
class="text-sm italic text-gray-400"
>
所有常用模型已在限制列表中
</span>
</div>
<!-- 手动输入 -->
<div class="flex gap-2">
<input
v-model="form.modelInput"
class="form-input flex-1"
placeholder="输入模型名称,按回车添加"
type="text"
@keydown.enter.prevent="addRestrictedModel"
/>
<button
class="rounded-lg bg-red-500 px-4 py-2 text-white transition-colors hover:bg-red-600"
type="button"
@click="addRestrictedModel"
>
<i class="fas fa-plus" />
</button>
</div>
</div>
<p class="mt-2 text-xs text-gray-500">
设置此API Key无法访问的模型例如claude-opus-4-20250514
</p>
</div>
</div>
</div>
<!-- 客户端限制 -->
<div>
<div class="mb-2 flex items-center">
<input
id="enableClientRestriction"
v-model="form.enableClientRestriction"
class="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-blue-500"
type="checkbox"
/>
<label
class="ml-2 cursor-pointer text-sm font-semibold text-gray-700"
for="enableClientRestriction"
>
启用客户端限制
</label>
</div>
<div
v-if="form.enableClientRestriction"
class="rounded-lg border border-green-200 bg-green-50 p-3"
>
<div>
<label class="mb-2 block text-xs font-medium text-gray-700">允许的客户端</label>
<div class="space-y-1">
<div v-for="client in supportedClients" :key="client.id" class="flex items-start">
<input
:id="`client_${client.id}`"
v-model="form.allowedClients"
class="mt-0.5 h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-blue-500"
type="checkbox"
:value="client.id"
/>
<label class="ml-2 flex-1 cursor-pointer" :for="`client_${client.id}`">
<span class="text-sm font-medium text-gray-700">{{ client.name }}</span>
<span class="block text-xs text-gray-500">{{ client.description }}</span>
</label>
</div>
</div>
</div>
</div>
</div>
<div class="flex gap-3 pt-2">
<button
class="flex-1 rounded-lg bg-gray-100 px-4 py-2.5 text-sm font-semibold text-gray-700 transition-colors hover:bg-gray-200"
type="button"
@click="$emit('close')"
>
取消
</button>
<button
class="btn btn-primary flex-1 px-4 py-2.5 text-sm font-semibold"
:disabled="loading"
type="submit"
>
<div v-if="loading" class="loading-spinner mr-2" />
<i v-else class="fas fa-plus mr-2" />
{{ loading ? '创建中...' : '创建' }}
</button>
</div>
</form>
</div>
</div>
</Teleport>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
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 props = defineProps({
accounts: {
type: Object,
default: () => ({ claude: [], gemini: [] })
}
})
const emit = defineEmits(['close', 'success', 'batch-success'])
const clientsStore = useClientsStore()
const apiKeysStore = useApiKeysStore()
const loading = ref(false)
const accountsLoading = ref(false)
const localAccounts = ref({ claude: [], gemini: [], claudeGroups: [], geminiGroups: [] })
// 表单验证状态
const errors = ref({
name: ''
})
// 标签相关
const newTag = ref('')
const availableTags = ref([])
// 计算未选择的标签
const unselectedTags = computed(() => {
return availableTags.value.filter((tag) => !form.tags.includes(tag))
})
// 支持的客户端列表
const supportedClients = ref([])
// 表单数据
const form = reactive({
createType: 'single',
batchCount: 10,
name: '',
description: '',
tokenLimit: '',
rateLimitWindow: '',
rateLimitRequests: '',
concurrencyLimit: '',
dailyCostLimit: '',
expireDuration: '',
customExpireDate: '',
expiresAt: null,
permissions: 'all',
claudeAccountId: '',
geminiAccountId: '',
enableModelRestriction: false,
restrictedModels: [],
modelInput: '',
enableClientRestriction: false,
allowedClients: [],
tags: []
})
// 加载支持的客户端和已存在的标签
onMounted(async () => {
supportedClients.value = await clientsStore.loadSupportedClients()
availableTags.value = await apiKeysStore.fetchTags()
// 初始化账号数据
if (props.accounts) {
localAccounts.value = {
claude: props.accounts.claude || [],
gemini: props.accounts.gemini || [],
claudeGroups: props.accounts.claudeGroups || [],
geminiGroups: props.accounts.geminiGroups || []
}
}
})
// 刷新账号列表
const refreshAccounts = async () => {
accountsLoading.value = true
try {
const [claudeData, claudeConsoleData, geminiData, groupsData] = await Promise.all([
apiClient.get('/admin/claude-accounts'),
apiClient.get('/admin/claude-console-accounts'),
apiClient.get('/admin/gemini-accounts'),
apiClient.get('/admin/account-groups')
])
// 合并Claude OAuth账户和Claude Console账户
const claudeAccounts = []
if (claudeData.success) {
claudeData.data?.forEach((account) => {
claudeAccounts.push({
...account,
platform: 'claude-oauth',
isDedicated: account.accountType === 'dedicated' // 保留以便向后兼容
})
})
}
if (claudeConsoleData.success) {
claudeConsoleData.data?.forEach((account) => {
claudeAccounts.push({
...account,
platform: 'claude-console',
isDedicated: account.accountType === 'dedicated' // 保留以便向后兼容
})
})
}
localAccounts.value.claude = claudeAccounts
if (geminiData.success) {
localAccounts.value.gemini = (geminiData.data || []).map((account) => ({
...account,
isDedicated: account.accountType === 'dedicated' // 保留以便向后兼容
}))
}
// 处理分组数据
if (groupsData.success) {
const allGroups = groupsData.data || []
localAccounts.value.claudeGroups = allGroups.filter((g) => g.platform === 'claude')
localAccounts.value.geminiGroups = allGroups.filter((g) => g.platform === 'gemini')
}
showToast('账号列表已刷新', 'success')
} catch (error) {
showToast('刷新账号列表失败', 'error')
} finally {
accountsLoading.value = false
}
}
// 计算最小日期时间
const minDateTime = computed(() => {
const now = new Date()
now.setMinutes(now.getMinutes() + 1)
return now.toISOString().slice(0, 16)
})
// 更新过期时间
const updateExpireAt = () => {
if (!form.expireDuration) {
form.expiresAt = null
return
}
if (form.expireDuration === 'custom') {
return
}
const now = new Date()
const duration = form.expireDuration
const match = duration.match(/(\d+)([dhmy])/)
if (match) {
const [, value, unit] = match
const num = parseInt(value)
switch (unit) {
case 'd':
now.setDate(now.getDate() + num)
break
case 'h':
now.setHours(now.getHours() + num)
break
case 'm':
now.setMonth(now.getMonth() + num)
break
case 'y':
now.setFullYear(now.getFullYear() + num)
break
}
form.expiresAt = now.toISOString()
}
}
// 更新自定义过期时间
const updateCustomExpireAt = () => {
if (form.customExpireDate) {
form.expiresAt = new Date(form.customExpireDate).toISOString()
}
}
// 格式化过期日期
const formatExpireDate = (dateString) => {
const date = new Date(dateString)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
// 添加限制的模型
const addRestrictedModel = () => {
if (form.modelInput && !form.restrictedModels.includes(form.modelInput)) {
form.restrictedModels.push(form.modelInput)
form.modelInput = ''
}
}
// 移除限制的模型
const removeRestrictedModel = (index) => {
form.restrictedModels.splice(index, 1)
}
// 常用模型列表
const commonModels = ref(['claude-opus-4-20250514', 'claude-opus-4-1-20250805'])
// 可用的快捷模型(过滤掉已在限制列表中的)
const availableQuickModels = computed(() => {
return commonModels.value.filter((model) => !form.restrictedModels.includes(model))
})
// 快速添加限制的模型
const quickAddRestrictedModel = (model) => {
if (!form.restrictedModels.includes(model)) {
form.restrictedModels.push(model)
}
}
// 标签管理方法
const addTag = () => {
if (newTag.value && newTag.value.trim()) {
const tag = newTag.value.trim()
if (!form.tags.includes(tag)) {
form.tags.push(tag)
}
newTag.value = ''
}
}
const selectTag = (tag) => {
if (!form.tags.includes(tag)) {
form.tags.push(tag)
}
}
const removeTag = (index) => {
form.tags.splice(index, 1)
}
// 创建 API Key
const createApiKey = async () => {
// 验证表单
errors.value.name = ''
if (!form.name || !form.name.trim()) {
errors.value.name = '请输入API Key名称'
return
}
// 批量创建时验证数量
if (form.createType === 'batch') {
if (!form.batchCount || form.batchCount < 2 || form.batchCount > 500) {
showToast('批量创建数量必须在 2-500 之间', 'error')
return
}
}
loading.value = true
try {
// 准备提交的数据
const baseData = {
description: form.description || undefined,
tokenLimit:
form.tokenLimit !== '' && form.tokenLimit !== null ? parseInt(form.tokenLimit) : null,
rateLimitWindow:
form.rateLimitWindow !== '' && form.rateLimitWindow !== null
? parseInt(form.rateLimitWindow)
: null,
rateLimitRequests:
form.rateLimitRequests !== '' && form.rateLimitRequests !== null
? parseInt(form.rateLimitRequests)
: null,
concurrencyLimit:
form.concurrencyLimit !== '' && form.concurrencyLimit !== null
? parseInt(form.concurrencyLimit)
: 0,
dailyCostLimit:
form.dailyCostLimit !== '' && form.dailyCostLimit !== null
? parseFloat(form.dailyCostLimit)
: 0,
expiresAt: form.expiresAt || undefined,
permissions: form.permissions,
tags: form.tags.length > 0 ? form.tags : undefined,
enableModelRestriction: form.enableModelRestriction,
restrictedModels: form.restrictedModels,
enableClientRestriction: form.enableClientRestriction,
allowedClients: form.allowedClients
}
// 处理Claude账户绑定区分OAuth和Console
if (form.claudeAccountId) {
if (form.claudeAccountId.startsWith('console:')) {
// Claude Console账户
baseData.claudeConsoleAccountId = form.claudeAccountId.substring(8)
// 确保不会同时设置OAuth账号
delete baseData.claudeAccountId
} else {
// Claude OAuth账户或分组
baseData.claudeAccountId = form.claudeAccountId
// 确保不会同时设置Console账号
delete baseData.claudeConsoleAccountId
}
}
// Gemini账户绑定
if (form.geminiAccountId) {
baseData.geminiAccountId = form.geminiAccountId
}
if (form.createType === 'single') {
// 单个创建
const data = {
...baseData,
name: form.name
}
const result = await apiClient.post('/admin/api-keys', data)
if (result.success) {
showToast('API Key 创建成功', 'success')
emit('success', result.data)
emit('close')
} else {
showToast(result.message || '创建失败', 'error')
}
} else {
// 批量创建
const data = {
...baseData,
createType: 'batch',
baseName: form.name,
count: form.batchCount
}
const result = await apiClient.post('/admin/api-keys/batch', data)
if (result.success) {
showToast(`成功创建 ${result.data.length} 个 API Key`, 'success')
emit('batch-success', result.data)
emit('close')
} else {
showToast(result.message || '批量创建失败', 'error')
}
}
} catch (error) {
showToast('创建失败', 'error')
} finally {
loading.value = false
}
}
</script>
<style scoped>
/* 表单样式由全局样式提供 */
</style>