diff --git a/src/routes/admin.js b/src/routes/admin.js index fe1ffd8b..49000f8f 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -534,7 +534,8 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => { weeklyOpusCostLimit, tags, activationDays, // 新增:激活后有效天数 - expirationMode // 新增:过期模式 + expirationMode, // 新增:过期模式 + icon // 新增:图标(base64编码) } = req.body // 输入验证 @@ -660,7 +661,8 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => { weeklyOpusCostLimit, tags, activationDays, - expirationMode + expirationMode, + icon }) logger.success(`🔑 Admin created new API key: ${name}`) @@ -696,7 +698,8 @@ router.post('/api-keys/batch', authenticateAdmin, async (req, res) => { weeklyOpusCostLimit, tags, activationDays, - expirationMode + expirationMode, + icon } = req.body // 输入验证 @@ -742,7 +745,8 @@ router.post('/api-keys/batch', authenticateAdmin, async (req, res) => { weeklyOpusCostLimit, tags, activationDays, - expirationMode + expirationMode, + icon }) // 保留原始 API Key 供返回 @@ -985,7 +989,8 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => { dailyCostLimit, weeklyOpusCostLimit, tags, - ownerId // 新增:所有者ID字段 + ownerId, // 新增:所有者ID字段 + icon // 新增:图标(base64编码) } = req.body // 只允许更新指定字段 @@ -1159,6 +1164,19 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => { updates.tags = tags } + // 处理图标 + if (icon !== undefined) { + // icon 可以是空字符串(清除图标)或 base64 编码的字符串 + if (icon !== '' && typeof icon !== 'string') { + return res.status(400).json({ error: 'Icon must be a string' }) + } + // 简单验证 base64 格式(如果不为空) + if (icon && !icon.startsWith('data:image/')) { + return res.status(400).json({ error: 'Icon must be a valid base64 image' }) + } + updates.icon = icon + } + // 处理活跃/禁用状态状态, 放在过期处理后,以确保后续增加禁用key功能 if (isActive !== undefined) { if (typeof isActive !== 'boolean') { diff --git a/src/services/apiKeyService.js b/src/services/apiKeyService.js index 8ee94337..4291d51d 100644 --- a/src/services/apiKeyService.js +++ b/src/services/apiKeyService.js @@ -36,7 +36,8 @@ class ApiKeyService { weeklyOpusCostLimit = 0, tags = [], activationDays = 0, // 新增:激活后有效天数(0表示不使用此功能) - expirationMode = 'fixed' // 新增:过期模式 'fixed'(固定时间) 或 'activation'(首次使用后激活) + expirationMode = 'fixed', // 新增:过期模式 'fixed'(固定时间) 或 'activation'(首次使用后激活) + icon = '' // 新增:图标(base64编码) } = options // 生成简单的API Key (64字符十六进制) @@ -78,7 +79,8 @@ class ApiKeyService { expiresAt: expirationMode === 'fixed' ? expiresAt || '' : '', // 固定模式才设置过期时间 createdBy: options.createdBy || 'admin', userId: options.userId || '', - userUsername: options.userUsername || '' + userUsername: options.userUsername || '', + icon: icon || '' // 新增:图标(base64编码) } // 保存API Key数据并建立哈希映射 @@ -410,7 +412,8 @@ class ApiKeyService { 'tags', 'userId', // 新增:用户ID(所有者变更) 'userUsername', // 新增:用户名(所有者变更) - 'createdBy' // 新增:创建者(所有者变更) + 'createdBy', // 新增:创建者(所有者变更) + 'icon' // 新增:图标(base64编码) ] const updatedData = { ...keyData } diff --git a/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue b/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue index 7c19cdfc..340ac507 100644 --- a/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue +++ b/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue @@ -110,19 +110,23 @@ class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-2 sm:text-sm" >名称 * - +
{{ errors.name }}
@@ -807,6 +811,7 @@ import { useClientsStore } from '@/stores/clients' import { useApiKeysStore } from '@/stores/apiKeys' import { apiClient } from '@/config/api' import AccountSelector from '@/components/common/AccountSelector.vue' +import IconPicker from '@/components/common/IconPicker.vue' const props = defineProps({ accounts: { @@ -853,6 +858,7 @@ const form = reactive({ createType: 'single', batchCount: 10, name: '', + icon: '', description: '', rateLimitWindow: '', rateLimitRequests: '', @@ -1198,7 +1204,8 @@ const createApiKey = async () => { // 单个创建 const data = { ...baseData, - name: form.name + name: form.name, + icon: form.icon || '' } const result = await apiClient.post('/admin/api-keys', data) @@ -1216,7 +1223,8 @@ const createApiKey = async () => { ...baseData, createType: 'batch', baseName: form.name, - count: form.batchCount + count: form.batchCount, + icon: form.icon || '' } const result = await apiClient.post('/admin/api-keys/batch', data) diff --git a/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue b/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue index 07d9eaa5..1bada0a5 100644 --- a/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue +++ b/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue @@ -32,14 +32,18 @@ class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-3 sm:text-sm" >名称 - +用于识别此 API Key 的用途
@@ -658,6 +662,7 @@ import { useClientsStore } from '@/stores/clients' import { useApiKeysStore } from '@/stores/apiKeys' import { apiClient } from '@/config/api' import AccountSelector from '@/components/common/AccountSelector.vue' +import IconPicker from '@/components/common/IconPicker.vue' const props = defineProps({ apiKey: { @@ -705,6 +710,7 @@ const unselectedTags = computed(() => { // 表单数据 const form = reactive({ name: '', + icon: '', tokenLimit: '', // 保留用于检测历史数据 rateLimitWindow: '', rateLimitRequests: '', @@ -803,6 +809,7 @@ const updateApiKey = async () => { // 准备提交的数据 const data = { name: form.name, // 添加名称字段 + icon: form.icon || '', // 添加图标字段 tokenLimit: 0, // 清除历史token限制 rateLimitWindow: form.rateLimitWindow !== '' && form.rateLimitWindow !== null @@ -1035,6 +1042,7 @@ onMounted(async () => { } form.name = props.apiKey.name + form.icon = props.apiKey.icon || '' // 处理速率限制迁移:如果有tokenLimit且没有rateLimitCost,提示用户 form.tokenLimit = props.apiKey.tokenLimit || '' diff --git a/web/admin-spa/src/components/common/IconPicker.vue b/web/admin-spa/src/components/common/IconPicker.vue new file mode 100644 index 00000000..253f2fe2 --- /dev/null +++ b/web/admin-spa/src/components/common/IconPicker.vue @@ -0,0 +1,1024 @@ + +