mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 19:18:59 +00:00
feat: API Keys图标系统和UI优化
主要功能增强: - 实现API Key自定义图标功能,支持图片上传、裁剪和智能压缩 - 新增IconPicker组件,提供内置图标选择和图片上传功能 - 支持固定尺寸裁剪区域,可拖拽定位选择头像区域 - 智能图片压缩:PNG保留透明度,JPEG用于不透明图片 UI/UX改进: - 优化表格布局:移除账号列,在名称下方显示账号绑定信息 - 调整行高和字体大小,提升信息密度 - 最后使用时间改为相对时间显示,悬浮显示具体时间 - 过期时间编辑改为点击文本触发,带悬浮下划线效果 - 更新默认API Key图标为蓝色渐变设计 - 修复表格悬浮偏移和横向滚动条问题 - 将"TOKEN 数量"改为"Token数" 后端支持: - apiKeyService增加icon字段持久化 - admin路由增加图标数据处理和验证 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -534,7 +534,8 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
weeklyOpusCostLimit,
|
weeklyOpusCostLimit,
|
||||||
tags,
|
tags,
|
||||||
activationDays, // 新增:激活后有效天数
|
activationDays, // 新增:激活后有效天数
|
||||||
expirationMode // 新增:过期模式
|
expirationMode, // 新增:过期模式
|
||||||
|
icon // 新增:图标(base64编码)
|
||||||
} = req.body
|
} = req.body
|
||||||
|
|
||||||
// 输入验证
|
// 输入验证
|
||||||
@@ -660,7 +661,8 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
weeklyOpusCostLimit,
|
weeklyOpusCostLimit,
|
||||||
tags,
|
tags,
|
||||||
activationDays,
|
activationDays,
|
||||||
expirationMode
|
expirationMode,
|
||||||
|
icon
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.success(`🔑 Admin created new API key: ${name}`)
|
logger.success(`🔑 Admin created new API key: ${name}`)
|
||||||
@@ -696,7 +698,8 @@ router.post('/api-keys/batch', authenticateAdmin, async (req, res) => {
|
|||||||
weeklyOpusCostLimit,
|
weeklyOpusCostLimit,
|
||||||
tags,
|
tags,
|
||||||
activationDays,
|
activationDays,
|
||||||
expirationMode
|
expirationMode,
|
||||||
|
icon
|
||||||
} = req.body
|
} = req.body
|
||||||
|
|
||||||
// 输入验证
|
// 输入验证
|
||||||
@@ -742,7 +745,8 @@ router.post('/api-keys/batch', authenticateAdmin, async (req, res) => {
|
|||||||
weeklyOpusCostLimit,
|
weeklyOpusCostLimit,
|
||||||
tags,
|
tags,
|
||||||
activationDays,
|
activationDays,
|
||||||
expirationMode
|
expirationMode,
|
||||||
|
icon
|
||||||
})
|
})
|
||||||
|
|
||||||
// 保留原始 API Key 供返回
|
// 保留原始 API Key 供返回
|
||||||
@@ -985,7 +989,8 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
|||||||
dailyCostLimit,
|
dailyCostLimit,
|
||||||
weeklyOpusCostLimit,
|
weeklyOpusCostLimit,
|
||||||
tags,
|
tags,
|
||||||
ownerId // 新增:所有者ID字段
|
ownerId, // 新增:所有者ID字段
|
||||||
|
icon // 新增:图标(base64编码)
|
||||||
} = req.body
|
} = req.body
|
||||||
|
|
||||||
// 只允许更新指定字段
|
// 只允许更新指定字段
|
||||||
@@ -1159,6 +1164,19 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
|||||||
updates.tags = tags
|
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功能
|
// 处理活跃/禁用状态状态, 放在过期处理后,以确保后续增加禁用key功能
|
||||||
if (isActive !== undefined) {
|
if (isActive !== undefined) {
|
||||||
if (typeof isActive !== 'boolean') {
|
if (typeof isActive !== 'boolean') {
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ class ApiKeyService {
|
|||||||
weeklyOpusCostLimit = 0,
|
weeklyOpusCostLimit = 0,
|
||||||
tags = [],
|
tags = [],
|
||||||
activationDays = 0, // 新增:激活后有效天数(0表示不使用此功能)
|
activationDays = 0, // 新增:激活后有效天数(0表示不使用此功能)
|
||||||
expirationMode = 'fixed' // 新增:过期模式 'fixed'(固定时间) 或 'activation'(首次使用后激活)
|
expirationMode = 'fixed', // 新增:过期模式 'fixed'(固定时间) 或 'activation'(首次使用后激活)
|
||||||
|
icon = '' // 新增:图标(base64编码)
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
// 生成简单的API Key (64字符十六进制)
|
// 生成简单的API Key (64字符十六进制)
|
||||||
@@ -78,7 +79,8 @@ class ApiKeyService {
|
|||||||
expiresAt: expirationMode === 'fixed' ? expiresAt || '' : '', // 固定模式才设置过期时间
|
expiresAt: expirationMode === 'fixed' ? expiresAt || '' : '', // 固定模式才设置过期时间
|
||||||
createdBy: options.createdBy || 'admin',
|
createdBy: options.createdBy || 'admin',
|
||||||
userId: options.userId || '',
|
userId: options.userId || '',
|
||||||
userUsername: options.userUsername || ''
|
userUsername: options.userUsername || '',
|
||||||
|
icon: icon || '' // 新增:图标(base64编码)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存API Key数据并建立哈希映射
|
// 保存API Key数据并建立哈希映射
|
||||||
@@ -410,7 +412,8 @@ class ApiKeyService {
|
|||||||
'tags',
|
'tags',
|
||||||
'userId', // 新增:用户ID(所有者变更)
|
'userId', // 新增:用户ID(所有者变更)
|
||||||
'userUsername', // 新增:用户名(所有者变更)
|
'userUsername', // 新增:用户名(所有者变更)
|
||||||
'createdBy' // 新增:创建者(所有者变更)
|
'createdBy', // 新增:创建者(所有者变更)
|
||||||
|
'icon' // 新增:图标(base64编码)
|
||||||
]
|
]
|
||||||
const updatedData = { ...keyData }
|
const updatedData = { ...keyData }
|
||||||
|
|
||||||
|
|||||||
@@ -110,9 +110,12 @@
|
|||||||
class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-2 sm:text-sm"
|
class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-2 sm:text-sm"
|
||||||
>名称 <span class="text-red-500">*</span></label
|
>名称 <span class="text-red-500">*</span></label
|
||||||
>
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<!-- 图标选择器 -->
|
||||||
|
<IconPicker v-model="form.icon" size="medium" />
|
||||||
<input
|
<input
|
||||||
v-model="form.name"
|
v-model="form.name"
|
||||||
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
class="form-input flex-1 border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||||
:class="{ 'border-red-500': errors.name }"
|
:class="{ 'border-red-500': errors.name }"
|
||||||
:placeholder="
|
:placeholder="
|
||||||
form.createType === 'batch'
|
form.createType === 'batch'
|
||||||
@@ -123,6 +126,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
@input="errors.name = ''"
|
@input="errors.name = ''"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<p v-if="errors.name" class="mt-1 text-xs text-red-500 dark:text-red-400">
|
<p v-if="errors.name" class="mt-1 text-xs text-red-500 dark:text-red-400">
|
||||||
{{ errors.name }}
|
{{ errors.name }}
|
||||||
</p>
|
</p>
|
||||||
@@ -807,6 +811,7 @@ import { useClientsStore } from '@/stores/clients'
|
|||||||
import { useApiKeysStore } from '@/stores/apiKeys'
|
import { useApiKeysStore } from '@/stores/apiKeys'
|
||||||
import { apiClient } from '@/config/api'
|
import { apiClient } from '@/config/api'
|
||||||
import AccountSelector from '@/components/common/AccountSelector.vue'
|
import AccountSelector from '@/components/common/AccountSelector.vue'
|
||||||
|
import IconPicker from '@/components/common/IconPicker.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
accounts: {
|
accounts: {
|
||||||
@@ -853,6 +858,7 @@ const form = reactive({
|
|||||||
createType: 'single',
|
createType: 'single',
|
||||||
batchCount: 10,
|
batchCount: 10,
|
||||||
name: '',
|
name: '',
|
||||||
|
icon: '',
|
||||||
description: '',
|
description: '',
|
||||||
rateLimitWindow: '',
|
rateLimitWindow: '',
|
||||||
rateLimitRequests: '',
|
rateLimitRequests: '',
|
||||||
@@ -1198,7 +1204,8 @@ const createApiKey = async () => {
|
|||||||
// 单个创建
|
// 单个创建
|
||||||
const data = {
|
const data = {
|
||||||
...baseData,
|
...baseData,
|
||||||
name: form.name
|
name: form.name,
|
||||||
|
icon: form.icon || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await apiClient.post('/admin/api-keys', data)
|
const result = await apiClient.post('/admin/api-keys', data)
|
||||||
@@ -1216,7 +1223,8 @@ const createApiKey = async () => {
|
|||||||
...baseData,
|
...baseData,
|
||||||
createType: 'batch',
|
createType: 'batch',
|
||||||
baseName: form.name,
|
baseName: form.name,
|
||||||
count: form.batchCount
|
count: form.batchCount,
|
||||||
|
icon: form.icon || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await apiClient.post('/admin/api-keys/batch', data)
|
const result = await apiClient.post('/admin/api-keys/batch', data)
|
||||||
|
|||||||
@@ -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"
|
class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-3 sm:text-sm"
|
||||||
>名称</label
|
>名称</label
|
||||||
>
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<!-- 图标选择器 -->
|
||||||
|
<IconPicker v-model="form.icon" size="medium" />
|
||||||
<input
|
<input
|
||||||
v-model="form.name"
|
v-model="form.name"
|
||||||
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
class="form-input flex-1 border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||||
maxlength="100"
|
maxlength="100"
|
||||||
placeholder="请输入API Key名称"
|
placeholder="请输入API Key名称"
|
||||||
required
|
required
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 sm:mt-2">
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 sm:mt-2">
|
||||||
用于识别此 API Key 的用途
|
用于识别此 API Key 的用途
|
||||||
</p>
|
</p>
|
||||||
@@ -658,6 +662,7 @@ import { useClientsStore } from '@/stores/clients'
|
|||||||
import { useApiKeysStore } from '@/stores/apiKeys'
|
import { useApiKeysStore } from '@/stores/apiKeys'
|
||||||
import { apiClient } from '@/config/api'
|
import { apiClient } from '@/config/api'
|
||||||
import AccountSelector from '@/components/common/AccountSelector.vue'
|
import AccountSelector from '@/components/common/AccountSelector.vue'
|
||||||
|
import IconPicker from '@/components/common/IconPicker.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
apiKey: {
|
apiKey: {
|
||||||
@@ -705,6 +710,7 @@ const unselectedTags = computed(() => {
|
|||||||
// 表单数据
|
// 表单数据
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
|
icon: '',
|
||||||
tokenLimit: '', // 保留用于检测历史数据
|
tokenLimit: '', // 保留用于检测历史数据
|
||||||
rateLimitWindow: '',
|
rateLimitWindow: '',
|
||||||
rateLimitRequests: '',
|
rateLimitRequests: '',
|
||||||
@@ -803,6 +809,7 @@ const updateApiKey = async () => {
|
|||||||
// 准备提交的数据
|
// 准备提交的数据
|
||||||
const data = {
|
const data = {
|
||||||
name: form.name, // 添加名称字段
|
name: form.name, // 添加名称字段
|
||||||
|
icon: form.icon || '', // 添加图标字段
|
||||||
tokenLimit: 0, // 清除历史token限制
|
tokenLimit: 0, // 清除历史token限制
|
||||||
rateLimitWindow:
|
rateLimitWindow:
|
||||||
form.rateLimitWindow !== '' && form.rateLimitWindow !== null
|
form.rateLimitWindow !== '' && form.rateLimitWindow !== null
|
||||||
@@ -1035,6 +1042,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
form.name = props.apiKey.name
|
form.name = props.apiKey.name
|
||||||
|
form.icon = props.apiKey.icon || ''
|
||||||
|
|
||||||
// 处理速率限制迁移:如果有tokenLimit且没有rateLimitCost,提示用户
|
// 处理速率限制迁移:如果有tokenLimit且没有rateLimitCost,提示用户
|
||||||
form.tokenLimit = props.apiKey.tokenLimit || ''
|
form.tokenLimit = props.apiKey.tokenLimit || ''
|
||||||
|
|||||||
1024
web/admin-spa/src/components/common/IconPicker.vue
Normal file
1024
web/admin-spa/src/components/common/IconPicker.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -125,7 +125,7 @@
|
|||||||
<div class="relative flex items-center">
|
<div class="relative flex items-center">
|
||||||
<input
|
<input
|
||||||
v-model="searchKeyword"
|
v-model="searchKeyword"
|
||||||
class="w-full rounded-lg border border-gray-200 bg-white px-3 py-2 pl-9 text-sm text-gray-700 placeholder-gray-400 shadow-sm transition-all duration-200 hover:border-gray-300 focus:border-cyan-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/20 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:placeholder-gray-500 dark:hover:border-gray-500"
|
class="w-full rounded-lg border border-gray-200 bg-white px-3 py-1.5 pl-9 text-sm text-gray-700 placeholder-gray-400 shadow-sm transition-all duration-200 hover:border-gray-300 focus:border-cyan-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/20 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:placeholder-gray-500 dark:hover:border-gray-500"
|
||||||
:placeholder="isLdapEnabled ? '搜索名称或所有者...' : '搜索名称...'"
|
:placeholder="isLdapEnabled ? '搜索名称或所有者...' : '搜索名称...'"
|
||||||
type="text"
|
type="text"
|
||||||
@input="currentPage = 1"
|
@input="currentPage = 1"
|
||||||
@@ -209,7 +209,7 @@
|
|||||||
|
|
||||||
<!-- 创建按钮 - 独立在右侧 -->
|
<!-- 创建按钮 - 独立在右侧 -->
|
||||||
<button
|
<button
|
||||||
class="flex w-full items-center justify-center gap-2 rounded-lg bg-gradient-to-r from-blue-500 to-blue-600 px-5 py-2.5 text-sm font-medium text-white shadow-md transition-all duration-200 hover:from-blue-600 hover:to-blue-700 hover:shadow-lg sm:w-auto"
|
class="flex w-full items-center justify-center gap-2 rounded-lg bg-gradient-to-r from-blue-500 to-blue-600 px-5 py-2 text-sm font-medium text-white shadow-md transition-all duration-200 hover:from-blue-600 hover:to-blue-700 hover:shadow-lg sm:w-auto"
|
||||||
@click.stop="openCreateApiKeyModal"
|
@click.stop="openCreateApiKeyModal"
|
||||||
>
|
>
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
@@ -250,7 +250,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[20%] min-w-[160px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
class="w-[18%] min-w-[140px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||||
@click="sortApiKeys('name')"
|
@click="sortApiKeys('name')"
|
||||||
>
|
>
|
||||||
名称
|
名称
|
||||||
@@ -264,11 +264,6 @@
|
|||||||
/>
|
/>
|
||||||
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
||||||
</th>
|
</th>
|
||||||
<th
|
|
||||||
class="w-[10%] min-w-[90px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
账号
|
|
||||||
</th>
|
|
||||||
<th
|
<th
|
||||||
class="w-[10%] min-w-[80px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="w-[10%] min-w-[80px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
@@ -323,7 +318,7 @@
|
|||||||
class="w-[7%] min-w-[70px] cursor-pointer px-3 py-4 text-right text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
class="w-[7%] min-w-[70px] cursor-pointer px-3 py-4 text-right text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||||
@click="sortApiKeys('periodTokens')"
|
@click="sortApiKeys('periodTokens')"
|
||||||
>
|
>
|
||||||
Token数量
|
Token数
|
||||||
<i
|
<i
|
||||||
v-if="apiKeysSortBy === 'periodTokens'"
|
v-if="apiKeysSortBy === 'periodTokens'"
|
||||||
:class="[
|
:class="[
|
||||||
@@ -390,7 +385,7 @@
|
|||||||
<template v-for="key in paginatedApiKeys" :key="key.id">
|
<template v-for="key in paginatedApiKeys" :key="key.id">
|
||||||
<!-- API Key 主行 -->
|
<!-- API Key 主行 -->
|
||||||
<tr class="table-row">
|
<tr class="table-row">
|
||||||
<td v-if="shouldShowCheckboxes" class="px-3 py-2.5">
|
<td v-if="shouldShowCheckboxes" class="px-3 py-1.5">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<input
|
<input
|
||||||
v-model="selectedApiKeys"
|
v-model="selectedApiKeys"
|
||||||
@@ -401,16 +396,149 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-2.5">
|
<td class="px-3 py-1.5">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
|
<div class="flex items-start gap-2.5">
|
||||||
|
<!-- API Key 图标 -->
|
||||||
|
<IconPicker
|
||||||
|
v-model="key.icon"
|
||||||
|
class="mt-0.5"
|
||||||
|
size="small"
|
||||||
|
@update:model-value="(val) => updateApiKeyIcon(key.id, val)"
|
||||||
|
/>
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<!-- 名称 -->
|
||||||
<div
|
<div
|
||||||
class="truncate text-sm font-semibold text-gray-900 dark:text-gray-100"
|
class="truncate text-sm font-semibold text-gray-900 dark:text-gray-100"
|
||||||
:title="key.name"
|
:title="key.name"
|
||||||
>
|
>
|
||||||
{{ key.name }}
|
{{ key.name }}
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 次要信息显示:所属账号 -->
|
||||||
|
<div class="mt-0.5 text-[11px] text-gray-500 dark:text-gray-400">
|
||||||
|
<!-- Claude OAuth 账号 -->
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
key.claudeAccountId && !key.claudeAccountId.startsWith('group:')
|
||||||
|
"
|
||||||
|
class="inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<i class="fas fa-robot text-[9px] text-blue-500" />
|
||||||
|
<span>{{
|
||||||
|
getClaudeBindingInfo(key)
|
||||||
|
.replace(/^🔒\s*专属-/, '')
|
||||||
|
.replace(/^⚠️\s*/, '')
|
||||||
|
}}</span>
|
||||||
|
</span>
|
||||||
|
<!-- Claude Console 账号 -->
|
||||||
|
<span
|
||||||
|
v-else-if="key.claudeConsoleAccountId"
|
||||||
|
class="inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<i class="fas fa-terminal text-[9px] text-purple-500" />
|
||||||
|
<span>{{
|
||||||
|
getClaudeBindingInfo(key)
|
||||||
|
.replace(/^🔒\s*专属-/, '')
|
||||||
|
.replace(/^⚠️\s*/, '')
|
||||||
|
}}</span>
|
||||||
|
</span>
|
||||||
|
<!-- Claude 分组 -->
|
||||||
|
<span
|
||||||
|
v-else-if="
|
||||||
|
key.claudeAccountId && key.claudeAccountId.startsWith('group:')
|
||||||
|
"
|
||||||
|
class="inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<i class="fas fa-layer-group text-[9px] text-blue-500" />
|
||||||
|
<span>{{ getClaudeBindingInfo(key) }}</span>
|
||||||
|
</span>
|
||||||
|
<!-- Gemini 账号 -->
|
||||||
|
<span
|
||||||
|
v-else-if="
|
||||||
|
key.geminiAccountId && !key.geminiAccountId.startsWith('group:')
|
||||||
|
"
|
||||||
|
class="inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<i class="fas fa-gem text-[9px] text-green-500" />
|
||||||
|
<span>{{
|
||||||
|
getGeminiBindingInfo(key)
|
||||||
|
.replace(/^🔒\s*专属-/, '')
|
||||||
|
.replace(/^⚠️\s*/, '')
|
||||||
|
}}</span>
|
||||||
|
</span>
|
||||||
|
<!-- Gemini 分组 -->
|
||||||
|
<span
|
||||||
|
v-else-if="
|
||||||
|
key.geminiAccountId && key.geminiAccountId.startsWith('group:')
|
||||||
|
"
|
||||||
|
class="inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<i class="fas fa-layer-group text-[9px] text-green-500" />
|
||||||
|
<span>{{ getGeminiBindingInfo(key) }}</span>
|
||||||
|
</span>
|
||||||
|
<!-- OpenAI 账号 -->
|
||||||
|
<span
|
||||||
|
v-else-if="
|
||||||
|
key.openaiAccountId && !key.openaiAccountId.startsWith('group:')
|
||||||
|
"
|
||||||
|
class="inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<i class="fas fa-brain text-[9px] text-orange-500" />
|
||||||
|
<span>{{
|
||||||
|
getOpenAIBindingInfo(key)
|
||||||
|
.replace(/^🔒\s*专属-/, '')
|
||||||
|
.replace(/^⚠️\s*/, '')
|
||||||
|
}}</span>
|
||||||
|
</span>
|
||||||
|
<!-- OpenAI 分组 -->
|
||||||
|
<span
|
||||||
|
v-else-if="
|
||||||
|
key.openaiAccountId && key.openaiAccountId.startsWith('group:')
|
||||||
|
"
|
||||||
|
class="inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<i class="fas fa-layer-group text-[9px] text-orange-500" />
|
||||||
|
<span>{{ getOpenAIBindingInfo(key) }}</span>
|
||||||
|
</span>
|
||||||
|
<!-- Bedrock 账号 -->
|
||||||
|
<span
|
||||||
|
v-else-if="
|
||||||
|
key.bedrockAccountId &&
|
||||||
|
!key.bedrockAccountId.startsWith('group:')
|
||||||
|
"
|
||||||
|
class="inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<i class="fas fa-cube text-[9px] text-indigo-500" />
|
||||||
|
<span>{{
|
||||||
|
getBedrockBindingInfo(key)
|
||||||
|
.replace(/^🔒\s*专属-/, '')
|
||||||
|
.replace(/^⚠️\s*/, '')
|
||||||
|
}}</span>
|
||||||
|
</span>
|
||||||
|
<!-- Bedrock 分组 -->
|
||||||
|
<span
|
||||||
|
v-else-if="
|
||||||
|
key.bedrockAccountId &&
|
||||||
|
key.bedrockAccountId.startsWith('group:')
|
||||||
|
"
|
||||||
|
class="inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<i class="fas fa-layer-group text-[9px] text-indigo-500" />
|
||||||
|
<span>{{ getBedrockBindingInfo(key) }}</span>
|
||||||
|
</span>
|
||||||
|
<!-- 共享池 -->
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="inline-flex items-center gap-1 text-gray-400 dark:text-gray-500"
|
||||||
|
>
|
||||||
|
<i class="fas fa-share-alt text-[9px]" />
|
||||||
|
<span>共享池</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- 账户绑定信息 -->
|
<!-- 账户绑定信息 -->
|
||||||
<div class="mt-1.5 space-y-1">
|
<div class="mt-1.5 space-y-1 pl-12">
|
||||||
<!-- Claude 绑定 -->
|
<!-- Claude 绑定 -->
|
||||||
<div
|
<div
|
||||||
v-if="key.claudeAccountId || key.claudeConsoleAccountId"
|
v-if="key.claudeAccountId || key.claudeConsoleAccountId"
|
||||||
@@ -469,65 +597,14 @@
|
|||||||
<!-- 显示所有者信息 -->
|
<!-- 显示所有者信息 -->
|
||||||
<div
|
<div
|
||||||
v-if="isLdapEnabled && key.ownerDisplayName"
|
v-if="isLdapEnabled && key.ownerDisplayName"
|
||||||
class="mt-1 text-xs text-red-600"
|
class="mt-1 pl-12 text-xs text-red-600"
|
||||||
>
|
>
|
||||||
<i class="fas fa-user mr-1" />
|
<i class="fas fa-user mr-1" />
|
||||||
{{ key.ownerDisplayName }}
|
{{ key.ownerDisplayName }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<!-- 账号 -->
|
<td class="px-3 py-1.5">
|
||||||
<td class="whitespace-nowrap px-3 py-2.5 text-sm">
|
|
||||||
<div class="text-gray-700 dark:text-gray-300">
|
|
||||||
<!-- Claude 账号 -->
|
|
||||||
<span v-if="key.claudeAccountId" class="inline-flex items-center">
|
|
||||||
<i class="fas fa-robot mr-1 text-xs text-blue-500" />
|
|
||||||
<span class="text-xs">{{
|
|
||||||
getClaudeAccountName(key.claudeAccountId)
|
|
||||||
}}</span>
|
|
||||||
</span>
|
|
||||||
<!-- Claude Console 账号 -->
|
|
||||||
<span
|
|
||||||
v-else-if="key.claudeConsoleAccountId"
|
|
||||||
class="inline-flex items-center"
|
|
||||||
>
|
|
||||||
<i class="fas fa-terminal mr-1 text-xs text-purple-500" />
|
|
||||||
<span class="text-xs">{{
|
|
||||||
getClaudeConsoleAccountName(key.claudeConsoleAccountId)
|
|
||||||
}}</span>
|
|
||||||
</span>
|
|
||||||
<!-- Gemini 账号 -->
|
|
||||||
<span v-else-if="key.geminiAccountId" class="inline-flex items-center">
|
|
||||||
<i class="fas fa-gem mr-1 text-xs text-green-500" />
|
|
||||||
<span class="text-xs">{{
|
|
||||||
getGeminiAccountName(key.geminiAccountId)
|
|
||||||
}}</span>
|
|
||||||
</span>
|
|
||||||
<!-- OpenAI 账号 -->
|
|
||||||
<span v-else-if="key.openaiAccountId" class="inline-flex items-center">
|
|
||||||
<i class="fas fa-brain mr-1 text-xs text-orange-500" />
|
|
||||||
<span class="text-xs">{{
|
|
||||||
getOpenAIAccountName(key.openaiAccountId)
|
|
||||||
}}</span>
|
|
||||||
</span>
|
|
||||||
<!-- Bedrock 账号 -->
|
|
||||||
<span v-else-if="key.bedrockAccountId" class="inline-flex items-center">
|
|
||||||
<i class="fas fa-layer-group mr-1 text-xs text-indigo-500" />
|
|
||||||
<span class="text-xs">{{
|
|
||||||
getBedrockAccountName(key.bedrockAccountId)
|
|
||||||
}}</span>
|
|
||||||
</span>
|
|
||||||
<!-- 共享池 -->
|
|
||||||
<span
|
|
||||||
v-else
|
|
||||||
class="inline-flex items-center text-gray-500 dark:text-gray-400"
|
|
||||||
>
|
|
||||||
<i class="fas fa-share-alt mr-1 text-xs" />
|
|
||||||
<span class="text-xs">共享池</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-3 py-2.5">
|
|
||||||
<div class="flex flex-wrap gap-1">
|
<div class="flex flex-wrap gap-1">
|
||||||
<span
|
<span
|
||||||
v-for="tag in key.tags || []"
|
v-for="tag in key.tags || []"
|
||||||
@@ -543,7 +620,7 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="whitespace-nowrap px-3 py-2.5">
|
<td class="whitespace-nowrap px-3 py-1.5">
|
||||||
<span
|
<span
|
||||||
:class="[
|
:class="[
|
||||||
'inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold',
|
'inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold',
|
||||||
@@ -562,7 +639,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<!-- 请求数 -->
|
<!-- 请求数 -->
|
||||||
<td class="whitespace-nowrap px-3 py-2.5 text-right text-sm">
|
<td class="whitespace-nowrap px-3 py-1.5 text-right text-sm">
|
||||||
<div class="flex items-center justify-end gap-1">
|
<div class="flex items-center justify-end gap-1">
|
||||||
<span class="font-medium text-gray-900 dark:text-gray-100">
|
<span class="font-medium text-gray-900 dark:text-gray-100">
|
||||||
{{ formatNumber(getPeriodRequests(key)) }}
|
{{ formatNumber(getPeriodRequests(key)) }}
|
||||||
@@ -571,7 +648,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<!-- 费用 -->
|
<!-- 费用 -->
|
||||||
<td class="whitespace-nowrap px-3 py-2.5 text-right text-sm">
|
<td class="whitespace-nowrap px-3 py-1.5 text-right text-sm">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<span class="font-medium text-blue-600 dark:text-blue-400">
|
<span class="font-medium text-blue-600 dark:text-blue-400">
|
||||||
${{ getPeriodCost(key).toFixed(4) }}
|
${{ getPeriodCost(key).toFixed(4) }}
|
||||||
@@ -634,7 +711,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<!-- Token数量 -->
|
<!-- Token数量 -->
|
||||||
<td class="whitespace-nowrap px-3 py-2.5 text-right text-sm">
|
<td class="whitespace-nowrap px-3 py-1.5 text-right text-sm">
|
||||||
<div class="flex items-center justify-end gap-1">
|
<div class="flex items-center justify-end gap-1">
|
||||||
<span class="font-medium text-purple-600 dark:text-purple-400">
|
<span class="font-medium text-purple-600 dark:text-purple-400">
|
||||||
{{ formatTokenCount(getPeriodTokens(key)) }}
|
{{ formatTokenCount(getPeriodTokens(key)) }}
|
||||||
@@ -643,17 +720,24 @@
|
|||||||
</td>
|
</td>
|
||||||
<!-- 最后使用 -->
|
<!-- 最后使用 -->
|
||||||
<td
|
<td
|
||||||
class="whitespace-nowrap px-3 py-2.5 text-sm text-gray-700 dark:text-gray-300"
|
class="whitespace-nowrap px-3 py-1.5 text-sm text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="key.lastUsedAt"
|
||||||
|
class="cursor-help"
|
||||||
|
:title="new Date(key.lastUsedAt).toLocaleString('zh-CN')"
|
||||||
>
|
>
|
||||||
{{ formatLastUsed(key.lastUsedAt) }}
|
{{ formatLastUsed(key.lastUsedAt) }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-gray-400">从未使用</span>
|
||||||
</td>
|
</td>
|
||||||
<!-- 创建时间 -->
|
<!-- 创建时间 -->
|
||||||
<td
|
<td
|
||||||
class="whitespace-nowrap px-3 py-2.5 text-sm text-gray-500 dark:text-gray-400"
|
class="whitespace-nowrap px-3 py-1.5 text-sm text-gray-500 dark:text-gray-400"
|
||||||
>
|
>
|
||||||
{{ new Date(key.createdAt).toLocaleDateString() }}
|
{{ new Date(key.createdAt).toLocaleDateString() }}
|
||||||
</td>
|
</td>
|
||||||
<td class="whitespace-nowrap px-3 py-2.5 text-sm">
|
<td class="whitespace-nowrap px-3 py-1.5 text-sm">
|
||||||
<div class="inline-flex items-center gap-1.5">
|
<div class="inline-flex items-center gap-1.5">
|
||||||
<!-- 未激活状态 -->
|
<!-- 未激活状态 -->
|
||||||
<span
|
<span
|
||||||
@@ -667,52 +751,40 @@
|
|||||||
<span v-else-if="key.expiresAt">
|
<span v-else-if="key.expiresAt">
|
||||||
<span
|
<span
|
||||||
v-if="isApiKeyExpired(key.expiresAt)"
|
v-if="isApiKeyExpired(key.expiresAt)"
|
||||||
class="inline-flex items-center text-red-600"
|
class="inline-flex cursor-pointer items-center text-red-600 hover:underline"
|
||||||
|
@click.stop="startEditExpiry(key)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-exclamation-circle mr-1" />
|
<i class="fas fa-exclamation-circle mr-1" />
|
||||||
已过期
|
已过期
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else-if="isApiKeyExpiringSoon(key.expiresAt)"
|
v-else-if="isApiKeyExpiringSoon(key.expiresAt)"
|
||||||
class="inline-flex items-center text-orange-600"
|
class="inline-flex cursor-pointer items-center text-orange-600 hover:underline"
|
||||||
|
@click.stop="startEditExpiry(key)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-clock mr-1" />
|
<i class="fas fa-clock mr-1" />
|
||||||
{{ formatExpireDate(key.expiresAt) }}
|
{{ formatExpireDate(key.expiresAt) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="text-gray-600 dark:text-gray-400">
|
<span
|
||||||
|
v-else
|
||||||
|
class="cursor-pointer text-gray-600 hover:underline dark:text-gray-400"
|
||||||
|
@click.stop="startEditExpiry(key)"
|
||||||
|
>
|
||||||
{{ formatExpireDate(key.expiresAt) }}
|
{{ formatExpireDate(key.expiresAt) }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<!-- 永不过期 -->
|
<!-- 永不过期 -->
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
class="inline-flex items-center text-gray-400 dark:text-gray-500"
|
class="inline-flex cursor-pointer items-center text-gray-400 hover:underline dark:text-gray-500"
|
||||||
|
@click.stop="startEditExpiry(key)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-infinity mr-1" />
|
<i class="fas fa-infinity mr-1" />
|
||||||
永不过期
|
永不过期
|
||||||
</span>
|
</span>
|
||||||
<button
|
|
||||||
class="inline-flex h-6 w-6 items-center justify-center rounded-md text-gray-300 transition-all duration-200 hover:bg-blue-50 hover:text-blue-500 dark:hover:bg-blue-900/20"
|
|
||||||
title="编辑过期时间"
|
|
||||||
@click.stop="startEditExpiry(key)"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="h-3 w-3"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="whitespace-nowrap px-3 py-2.5 text-sm">
|
<td class="whitespace-nowrap px-3 py-1.5 text-sm">
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<button
|
<button
|
||||||
class="rounded px-2 py-1 text-xs font-medium text-purple-600 transition-colors hover:bg-purple-50 hover:text-purple-900 dark:hover:bg-purple-900/20"
|
class="rounded px-2 py-1 text-xs font-medium text-purple-600 transition-colors hover:bg-purple-50 hover:text-purple-900 dark:hover:bg-purple-900/20"
|
||||||
@@ -1065,11 +1137,12 @@
|
|||||||
:value="key.id"
|
:value="key.id"
|
||||||
@change="updateSelectAllState"
|
@change="updateSelectAllState"
|
||||||
/>
|
/>
|
||||||
<div
|
<!-- API Key 图标 -->
|
||||||
class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-blue-600"
|
<IconPicker
|
||||||
>
|
v-model="key.icon"
|
||||||
<i class="fas fa-key text-sm text-white" />
|
size="medium"
|
||||||
</div>
|
@update:model-value="(val) => updateApiKeyIcon(key.id, val)"
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
<h4 class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{{ key.name }}
|
{{ key.name }}
|
||||||
@@ -1294,14 +1367,14 @@
|
|||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<div class="mt-3 flex gap-2 border-t border-gray-100 pt-3 dark:border-gray-600">
|
<div class="mt-3 flex gap-2 border-t border-gray-100 pt-3 dark:border-gray-600">
|
||||||
<button
|
<button
|
||||||
class="flex flex-1 items-center justify-center gap-1 rounded-lg bg-blue-50 px-3 py-2 text-xs text-blue-600 transition-colors hover:bg-blue-100 dark:bg-blue-900/30 dark:hover:bg-blue-900/50"
|
class="flex flex-1 items-center justify-center gap-1 rounded-lg bg-blue-50 px-3 py-1.5 text-xs text-blue-600 transition-colors hover:bg-blue-100 dark:bg-blue-900/30 dark:hover:bg-blue-900/50"
|
||||||
@click="showUsageDetails(key)"
|
@click="showUsageDetails(key)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-chart-line" />
|
<i class="fas fa-chart-line" />
|
||||||
查看详情
|
查看详情
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="flex-1 rounded-lg bg-gray-50 px-3 py-2 text-xs text-gray-600 transition-colors hover:bg-gray-100 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
class="flex-1 rounded-lg bg-gray-50 px-3 py-1.5 text-xs text-gray-600 transition-colors hover:bg-gray-100 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||||
@click="openEditApiKeyModal(key)"
|
@click="openEditApiKeyModal(key)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-edit mr-1" />
|
<i class="fas fa-edit mr-1" />
|
||||||
@@ -1312,7 +1385,7 @@
|
|||||||
key.expiresAt &&
|
key.expiresAt &&
|
||||||
(isApiKeyExpired(key.expiresAt) || isApiKeyExpiringSoon(key.expiresAt))
|
(isApiKeyExpired(key.expiresAt) || isApiKeyExpiringSoon(key.expiresAt))
|
||||||
"
|
"
|
||||||
class="flex-1 rounded-lg bg-orange-50 px-3 py-2 text-xs text-orange-600 transition-colors hover:bg-orange-100 dark:bg-orange-900/30 dark:hover:bg-orange-900/50"
|
class="flex-1 rounded-lg bg-orange-50 px-3 py-1.5 text-xs text-orange-600 transition-colors hover:bg-orange-100 dark:bg-orange-900/30 dark:hover:bg-orange-900/50"
|
||||||
@click="openRenewApiKeyModal(key)"
|
@click="openRenewApiKeyModal(key)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-clock mr-1" />
|
<i class="fas fa-clock mr-1" />
|
||||||
@@ -1323,7 +1396,7 @@
|
|||||||
key.isActive
|
key.isActive
|
||||||
? 'bg-orange-50 text-orange-600 hover:bg-orange-100 dark:bg-orange-900/30 dark:hover:bg-orange-900/50'
|
? 'bg-orange-50 text-orange-600 hover:bg-orange-100 dark:bg-orange-900/30 dark:hover:bg-orange-900/50'
|
||||||
: 'bg-green-50 text-green-600 hover:bg-green-100 dark:bg-green-900/30 dark:hover:bg-green-900/50',
|
: 'bg-green-50 text-green-600 hover:bg-green-100 dark:bg-green-900/30 dark:hover:bg-green-900/50',
|
||||||
'rounded-lg px-3 py-2 text-xs transition-colors'
|
'rounded-lg px-3 py-1.5 text-xs transition-colors'
|
||||||
]"
|
]"
|
||||||
@click="toggleApiKeyStatus(key)"
|
@click="toggleApiKeyStatus(key)"
|
||||||
>
|
>
|
||||||
@@ -1331,7 +1404,7 @@
|
|||||||
{{ key.isActive ? '禁用' : '激活' }}
|
{{ key.isActive ? '禁用' : '激活' }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="rounded-lg bg-red-50 px-3 py-2 text-xs text-red-600 transition-colors hover:bg-red-100 dark:bg-red-900/30 dark:hover:bg-red-900/50"
|
class="rounded-lg bg-red-50 px-3 py-1.5 text-xs text-red-600 transition-colors hover:bg-red-100 dark:bg-red-900/30 dark:hover:bg-red-900/50"
|
||||||
@click="deleteApiKey(key.id)"
|
@click="deleteApiKey(key.id)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-trash" />
|
<i class="fas fa-trash" />
|
||||||
@@ -1512,7 +1585,7 @@
|
|||||||
<th
|
<th
|
||||||
class="w-[7%] min-w-[70px] px-3 py-4 text-right text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="w-[7%] min-w-[70px] px-3 py-4 text-right text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
Token数量
|
Token数
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[9%] min-w-[80px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="w-[9%] min-w-[80px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
@@ -1528,7 +1601,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200/50 dark:divide-gray-600/50">
|
<tbody class="divide-y divide-gray-200/50 dark:divide-gray-600/50">
|
||||||
<tr v-for="key in deletedApiKeys" :key="key.id" class="table-row">
|
<tr v-for="key in deletedApiKeys" :key="key.id" class="table-row">
|
||||||
<td class="px-3 py-2.5">
|
<td class="px-3 py-1.5">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div
|
<div
|
||||||
class="mr-2 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-red-500 to-red-600"
|
class="mr-2 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-red-500 to-red-600"
|
||||||
@@ -1545,58 +1618,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<!-- 账号 -->
|
<td v-if="isLdapEnabled" class="px-3 py-1.5">
|
||||||
<td class="whitespace-nowrap px-3 py-2.5 text-sm">
|
|
||||||
<div class="text-gray-700 dark:text-gray-300">
|
|
||||||
<!-- Claude 账号 -->
|
|
||||||
<span v-if="key.claudeAccountId" class="inline-flex items-center">
|
|
||||||
<i class="fas fa-robot mr-1 text-xs text-blue-500" />
|
|
||||||
<span class="text-xs">{{
|
|
||||||
getClaudeAccountName(key.claudeAccountId)
|
|
||||||
}}</span>
|
|
||||||
</span>
|
|
||||||
<!-- Claude Console 账号 -->
|
|
||||||
<span
|
|
||||||
v-else-if="key.claudeConsoleAccountId"
|
|
||||||
class="inline-flex items-center"
|
|
||||||
>
|
|
||||||
<i class="fas fa-terminal mr-1 text-xs text-purple-500" />
|
|
||||||
<span class="text-xs">{{
|
|
||||||
getClaudeConsoleAccountName(key.claudeConsoleAccountId)
|
|
||||||
}}</span>
|
|
||||||
</span>
|
|
||||||
<!-- Gemini 账号 -->
|
|
||||||
<span v-else-if="key.geminiAccountId" class="inline-flex items-center">
|
|
||||||
<i class="fas fa-gem mr-1 text-xs text-green-500" />
|
|
||||||
<span class="text-xs">{{
|
|
||||||
getGeminiAccountName(key.geminiAccountId)
|
|
||||||
}}</span>
|
|
||||||
</span>
|
|
||||||
<!-- OpenAI 账号 -->
|
|
||||||
<span v-else-if="key.openaiAccountId" class="inline-flex items-center">
|
|
||||||
<i class="fas fa-brain mr-1 text-xs text-orange-500" />
|
|
||||||
<span class="text-xs">{{
|
|
||||||
getOpenAIAccountName(key.openaiAccountId)
|
|
||||||
}}</span>
|
|
||||||
</span>
|
|
||||||
<!-- Bedrock 账号 -->
|
|
||||||
<span v-else-if="key.bedrockAccountId" class="inline-flex items-center">
|
|
||||||
<i class="fas fa-layer-group mr-1 text-xs text-indigo-500" />
|
|
||||||
<span class="text-xs">{{
|
|
||||||
getBedrockAccountName(key.bedrockAccountId)
|
|
||||||
}}</span>
|
|
||||||
</span>
|
|
||||||
<!-- 共享池 -->
|
|
||||||
<span
|
|
||||||
v-else
|
|
||||||
class="inline-flex items-center text-gray-500 dark:text-gray-400"
|
|
||||||
>
|
|
||||||
<i class="fas fa-share-alt mr-1 text-xs" />
|
|
||||||
<span class="text-xs">共享池</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td v-if="isLdapEnabled" class="px-3 py-2.5">
|
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<span v-if="key.createdBy === 'admin'" class="text-blue-600">
|
<span v-if="key.createdBy === 'admin'" class="text-blue-600">
|
||||||
<i class="fas fa-user-shield mr-1" />
|
<i class="fas fa-user-shield mr-1" />
|
||||||
@@ -1613,11 +1635,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
class="whitespace-nowrap px-3 py-2.5 text-sm text-gray-500 dark:text-gray-400"
|
class="whitespace-nowrap px-3 py-1.5 text-sm text-gray-500 dark:text-gray-400"
|
||||||
>
|
>
|
||||||
{{ formatDate(key.createdAt) }}
|
{{ formatDate(key.createdAt) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-2.5">
|
<td class="px-3 py-1.5">
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<span v-if="key.deletedByType === 'admin'" class="text-blue-600">
|
<span v-if="key.deletedByType === 'admin'" class="text-blue-600">
|
||||||
<i class="fas fa-user-shield mr-1" />
|
<i class="fas fa-user-shield mr-1" />
|
||||||
@@ -1634,12 +1656,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
class="whitespace-nowrap px-3 py-2.5 text-sm text-gray-500 dark:text-gray-400"
|
class="whitespace-nowrap px-3 py-1.5 text-sm text-gray-500 dark:text-gray-400"
|
||||||
>
|
>
|
||||||
{{ formatDate(key.deletedAt) }}
|
{{ formatDate(key.deletedAt) }}
|
||||||
</td>
|
</td>
|
||||||
<!-- 请求数 -->
|
<!-- 请求数 -->
|
||||||
<td class="whitespace-nowrap px-3 py-2.5 text-right text-sm">
|
<td class="whitespace-nowrap px-3 py-1.5 text-right text-sm">
|
||||||
<div class="flex items-center justify-end gap-1">
|
<div class="flex items-center justify-end gap-1">
|
||||||
<span class="font-medium text-gray-900 dark:text-gray-100">
|
<span class="font-medium text-gray-900 dark:text-gray-100">
|
||||||
{{ formatNumber(key.usage?.total?.requests || 0) }}
|
{{ formatNumber(key.usage?.total?.requests || 0) }}
|
||||||
@@ -1648,26 +1670,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<!-- 费用 -->
|
<!-- 费用 -->
|
||||||
<td class="whitespace-nowrap px-3 py-2.5 text-right text-sm">
|
<td class="whitespace-nowrap px-3 py-1.5 text-right text-sm">
|
||||||
<span class="font-medium text-green-600 dark:text-green-400">
|
<span class="font-medium text-green-600 dark:text-green-400">
|
||||||
${{ (key.usage?.total?.cost || 0).toFixed(4) }}
|
${{ (key.usage?.total?.cost || 0).toFixed(4) }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<!-- Token数量 -->
|
<!-- Token数量 -->
|
||||||
<td class="whitespace-nowrap px-3 py-2.5 text-right text-sm">
|
<td class="whitespace-nowrap px-3 py-1.5 text-right text-sm">
|
||||||
<span class="font-medium text-purple-600 dark:text-purple-400">
|
<span class="font-medium text-purple-600 dark:text-purple-400">
|
||||||
{{ formatTokenCount(key.usage?.total?.tokens || 0) }}
|
{{ formatTokenCount(key.usage?.total?.tokens || 0) }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
class="whitespace-nowrap px-3 py-2.5 text-sm text-gray-700 dark:text-gray-300"
|
class="whitespace-nowrap px-3 py-1.5 text-sm text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
<span v-if="key.lastUsedAt">
|
<span v-if="key.lastUsedAt">
|
||||||
{{ formatLastUsed(key.lastUsedAt) }}
|
{{ formatLastUsed(key.lastUsedAt) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="text-gray-400">从未使用</span>
|
<span v-else class="text-gray-400">从未使用</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-2.5">
|
<td class="px-3 py-1.5">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
v-if="key.canRestore"
|
v-if="key.canRestore"
|
||||||
@@ -1767,6 +1789,7 @@ import { apiClient } from '@/config/api'
|
|||||||
import { useClientsStore } from '@/stores/clients'
|
import { useClientsStore } from '@/stores/clients'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import * as XLSX from 'xlsx-js-style'
|
import * as XLSX from 'xlsx-js-style'
|
||||||
|
import IconPicker from '@/components/common/IconPicker.vue'
|
||||||
import CreateApiKeyModal from '@/components/apikeys/CreateApiKeyModal.vue'
|
import CreateApiKeyModal from '@/components/apikeys/CreateApiKeyModal.vue'
|
||||||
import EditApiKeyModal from '@/components/apikeys/EditApiKeyModal.vue'
|
import EditApiKeyModal from '@/components/apikeys/EditApiKeyModal.vue'
|
||||||
import RenewApiKeyModal from '@/components/apikeys/RenewApiKeyModal.vue'
|
import RenewApiKeyModal from '@/components/apikeys/RenewApiKeyModal.vue'
|
||||||
@@ -2980,6 +3003,29 @@ const toggleApiKeyStatus = async (key) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新API Key图标
|
||||||
|
const updateApiKeyIcon = async (keyId, icon) => {
|
||||||
|
try {
|
||||||
|
const data = await apiClient.put(`/admin/api-keys/${keyId}`, {
|
||||||
|
icon: icon
|
||||||
|
})
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
// 更新本地数据
|
||||||
|
const localKey = apiKeys.value.find((k) => k.id === keyId)
|
||||||
|
if (localKey) {
|
||||||
|
localKey.icon = icon
|
||||||
|
}
|
||||||
|
showToast('图标已更新', 'success')
|
||||||
|
} else {
|
||||||
|
showToast(data.message || '更新图标失败', 'error')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新图标失败:', error)
|
||||||
|
showToast('更新图标失败,请重试', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 删除API Key
|
// 删除API Key
|
||||||
const deleteApiKey = async (keyId) => {
|
const deleteApiKey = async (keyId) => {
|
||||||
let confirmed = false
|
let confirmed = false
|
||||||
@@ -3592,7 +3638,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.table-container {
|
||||||
overflow-x: auto;
|
overflow-x: hidden;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -3625,7 +3671,6 @@ onMounted(async () => {
|
|||||||
|
|
||||||
.table-row {
|
.table-row {
|
||||||
transition: background-color 0.2s ease;
|
transition: background-color 0.2s ease;
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-row:hover {
|
.table-row:hover {
|
||||||
|
|||||||
Reference in New Issue
Block a user