mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:38:02 +00:00
Merge branch 'main' of https://github.com/Wei-Shaw/claude-relay-service
# Conflicts: # web/admin-spa/dist/assets/LoginView-BJ0LLv16.js # web/admin-spa/dist/assets/LogoTitle-DHj-MjwS.js # web/admin-spa/dist/assets/MainLayout-CLydIeqJ.js # web/admin-spa/dist/assets/SettingsView-DicW12bL.js # web/admin-spa/dist/assets/index-HYE9xPuR.js # web/admin-spa/dist/index.html # web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue # web/admin-spa/src/components/apikeys/EditApiKeyModal.vue # web/admin-spa/src/views/ApiKeysView.vue
This commit is contained in:
@@ -2,425 +2,550 @@
|
||||
<Teleport to="body">
|
||||
<div class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
|
||||
<div class="modal-content w-full max-w-4xl p-6 mx-auto max-h-[90vh] flex flex-col">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center">
|
||||
<i class="fas fa-key text-white"></i>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center">
|
||||
<i class="fas fa-key text-white" />
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900">
|
||||
创建新的 API Key
|
||||
</h3>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900">创建新的 API Key</h3>
|
||||
</div>
|
||||
<button
|
||||
@click="$emit('close')"
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="createApiKey" class="space-y-4 modal-scroll-content custom-scrollbar flex-1">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">名称 <span class="text-red-500">*</span></label>
|
||||
<input
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
required
|
||||
class="form-input w-full"
|
||||
:class="{ 'border-red-500': errors.name }"
|
||||
placeholder="为您的 API Key 取一个名称"
|
||||
@input="errors.name = ''"
|
||||
<button
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<p v-if="errors.name" class="text-red-500 text-xs mt-1">{{ errors.name }}</p>
|
||||
<i class="fas fa-times text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 标签 -->
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">标签</label>
|
||||
<div class="space-y-4">
|
||||
<!-- 已选择的标签 -->
|
||||
<div v-if="form.tags.length > 0">
|
||||
<div class="text-xs font-medium text-gray-600 mb-2">已选择的标签:</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 px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full">
|
||||
{{ tag }}
|
||||
<button type="button" @click="removeTag(index)"
|
||||
class="ml-1 hover:text-blue-900">
|
||||
<i class="fas fa-times text-xs"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 可选择的已有标签 -->
|
||||
<div v-if="unselectedTags.length > 0">
|
||||
<div class="text-xs font-medium text-gray-600 mb-2">点击选择已有标签:</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="tag in unselectedTags"
|
||||
:key="'available-' + tag"
|
||||
type="button"
|
||||
@click="selectTag(tag)"
|
||||
class="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 text-gray-700 text-sm rounded-full hover:bg-blue-100 hover:text-blue-700 transition-colors"
|
||||
>
|
||||
<i class="fas fa-tag text-gray-500 text-xs"></i>
|
||||
{{ tag }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建新标签 -->
|
||||
<div>
|
||||
<div class="text-xs font-medium text-gray-600 mb-2">创建新标签:</div>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="newTag"
|
||||
type="text"
|
||||
class="form-input flex-1"
|
||||
placeholder="输入新标签名称"
|
||||
@keypress.enter.prevent="addTag"
|
||||
>
|
||||
<button type="button" @click="addTag"
|
||||
class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-500">用于标记不同团队或用途,方便筛选管理</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 速率限制设置 -->
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<div class="w-6 h-6 bg-blue-500 rounded flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-tachometer-alt text-white text-xs"></i>
|
||||
</div>
|
||||
<h4 class="font-semibold text-gray-800 text-sm">速率限制设置 (可选)</h4>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-2">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">时间窗口 (分钟)</label>
|
||||
<input
|
||||
v-model="form.rateLimitWindow"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="无限制"
|
||||
class="form-input w-full text-sm"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-0.5 ml-2">时间段单位</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">请求次数限制</label>
|
||||
<input
|
||||
v-model="form.rateLimitRequests"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="无限制"
|
||||
class="form-input w-full text-sm"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-0.5 ml-2">窗口内最大请求</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Token 限制</label>
|
||||
<input
|
||||
v-model="form.tokenLimit"
|
||||
type="number"
|
||||
placeholder="无限制"
|
||||
class="form-input w-full text-sm"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-0.5 ml-2">窗口内最大Token</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 示例说明 -->
|
||||
<div class="bg-blue-100 rounded-lg p-2">
|
||||
<h5 class="text-xs font-semibold text-blue-800 mb-1">💡 使用示例</h5>
|
||||
<div class="text-xs text-blue-700 space-y-0.5">
|
||||
<div><strong>示例1:</strong> 时间窗口=60,请求次数=1000 → 每60分钟最多1000次请求</div>
|
||||
<div><strong>示例2:</strong> 时间窗口=1,Token=10000 → 每分钟最多10,000个Token</div>
|
||||
<div><strong>示例3:</strong> 窗口=30,请求=50,Token=100000 → 每30分钟50次请求且不超10万Token</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">每日费用限制 (美元)</label>
|
||||
<div class="space-y-2">
|
||||
<div class="flex gap-2">
|
||||
<button type="button" @click="form.dailyCostLimit = '50'" class="px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium">$50</button>
|
||||
<button type="button" @click="form.dailyCostLimit = '100'" class="px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium">$100</button>
|
||||
<button type="button" @click="form.dailyCostLimit = '200'" class="px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium">$200</button>
|
||||
<button type="button" @click="form.dailyCostLimit = ''" class="px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium">自定义</button>
|
||||
</div>
|
||||
|
||||
<form
|
||||
class="space-y-4 modal-scroll-content custom-scrollbar flex-1"
|
||||
@submit.prevent="createApiKey"
|
||||
>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">名称 <span class="text-red-500">*</span></label>
|
||||
<input
|
||||
v-model="form.dailyCostLimit"
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
required
|
||||
class="form-input w-full"
|
||||
:class="{ 'border-red-500': errors.name }"
|
||||
placeholder="为您的 API Key 取一个名称"
|
||||
@input="errors.name = ''"
|
||||
>
|
||||
<p
|
||||
v-if="errors.name"
|
||||
class="text-red-500 text-xs mt-1"
|
||||
>
|
||||
{{ errors.name }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 标签 -->
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">标签</label>
|
||||
<div class="space-y-4">
|
||||
<!-- 已选择的标签 -->
|
||||
<div v-if="form.tags.length > 0">
|
||||
<div class="text-xs font-medium text-gray-600 mb-2">
|
||||
已选择的标签:
|
||||
</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 px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full"
|
||||
>
|
||||
{{ tag }}
|
||||
<button
|
||||
type="button"
|
||||
class="ml-1 hover:text-blue-900"
|
||||
@click="removeTag(index)"
|
||||
>
|
||||
<i class="fas fa-times text-xs" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 可选择的已有标签 -->
|
||||
<div v-if="unselectedTags.length > 0">
|
||||
<div class="text-xs font-medium text-gray-600 mb-2">
|
||||
点击选择已有标签:
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="tag in unselectedTags"
|
||||
:key="'available-' + tag"
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 text-gray-700 text-sm rounded-full hover:bg-blue-100 hover:text-blue-700 transition-colors"
|
||||
@click="selectTag(tag)"
|
||||
>
|
||||
<i class="fas fa-tag text-gray-500 text-xs" />
|
||||
{{ tag }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建新标签 -->
|
||||
<div>
|
||||
<div class="text-xs font-medium text-gray-600 mb-2">
|
||||
创建新标签:
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="newTag"
|
||||
type="text"
|
||||
class="form-input flex-1"
|
||||
placeholder="输入新标签名称"
|
||||
@keypress.enter.prevent="addTag"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors"
|
||||
@click="addTag"
|
||||
>
|
||||
<i class="fas fa-plus" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-500">
|
||||
用于标记不同团队或用途,方便筛选管理
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 速率限制设置 -->
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<div class="w-6 h-6 bg-blue-500 rounded flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-tachometer-alt text-white text-xs" />
|
||||
</div>
|
||||
<h4 class="font-semibold text-gray-800 text-sm">
|
||||
速率限制设置 (可选)
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-2">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">时间窗口 (分钟)</label>
|
||||
<input
|
||||
v-model="form.rateLimitWindow"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="无限制"
|
||||
class="form-input w-full text-sm"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-0.5 ml-2">
|
||||
时间段单位
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">请求次数限制</label>
|
||||
<input
|
||||
v-model="form.rateLimitRequests"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="无限制"
|
||||
class="form-input w-full text-sm"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-0.5 ml-2">
|
||||
窗口内最大请求
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Token 限制</label>
|
||||
<input
|
||||
v-model="form.tokenLimit"
|
||||
type="number"
|
||||
placeholder="无限制"
|
||||
class="form-input w-full text-sm"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-0.5 ml-2">
|
||||
窗口内最大Token
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 示例说明 -->
|
||||
<div class="bg-blue-100 rounded-lg p-2">
|
||||
<h5 class="text-xs font-semibold text-blue-800 mb-1">
|
||||
💡 使用示例
|
||||
</h5>
|
||||
<div class="text-xs text-blue-700 space-y-0.5">
|
||||
<div><strong>示例1:</strong> 时间窗口=60,请求次数=1000 → 每60分钟最多1000次请求</div>
|
||||
<div><strong>示例2:</strong> 时间窗口=1,Token=10000 → 每分钟最多10,000个Token</div>
|
||||
<div><strong>示例3:</strong> 窗口=30,请求=50,Token=100000 → 每30分钟50次请求且不超10万Token</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">每日费用限制 (美元)</label>
|
||||
<div class="space-y-2">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium"
|
||||
@click="form.dailyCostLimit = '50'"
|
||||
>
|
||||
$50
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium"
|
||||
@click="form.dailyCostLimit = '100'"
|
||||
>
|
||||
$100
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium"
|
||||
@click="form.dailyCostLimit = '200'"
|
||||
>
|
||||
$200
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium"
|
||||
@click="form.dailyCostLimit = ''"
|
||||
>
|
||||
自定义
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
v-model="form.dailyCostLimit"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
placeholder="0 表示无限制"
|
||||
class="form-input w-full"
|
||||
>
|
||||
<p class="text-xs text-gray-500">
|
||||
设置此 API Key 每日的费用限制,超过限制将拒绝请求,0 或留空表示无限制
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">并发限制 (可选)</label>
|
||||
<input
|
||||
v-model="form.concurrencyLimit"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
placeholder="0 表示无限制"
|
||||
class="form-input w-full"
|
||||
>
|
||||
<p class="text-xs text-gray-500">设置此 API Key 每日的费用限制,超过限制将拒绝请求,0 或留空表示无限制</p>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
设置此 API Key 可同时处理的最大请求数,0 或留空表示无限制
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">并发限制 (可选)</label>
|
||||
<input
|
||||
v-model="form.concurrencyLimit"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="0 表示无限制"
|
||||
class="form-input w-full"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-2">设置此 API Key 可同时处理的最大请求数,0 或留空表示无限制</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">备注 (可选)</label>
|
||||
<textarea
|
||||
v-model="form.description"
|
||||
rows="2"
|
||||
class="form-input w-full resize-none text-sm"
|
||||
placeholder="描述此 API Key 的用途..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">备注 (可选)</label>
|
||||
<textarea
|
||||
v-model="form.description"
|
||||
rows="2"
|
||||
class="form-input w-full resize-none text-sm"
|
||||
placeholder="描述此 API Key 的用途..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">有效期限</label>
|
||||
<select
|
||||
v-model="form.expireDuration"
|
||||
@change="updateExpireAt"
|
||||
class="form-input w-full"
|
||||
>
|
||||
<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"
|
||||
type="datetime-local"
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">有效期限</label>
|
||||
<select
|
||||
v-model="form.expireDuration"
|
||||
class="form-input w-full"
|
||||
:min="minDateTime"
|
||||
@change="updateCustomExpireAt"
|
||||
@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"
|
||||
>
|
||||
</div>
|
||||
<p v-if="form.expiresAt" class="text-xs text-gray-500 mt-2">
|
||||
将于 {{ formatExpireDate(form.expiresAt) }} 过期
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">服务权限</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
v-model="form.permissions"
|
||||
value="all"
|
||||
class="mr-2"
|
||||
>
|
||||
<span class="text-sm text-gray-700">全部服务</span>
|
||||
</label>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
v-model="form.permissions"
|
||||
value="claude"
|
||||
class="mr-2"
|
||||
>
|
||||
<span class="text-sm text-gray-700">仅 Claude</span>
|
||||
</label>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
v-model="form.permissions"
|
||||
value="gemini"
|
||||
class="mr-2"
|
||||
>
|
||||
<span class="text-sm text-gray-700">仅 Gemini</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">控制此 API Key 可以访问哪些服务</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">专属账号绑定 (可选)</label>
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-600 mb-1">Claude 专属账号</label>
|
||||
<select
|
||||
v-model="form.claudeAccountId"
|
||||
v-model="form.customExpireDate"
|
||||
type="datetime-local"
|
||||
class="form-input w-full"
|
||||
:disabled="form.permissions === 'gemini'"
|
||||
:min="minDateTime"
|
||||
@change="updateCustomExpireAt"
|
||||
>
|
||||
<option value="">使用共享账号池</option>
|
||||
<optgroup v-if="accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-oauth').length > 0" label="Claude OAuth 账号">
|
||||
</div>
|
||||
<p
|
||||
v-if="form.expiresAt"
|
||||
class="text-xs text-gray-500 mt-2"
|
||||
>
|
||||
将于 {{ formatExpireDate(form.expiresAt) }} 过期
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">服务权限</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
v-model="form.permissions"
|
||||
type="radio"
|
||||
value="all"
|
||||
class="mr-2"
|
||||
>
|
||||
<span class="text-sm text-gray-700">全部服务</span>
|
||||
</label>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
v-model="form.permissions"
|
||||
type="radio"
|
||||
value="claude"
|
||||
class="mr-2"
|
||||
>
|
||||
<span class="text-sm text-gray-700">仅 Claude</span>
|
||||
</label>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
v-model="form.permissions"
|
||||
type="radio"
|
||||
value="gemini"
|
||||
class="mr-2"
|
||||
>
|
||||
<span class="text-sm text-gray-700">仅 Gemini</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
控制此 API Key 可以访问哪些服务
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">专属账号绑定 (可选)</label>
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-600 mb-1">Claude 专属账号</label>
|
||||
<select
|
||||
v-model="form.claudeAccountId"
|
||||
class="form-input w-full"
|
||||
:disabled="form.permissions === 'gemini'"
|
||||
>
|
||||
<option value="">
|
||||
使用共享账号池
|
||||
</option>
|
||||
<optgroup v-if="accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-oauth').length > 0" label="Claude OAuth 账号">
|
||||
<option
|
||||
v-for="account in accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-oauth')"
|
||||
:key="account.id"
|
||||
:value="account.id"
|
||||
>
|
||||
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
|
||||
</option>
|
||||
</optgroup>
|
||||
<optgroup v-if="accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-console').length > 0" label="Claude Console 账号">
|
||||
<option
|
||||
v-for="account in accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-console')"
|
||||
:key="account.id"
|
||||
:value="`console:${account.id}`"
|
||||
>
|
||||
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-600 mb-1">Gemini 专属账号</label>
|
||||
<select
|
||||
v-model="form.geminiAccountId"
|
||||
class="form-input w-full"
|
||||
:disabled="form.permissions === 'claude'"
|
||||
>
|
||||
<option value="">
|
||||
使用共享账号池
|
||||
</option>
|
||||
<option
|
||||
v-for="account in accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-oauth')"
|
||||
v-for="account in accounts.gemini.filter(a => a.isDedicated)"
|
||||
:key="account.id"
|
||||
:value="account.id"
|
||||
>
|
||||
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
|
||||
</option>
|
||||
</optgroup>
|
||||
<optgroup v-if="accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-console').length > 0" label="Claude Console 账号">
|
||||
<option
|
||||
v-for="account in accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-console')"
|
||||
:key="account.id"
|
||||
:value="`console:${account.id}`"
|
||||
>
|
||||
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-600 mb-1">Gemini 专属账号</label>
|
||||
<select
|
||||
v-model="form.geminiAccountId"
|
||||
class="form-input w-full"
|
||||
:disabled="form.permissions === 'claude'"
|
||||
>
|
||||
<option value="">使用共享账号池</option>
|
||||
<option
|
||||
v-for="account in accounts.gemini.filter(a => a.isDedicated)"
|
||||
:key="account.id"
|
||||
:value="account.id"
|
||||
>
|
||||
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
|
||||
</option>
|
||||
</select>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
选择专属账号后,此API Key将只使用该账号,不选择则使用共享账号池
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">选择专属账号后,此API Key将只使用该账号,不选择则使用共享账号池</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center mb-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="form.enableModelRestriction"
|
||||
id="enableModelRestriction"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||
>
|
||||
<label for="enableModelRestriction" class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer">
|
||||
启用模型限制
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center mb-2">
|
||||
<input
|
||||
id="enableModelRestriction"
|
||||
v-model="form.enableModelRestriction"
|
||||
type="checkbox"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||
>
|
||||
<label
|
||||
for="enableModelRestriction"
|
||||
class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer"
|
||||
>
|
||||
启用模型限制
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="form.enableModelRestriction" class="space-y-2 bg-red-50 border border-red-200 rounded-lg p-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">限制的模型列表</label>
|
||||
<div class="flex flex-wrap gap-1 mb-2 min-h-[24px]">
|
||||
<span
|
||||
v-for="(model, index) in form.restrictedModels"
|
||||
:key="index"
|
||||
class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-red-100 text-red-800"
|
||||
>
|
||||
{{ model }}
|
||||
<div
|
||||
v-if="form.enableModelRestriction"
|
||||
class="space-y-2 bg-red-50 border border-red-200 rounded-lg p-3"
|
||||
>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">限制的模型列表</label>
|
||||
<div class="flex flex-wrap gap-1 mb-2 min-h-[24px]">
|
||||
<span
|
||||
v-for="(model, index) in form.restrictedModels"
|
||||
:key="index"
|
||||
class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-red-100 text-red-800"
|
||||
>
|
||||
{{ model }}
|
||||
<button
|
||||
type="button"
|
||||
class="ml-1 text-red-600 hover:text-red-800"
|
||||
@click="removeRestrictedModel(index)"
|
||||
>
|
||||
<i class="fas fa-times text-xs" />
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
v-if="form.restrictedModels.length === 0"
|
||||
class="text-gray-400 text-xs"
|
||||
>
|
||||
暂无限制的模型
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="form.modelInput"
|
||||
type="text"
|
||||
placeholder="输入模型名称,按回车添加"
|
||||
class="form-input flex-1 text-sm"
|
||||
@keydown.enter.prevent="addRestrictedModel"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@click="removeRestrictedModel(index)"
|
||||
class="ml-1 text-red-600 hover:text-red-800"
|
||||
class="px-3 py-1.5 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors text-sm"
|
||||
@click="addRestrictedModel"
|
||||
>
|
||||
<i class="fas fa-times text-xs"></i>
|
||||
<i class="fas fa-plus" />
|
||||
</button>
|
||||
</span>
|
||||
<span v-if="form.restrictedModels.length === 0" class="text-gray-400 text-xs">
|
||||
暂无限制的模型
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
例如:claude-opus-4-20250514
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="form.modelInput"
|
||||
@keydown.enter.prevent="addRestrictedModel"
|
||||
type="text"
|
||||
placeholder="输入模型名称,按回车添加"
|
||||
class="form-input flex-1 text-sm"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@click="addRestrictedModel"
|
||||
class="px-3 py-1.5 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors text-sm"
|
||||
>
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-1">例如:claude-opus-4-20250514</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 客户端限制 -->
|
||||
<div>
|
||||
<div class="flex items-center mb-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="form.enableClientRestriction"
|
||||
id="enableClientRestriction"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||
>
|
||||
<label for="enableClientRestriction" class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer">
|
||||
启用客户端限制
|
||||
</label>
|
||||
</div>
|
||||
<!-- 客户端限制 -->
|
||||
<div>
|
||||
<div class="flex items-center mb-2">
|
||||
<input
|
||||
id="enableClientRestriction"
|
||||
v-model="form.enableClientRestriction"
|
||||
type="checkbox"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||
>
|
||||
<label
|
||||
for="enableClientRestriction"
|
||||
class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer"
|
||||
>
|
||||
启用客户端限制
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="form.enableClientRestriction" class="bg-green-50 border border-green-200 rounded-lg p-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-2">允许的客户端</label>
|
||||
<div class="space-y-1">
|
||||
<div v-for="client in supportedClients" :key="client.id" class="flex items-start">
|
||||
<input
|
||||
type="checkbox"
|
||||
:id="`client_${client.id}`"
|
||||
:value="client.id"
|
||||
v-model="form.allowedClients"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 mt-0.5"
|
||||
<div
|
||||
v-if="form.enableClientRestriction"
|
||||
class="bg-green-50 border border-green-200 rounded-lg p-3"
|
||||
>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-2">允许的客户端</label>
|
||||
<div class="space-y-1">
|
||||
<div
|
||||
v-for="client in supportedClients"
|
||||
:key="client.id"
|
||||
class="flex items-start"
|
||||
>
|
||||
<label :for="`client_${client.id}`" class="ml-2 flex-1 cursor-pointer">
|
||||
<span class="text-sm font-medium text-gray-700">{{ client.name }}</span>
|
||||
<span class="text-xs text-gray-500 block">{{ client.description }}</span>
|
||||
</label>
|
||||
<input
|
||||
:id="`client_${client.id}`"
|
||||
v-model="form.allowedClients"
|
||||
type="checkbox"
|
||||
:value="client.id"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 mt-0.5"
|
||||
>
|
||||
<label
|
||||
:for="`client_${client.id}`"
|
||||
class="ml-2 flex-1 cursor-pointer"
|
||||
>
|
||||
<span class="text-sm font-medium text-gray-700">{{ client.name }}</span>
|
||||
<span class="text-xs text-gray-500 block">{{ client.description }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
@click="$emit('close')"
|
||||
class="flex-1 px-4 py-2.5 bg-gray-100 text-gray-700 rounded-lg font-semibold hover:bg-gray-200 transition-colors text-sm"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
class="btn btn-primary flex-1 py-2.5 px-4 font-semibold text-sm"
|
||||
>
|
||||
<div v-if="loading" class="loading-spinner mr-2"></div>
|
||||
<i v-else class="fas fa-plus mr-2"></i>
|
||||
{{ loading ? '创建中...' : '创建' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="flex gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 px-4 py-2.5 bg-gray-100 text-gray-700 rounded-lg font-semibold hover:bg-gray-200 transition-colors text-sm"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
class="btn btn-primary flex-1 py-2.5 px-4 font-semibold text-sm"
|
||||
>
|
||||
<div
|
||||
v-if="loading"
|
||||
class="loading-spinner mr-2"
|
||||
/>
|
||||
<i
|
||||
v-else
|
||||
class="fas fa-plus mr-2"
|
||||
/>
|
||||
{{ loading ? '创建中...' : '创建' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -2,390 +2,491 @@
|
||||
<Teleport to="body">
|
||||
<div class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
|
||||
<div class="modal-content w-full max-w-4xl p-8 mx-auto max-h-[90vh] flex flex-col">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center">
|
||||
<i class="fas fa-edit text-white"></i>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center">
|
||||
<i class="fas fa-edit text-white" />
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900">
|
||||
编辑 API Key
|
||||
</h3>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900">编辑 API Key</h3>
|
||||
</div>
|
||||
<button
|
||||
@click="$emit('close')"
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="updateApiKey" class="space-y-6 modal-scroll-content custom-scrollbar flex-1">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">名称</label>
|
||||
<input
|
||||
:value="form.name"
|
||||
type="text"
|
||||
disabled
|
||||
class="form-input w-full bg-gray-100 cursor-not-allowed"
|
||||
<button
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-2">名称不可修改</p>
|
||||
<i class="fas fa-times text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 标签 -->
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">标签</label>
|
||||
<div class="space-y-4">
|
||||
<!-- 已选择的标签 -->
|
||||
<div v-if="form.tags.length > 0">
|
||||
<div class="text-xs font-medium text-gray-600 mb-2">已选择的标签:</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 px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full">
|
||||
{{ tag }}
|
||||
<button type="button" @click="removeTag(index)"
|
||||
class="ml-1 hover:text-blue-900">
|
||||
<i class="fas fa-times text-xs"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 可选择的已有标签 -->
|
||||
<div v-if="unselectedTags.length > 0">
|
||||
<div class="text-xs font-medium text-gray-600 mb-2">点击选择已有标签:</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="tag in unselectedTags"
|
||||
:key="'available-' + tag"
|
||||
type="button"
|
||||
@click="selectTag(tag)"
|
||||
class="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 text-gray-700 text-sm rounded-full hover:bg-blue-100 hover:text-blue-700 transition-colors"
|
||||
>
|
||||
<i class="fas fa-tag text-gray-500 text-xs"></i>
|
||||
{{ tag }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建新标签 -->
|
||||
<div>
|
||||
<div class="text-xs font-medium text-gray-600 mb-2">创建新标签:</div>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="newTag"
|
||||
type="text"
|
||||
class="form-input flex-1"
|
||||
placeholder="输入新标签名称"
|
||||
@keypress.enter.prevent="addTag"
|
||||
>
|
||||
<button type="button" @click="addTag"
|
||||
class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-500">用于标记不同团队或用途,方便筛选管理</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 速率限制设置 -->
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<div class="w-6 h-6 bg-blue-500 rounded flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-tachometer-alt text-white text-xs"></i>
|
||||
</div>
|
||||
<h4 class="font-semibold text-gray-800 text-sm">速率限制设置 (可选)</h4>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-2">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">时间窗口 (分钟)</label>
|
||||
<input
|
||||
v-model="form.rateLimitWindow"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="无限制"
|
||||
class="form-input w-full text-sm"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-0.5 ml-2">时间段单位</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">请求次数限制</label>
|
||||
<input
|
||||
v-model="form.rateLimitRequests"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="无限制"
|
||||
class="form-input w-full text-sm"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-0.5 ml-2">窗口内最大请求</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Token 限制</label>
|
||||
<input
|
||||
v-model="form.tokenLimit"
|
||||
type="number"
|
||||
placeholder="无限制"
|
||||
class="form-input w-full text-sm"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-0.5 ml-2">窗口内最大Token</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 示例说明 -->
|
||||
<div class="bg-blue-100 rounded-lg p-2">
|
||||
<h5 class="text-xs font-semibold text-blue-800 mb-1">💡 使用示例</h5>
|
||||
<div class="text-xs text-blue-700 space-y-0.5">
|
||||
<div><strong>示例1:</strong> 时间窗口=60,请求次数=1000 → 每60分钟最多1000次请求</div>
|
||||
<div><strong>示例2:</strong> 时间窗口=1,Token=10000 → 每分钟最多10,000个Token</div>
|
||||
<div><strong>示例3:</strong> 窗口=30,请求=50,Token=100000 → 每30分钟50次请求且不超10万Token</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">每日费用限制 (美元)</label>
|
||||
<div class="space-y-3">
|
||||
<div class="flex gap-2">
|
||||
<button type="button" @click="form.dailyCostLimit = '50'" class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium">$50</button>
|
||||
<button type="button" @click="form.dailyCostLimit = '100'" class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium">$100</button>
|
||||
<button type="button" @click="form.dailyCostLimit = '200'" class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium">$200</button>
|
||||
<button type="button" @click="form.dailyCostLimit = ''" class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium">自定义</button>
|
||||
</div>
|
||||
|
||||
<form
|
||||
class="space-y-6 modal-scroll-content custom-scrollbar flex-1"
|
||||
@submit.prevent="updateApiKey"
|
||||
>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">名称</label>
|
||||
<input
|
||||
v-model="form.dailyCostLimit"
|
||||
:value="form.name"
|
||||
type="text"
|
||||
disabled
|
||||
class="form-input w-full bg-gray-100 cursor-not-allowed"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
名称不可修改
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 标签 -->
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">标签</label>
|
||||
<div class="space-y-4">
|
||||
<!-- 已选择的标签 -->
|
||||
<div v-if="form.tags.length > 0">
|
||||
<div class="text-xs font-medium text-gray-600 mb-2">
|
||||
已选择的标签:
|
||||
</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 px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full"
|
||||
>
|
||||
{{ tag }}
|
||||
<button
|
||||
type="button"
|
||||
class="ml-1 hover:text-blue-900"
|
||||
@click="removeTag(index)"
|
||||
>
|
||||
<i class="fas fa-times text-xs" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 可选择的已有标签 -->
|
||||
<div v-if="unselectedTags.length > 0">
|
||||
<div class="text-xs font-medium text-gray-600 mb-2">
|
||||
点击选择已有标签:
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="tag in unselectedTags"
|
||||
:key="'available-' + tag"
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 text-gray-700 text-sm rounded-full hover:bg-blue-100 hover:text-blue-700 transition-colors"
|
||||
@click="selectTag(tag)"
|
||||
>
|
||||
<i class="fas fa-tag text-gray-500 text-xs" />
|
||||
{{ tag }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建新标签 -->
|
||||
<div>
|
||||
<div class="text-xs font-medium text-gray-600 mb-2">
|
||||
创建新标签:
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="newTag"
|
||||
type="text"
|
||||
class="form-input flex-1"
|
||||
placeholder="输入新标签名称"
|
||||
@keypress.enter.prevent="addTag"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors"
|
||||
@click="addTag"
|
||||
>
|
||||
<i class="fas fa-plus" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-500">
|
||||
用于标记不同团队或用途,方便筛选管理
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 速率限制设置 -->
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<div class="w-6 h-6 bg-blue-500 rounded flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-tachometer-alt text-white text-xs" />
|
||||
</div>
|
||||
<h4 class="font-semibold text-gray-800 text-sm">
|
||||
速率限制设置 (可选)
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-2">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">时间窗口 (分钟)</label>
|
||||
<input
|
||||
v-model="form.rateLimitWindow"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="无限制"
|
||||
class="form-input w-full text-sm"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-0.5 ml-2">
|
||||
时间段单位
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">请求次数限制</label>
|
||||
<input
|
||||
v-model="form.rateLimitRequests"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="无限制"
|
||||
class="form-input w-full text-sm"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-0.5 ml-2">
|
||||
窗口内最大请求
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Token 限制</label>
|
||||
<input
|
||||
v-model="form.tokenLimit"
|
||||
type="number"
|
||||
placeholder="无限制"
|
||||
class="form-input w-full text-sm"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-0.5 ml-2">
|
||||
窗口内最大Token
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 示例说明 -->
|
||||
<div class="bg-blue-100 rounded-lg p-2">
|
||||
<h5 class="text-xs font-semibold text-blue-800 mb-1">
|
||||
💡 使用示例
|
||||
</h5>
|
||||
<div class="text-xs text-blue-700 space-y-0.5">
|
||||
<div><strong>示例1:</strong> 时间窗口=60,请求次数=1000 → 每60分钟最多1000次请求</div>
|
||||
<div><strong>示例2:</strong> 时间窗口=1,Token=10000 → 每分钟最多10,000个Token</div>
|
||||
<div><strong>示例3:</strong> 窗口=30,请求=50,Token=100000 → 每30分钟50次请求且不超10万Token</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">每日费用限制 (美元)</label>
|
||||
<div class="space-y-3">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium"
|
||||
@click="form.dailyCostLimit = '50'"
|
||||
>
|
||||
$50
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium"
|
||||
@click="form.dailyCostLimit = '100'"
|
||||
>
|
||||
$100
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium"
|
||||
@click="form.dailyCostLimit = '200'"
|
||||
>
|
||||
$200
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium"
|
||||
@click="form.dailyCostLimit = ''"
|
||||
>
|
||||
自定义
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
v-model="form.dailyCostLimit"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
placeholder="0 表示无限制"
|
||||
class="form-input w-full"
|
||||
>
|
||||
<p class="text-xs text-gray-500">
|
||||
设置此 API Key 每日的费用限制,超过限制将拒绝请求,0 或留空表示无限制
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">并发限制</label>
|
||||
<input
|
||||
v-model="form.concurrencyLimit"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
placeholder="0 表示无限制"
|
||||
class="form-input w-full"
|
||||
>
|
||||
<p class="text-xs text-gray-500">设置此 API Key 每日的费用限制,超过限制将拒绝请求,0 或留空表示无限制</p>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
设置此 API Key 可同时处理的最大请求数
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">并发限制</label>
|
||||
<input
|
||||
v-model="form.concurrencyLimit"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="0 表示无限制"
|
||||
class="form-input w-full"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-2">设置此 API Key 可同时处理的最大请求数</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">服务权限</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
v-model="form.permissions"
|
||||
value="all"
|
||||
class="mr-2"
|
||||
>
|
||||
<span class="text-sm text-gray-700">全部服务</span>
|
||||
</label>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
v-model="form.permissions"
|
||||
value="claude"
|
||||
class="mr-2"
|
||||
>
|
||||
<span class="text-sm text-gray-700">仅 Claude</span>
|
||||
</label>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
v-model="form.permissions"
|
||||
value="gemini"
|
||||
class="mr-2"
|
||||
>
|
||||
<span class="text-sm text-gray-700">仅 Gemini</span>
|
||||
</label>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">服务权限</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
v-model="form.permissions"
|
||||
type="radio"
|
||||
value="all"
|
||||
class="mr-2"
|
||||
>
|
||||
<span class="text-sm text-gray-700">全部服务</span>
|
||||
</label>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
v-model="form.permissions"
|
||||
type="radio"
|
||||
value="claude"
|
||||
class="mr-2"
|
||||
>
|
||||
<span class="text-sm text-gray-700">仅 Claude</span>
|
||||
</label>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
v-model="form.permissions"
|
||||
type="radio"
|
||||
value="gemini"
|
||||
class="mr-2"
|
||||
>
|
||||
<span class="text-sm text-gray-700">仅 Gemini</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
控制此 API Key 可以访问哪些服务
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">控制此 API Key 可以访问哪些服务</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">专属账号绑定</label>
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-600 mb-1">Claude 专属账号</label>
|
||||
<select
|
||||
v-model="form.claudeAccountId"
|
||||
class="form-input w-full"
|
||||
:disabled="form.permissions === 'gemini'"
|
||||
>
|
||||
<option value="">使用共享账号池</option>
|
||||
<optgroup v-if="accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-oauth').length > 0" label="Claude OAuth 账号">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">专属账号绑定</label>
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-600 mb-1">Claude 专属账号</label>
|
||||
<select
|
||||
v-model="form.claudeAccountId"
|
||||
class="form-input w-full"
|
||||
:disabled="form.permissions === 'gemini'"
|
||||
>
|
||||
<option value="">
|
||||
使用共享账号池
|
||||
</option>
|
||||
<optgroup v-if="accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-oauth').length > 0" label="Claude OAuth 账号">
|
||||
<option
|
||||
v-for="account in accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-oauth')"
|
||||
:key="account.id"
|
||||
:value="account.id"
|
||||
>
|
||||
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
|
||||
</option>
|
||||
</optgroup>
|
||||
<optgroup v-if="accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-console').length > 0" label="Claude Console 账号">
|
||||
<option
|
||||
v-for="account in accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-console')"
|
||||
:key="account.id"
|
||||
:value="`console:${account.id}`"
|
||||
>
|
||||
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-600 mb-1">Gemini 专属账号</label>
|
||||
<select
|
||||
v-model="form.geminiAccountId"
|
||||
class="form-input w-full"
|
||||
:disabled="form.permissions === 'claude'"
|
||||
>
|
||||
<option value="">
|
||||
使用共享账号池
|
||||
</option>
|
||||
<option
|
||||
v-for="account in accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-oauth')"
|
||||
v-for="account in accounts.gemini"
|
||||
:key="account.id"
|
||||
:value="account.id"
|
||||
>
|
||||
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
|
||||
</option>
|
||||
</optgroup>
|
||||
<optgroup v-if="accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-console').length > 0" label="Claude Console 账号">
|
||||
<option
|
||||
v-for="account in accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-console')"
|
||||
:key="account.id"
|
||||
:value="`console:${account.id}`"
|
||||
>
|
||||
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-600 mb-1">Gemini 专属账号</label>
|
||||
<select
|
||||
v-model="form.geminiAccountId"
|
||||
class="form-input w-full"
|
||||
:disabled="form.permissions === 'claude'"
|
||||
>
|
||||
<option value="">使用共享账号池</option>
|
||||
<option
|
||||
v-for="account in accounts.gemini.filter(a => a.isDedicated)"
|
||||
:key="account.id"
|
||||
:value="account.id"
|
||||
>
|
||||
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
|
||||
</option>
|
||||
</select>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
修改绑定账号将影响此API Key的请求路由
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">修改绑定账号将影响此API Key的请求路由</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center mb-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="form.enableModelRestriction"
|
||||
id="editEnableModelRestriction"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||
>
|
||||
<label for="editEnableModelRestriction" class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer">
|
||||
启用模型限制
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center mb-3">
|
||||
<input
|
||||
id="editEnableModelRestriction"
|
||||
v-model="form.enableModelRestriction"
|
||||
type="checkbox"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||
>
|
||||
<label
|
||||
for="editEnableModelRestriction"
|
||||
class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer"
|
||||
>
|
||||
启用模型限制
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="form.enableModelRestriction" class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-600 mb-2">限制的模型列表</label>
|
||||
<div class="flex flex-wrap gap-2 mb-3 min-h-[32px] p-2 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<span
|
||||
v-for="(model, index) in form.restrictedModels"
|
||||
:key="index"
|
||||
class="inline-flex items-center px-3 py-1 rounded-full text-sm bg-red-100 text-red-800"
|
||||
>
|
||||
{{ model }}
|
||||
<div
|
||||
v-if="form.enableModelRestriction"
|
||||
class="space-y-3"
|
||||
>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-600 mb-2">限制的模型列表</label>
|
||||
<div class="flex flex-wrap gap-2 mb-3 min-h-[32px] p-2 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<span
|
||||
v-for="(model, index) in form.restrictedModels"
|
||||
:key="index"
|
||||
class="inline-flex items-center px-3 py-1 rounded-full text-sm bg-red-100 text-red-800"
|
||||
>
|
||||
{{ model }}
|
||||
<button
|
||||
type="button"
|
||||
class="ml-2 text-red-600 hover:text-red-800"
|
||||
@click="removeRestrictedModel(index)"
|
||||
>
|
||||
<i class="fas fa-times text-xs" />
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
v-if="form.restrictedModels.length === 0"
|
||||
class="text-gray-400 text-sm"
|
||||
>
|
||||
暂无限制的模型
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="form.modelInput"
|
||||
type="text"
|
||||
placeholder="输入模型名称,按回车添加"
|
||||
class="form-input flex-1"
|
||||
@keydown.enter.prevent="addRestrictedModel"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@click="removeRestrictedModel(index)"
|
||||
class="ml-2 text-red-600 hover:text-red-800"
|
||||
class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
|
||||
@click="addRestrictedModel"
|
||||
>
|
||||
<i class="fas fa-times text-xs"></i>
|
||||
<i class="fas fa-plus" />
|
||||
</button>
|
||||
</span>
|
||||
<span v-if="form.restrictedModels.length === 0" class="text-gray-400 text-sm">
|
||||
暂无限制的模型
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
设置此API Key无法访问的模型,例如:claude-opus-4-20250514
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="form.modelInput"
|
||||
@keydown.enter.prevent="addRestrictedModel"
|
||||
type="text"
|
||||
placeholder="输入模型名称,按回车添加"
|
||||
class="form-input flex-1"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@click="addRestrictedModel"
|
||||
class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
|
||||
>
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">设置此API Key无法访问的模型,例如:claude-opus-4-20250514</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 客户端限制 -->
|
||||
<div>
|
||||
<div class="flex items-center mb-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="form.enableClientRestriction"
|
||||
id="editEnableClientRestriction"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||
>
|
||||
<label for="editEnableClientRestriction" class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer">
|
||||
启用客户端限制
|
||||
</label>
|
||||
</div>
|
||||
<!-- 客户端限制 -->
|
||||
<div>
|
||||
<div class="flex items-center mb-3">
|
||||
<input
|
||||
id="editEnableClientRestriction"
|
||||
v-model="form.enableClientRestriction"
|
||||
type="checkbox"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||
>
|
||||
<label
|
||||
for="editEnableClientRestriction"
|
||||
class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer"
|
||||
>
|
||||
启用客户端限制
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="form.enableClientRestriction" class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-600 mb-2">允许的客户端</label>
|
||||
<p class="text-xs text-gray-500 mb-3">勾选允许使用此API Key的客户端</p>
|
||||
<div class="space-y-2">
|
||||
<div v-for="client in supportedClients" :key="client.id" class="flex items-start">
|
||||
<input
|
||||
type="checkbox"
|
||||
:id="`edit_client_${client.id}`"
|
||||
:value="client.id"
|
||||
v-model="form.allowedClients"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 mt-0.5"
|
||||
<div
|
||||
v-if="form.enableClientRestriction"
|
||||
class="space-y-3"
|
||||
>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-600 mb-2">允许的客户端</label>
|
||||
<p class="text-xs text-gray-500 mb-3">
|
||||
勾选允许使用此API Key的客户端
|
||||
</p>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="client in supportedClients"
|
||||
:key="client.id"
|
||||
class="flex items-start"
|
||||
>
|
||||
<label :for="`edit_client_${client.id}`" class="ml-2 flex-1 cursor-pointer">
|
||||
<span class="text-sm font-medium text-gray-700">{{ client.name }}</span>
|
||||
<span class="text-xs text-gray-500 block">{{ client.description }}</span>
|
||||
</label>
|
||||
<input
|
||||
:id="`edit_client_${client.id}`"
|
||||
v-model="form.allowedClients"
|
||||
type="checkbox"
|
||||
:value="client.id"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 mt-0.5"
|
||||
>
|
||||
<label
|
||||
:for="`edit_client_${client.id}`"
|
||||
class="ml-2 flex-1 cursor-pointer"
|
||||
>
|
||||
<span class="text-sm font-medium text-gray-700">{{ client.name }}</span>
|
||||
<span class="text-xs text-gray-500 block">{{ client.description }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
@click="$emit('close')"
|
||||
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
||||
>
|
||||
<div v-if="loading" class="loading-spinner mr-2"></div>
|
||||
<i v-else class="fas fa-save mr-2"></i>
|
||||
{{ loading ? '保存中...' : '保存修改' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
||||
>
|
||||
<div
|
||||
v-if="loading"
|
||||
class="loading-spinner mr-2"
|
||||
/>
|
||||
<i
|
||||
v-else
|
||||
class="fas fa-save mr-2"
|
||||
/>
|
||||
{{ loading ? '保存中...' : '保存修改' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { showToast } from '@/utils/toast'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useClientsStore } from '@/stores/clients'
|
||||
import { useApiKeysStore } from '@/stores/apiKeys'
|
||||
import { apiClient } from '@/config/api'
|
||||
@@ -403,7 +504,6 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['close', 'success'])
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const clientsStore = useClientsStore()
|
||||
const apiKeysStore = useApiKeysStore()
|
||||
const loading = ref(false)
|
||||
@@ -558,8 +658,9 @@ onMounted(async () => {
|
||||
form.restrictedModels = props.apiKey.restrictedModels || []
|
||||
form.allowedClients = props.apiKey.allowedClients || []
|
||||
form.tags = props.apiKey.tags || []
|
||||
form.enableModelRestriction = form.restrictedModels.length > 0
|
||||
form.enableClientRestriction = form.allowedClients.length > 0
|
||||
// 从后端数据中获取实际的启用状态,而不是根据数组长度推断
|
||||
form.enableModelRestriction = props.apiKey.enableModelRestriction || false
|
||||
form.enableClientRestriction = props.apiKey.enableClientRestriction || false
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,97 +2,103 @@
|
||||
<Teleport to="body">
|
||||
<div class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
|
||||
<div class="modal-content w-full max-w-lg p-8 mx-auto max-h-[90vh] overflow-y-auto custom-scrollbar">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-12 h-12 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center">
|
||||
<i class="fas fa-check text-white text-lg"></i>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-12 h-12 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center">
|
||||
<i class="fas fa-check text-white text-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-gray-900">
|
||||
API Key 创建成功
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600">
|
||||
请妥善保存您的 API Key
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-gray-900">API Key 创建成功</h3>
|
||||
<p class="text-sm text-gray-600">请妥善保存您的 API Key</p>
|
||||
<button
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
title="直接关闭(不推荐)"
|
||||
@click="handleDirectClose"
|
||||
>
|
||||
<i class="fas fa-times text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 警告提示 -->
|
||||
<div class="bg-amber-50 border-l-4 border-amber-400 p-4 mb-6">
|
||||
<div class="flex items-start">
|
||||
<div class="w-6 h-6 bg-amber-400 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<i class="fas fa-exclamation-triangle text-white text-sm" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h5 class="font-semibold text-amber-900 mb-1">
|
||||
重要提醒
|
||||
</h5>
|
||||
<p class="text-sm text-amber-800">
|
||||
这是您唯一能看到完整 API Key 的机会。关闭此窗口后,系统将不再显示完整的 API Key。请立即复制并妥善保存。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="handleDirectClose"
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
title="直接关闭(不推荐)"
|
||||
>
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 警告提示 -->
|
||||
<div class="bg-amber-50 border-l-4 border-amber-400 p-4 mb-6">
|
||||
<div class="flex items-start">
|
||||
<div class="w-6 h-6 bg-amber-400 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<i class="fas fa-exclamation-triangle text-white text-sm"></i>
|
||||
<!-- API Key 信息 -->
|
||||
<div class="space-y-4 mb-6">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">API Key 名称</label>
|
||||
<div class="p-3 bg-gray-50 rounded-lg border">
|
||||
<span class="text-gray-900 font-medium">{{ apiKey.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h5 class="font-semibold text-amber-900 mb-1">重要提醒</h5>
|
||||
<p class="text-sm text-amber-800">
|
||||
这是您唯一能看到完整 API Key 的机会。关闭此窗口后,系统将不再显示完整的 API Key。请立即复制并妥善保存。
|
||||
|
||||
<div v-if="apiKey.description">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">备注</label>
|
||||
<div class="p-3 bg-gray-50 rounded-lg border">
|
||||
<span class="text-gray-700">{{ apiKey.description || '无描述' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">API Key</label>
|
||||
<div class="relative">
|
||||
<div class="p-4 pr-14 bg-gray-900 rounded-lg border font-mono text-sm text-white break-all min-h-[60px] flex items-center">
|
||||
{{ getDisplayedApiKey() }}
|
||||
</div>
|
||||
<div class="absolute top-3 right-3">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-icon-sm hover:bg-gray-800 bg-gray-700"
|
||||
:title="showFullKey ? '隐藏API Key' : '显示完整API Key'"
|
||||
@click="toggleKeyVisibility"
|
||||
>
|
||||
<i :class="['fas', showFullKey ? 'fa-eye-slash' : 'fa-eye', 'text-gray-300']" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
点击眼睛图标切换显示模式,使用下方按钮复制完整 API Key
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Key 信息 -->
|
||||
<div class="space-y-4 mb-6">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">API Key 名称</label>
|
||||
<div class="p-3 bg-gray-50 rounded-lg border">
|
||||
<span class="text-gray-900 font-medium">{{ apiKey.name }}</span>
|
||||
</div>
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
class="flex-1 btn btn-primary py-3 px-6 font-semibold flex items-center justify-center gap-2"
|
||||
@click="copyApiKey"
|
||||
>
|
||||
<i class="fas fa-copy" />
|
||||
复制 API Key
|
||||
</button>
|
||||
<button
|
||||
class="px-6 py-3 bg-gray-200 text-gray-800 rounded-xl font-semibold hover:bg-gray-300 transition-colors border border-gray-300"
|
||||
@click="handleClose"
|
||||
>
|
||||
我已保存
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="apiKey.description">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">备注</label>
|
||||
<div class="p-3 bg-gray-50 rounded-lg border">
|
||||
<span class="text-gray-700">{{ apiKey.description || '无描述' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">API Key</label>
|
||||
<div class="relative">
|
||||
<div class="p-4 pr-14 bg-gray-900 rounded-lg border font-mono text-sm text-white break-all min-h-[60px] flex items-center">
|
||||
{{ getDisplayedApiKey() }}
|
||||
</div>
|
||||
<div class="absolute top-3 right-3">
|
||||
<button
|
||||
@click="toggleKeyVisibility"
|
||||
type="button"
|
||||
class="btn-icon-sm hover:bg-gray-800 bg-gray-700"
|
||||
:title="showFullKey ? '隐藏API Key' : '显示完整API Key'"
|
||||
>
|
||||
<i :class="['fas', showFullKey ? 'fa-eye-slash' : 'fa-eye', 'text-gray-300']"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
点击眼睛图标切换显示模式,使用下方按钮复制完整 API Key
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
@click="copyApiKey"
|
||||
class="flex-1 btn btn-primary py-3 px-6 font-semibold flex items-center justify-center gap-2"
|
||||
>
|
||||
<i class="fas fa-copy"></i>
|
||||
复制 API Key
|
||||
</button>
|
||||
<button
|
||||
@click="handleClose"
|
||||
class="px-6 py-3 bg-gray-200 text-gray-800 rounded-xl font-semibold hover:bg-gray-300 transition-colors border border-gray-300"
|
||||
>
|
||||
我已保存
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -2,88 +2,120 @@
|
||||
<Teleport to="body">
|
||||
<div class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
|
||||
<div class="modal-content w-full max-w-md p-8 mx-auto max-h-[90vh] flex flex-col">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center">
|
||||
<i class="fas fa-clock text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900">续期 API Key</h3>
|
||||
</div>
|
||||
<button
|
||||
@click="$emit('close')"
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6 modal-scroll-content custom-scrollbar flex-1">
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-info text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-800 mb-1">API Key 信息</h4>
|
||||
<p class="text-sm text-gray-700">{{ apiKey.name }}</p>
|
||||
<p class="text-xs text-gray-600 mt-1">
|
||||
当前过期时间:{{ apiKey.expiresAt ? formatExpireDate(apiKey.expiresAt) : '永不过期' }}
|
||||
</p>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center">
|
||||
<i class="fas fa-clock text-white" />
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900">
|
||||
续期 API Key
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">续期时长</label>
|
||||
<select
|
||||
v-model="form.renewDuration"
|
||||
@change="updateRenewExpireAt"
|
||||
class="form-input w-full"
|
||||
<button
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<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
|
||||
v-model="form.customExpireDate"
|
||||
type="datetime-local"
|
||||
class="form-input w-full"
|
||||
:min="minDateTime"
|
||||
@change="updateCustomRenewExpireAt"
|
||||
>
|
||||
</div>
|
||||
<p v-if="form.newExpiresAt" class="text-xs text-gray-500 mt-2">
|
||||
新的过期时间:{{ formatExpireDate(form.newExpiresAt) }}
|
||||
</p>
|
||||
<i class="fas fa-times text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
@click="$emit('close')"
|
||||
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="renewApiKey"
|
||||
:disabled="loading || !form.renewDuration"
|
||||
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
||||
>
|
||||
<div v-if="loading" class="loading-spinner mr-2"></div>
|
||||
<i v-else class="fas fa-clock mr-2"></i>
|
||||
{{ loading ? '续期中...' : '确认续期' }}
|
||||
</button>
|
||||
<div class="space-y-6 modal-scroll-content custom-scrollbar flex-1">
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-info text-white text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-800 mb-1">
|
||||
API Key 信息
|
||||
</h4>
|
||||
<p class="text-sm text-gray-700">
|
||||
{{ apiKey.name }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-600 mt-1">
|
||||
当前过期时间:{{ apiKey.expiresAt ? formatExpireDate(apiKey.expiresAt) : '永不过期' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">续期时长</label>
|
||||
<select
|
||||
v-model="form.renewDuration"
|
||||
class="form-input w-full"
|
||||
@change="updateRenewExpireAt"
|
||||
>
|
||||
<option value="7d">
|
||||
延长 7 天
|
||||
</option>
|
||||
<option value="30d">
|
||||
延长 30 天
|
||||
</option>
|
||||
<option value="90d">
|
||||
延长 90 天
|
||||
</option>
|
||||
<option value="180d">
|
||||
延长 180 天
|
||||
</option>
|
||||
<option value="365d">
|
||||
延长 365 天
|
||||
</option>
|
||||
<option value="custom">
|
||||
自定义日期
|
||||
</option>
|
||||
<option value="permanent">
|
||||
设为永不过期
|
||||
</option>
|
||||
</select>
|
||||
<div
|
||||
v-if="form.renewDuration === 'custom'"
|
||||
class="mt-3"
|
||||
>
|
||||
<input
|
||||
v-model="form.customExpireDate"
|
||||
type="datetime-local"
|
||||
class="form-input w-full"
|
||||
:min="minDateTime"
|
||||
@change="updateCustomRenewExpireAt"
|
||||
>
|
||||
</div>
|
||||
<p
|
||||
v-if="form.newExpiresAt"
|
||||
class="text-xs text-gray-500 mt-2"
|
||||
>
|
||||
新的过期时间:{{ formatExpireDate(form.newExpiresAt) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:disabled="loading || !form.renewDuration"
|
||||
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
||||
@click="renewApiKey"
|
||||
>
|
||||
<div
|
||||
v-if="loading"
|
||||
class="loading-spinner mr-2"
|
||||
/>
|
||||
<i
|
||||
v-else
|
||||
class="fas fa-clock mr-2"
|
||||
/>
|
||||
{{ loading ? '续期中...' : '确认续期' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user