feat: 实现账户分组管理功能和优化响应式设计

主要更新:
- 实现账户分组管理功能,支持创建、编辑、删除分组
- 支持将账户添加到分组进行统一调度
- 优化 API Keys 页面响应式设计,解决操作栏被隐藏的问题
- 优化账户管理页面布局,合并平台/类型列,改进操作按钮布局
- 修复代理信息显示溢出问题
- 改进表格列宽分配,充分利用屏幕空间

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
shaw
2025-08-03 21:37:28 +08:00
parent 329904ba72
commit 9c9afe1528
20 changed files with 3588 additions and 717 deletions

View File

@@ -11,27 +11,44 @@
</p>
</div>
<div class="flex flex-col sm:flex-row gap-2 sm:items-center sm:justify-between">
<select
v-model="accountSortBy"
class="form-input px-3 py-2 text-sm w-full sm:w-auto"
@change="sortAccounts()"
>
<option value="name">
按名称排序
</option>
<option value="dailyTokens">
按今日Token排序
</option>
<option value="dailyRequests">
按今日请求数排序
</option>
<option value="totalTokens">
按总Token排序
</option>
<option value="lastUsed">
按最后使用排序
</option>
</select>
<div class="flex flex-col sm:flex-row gap-2">
<select
v-model="accountSortBy"
class="form-input px-3 py-2 text-sm w-full sm:w-auto"
@change="sortAccounts()"
>
<option value="name">
按名称排序
</option>
<option value="dailyTokens">
按今日Token排序
</option>
<option value="dailyRequests">
按今日请求数排序
</option>
<option value="totalTokens">
按总Token排序
</option>
<option value="lastUsed">
按最后使用排序
</option>
</select>
<select
v-model="groupFilter"
class="form-input px-3 py-2 text-sm w-full sm:w-auto"
@change="filterByGroup"
>
<option value="all">所有账户</option>
<option value="ungrouped">未分组账户</option>
<option
v-for="group in accountGroups"
:key="group.id"
:value="group.id"
>
{{ group.name }} ({{ group.platform === 'claude' ? 'Claude' : 'Gemini' }})
</option>
</select>
</div>
<button
class="btn btn-success px-4 sm:px-6 py-2 sm:py-3 flex items-center gap-2 w-full sm:w-auto justify-center"
@click.stop="openCreateAccountModal"
@@ -69,13 +86,13 @@
<!-- 桌面端表格视图 -->
<div
v-else
class="hidden lg:block table-container"
class="hidden md:block table-container"
>
<table class="min-w-full">
<table class="w-full table-fixed">
<thead class="bg-gray-50/80 backdrop-blur-sm">
<tr>
<th
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
class="px-3 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100 w-[22%] min-w-[180px]"
@click="sortAccounts('name')"
>
名称
@@ -89,10 +106,10 @@
/>
</th>
<th
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
class="px-3 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100 w-[15%] min-w-[120px]"
@click="sortAccounts('platform')"
>
平台
平台/类型
<i
v-if="accountsSortBy === 'platform'"
:class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"
@@ -103,21 +120,7 @@
/>
</th>
<th
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
@click="sortAccounts('accountType')"
>
类型
<i
v-if="accountsSortBy === 'accountType'"
:class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"
/>
<i
v-else
class="fas fa-sort ml-1 text-gray-400"
/>
</th>
<th
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
class="px-3 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100 w-[12%] min-w-[100px]"
@click="sortAccounts('status')"
>
状态
@@ -131,7 +134,7 @@
/>
</th>
<th
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
class="px-3 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100 w-[8%] min-w-[80px]"
@click="sortAccounts('priority')"
>
优先级
@@ -144,19 +147,19 @@
class="fas fa-sort ml-1 text-gray-400"
/>
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
<th class="px-3 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider w-[10%] min-w-[100px]">
代理
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
<th class="px-3 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider w-[10%] min-w-[90px]">
今日使用
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
<th class="px-3 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider w-[10%] min-w-[100px]">
会话窗口
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
<th class="px-3 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider w-[8%] min-w-[80px]">
最后使用
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
<th class="px-3 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider w-[15%] min-w-[180px]">
操作
</th>
</tr>
@@ -167,14 +170,14 @@
:key="account.id"
class="table-row"
>
<td class="px-6 py-4 whitespace-nowrap">
<td class="px-3 py-4">
<div class="flex items-center">
<div class="w-8 h-8 bg-gradient-to-br from-green-500 to-green-600 rounded-lg flex items-center justify-center mr-3">
<div class="w-8 h-8 bg-gradient-to-br from-green-500 to-green-600 rounded-lg flex items-center justify-center mr-2 flex-shrink-0">
<i class="fas fa-user-circle text-white text-xs" />
</div>
<div>
<div class="min-w-0">
<div class="flex items-center gap-2">
<div class="text-sm font-semibold text-gray-900">
<div class="text-sm font-semibold text-gray-900 truncate" :title="account.name">
{{ account.name }}
</div>
<span
@@ -183,60 +186,69 @@
>
<i class="fas fa-lock mr-1" />专属
</span>
<span
v-else-if="account.accountType === 'group'"
class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
>
<i class="fas fa-layer-group mr-1" />分组调度
</span>
<span
v-else
class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"
>
<i class="fas fa-share-alt mr-1" />共享
</span>
<span
v-if="account.groupInfo"
class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-600 ml-1"
:title="`所属分组: ${account.groupInfo.name}`"
>
<i class="fas fa-folder mr-1" />{{ account.groupInfo.name }}
</span>
</div>
<div class="text-xs text-gray-500">
<div class="text-xs text-gray-500 truncate" :title="account.id">
{{ account.id }}
</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span
v-if="account.platform === 'gemini'"
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-800"
>
<i class="fas fa-robot mr-1" />Gemini
</span>
<span
v-else-if="account.platform === 'claude-console'"
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-purple-100 text-purple-800"
>
<i class="fas fa-terminal mr-1" />Claude Console
</span>
<span
v-else
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-indigo-100 text-indigo-800"
>
<i class="fas fa-brain mr-1" />Claude
</span>
<td class="px-3 py-4">
<div class="flex items-center gap-1">
<!-- 平台图标和名称 -->
<div
v-if="account.platform === 'gemini'"
class="flex items-center gap-1.5 px-2.5 py-1 bg-gradient-to-r from-yellow-100 to-amber-100 rounded-lg border border-yellow-200"
>
<i class="fas fa-robot text-yellow-700 text-xs" />
<span class="text-xs font-semibold text-yellow-800">Gemini</span>
<span class="w-px h-4 bg-yellow-300 mx-1"></span>
<span class="text-xs font-medium text-yellow-700">
{{ (account.scopes && account.scopes.length > 0) ? 'OAuth' : '传统' }}
</span>
</div>
<div
v-else-if="account.platform === 'claude-console'"
class="flex items-center gap-1.5 px-2.5 py-1 bg-gradient-to-r from-purple-100 to-pink-100 rounded-lg border border-purple-200"
>
<i class="fas fa-terminal text-purple-700 text-xs" />
<span class="text-xs font-semibold text-purple-800">Console</span>
<span class="w-px h-4 bg-purple-300 mx-1"></span>
<span class="text-xs font-medium text-purple-700">API Key</span>
</div>
<div
v-else
class="flex items-center gap-1.5 px-2.5 py-1 bg-gradient-to-r from-indigo-100 to-blue-100 rounded-lg border border-indigo-200"
>
<i class="fas fa-brain text-indigo-700 text-xs" />
<span class="text-xs font-semibold text-indigo-800">Claude</span>
<span class="w-px h-4 bg-indigo-300 mx-1"></span>
<span class="text-xs font-medium text-indigo-700">
{{ (account.scopes && account.scopes.length > 0) ? 'OAuth' : '传统' }}
</span>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span
v-if="account.platform === 'claude-console'"
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-green-100 text-green-800"
>
<i class="fas fa-key mr-1" />API Key
</span>
<span
v-else-if="account.scopes && account.scopes.length > 0"
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-blue-100 text-blue-800"
>
<i class="fas fa-lock mr-1" />OAuth
</span>
<span
v-else
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-orange-100 text-orange-800"
>
<i class="fas fa-key mr-1" />传统
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<td class="px-3 py-4 whitespace-nowrap">
<div class="flex flex-col gap-1">
<span
:class="['inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold',
@@ -279,7 +291,7 @@
</span>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<td class="px-3 py-4 whitespace-nowrap">
<div
v-if="account.platform === 'claude' || account.platform === 'claude-console'"
class="flex items-center gap-2"
@@ -301,10 +313,11 @@
<span class="text-xs">N/A</span>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
<td class="px-3 py-4 text-sm text-gray-600">
<div
v-if="formatProxyDisplay(account.proxy)"
class="text-xs bg-blue-50 px-2 py-1 rounded font-mono"
class="text-xs bg-blue-50 px-2 py-1 rounded font-mono break-all"
:title="formatProxyDisplay(account.proxy)"
>
{{ formatProxyDisplay(account.proxy) }}
</div>
@@ -315,7 +328,7 @@
无代理
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">
<td class="px-3 py-4 whitespace-nowrap text-sm">
<div
v-if="account.usage && account.usage.daily"
class="space-y-1"
@@ -342,7 +355,7 @@
暂无数据
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<td class="px-3 py-4 whitespace-nowrap">
<div
v-if="account.platform === 'claude' && account.sessionWindow && account.sessionWindow.hasActiveWindow"
class="space-y-2"
@@ -381,16 +394,16 @@
<span class="text-xs">N/A</span>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-600">
{{ formatLastUsed(account.lastUsedAt) }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div class="flex items-center gap-2">
<td class="px-3 py-4 whitespace-nowrap text-sm font-medium">
<div class="flex items-center gap-1 flex-wrap">
<button
v-if="account.platform === 'claude' && account.scopes"
:disabled="account.isRefreshing"
:class="[
'px-3 py-1.5 rounded-lg text-xs font-medium transition-colors',
'px-2.5 py-1 rounded text-xs font-medium transition-colors',
account.isRefreshing
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
: 'bg-blue-100 text-blue-700 hover:bg-blue-200'
@@ -404,11 +417,12 @@
account.isRefreshing ? 'animate-spin' : ''
]"
/>
<span class="ml-1">刷新</span>
</button>
<button
:disabled="account.isTogglingSchedulable"
:class="[
'px-3 py-1.5 rounded-lg text-xs font-medium transition-colors',
'px-2.5 py-1 rounded text-xs font-medium transition-colors',
account.isTogglingSchedulable
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
: account.schedulable
@@ -424,18 +438,23 @@
account.schedulable ? 'fa-toggle-on' : 'fa-toggle-off'
]"
/>
<span class="ml-1">{{ account.schedulable ? '调度' : '停用' }}</span>
</button>
<button
class="px-3 py-1.5 bg-blue-100 text-blue-700 rounded-lg text-xs font-medium hover:bg-blue-200 transition-colors"
class="px-2.5 py-1 bg-blue-100 text-blue-700 rounded text-xs font-medium hover:bg-blue-200 transition-colors"
:title="'编辑账户'"
@click="editAccount(account)"
>
<i class="fas fa-edit" />
<span class="ml-1">编辑</span>
</button>
<button
class="px-3 py-1.5 bg-red-100 text-red-700 rounded-lg text-xs font-medium hover:bg-red-200 transition-colors"
class="px-2.5 py-1 bg-red-100 text-red-700 rounded text-xs font-medium hover:bg-red-200 transition-colors"
:title="'删除账户'"
@click="deleteAccount(account)"
>
<i class="fas fa-trash" />
<span class="ml-1">删除</span>
</button>
</div>
</td>
@@ -447,7 +466,7 @@
<!-- 移动端卡片视图 -->
<div
v-if="!accountsLoading && sortedAccounts.length > 0"
class="lg:hidden space-y-3"
class="md:hidden space-y-3"
>
<div
v-for="account in sortedAccounts"
@@ -670,6 +689,9 @@ const accountsSortBy = ref('')
const accountsSortOrder = ref('asc')
const apiKeys = ref([])
const refreshingTokens = ref({})
const accountGroups = ref([])
const groupFilter = ref('all')
const filteredAccounts = ref([])
// 模态框状态
const showCreateAccountModal = ref(false)
@@ -678,9 +700,10 @@ const editingAccount = ref(null)
// 计算排序后的账户列表
const sortedAccounts = computed(() => {
if (!accountsSortBy.value) return accounts.value
const sourceAccounts = filteredAccounts.value.length > 0 ? filteredAccounts.value : accounts.value
if (!accountsSortBy.value) return sourceAccounts
const sorted = [...accounts.value].sort((a, b) => {
const sorted = [...sourceAccounts].sort((a, b) => {
let aVal = a[accountsSortBy.value]
let bVal = b[accountsSortBy.value]
@@ -720,11 +743,12 @@ const sortedAccounts = computed(() => {
const loadAccounts = async () => {
accountsLoading.value = true
try {
const [claudeData, claudeConsoleData, geminiData, apiKeysData] = await Promise.all([
const [claudeData, claudeConsoleData, geminiData, apiKeysData, groupsData] = await Promise.all([
apiClient.get('/admin/claude-accounts'),
apiClient.get('/admin/claude-console-accounts'),
apiClient.get('/admin/gemini-accounts'),
apiClient.get('/admin/api-keys')
apiClient.get('/admin/api-keys'),
apiClient.get('/admin/account-groups')
])
// 更新API Keys列表
@@ -732,22 +756,49 @@ const loadAccounts = async () => {
apiKeys.value = apiKeysData.data || []
}
// 更新分组列表
if (groupsData.success) {
accountGroups.value = groupsData.data || []
}
// 创建分组ID到分组信息的映射
const groupMap = new Map()
const accountGroupMap = new Map()
// 获取所有分组的成员信息
for (const group of accountGroups.value) {
groupMap.set(group.id, group)
try {
const membersResponse = await apiClient.get(`/admin/account-groups/${group.id}/members`)
if (membersResponse.success) {
const members = membersResponse.data || []
members.forEach(member => {
accountGroupMap.set(member.id, group)
})
}
} catch (error) {
console.error(`Failed to load members for group ${group.id}:`, error)
}
}
const allAccounts = []
if (claudeData.success) {
const claudeAccounts = (claudeData.data || []).map(acc => {
// 计算每个Claude账户绑定的API Key数量
const boundApiKeysCount = apiKeys.value.filter(key => key.claudeAccountId === acc.id).length
return { ...acc, platform: 'claude', boundApiKeysCount }
// 检查是否属于某个分组
const groupInfo = accountGroupMap.get(acc.id) || null
return { ...acc, platform: 'claude', boundApiKeysCount, groupInfo }
})
allAccounts.push(...claudeAccounts)
}
if (claudeConsoleData.success) {
const claudeConsoleAccounts = (claudeConsoleData.data || []).map(acc => {
// 计算每个Claude Console账户绑定的API Key数量
const boundApiKeysCount = apiKeys.value.filter(key => key.claudeConsoleAccountId === acc.id).length
return { ...acc, platform: 'claude-console', boundApiKeysCount }
// Claude Console账户暂时不支持直接绑定
const groupInfo = accountGroupMap.get(acc.id) || null
return { ...acc, platform: 'claude-console', boundApiKeysCount: 0, groupInfo }
})
allAccounts.push(...claudeConsoleAccounts)
}
@@ -756,12 +807,15 @@ const loadAccounts = async () => {
const geminiAccounts = (geminiData.data || []).map(acc => {
// 计算每个Gemini账户绑定的API Key数量
const boundApiKeysCount = apiKeys.value.filter(key => key.geminiAccountId === acc.id).length
return { ...acc, platform: 'gemini', boundApiKeysCount }
const groupInfo = accountGroupMap.get(acc.id) || null
return { ...acc, platform: 'gemini', boundApiKeysCount, groupInfo }
})
allAccounts.push(...geminiAccounts)
}
accounts.value = allAccounts
// 初始化过滤后的账户列表
filterByGroup()
} catch (error) {
showToast('加载账户失败', 'error')
} finally {
@@ -819,19 +873,38 @@ const loadApiKeys = async () => {
}
}
// 按分组筛选账户
const filterByGroup = () => {
if (groupFilter.value === 'all') {
filteredAccounts.value = accounts.value
} else if (groupFilter.value === 'ungrouped') {
filteredAccounts.value = accounts.value.filter(acc => !acc.groupInfo)
} else {
// 按特定分组筛选
filteredAccounts.value = accounts.value.filter(acc =>
acc.groupInfo && acc.groupInfo.id === groupFilter.value
)
}
}
// 格式化代理信息显示
const formatProxyDisplay = (proxy) => {
if (!proxy || !proxy.host || !proxy.port) return null
let display = `${proxy.type}://${proxy.host}:${proxy.port}`
// 缩短类型名称
const typeShort = proxy.type === 'socks5' ? 'S5' : proxy.type.toUpperCase()
// 缩短主机名(如果太长)
let host = proxy.host
if (host.length > 15) {
host = host.substring(0, 12) + '...'
}
let display = `${typeShort}://${host}:${proxy.port}`
// 如果有用户名密码,添加认证信息(部分隐藏)
if (proxy.username) {
const maskedUsername = proxy.username.length > 2
? proxy.username[0] + '***' + proxy.username[proxy.username.length - 1]
: '***'
const maskedPassword = proxy.password ? '****' : ''
display = `${proxy.type}://${maskedUsername}:${maskedPassword}@${proxy.host}:${proxy.port}`
display = `${typeShort}://***@${host}:${proxy.port}`
}
return display
@@ -1104,6 +1177,33 @@ onMounted(() => {
</script>
<style scoped>
.table-container {
overflow-x: auto;
border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.05);
}
.table-row {
transition: all 0.2s ease;
}
.table-row:hover {
background-color: rgba(0, 0, 0, 0.02);
}
.loading-spinner {
width: 24px;
height: 24px;
border: 2px solid #e5e7eb;
border-top: 2px solid #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.accounts-container {
min-height: calc(100vh - 300px);
}

View File

@@ -88,11 +88,11 @@
v-else
class="hidden md:block table-container"
>
<table class="min-w-full">
<table class="w-full table-fixed">
<thead class="bg-gray-50/80 backdrop-blur-sm">
<tr>
<th
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
class="px-3 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100 w-[25%] min-w-[200px]"
@click="sortApiKeys('name')"
>
名称
@@ -105,11 +105,11 @@
class="fas fa-sort ml-1 text-gray-400"
/>
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
<th class="px-3 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider w-[10%] min-w-[80px]">
标签
</th>
<th
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
class="px-3 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100 w-[8%] min-w-[70px]"
@click="sortApiKeys('status')"
>
状态
@@ -122,7 +122,7 @@
class="fas fa-sort ml-1 text-gray-400"
/>
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
<th class="px-3 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider w-[17%] min-w-[140px]">
使用统计
<span
class="cursor-pointer hover:bg-gray-100 px-2 py-1 rounded"
@@ -140,7 +140,7 @@
</span>
</th>
<th
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
class="px-3 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100 w-[10%] min-w-[90px]"
@click="sortApiKeys('createdAt')"
>
创建时间
@@ -154,7 +154,7 @@
/>
</th>
<th
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
class="px-3 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100 w-[10%] min-w-[90px]"
@click="sortApiKeys('expiresAt')"
>
过期时间
@@ -167,7 +167,7 @@
class="fas fa-sort ml-1 text-gray-400"
/>
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
<th class="px-3 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider w-[20%] min-w-[180px]">
操作
</th>
</tr>
@@ -179,32 +179,32 @@
>
<!-- API Key 主行 -->
<tr class="table-row">
<td class="px-6 py-4 whitespace-nowrap">
<td class="px-3 py-4">
<div class="flex items-center">
<div class="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center mr-3">
<div class="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center mr-2 flex-shrink-0">
<i class="fas fa-key text-white text-xs" />
</div>
<div>
<div class="text-sm font-semibold text-gray-900">
<div class="min-w-0">
<div class="text-sm font-semibold text-gray-900 truncate" :title="key.name">
{{ key.name }}
</div>
<div class="text-xs text-gray-500">
<div class="text-xs text-gray-500 truncate" :title="key.id">
{{ key.id }}
</div>
<div class="text-xs text-gray-500 mt-1">
<span v-if="key.claudeAccountId || key.claudeConsoleAccountId">
<div class="text-xs text-gray-500 mt-1 truncate">
<span v-if="key.claudeAccountId" :title="`绑定: ${getBoundAccountName(key.claudeAccountId)}`">
<i class="fas fa-link mr-1" />
绑定: {{ getBoundAccountName(key.claudeAccountId, key.claudeConsoleAccountId) }}
{{ getBoundAccountName(key.claudeAccountId) }}
</span>
<span v-else>
<i class="fas fa-share-alt mr-1" />
使用共享池
共享池
</span>
</div>
</div>
</div>
</td>
<td class="px-6 py-4">
<td class="px-3 py-4">
<div class="flex flex-wrap gap-1">
<span
v-for="tag in (key.tags || [])"
@@ -219,7 +219,7 @@
>无标签</span>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<td class="px-3 py-4 whitespace-nowrap">
<span
:class="['inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold',
key.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800']"
@@ -231,7 +231,7 @@
{{ key.isActive ? '活跃' : '禁用' }}
</span>
</td>
<td class="px-6 py-4">
<td class="px-3 py-4">
<div class="space-y-1">
<!-- 请求统计 -->
<div class="flex justify-between text-sm">
@@ -328,10 +328,10 @@
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-500">
{{ new Date(key.createdAt).toLocaleDateString() }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">
<td class="px-3 py-4 whitespace-nowrap text-sm">
<div class="inline-flex items-center gap-1 group">
<span v-if="key.expiresAt">
<span
@@ -371,33 +371,40 @@
</button>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">
<div class="flex gap-2">
<td class="px-3 py-4 whitespace-nowrap text-sm">
<div class="flex gap-1">
<button
class="text-purple-600 hover:text-purple-900 font-medium hover:bg-purple-50 px-3 py-1 rounded-lg transition-colors"
class="text-purple-600 hover:text-purple-900 font-medium hover:bg-purple-50 px-2 py-1 rounded transition-colors text-xs"
title="复制统计页面链接"
@click="copyApiStatsLink(key)"
>
<i class="fas fa-chart-bar mr-1" />统计
<i class="fas fa-chart-bar" />
<span class="hidden xl:inline ml-1">统计</span>
</button>
<button
class="text-blue-600 hover:text-blue-900 font-medium hover:bg-blue-50 px-3 py-1 rounded-lg transition-colors"
class="text-blue-600 hover:text-blue-900 font-medium hover:bg-blue-50 px-2 py-1 rounded transition-colors text-xs"
title="编辑"
@click="openEditApiKeyModal(key)"
>
<i class="fas fa-edit mr-1" />编辑
<i class="fas fa-edit" />
<span class="hidden xl:inline ml-1">编辑</span>
</button>
<button
v-if="key.expiresAt && (isApiKeyExpired(key.expiresAt) || isApiKeyExpiringSoon(key.expiresAt))"
class="text-green-600 hover:text-green-900 font-medium hover:bg-green-50 px-3 py-1 rounded-lg transition-colors"
class="text-green-600 hover:text-green-900 font-medium hover:bg-green-50 px-2 py-1 rounded transition-colors text-xs"
title="续期"
@click="openRenewApiKeyModal(key)"
>
<i class="fas fa-clock mr-1" />续期
<i class="fas fa-clock" />
<span class="hidden xl:inline ml-1">续期</span>
</button>
<button
class="text-red-600 hover:text-red-900 font-medium hover:bg-red-50 px-3 py-1 rounded-lg transition-colors"
class="text-red-600 hover:text-red-900 font-medium hover:bg-red-50 px-2 py-1 rounded transition-colors text-xs"
title="删除"
@click="deleteApiKey(key.id)"
>
<i class="fas fa-trash mr-1" />删除
<i class="fas fa-trash" />
<span class="hidden xl:inline ml-1">删除</span>
</button>
</div>
</td>
@@ -406,8 +413,8 @@
<!-- 模型统计展开区域 -->
<tr v-if="key && key.id && expandedApiKeys[key.id]">
<td
colspan="6"
class="px-6 py-4 bg-gray-50"
colspan="7"
class="px-3 py-4 bg-gray-50"
>
<div
v-if="!apiKeyModelStats[key.id]"