Files
claude-relay-service/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue
千羽 8a74bf5afe refactor: standardize code formatting and linting configuration
- Replace .eslintrc.js with .eslintrc.cjs for better ES module compatibility
- Add .prettierrc configuration for consistent code formatting
- Update package.json with new lint and format scripts
- Add nodemon.json for development hot reloading configuration
- Standardize code formatting across all JavaScript and Vue files
- Update web admin SPA with improved linting rules and formatting
- Add prettier configuration to web admin SPA

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-07 18:19:31 +09:00

910 lines
32 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-2 rounded-lg border border-red-200 bg-red-50 p-3"
>
<div>
<label class="mb-1 block text-xs font-medium text-gray-700">限制的模型列表</label>
<div class="mb-2 flex min-h-[24px] flex-wrap gap-1">
<span
v-for="(model, index) in form.restrictedModels"
:key="index"
class="inline-flex items-center rounded-full bg-red-100 px-2 py-1 text-xs text-red-800"
>
{{ model }}
<button
class="ml-1 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-xs text-gray-400">
暂无限制的模型
</span>
</div>
<div class="flex gap-2">
<input
v-model="form.modelInput"
class="form-input flex-1 text-sm"
placeholder="输入模型名称,按回车添加"
type="text"
@keydown.enter.prevent="addRestrictedModel"
/>
<button
class="rounded-lg bg-red-500 px-3 py-1.5 text-sm text-white transition-colors hover:bg-red-600"
type="button"
@click="addRestrictedModel"
>
<i class="fas fa-plus" />
</button>
</div>
<p class="mt-1 text-xs text-gray-500">例如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 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>