feat(admin-spa): 添加 API Key 标签管理功能

基于 PR #114 的功能需求,为新版 admin-spa 实现完整的标签系统:

后端改进:
- apiKeyService 支持标签的创建、查询和更新
- admin 路由添加标签验证和处理逻辑
- 标签以 JSON 数组形式存储在 Redis 中

前端功能:
- API Key 列表增加标签列,显示彩色标签徽章
- 添加标签筛选器,支持按标签过滤 API Keys
- 创建和编辑 API Key 时可添加/删除标签
- 标签输入支持 Enter 键快速添加
- 自动收集并排序所有可用标签

界面优化:
- 使用蓝色圆角标签样式,视觉清晰
- 无标签时显示"无标签"提示
- 标签管理操作流畅,支持即时添加删除

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
shaw
2025-07-29 16:13:46 +08:00
parent f23b9bd222
commit 03a5300b78
5 changed files with 202 additions and 12 deletions

View File

@@ -27,7 +27,8 @@ class ApiKeyService {
restrictedModels = [],
enableClientRestriction = false,
allowedClients = [],
dailyCostLimit = 0
dailyCostLimit = 0,
tags = []
} = options;
// 生成简单的API Key (64字符十六进制)
@@ -53,6 +54,7 @@ class ApiKeyService {
enableClientRestriction: String(enableClientRestriction || false),
allowedClients: JSON.stringify(allowedClients || []),
dailyCostLimit: String(dailyCostLimit || 0),
tags: JSON.stringify(tags || []),
createdAt: new Date().toISOString(),
lastUsedAt: '',
expiresAt: expiresAt || '',
@@ -82,6 +84,7 @@ class ApiKeyService {
enableClientRestriction: keyData.enableClientRestriction === 'true',
allowedClients: JSON.parse(keyData.allowedClients || '[]'),
dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0),
tags: JSON.parse(keyData.tags || '[]'),
createdAt: keyData.createdAt,
expiresAt: keyData.expiresAt,
createdBy: keyData.createdBy
@@ -142,6 +145,14 @@ class ApiKeyService {
allowedClients = [];
}
// 解析标签
let tags = [];
try {
tags = keyData.tags ? JSON.parse(keyData.tags) : [];
} catch (e) {
tags = [];
}
return {
valid: true,
keyData: {
@@ -163,6 +174,7 @@ class ApiKeyService {
allowedClients: allowedClients,
dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0),
dailyCost: dailyCost || 0,
tags: tags,
usage
}
};
@@ -201,6 +213,11 @@ class ApiKeyService {
} catch (e) {
key.allowedClients = [];
}
try {
key.tags = key.tags ? JSON.parse(key.tags) : [];
} catch (e) {
key.tags = [];
}
delete key.apiKey; // 不返回哈希后的key
}
@@ -220,12 +237,12 @@ class ApiKeyService {
}
// 允许更新的字段
const allowedUpdates = ['name', 'description', 'tokenLimit', 'concurrencyLimit', 'rateLimitWindow', 'rateLimitRequests', 'isActive', 'claudeAccountId', 'geminiAccountId', 'permissions', 'expiresAt', 'enableModelRestriction', 'restrictedModels', 'enableClientRestriction', 'allowedClients', 'dailyCostLimit'];
const allowedUpdates = ['name', 'description', 'tokenLimit', 'concurrencyLimit', 'rateLimitWindow', 'rateLimitRequests', 'isActive', 'claudeAccountId', 'geminiAccountId', 'permissions', 'expiresAt', 'enableModelRestriction', 'restrictedModels', 'enableClientRestriction', 'allowedClients', 'dailyCostLimit', 'tags'];
const updatedData = { ...keyData };
for (const [field, value] of Object.entries(updates)) {
if (allowedUpdates.includes(field)) {
if (field === 'restrictedModels' || field === 'allowedClients') {
if (field === 'restrictedModels' || field === 'allowedClients' || field === 'tags') {
// 特殊处理数组字段
updatedData[field] = JSON.stringify(value || []);
} else if (field === 'enableModelRestriction' || field === 'enableClientRestriction') {