mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
feat: 增强账户管理页面的平台筛选和缓存优化功能
- 添加平台筛选功能到账户管理页面 * 后端:在所有账户接口中支持platform和groupId查询参数 * 前端:添加平台筛选下拉框,支持条件性API请求 - 使用智能缓存机制优化数据加载 * 缓存API Keys、账户分组和分组成员数据 * 通过Ctrl/⌘+点击刷新按钮实现强制重新加载 * 在数据变更时自动清除相关缓存(创建/编辑/删除) - 改进Gemini账户限流状态显示 * 在geminiAccountService中添加限流信息支持 * 统一所有平台的限流状态格式 * 修复仪表板统计,排除被限流的账户 - 提升用户界面体验 * 将原生title提示替换为Element Plus的el-tooltip组件 * 支持跨平台键盘快捷键(Ctrl/⌘+点击) * ESLint规范合规和代码格式化改进 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,21 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 平台筛选器 -->
|
||||
<div class="group relative min-w-[140px]">
|
||||
<div
|
||||
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-blue-500 to-indigo-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
|
||||
></div>
|
||||
<CustomDropdown
|
||||
v-model="platformFilter"
|
||||
icon="fa-server"
|
||||
icon-color="text-blue-500"
|
||||
:options="platformOptions"
|
||||
placeholder="选择平台"
|
||||
@change="filterByPlatform"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 分组筛选器 -->
|
||||
<div class="group relative min-w-[160px]">
|
||||
<div
|
||||
@@ -40,22 +55,32 @@
|
||||
</div>
|
||||
|
||||
<!-- 刷新按钮 -->
|
||||
<button
|
||||
class="group relative flex items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-all duration-200 hover:border-gray-300 hover:shadow-md disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
|
||||
:disabled="accountsLoading"
|
||||
@click="loadAccounts()"
|
||||
>
|
||||
<div
|
||||
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-green-500 to-teal-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
|
||||
></div>
|
||||
<i
|
||||
:class="[
|
||||
'fas relative text-green-500',
|
||||
accountsLoading ? 'fa-spinner fa-spin' : 'fa-sync-alt'
|
||||
]"
|
||||
/>
|
||||
<span class="relative">刷新</span>
|
||||
</button>
|
||||
<div class="relative">
|
||||
<el-tooltip
|
||||
content="刷新数据 (Ctrl/⌘+点击强制刷新所有缓存)"
|
||||
effect="dark"
|
||||
placement="bottom"
|
||||
>
|
||||
<button
|
||||
class="group relative flex items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-all duration-200 hover:border-gray-300 hover:shadow-md disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
|
||||
:disabled="accountsLoading"
|
||||
@click.ctrl.exact="loadAccounts(true)"
|
||||
@click.exact="loadAccounts(false)"
|
||||
@click.meta.exact="loadAccounts(true)"
|
||||
>
|
||||
<div
|
||||
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-green-500 to-teal-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
|
||||
></div>
|
||||
<i
|
||||
:class="[
|
||||
'fas relative text-green-500',
|
||||
accountsLoading ? 'fa-spinner fa-spin' : 'fa-sync-alt'
|
||||
]"
|
||||
/>
|
||||
<span class="relative">刷新</span>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加账户按钮 -->
|
||||
@@ -307,11 +332,22 @@
|
||||
}}
|
||||
</span>
|
||||
<span
|
||||
v-if="account.rateLimitStatus && account.rateLimitStatus.isRateLimited"
|
||||
v-if="
|
||||
(account.rateLimitStatus && account.rateLimitStatus.isRateLimited) ||
|
||||
account.rateLimitStatus === 'limited'
|
||||
"
|
||||
class="inline-flex items-center rounded-full bg-yellow-100 px-3 py-1 text-xs font-semibold text-yellow-800"
|
||||
>
|
||||
<i class="fas fa-exclamation-triangle mr-1" />
|
||||
限流中 ({{ account.rateLimitStatus.minutesRemaining }}分钟)
|
||||
限流中
|
||||
<span
|
||||
v-if="
|
||||
account.rateLimitStatus &&
|
||||
typeof account.rateLimitStatus === 'object' &&
|
||||
account.rateLimitStatus.minutesRemaining > 0
|
||||
"
|
||||
>({{ account.rateLimitStatus.minutesRemaining }}分钟)</span
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
v-if="account.schedulable === false"
|
||||
@@ -458,6 +494,7 @@
|
||||
(account.status === 'unauthorized' ||
|
||||
account.status !== 'active' ||
|
||||
account.rateLimitStatus?.isRateLimited ||
|
||||
account.rateLimitStatus === 'limited' ||
|
||||
!account.isActive)
|
||||
"
|
||||
:class="[
|
||||
@@ -754,7 +791,13 @@ const apiKeys = ref([])
|
||||
const refreshingTokens = ref({})
|
||||
const accountGroups = ref([])
|
||||
const groupFilter = ref('all')
|
||||
const filteredAccounts = ref([])
|
||||
const platformFilter = ref('all')
|
||||
|
||||
// 缓存状态标志
|
||||
const apiKeysLoaded = ref(false)
|
||||
const groupsLoaded = ref(false)
|
||||
const groupMembersLoaded = ref(false)
|
||||
const accountGroupMap = ref(new Map())
|
||||
|
||||
// 下拉选项数据
|
||||
const sortOptions = ref([
|
||||
@@ -765,6 +808,14 @@ const sortOptions = ref([
|
||||
{ value: 'lastUsed', label: '按最后使用排序', icon: 'fa-clock' }
|
||||
])
|
||||
|
||||
const platformOptions = ref([
|
||||
{ value: 'all', label: '所有平台', icon: 'fa-globe' },
|
||||
{ value: 'claude', label: 'Claude', icon: 'fa-brain' },
|
||||
{ value: 'claude-console', label: 'Claude Console', icon: 'fa-terminal' },
|
||||
{ value: 'gemini', label: 'Gemini', icon: 'fa-robot' },
|
||||
{ value: 'bedrock', label: 'Bedrock', icon: 'fab fa-aws' }
|
||||
])
|
||||
|
||||
const groupOptions = computed(() => {
|
||||
const options = [
|
||||
{ value: 'all', label: '所有账户', icon: 'fa-globe' },
|
||||
@@ -787,7 +838,7 @@ const editingAccount = ref(null)
|
||||
|
||||
// 计算排序后的账户列表
|
||||
const sortedAccounts = computed(() => {
|
||||
const sourceAccounts = filteredAccounts.value.length > 0 ? filteredAccounts.value : accounts.value
|
||||
const sourceAccounts = accounts.value
|
||||
if (!accountsSortBy.value) return sourceAccounts
|
||||
|
||||
const sorted = [...sourceAccounts].sort((a, b) => {
|
||||
@@ -827,49 +878,75 @@ const sortedAccounts = computed(() => {
|
||||
})
|
||||
|
||||
// 加载账户列表
|
||||
const loadAccounts = async () => {
|
||||
const loadAccounts = async (forceReload = false) => {
|
||||
accountsLoading.value = true
|
||||
try {
|
||||
const [claudeData, claudeConsoleData, bedrockData, geminiData, apiKeysData, groupsData] =
|
||||
await Promise.all([
|
||||
apiClient.get('/admin/claude-accounts'),
|
||||
apiClient.get('/admin/claude-console-accounts'),
|
||||
apiClient.get('/admin/bedrock-accounts'),
|
||||
apiClient.get('/admin/gemini-accounts'),
|
||||
apiClient.get('/admin/api-keys'),
|
||||
apiClient.get('/admin/account-groups')
|
||||
])
|
||||
|
||||
// 更新API Keys列表
|
||||
if (apiKeysData.success) {
|
||||
apiKeys.value = apiKeysData.data || []
|
||||
// 构建查询参数
|
||||
const params = {}
|
||||
if (platformFilter.value !== 'all') {
|
||||
params.platform = platformFilter.value
|
||||
}
|
||||
if (groupFilter.value !== 'all') {
|
||||
params.groupId = groupFilter.value
|
||||
}
|
||||
|
||||
// 更新分组列表
|
||||
if (groupsData.success) {
|
||||
accountGroups.value = groupsData.data || []
|
||||
}
|
||||
// 根据平台筛选决定需要请求哪些接口
|
||||
const requests = []
|
||||
|
||||
// 创建分组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)
|
||||
if (platformFilter.value === 'all') {
|
||||
// 请求所有平台
|
||||
requests.push(
|
||||
apiClient.get('/admin/claude-accounts', { params }),
|
||||
apiClient.get('/admin/claude-console-accounts', { params }),
|
||||
apiClient.get('/admin/bedrock-accounts', { params }),
|
||||
apiClient.get('/admin/gemini-accounts', { params })
|
||||
)
|
||||
} else {
|
||||
// 只请求指定平台,其他平台设为null占位
|
||||
switch (platformFilter.value) {
|
||||
case 'claude':
|
||||
requests.push(
|
||||
apiClient.get('/admin/claude-accounts', { params }),
|
||||
Promise.resolve({ success: true, data: [] }), // claude-console 占位
|
||||
Promise.resolve({ success: true, data: [] }), // bedrock 占位
|
||||
Promise.resolve({ success: true, data: [] }) // gemini 占位
|
||||
)
|
||||
break
|
||||
case 'claude-console':
|
||||
requests.push(
|
||||
Promise.resolve({ success: true, data: [] }), // claude 占位
|
||||
apiClient.get('/admin/claude-console-accounts', { params }),
|
||||
Promise.resolve({ success: true, data: [] }), // bedrock 占位
|
||||
Promise.resolve({ success: true, data: [] }) // gemini 占位
|
||||
)
|
||||
break
|
||||
case 'bedrock':
|
||||
requests.push(
|
||||
Promise.resolve({ success: true, data: [] }), // claude 占位
|
||||
Promise.resolve({ success: true, data: [] }), // claude-console 占位
|
||||
apiClient.get('/admin/bedrock-accounts', { params }),
|
||||
Promise.resolve({ success: true, data: [] }) // gemini 占位
|
||||
)
|
||||
break
|
||||
case 'gemini':
|
||||
requests.push(
|
||||
Promise.resolve({ success: true, data: [] }), // claude 占位
|
||||
Promise.resolve({ success: true, data: [] }), // claude-console 占位
|
||||
Promise.resolve({ success: true, data: [] }), // bedrock 占位
|
||||
apiClient.get('/admin/gemini-accounts', { params })
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 使用缓存机制加载 API Keys 和分组数据
|
||||
await Promise.all([loadApiKeys(forceReload), loadAccountGroups(forceReload)])
|
||||
|
||||
// 加载分组成员关系(需要在分组数据加载完成后)
|
||||
await loadGroupMembers(forceReload)
|
||||
|
||||
const [claudeData, claudeConsoleData, bedrockData, geminiData] = await Promise.all(requests)
|
||||
|
||||
const allAccounts = []
|
||||
|
||||
if (claudeData.success) {
|
||||
@@ -879,7 +956,7 @@ const loadAccounts = async () => {
|
||||
(key) => key.claudeAccountId === acc.id
|
||||
).length
|
||||
// 检查是否属于某个分组
|
||||
const groupInfo = accountGroupMap.get(acc.id) || null
|
||||
const groupInfo = accountGroupMap.value.get(acc.id) || null
|
||||
return { ...acc, platform: 'claude', boundApiKeysCount, groupInfo }
|
||||
})
|
||||
allAccounts.push(...claudeAccounts)
|
||||
@@ -888,7 +965,7 @@ const loadAccounts = async () => {
|
||||
if (claudeConsoleData.success) {
|
||||
const claudeConsoleAccounts = (claudeConsoleData.data || []).map((acc) => {
|
||||
// Claude Console账户暂时不支持直接绑定
|
||||
const groupInfo = accountGroupMap.get(acc.id) || null
|
||||
const groupInfo = accountGroupMap.value.get(acc.id) || null
|
||||
return { ...acc, platform: 'claude-console', boundApiKeysCount: 0, groupInfo }
|
||||
})
|
||||
allAccounts.push(...claudeConsoleAccounts)
|
||||
@@ -897,7 +974,7 @@ const loadAccounts = async () => {
|
||||
if (bedrockData.success) {
|
||||
const bedrockAccounts = (bedrockData.data || []).map((acc) => {
|
||||
// Bedrock账户暂时不支持直接绑定
|
||||
const groupInfo = accountGroupMap.get(acc.id) || null
|
||||
const groupInfo = accountGroupMap.value.get(acc.id) || null
|
||||
return { ...acc, platform: 'bedrock', boundApiKeysCount: 0, groupInfo }
|
||||
})
|
||||
allAccounts.push(...bedrockAccounts)
|
||||
@@ -909,15 +986,13 @@ const loadAccounts = async () => {
|
||||
const boundApiKeysCount = apiKeys.value.filter(
|
||||
(key) => key.geminiAccountId === acc.id
|
||||
).length
|
||||
const groupInfo = accountGroupMap.get(acc.id) || null
|
||||
const groupInfo = accountGroupMap.value.get(acc.id) || null
|
||||
return { ...acc, platform: 'gemini', boundApiKeysCount, groupInfo }
|
||||
})
|
||||
allAccounts.push(...geminiAccounts)
|
||||
}
|
||||
|
||||
accounts.value = allAccounts
|
||||
// 初始化过滤后的账户列表
|
||||
filterByGroup()
|
||||
} catch (error) {
|
||||
showToast('加载账户失败', 'error')
|
||||
} finally {
|
||||
@@ -963,30 +1038,86 @@ const formatLastUsed = (dateString) => {
|
||||
return date.toLocaleDateString('zh-CN')
|
||||
}
|
||||
|
||||
// 加载API Keys列表
|
||||
const loadApiKeys = async () => {
|
||||
// 加载API Keys列表(缓存版本)
|
||||
const loadApiKeys = async (forceReload = false) => {
|
||||
if (!forceReload && apiKeysLoaded.value) {
|
||||
return // 使用缓存数据
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiClient.get('/admin/api-keys')
|
||||
if (response.success) {
|
||||
apiKeys.value = response.data
|
||||
apiKeys.value = response.data || []
|
||||
apiKeysLoaded.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load API keys:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载账户分组列表(缓存版本)
|
||||
const loadAccountGroups = async (forceReload = false) => {
|
||||
if (!forceReload && groupsLoaded.value) {
|
||||
return // 使用缓存数据
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiClient.get('/admin/account-groups')
|
||||
if (response.success) {
|
||||
accountGroups.value = response.data || []
|
||||
groupsLoaded.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load account groups:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载分组成员关系(缓存版本)
|
||||
const loadGroupMembers = async (forceReload = false) => {
|
||||
if (!forceReload && groupMembersLoaded.value) {
|
||||
return // 使用缓存数据
|
||||
}
|
||||
|
||||
try {
|
||||
// 重置映射
|
||||
accountGroupMap.value.clear()
|
||||
|
||||
// 获取所有分组的成员信息
|
||||
for (const group of accountGroups.value) {
|
||||
try {
|
||||
const membersResponse = await apiClient.get(`/admin/account-groups/${group.id}/members`)
|
||||
if (membersResponse.success) {
|
||||
const members = membersResponse.data || []
|
||||
members.forEach((member) => {
|
||||
accountGroupMap.value.set(member.id, group)
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load members for group ${group.id}:`, error)
|
||||
}
|
||||
}
|
||||
groupMembersLoaded.value = true
|
||||
} catch (error) {
|
||||
console.error('Failed to load group members:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 清空缓存的函数
|
||||
const clearCache = () => {
|
||||
apiKeysLoaded.value = false
|
||||
groupsLoaded.value = false
|
||||
groupMembersLoaded.value = false
|
||||
accountGroupMap.value.clear()
|
||||
}
|
||||
|
||||
// 按平台筛选账户
|
||||
const filterByPlatform = () => {
|
||||
loadAccounts()
|
||||
}
|
||||
|
||||
// 按分组筛选账户
|
||||
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
|
||||
)
|
||||
}
|
||||
loadAccounts()
|
||||
}
|
||||
|
||||
// 格式化代理信息显示
|
||||
@@ -1091,6 +1222,8 @@ const deleteAccount = async (account) => {
|
||||
|
||||
if (data.success) {
|
||||
showToast('账户已删除', 'success')
|
||||
// 清空分组成员缓存,因为账户可能从分组中移除
|
||||
groupMembersLoaded.value = false
|
||||
loadAccounts()
|
||||
} else {
|
||||
showToast(data.message || '删除失败', 'error')
|
||||
@@ -1196,6 +1329,8 @@ const toggleSchedulable = async (account) => {
|
||||
const handleCreateSuccess = () => {
|
||||
showCreateAccountModal.value = false
|
||||
showToast('账户创建成功', 'success')
|
||||
// 清空缓存,因为可能涉及分组关系变化
|
||||
clearCache()
|
||||
loadAccounts()
|
||||
}
|
||||
|
||||
@@ -1203,6 +1338,8 @@ const handleCreateSuccess = () => {
|
||||
const handleEditSuccess = () => {
|
||||
showEditAccountModal.value = false
|
||||
showToast('账户更新成功', 'success')
|
||||
// 清空分组成员缓存,因为账户类型和分组可能发生变化
|
||||
groupMembersLoaded.value = false
|
||||
loadAccounts()
|
||||
}
|
||||
|
||||
@@ -1216,7 +1353,8 @@ const getAccountStatusText = (account) => {
|
||||
if (
|
||||
account.isRateLimited ||
|
||||
account.status === 'rate_limited' ||
|
||||
(account.rateLimitStatus && account.rateLimitStatus.isRateLimited)
|
||||
(account.rateLimitStatus && account.rateLimitStatus.isRateLimited) ||
|
||||
account.rateLimitStatus === 'limited'
|
||||
)
|
||||
return '限流中'
|
||||
// 检查是否错误
|
||||
@@ -1238,7 +1376,8 @@ const getAccountStatusClass = (account) => {
|
||||
if (
|
||||
account.isRateLimited ||
|
||||
account.status === 'rate_limited' ||
|
||||
(account.rateLimitStatus && account.rateLimitStatus.isRateLimited)
|
||||
(account.rateLimitStatus && account.rateLimitStatus.isRateLimited) ||
|
||||
account.rateLimitStatus === 'limited'
|
||||
) {
|
||||
return 'bg-orange-100 text-orange-800'
|
||||
}
|
||||
@@ -1262,7 +1401,8 @@ const getAccountStatusDotClass = (account) => {
|
||||
if (
|
||||
account.isRateLimited ||
|
||||
account.status === 'rate_limited' ||
|
||||
(account.rateLimitStatus && account.rateLimitStatus.isRateLimited)
|
||||
(account.rateLimitStatus && account.rateLimitStatus.isRateLimited) ||
|
||||
account.rateLimitStatus === 'limited'
|
||||
) {
|
||||
return 'bg-orange-500'
|
||||
}
|
||||
@@ -1330,8 +1470,8 @@ watch(accountSortBy, (newVal) => {
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadAccounts()
|
||||
loadApiKeys()
|
||||
// 首次加载时强制刷新所有数据
|
||||
loadAccounts(true)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user