mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
feat: 增强 API Key 标签选择功能
- 添加获取已存在标签的 API 端点 /admin/api-keys/tags - 重构标签输入 UI,从下拉菜单改为平铺展示 - 支持点击选择已有标签,避免重复创建 - 增加弹框宽度 (max-w-4xl),改善布局和用户体验 - 统一创建和编辑 API Key 的标签管理体验 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -303,6 +303,34 @@ router.get('/supported-clients', authenticateAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 获取已存在的标签列表
|
||||||
|
router.get('/api-keys/tags', authenticateAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const apiKeys = await apiKeyService.getAllApiKeys();
|
||||||
|
const tagSet = new Set();
|
||||||
|
|
||||||
|
// 收集所有API Keys的标签
|
||||||
|
for (const apiKey of apiKeys) {
|
||||||
|
if (apiKey.tags && Array.isArray(apiKey.tags)) {
|
||||||
|
apiKey.tags.forEach(tag => {
|
||||||
|
if (tag && tag.trim()) {
|
||||||
|
tagSet.add(tag.trim());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为数组并排序
|
||||||
|
const tags = Array.from(tagSet).sort();
|
||||||
|
|
||||||
|
logger.info(`📋 Retrieved ${tags.length} unique tags from API keys`);
|
||||||
|
res.json({ success: true, data: tags });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('❌ Failed to get API key tags:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to get API key tags', message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 创建新的API Key
|
// 创建新的API Key
|
||||||
router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<div class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
|
<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-6 mx-auto max-h-[90vh] flex flex-col">
|
<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 justify-between mb-4">
|
||||||
<div class="flex items-center gap-3">
|
<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">
|
<div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center">
|
||||||
@@ -35,33 +35,57 @@
|
|||||||
<!-- 标签 -->
|
<!-- 标签 -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-2">标签</label>
|
<label class="block text-sm font-semibold text-gray-700 mb-2">标签</label>
|
||||||
<div class="space-y-2">
|
<div class="space-y-4">
|
||||||
<!-- 已添加的标签 -->
|
<!-- 已选择的标签 -->
|
||||||
<div v-if="form.tags.length > 0" class="flex flex-wrap gap-2">
|
<div v-if="form.tags.length > 0">
|
||||||
<span v-for="(tag, index) in form.tags" :key="index"
|
<div class="text-xs font-medium text-gray-600 mb-2">已选择的标签:</div>
|
||||||
class="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full">
|
<div class="flex flex-wrap gap-2">
|
||||||
{{ tag }}
|
<span v-for="(tag, index) in form.tags" :key="'selected-' + index"
|
||||||
<button type="button" @click="removeTag(index)"
|
class="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full">
|
||||||
class="ml-1 hover:text-blue-900">
|
{{ tag }}
|
||||||
<i class="fas fa-times text-xs"></i>
|
<button type="button" @click="removeTag(index)"
|
||||||
</button>
|
class="ml-1 hover:text-blue-900">
|
||||||
</span>
|
<i class="fas fa-times text-xs"></i>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 标签输入 -->
|
<!-- 可选择的已有标签 -->
|
||||||
<div class="flex gap-2">
|
<div v-if="unselectedTags.length > 0">
|
||||||
<input
|
<div class="text-xs font-medium text-gray-600 mb-2">点击选择已有标签:</div>
|
||||||
v-model="newTag"
|
<div class="flex flex-wrap gap-2">
|
||||||
type="text"
|
<button
|
||||||
class="form-input flex-1"
|
v-for="tag in unselectedTags"
|
||||||
placeholder="输入新标签名称"
|
:key="'available-' + tag"
|
||||||
@keypress.enter.prevent="addTag"
|
type="button"
|
||||||
>
|
@click="selectTag(tag)"
|
||||||
<button type="button" @click="addTag"
|
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"
|
||||||
class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
|
>
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-tag text-gray-500 text-xs"></i>
|
||||||
</button>
|
{{ tag }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</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>
|
<p class="text-xs text-gray-500">用于标记不同团队或用途,方便筛选管理</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,7 +110,7 @@
|
|||||||
placeholder="无限制"
|
placeholder="无限制"
|
||||||
class="form-input w-full text-sm"
|
class="form-input w-full text-sm"
|
||||||
>
|
>
|
||||||
<p class="text-xs text-gray-500 mt-0.5">时间段单位</p>
|
<p class="text-xs text-gray-500 mt-0.5 ml-2">时间段单位</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -98,7 +122,7 @@
|
|||||||
placeholder="无限制"
|
placeholder="无限制"
|
||||||
class="form-input w-full text-sm"
|
class="form-input w-full text-sm"
|
||||||
>
|
>
|
||||||
<p class="text-xs text-gray-500 mt-0.5">窗口内最大请求</p>
|
<p class="text-xs text-gray-500 mt-0.5 ml-2">窗口内最大请求</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -109,7 +133,7 @@
|
|||||||
placeholder="无限制"
|
placeholder="无限制"
|
||||||
class="form-input w-full text-sm"
|
class="form-input w-full text-sm"
|
||||||
>
|
>
|
||||||
<p class="text-xs text-gray-500 mt-0.5">窗口内最大Token</p>
|
<p class="text-xs text-gray-500 mt-0.5 ml-2">窗口内最大Token</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -394,6 +418,7 @@ import { ref, reactive, computed, onMounted } from 'vue'
|
|||||||
import { showToast } from '@/utils/toast'
|
import { showToast } from '@/utils/toast'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useClientsStore } from '@/stores/clients'
|
import { useClientsStore } from '@/stores/clients'
|
||||||
|
import { useApiKeysStore } from '@/stores/apiKeys'
|
||||||
import { apiClient } from '@/config/api'
|
import { apiClient } from '@/config/api'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -407,6 +432,7 @@ const emit = defineEmits(['close', 'success'])
|
|||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const clientsStore = useClientsStore()
|
const clientsStore = useClientsStore()
|
||||||
|
const apiKeysStore = useApiKeysStore()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
// 表单验证状态
|
// 表单验证状态
|
||||||
@@ -416,6 +442,12 @@ const errors = ref({
|
|||||||
|
|
||||||
// 标签相关
|
// 标签相关
|
||||||
const newTag = ref('')
|
const newTag = ref('')
|
||||||
|
const availableTags = ref([])
|
||||||
|
|
||||||
|
// 计算未选择的标签
|
||||||
|
const unselectedTags = computed(() => {
|
||||||
|
return availableTags.value.filter(tag => !form.tags.includes(tag))
|
||||||
|
})
|
||||||
|
|
||||||
// 支持的客户端列表
|
// 支持的客户端列表
|
||||||
const supportedClients = ref([])
|
const supportedClients = ref([])
|
||||||
@@ -443,9 +475,10 @@ const form = reactive({
|
|||||||
tags: []
|
tags: []
|
||||||
})
|
})
|
||||||
|
|
||||||
// 加载支持的客户端
|
// 加载支持的客户端和已存在的标签
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
supportedClients.value = await clientsStore.loadSupportedClients()
|
supportedClients.value = await clientsStore.loadSupportedClients()
|
||||||
|
availableTags.value = await apiKeysStore.fetchTags()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 计算最小日期时间
|
// 计算最小日期时间
|
||||||
@@ -536,6 +569,12 @@ const addTag = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectTag = (tag) => {
|
||||||
|
if (!form.tags.includes(tag)) {
|
||||||
|
form.tags.push(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const removeTag = (index) => {
|
const removeTag = (index) => {
|
||||||
form.tags.splice(index, 1)
|
form.tags.splice(index, 1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<div class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
|
<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="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 justify-between mb-6">
|
||||||
<div class="flex items-center gap-3">
|
<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">
|
<div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center">
|
||||||
@@ -32,95 +32,116 @@
|
|||||||
<!-- 标签 -->
|
<!-- 标签 -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-3">标签</label>
|
<label class="block text-sm font-semibold text-gray-700 mb-3">标签</label>
|
||||||
<div class="space-y-3">
|
<div class="space-y-4">
|
||||||
<!-- 已添加的标签 -->
|
<!-- 已选择的标签 -->
|
||||||
<div v-if="form.tags.length > 0" class="flex flex-wrap gap-2">
|
<div v-if="form.tags.length > 0">
|
||||||
<span v-for="(tag, index) in form.tags" :key="index"
|
<div class="text-xs font-medium text-gray-600 mb-2">已选择的标签:</div>
|
||||||
class="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full">
|
<div class="flex flex-wrap gap-2">
|
||||||
{{ tag }}
|
<span v-for="(tag, index) in form.tags" :key="'selected-' + index"
|
||||||
<button type="button" @click="removeTag(index)"
|
class="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full">
|
||||||
class="ml-1 hover:text-blue-900">
|
{{ tag }}
|
||||||
<i class="fas fa-times text-xs"></i>
|
<button type="button" @click="removeTag(index)"
|
||||||
</button>
|
class="ml-1 hover:text-blue-900">
|
||||||
</span>
|
<i class="fas fa-times text-xs"></i>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 标签输入 -->
|
<!-- 可选择的已有标签 -->
|
||||||
<div class="flex gap-2">
|
<div v-if="unselectedTags.length > 0">
|
||||||
<input
|
<div class="text-xs font-medium text-gray-600 mb-2">点击选择已有标签:</div>
|
||||||
v-model="newTag"
|
<div class="flex flex-wrap gap-2">
|
||||||
type="text"
|
<button
|
||||||
class="form-input flex-1"
|
v-for="tag in unselectedTags"
|
||||||
placeholder="输入新标签名称"
|
:key="'available-' + tag"
|
||||||
@keypress.enter.prevent="addTag"
|
type="button"
|
||||||
>
|
@click="selectTag(tag)"
|
||||||
<button type="button" @click="addTag"
|
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"
|
||||||
class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
|
>
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-tag text-gray-500 text-xs"></i>
|
||||||
</button>
|
{{ tag }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</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>
|
<p class="text-xs text-gray-500">用于标记不同团队或用途,方便筛选管理</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 速率限制设置 -->
|
<!-- 速率限制设置 -->
|
||||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 space-y-4">
|
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||||
<div class="flex items-start gap-3 mb-3">
|
<div class="flex items-center gap-2 mb-2">
|
||||||
<div class="w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
<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-sm"></i>
|
<i class="fas fa-tachometer-alt text-white text-xs"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<h4 class="font-semibold text-gray-800 text-sm">速率限制设置 (可选)</h4>
|
||||||
<h4 class="font-semibold text-gray-800 mb-1">速率限制设置</h4>
|
</div>
|
||||||
<p class="text-sm text-gray-600">控制 API Key 的使用频率和资源消耗</p>
|
|
||||||
|
<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>
|
||||||
</div>
|
|
||||||
|
<!-- 示例说明 -->
|
||||||
<div>
|
<div class="bg-blue-100 rounded-lg p-2">
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-3">时间窗口 (分钟)</label>
|
<h5 class="text-xs font-semibold text-blue-800 mb-1">💡 使用示例</h5>
|
||||||
<input
|
<div class="text-xs text-blue-700 space-y-0.5">
|
||||||
v-model="form.rateLimitWindow"
|
<div><strong>示例1:</strong> 时间窗口=60,请求次数=1000 → 每60分钟最多1000次请求</div>
|
||||||
type="number"
|
<div><strong>示例2:</strong> 时间窗口=1,Token=10000 → 每分钟最多10,000个Token</div>
|
||||||
min="1"
|
<div><strong>示例3:</strong> 窗口=30,请求=50,Token=100000 → 每30分钟50次请求且不超10万Token</div>
|
||||||
placeholder="留空表示无限制"
|
</div>
|
||||||
class="form-input w-full"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
<input
|
|
||||||
v-model="form.rateLimitRequests"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
placeholder="留空表示无限制"
|
|
||||||
class="form-input w-full"
|
|
||||||
>
|
|
||||||
<p class="text-xs text-gray-500 mt-2">在时间窗口内允许的最大请求次数(需要先设置时间窗口)</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-3">时间窗口内的 Token 使用量限制</label>
|
|
||||||
<input
|
|
||||||
v-model="form.tokenLimit"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
placeholder="0 表示无限制"
|
|
||||||
class="form-input w-full"
|
|
||||||
>
|
|
||||||
<p class="text-xs text-gray-500 mt-2">在时间窗口内允许消耗的最大 Token 数量(需要先设置时间窗口),0 或留空表示无限制</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 示例说明 -->
|
|
||||||
<div class="bg-blue-100 rounded-lg p-3 mt-3">
|
|
||||||
<h5 class="text-sm font-semibold text-blue-800 mb-2">💡 使用示例</h5>
|
|
||||||
<div class="text-xs text-blue-700 space-y-1">
|
|
||||||
<p><strong>示例1:</strong> 时间窗口=60,请求次数限制=100</p>
|
|
||||||
<p class="ml-4">→ 每60分钟内最多允许100次请求</p>
|
|
||||||
<p class="mt-2"><strong>示例2:</strong> 时间窗口=10,Token限制=50000</p>
|
|
||||||
<p class="ml-4">→ 每10分钟内最多消耗50,000个Token</p>
|
|
||||||
<p class="mt-2"><strong>示例3:</strong> 时间窗口=30,请求次数限制=50,Token限制=100000</p>
|
|
||||||
<p class="ml-4">→ 每30分钟内最多50次请求且总Token不超过100,000</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -351,10 +372,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { showToast } from '@/utils/toast'
|
import { showToast } from '@/utils/toast'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useClientsStore } from '@/stores/clients'
|
import { useClientsStore } from '@/stores/clients'
|
||||||
|
import { useApiKeysStore } from '@/stores/apiKeys'
|
||||||
import { apiClient } from '@/config/api'
|
import { apiClient } from '@/config/api'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -372,6 +394,7 @@ const emit = defineEmits(['close', 'success'])
|
|||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const clientsStore = useClientsStore()
|
const clientsStore = useClientsStore()
|
||||||
|
const apiKeysStore = useApiKeysStore()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
// 支持的客户端列表
|
// 支持的客户端列表
|
||||||
@@ -379,6 +402,12 @@ const supportedClients = ref([])
|
|||||||
|
|
||||||
// 标签相关
|
// 标签相关
|
||||||
const newTag = ref('')
|
const newTag = ref('')
|
||||||
|
const availableTags = ref([])
|
||||||
|
|
||||||
|
// 计算未选择的标签
|
||||||
|
const unselectedTags = computed(() => {
|
||||||
|
return availableTags.value.filter(tag => !form.tags.includes(tag))
|
||||||
|
})
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
@@ -424,6 +453,12 @@ const addTag = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectTag = (tag) => {
|
||||||
|
if (!form.tags.includes(tag)) {
|
||||||
|
form.tags.push(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const removeTag = (index) => {
|
const removeTag = (index) => {
|
||||||
form.tags.splice(index, 1)
|
form.tags.splice(index, 1)
|
||||||
}
|
}
|
||||||
@@ -471,8 +506,9 @@ const updateApiKey = async () => {
|
|||||||
|
|
||||||
// 初始化表单数据
|
// 初始化表单数据
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 加载支持的客户端
|
// 加载支持的客户端和已存在的标签
|
||||||
supportedClients.value = await clientsStore.loadSupportedClients()
|
supportedClients.value = await clientsStore.loadSupportedClients()
|
||||||
|
availableTags.value = await apiKeysStore.fetchTags()
|
||||||
|
|
||||||
form.name = props.apiKey.name
|
form.name = props.apiKey.name
|
||||||
form.tokenLimit = props.apiKey.tokenLimit || ''
|
form.tokenLimit = props.apiKey.tokenLimit || ''
|
||||||
|
|||||||
@@ -159,6 +159,21 @@ export const useApiKeysStore = defineStore('apiKeys', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取已存在的标签
|
||||||
|
const fetchTags = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get('/admin/api-keys/tags')
|
||||||
|
if (response.success) {
|
||||||
|
return response.data || []
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message || '获取标签失败')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取标签失败:', err)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 重置store
|
// 重置store
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
apiKeys.value = []
|
apiKeys.value = []
|
||||||
@@ -186,6 +201,7 @@ export const useApiKeysStore = defineStore('apiKeys', () => {
|
|||||||
renewApiKey,
|
renewApiKey,
|
||||||
deleteApiKey,
|
deleteApiKey,
|
||||||
fetchApiKeyStats,
|
fetchApiKeyStats,
|
||||||
|
fetchTags,
|
||||||
sortApiKeys,
|
sortApiKeys,
|
||||||
reset
|
reset
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user