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

@@ -320,7 +320,8 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
restrictedModels,
enableClientRestriction,
allowedClients,
dailyCostLimit
dailyCostLimit,
tags
} = req.body;
// 输入验证
@@ -371,6 +372,15 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
return res.status(400).json({ error: 'Allowed clients must be an array' });
}
// 验证标签字段
if (tags !== undefined && !Array.isArray(tags)) {
return res.status(400).json({ error: 'Tags must be an array' });
}
if (tags && tags.some(tag => typeof tag !== 'string' || tag.trim().length === 0)) {
return res.status(400).json({ error: 'All tags must be non-empty strings' });
}
const newKey = await apiKeyService.generateApiKey({
name,
description,
@@ -386,7 +396,8 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
restrictedModels,
enableClientRestriction,
allowedClients,
dailyCostLimit
dailyCostLimit,
tags
});
logger.success(`🔑 Admin created new API key: ${name}`);
@@ -401,7 +412,7 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
try {
const { keyId } = req.params;
const { tokenLimit, concurrencyLimit, rateLimitWindow, rateLimitRequests, claudeAccountId, geminiAccountId, permissions, enableModelRestriction, restrictedModels, enableClientRestriction, allowedClients, expiresAt, dailyCostLimit } = req.body;
const { tokenLimit, concurrencyLimit, rateLimitWindow, rateLimitRequests, claudeAccountId, geminiAccountId, permissions, enableModelRestriction, restrictedModels, enableClientRestriction, allowedClients, expiresAt, dailyCostLimit, tags } = req.body;
// 只允许更新指定字段
const updates = {};
@@ -506,6 +517,17 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
updates.dailyCostLimit = costLimit;
}
// 处理标签
if (tags !== undefined) {
if (!Array.isArray(tags)) {
return res.status(400).json({ error: 'Tags must be an array' });
}
if (tags.some(tag => typeof tag !== 'string' || tag.trim().length === 0)) {
return res.status(400).json({ error: 'All tags must be non-empty strings' });
}
updates.tags = tags;
}
await apiKeyService.updateApiKey(keyId, updates);
logger.success(`📝 Admin updated API key: ${keyId}`);