From 69a1006f4c63661b8a0ad56078654204a3cd7836 Mon Sep 17 00:00:00 2001 From: IanShaw027 Date: Wed, 3 Dec 2025 19:35:29 -0800 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E8=B4=A6?= =?UTF-8?q?=E6=88=B7=E7=AE=A1=E7=90=86=E9=A1=B5=E9=9D=A2=E7=9A=84=E8=BF=87?= =?UTF-8?q?=E6=BB=A4=E5=92=8C=E7=BB=9F=E8=AE=A1=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增状态过滤器:支持按正常/异常/全部筛选账户 - 新增限流时间过滤器:支持按1h/5h/12h/1d筛选限流账户 - 新增账户统计弹窗:按平台类型和状态汇总账户数量 - 优化账户列表过滤逻辑,支持组合过滤条件 - 默认状态过滤为'正常',提升用户体验 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- web/admin-spa/src/views/AccountsView.vue | 352 +++++++++++++++++++++++ 1 file changed, 352 insertions(+) diff --git a/web/admin-spa/src/views/AccountsView.vue b/web/admin-spa/src/views/AccountsView.vue index c2a31013..2f6f132a 100644 --- a/web/admin-spa/src/views/AccountsView.vue +++ b/web/admin-spa/src/views/AccountsView.vue @@ -58,6 +58,34 @@ /> + +
+
+ +
+ + +
+
+ +
+
+ +
+ + + +
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 平台类型 + + 正常 + + 限流≤1h + + 限流≤5h + + 限流≤12h + + 限流≤1d + + 异常 + + 合计 +
+ {{ stat.platformLabel }} + + {{ stat.normal }} + + {{ stat.rateLimit1h }} + + {{ stat.rateLimit5h }} + + {{ stat.rateLimit12h }} + + {{ stat.rateLimit1d }} + + {{ stat.abnormal }} + + {{ stat.total }} +
合计 + {{ + accountStatsTotal.normal + }} + + {{ + accountStatsTotal.rateLimit1h + }} + + {{ + accountStatsTotal.rateLimit5h + }} + + {{ + accountStatsTotal.rateLimit12h + }} + + {{ + accountStatsTotal.rateLimit1d + }} + + {{ + accountStatsTotal.abnormal + }} + + {{ accountStatsTotal.total }} +
+
+

+ 注:限流时间列表示剩余限流时间在指定范围内的账户数量 +

+
+
@@ -1880,6 +2038,8 @@ const bindingCounts = ref({}) // 轻量级绑定计数,用于显示"绑定: X const accountGroups = ref([]) const groupFilter = ref('all') const platformFilter = ref('all') +const statusFilter = ref('normal') // 新增:状态过滤 (normal/abnormal/all) +const rateLimitFilter = ref('all') // 新增:限流时间过滤 (all/1h/5h/12h/1d) const searchKeyword = ref('') const PAGE_SIZE_STORAGE_KEY = 'accountsPageSize' const getInitialPageSize = () => { @@ -1929,6 +2089,9 @@ const expiryEditModalRef = ref(null) const showAccountTestModal = ref(false) const testingAccount = ref(null) +// 账户统计弹窗状态 +const showAccountStatsModal = ref(false) + // 表格横向滚动检测 const tableContainerRef = ref(null) const needsHorizontalScroll = ref(false) @@ -1963,6 +2126,20 @@ const platformOptions = ref([ { value: 'droid', label: 'Droid', icon: 'fa-robot' } ]) +const statusOptions = ref([ + { value: 'normal', label: '正常', icon: 'fa-check-circle' }, + { value: 'abnormal', label: '异常', icon: 'fa-exclamation-triangle' }, + { value: 'all', label: '全部状态', icon: 'fa-list' } +]) + +const rateLimitOptions = ref([ + { value: 'all', label: '全部限流', icon: 'fa-infinity' }, + { value: '1h', label: '限流≤1小时', icon: 'fa-hourglass-start' }, + { value: '5h', label: '限流≤5小时', icon: 'fa-hourglass-half' }, + { value: '12h', label: '限流≤12小时', icon: 'fa-hourglass-end' }, + { value: '1d', label: '限流≤1天', icon: 'fa-calendar-day' } +]) + const groupOptions = computed(() => { const options = [ { value: 'all', label: '所有账户', icon: 'fa-globe' }, @@ -2199,6 +2376,47 @@ const sortedAccounts = computed(() => { ) } + // 状态过滤 (normal/abnormal/all) + if (statusFilter.value !== 'all') { + sourceAccounts = sourceAccounts.filter((account) => { + const isNormal = + account.isActive && + account.status !== 'blocked' && + account.status !== 'unauthorized' && + account.schedulable !== false && + !isAccountRateLimited(account) + + if (statusFilter.value === 'normal') { + return isNormal + } else if (statusFilter.value === 'abnormal') { + return !isNormal + } + return true + }) + } + + // 限流时间过滤 (all/1h/5h/12h/1d) + if (rateLimitFilter.value !== 'all') { + sourceAccounts = sourceAccounts.filter((account) => { + const rateLimitMinutes = getRateLimitRemainingMinutes(account) + if (!rateLimitMinutes || rateLimitMinutes <= 0) return false + + const minutes = Math.floor(rateLimitMinutes) + switch (rateLimitFilter.value) { + case '1h': + return minutes <= 60 + case '5h': + return minutes <= 300 + case '12h': + return minutes <= 720 + case '1d': + return minutes <= 1440 + default: + return true + } + }) + } + if (!accountsSortBy.value) return sourceAccounts const sorted = [...sourceAccounts].sort((a, b) => { @@ -2242,6 +2460,101 @@ const totalPages = computed(() => { return Math.ceil(total / pageSize.value) || 0 }) +// 账户统计数据(按平台和状态分类) +const accountStats = computed(() => { + const platforms = [ + { value: 'claude', label: 'Claude' }, + { value: 'claude-console', label: 'Claude Console' }, + { value: 'gemini', label: 'Gemini' }, + { value: 'gemini-api', label: 'Gemini API' }, + { value: 'openai', label: 'OpenAI' }, + { value: 'azure_openai', label: 'Azure OpenAI' }, + { value: 'bedrock', label: 'Bedrock' }, + { value: 'openai-responses', label: 'OpenAI-Responses' }, + { value: 'ccr', label: 'CCR' }, + { value: 'droid', label: 'Droid' } + ] + + return platforms + .map((p) => { + const platformAccounts = accounts.value.filter((acc) => acc.platform === p.value) + + const normal = platformAccounts.filter((acc) => { + return ( + acc.isActive && + acc.status !== 'blocked' && + acc.status !== 'unauthorized' && + acc.schedulable !== false && + !isAccountRateLimited(acc) + ) + }).length + + const abnormal = platformAccounts.filter((acc) => { + return !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized' + }).length + + const rateLimitedAccounts = platformAccounts.filter((acc) => isAccountRateLimited(acc)) + + const rateLimit1h = rateLimitedAccounts.filter((acc) => { + const minutes = getRateLimitRemainingMinutes(acc) + return minutes > 0 && minutes <= 60 + }).length + + const rateLimit5h = rateLimitedAccounts.filter((acc) => { + const minutes = getRateLimitRemainingMinutes(acc) + return minutes > 0 && minutes <= 300 + }).length + + const rateLimit12h = rateLimitedAccounts.filter((acc) => { + const minutes = getRateLimitRemainingMinutes(acc) + return minutes > 0 && minutes <= 720 + }).length + + const rateLimit1d = rateLimitedAccounts.filter((acc) => { + const minutes = getRateLimitRemainingMinutes(acc) + return minutes > 0 && minutes <= 1440 + }).length + + return { + platform: p.value, + platformLabel: p.label, + normal, + rateLimit1h, + rateLimit5h, + rateLimit12h, + rateLimit1d, + abnormal, + total: platformAccounts.length + } + }) + .filter((stat) => stat.total > 0) // 只显示有账户的平台 +}) + +// 账户统计合计 +const accountStatsTotal = computed(() => { + return accountStats.value.reduce( + (total, stat) => { + total.normal += stat.normal + total.rateLimit1h += stat.rateLimit1h + total.rateLimit5h += stat.rateLimit5h + total.rateLimit12h += stat.rateLimit12h + total.rateLimit1d += stat.rateLimit1d + total.abnormal += stat.abnormal + total.total += stat.total + return total + }, + { + normal: 0, + rateLimit1h: 0, + rateLimit5h: 0, + rateLimit12h: 0, + rateLimit1d: 0, + abnormal: 0, + total: 0 + } + ) +}) + const pageNumbers = computed(() => { const total = totalPages.value const current = currentPage.value @@ -3014,6 +3327,45 @@ const formatRateLimitTime = (minutes) => { } } +// 检查账户是否被限流 +const isAccountRateLimited = (account) => { + if (!account) return false + + // 检查 rateLimitStatus + if (account.rateLimitStatus) { + if (typeof account.rateLimitStatus === 'string' && account.rateLimitStatus === 'limited') { + return true + } + if ( + typeof account.rateLimitStatus === 'object' && + account.rateLimitStatus.isRateLimited === true + ) { + return true + } + } + + return false +} + +// 获取限流剩余时间(分钟) +const getRateLimitRemainingMinutes = (account) => { + if (!account || !account.rateLimitStatus) return 0 + + if (typeof account.rateLimitStatus === 'object' && account.rateLimitStatus.remainingMinutes) { + return account.rateLimitStatus.remainingMinutes + } + + // 如果有 rateLimitUntil 字段,计算剩余时间 + if (account.rateLimitUntil) { + const now = new Date().getTime() + const untilTime = new Date(account.rateLimitUntil).getTime() + const diff = untilTime - now + return diff > 0 ? Math.ceil(diff / 60000) : 0 + } + + return 0 +} + // 打开创建账户模态框 const openCreateAccountModal = () => { newAccountPlatform.value = null // 重置选择的平台 From 81971436e637dc565e55892b4c2e63adc8056896 Mon Sep 17 00:00:00 2001 From: IanShaw027 Date: Wed, 3 Dec 2025 19:41:37 -0800 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=E5=9C=A8=E4=BB=AA=E8=A1=A8?= =?UTF-8?q?=E7=9B=98=E6=B7=BB=E5=8A=A0=E4=BD=BF=E7=94=A8=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增后端API端点 /admin/dashboard/usage-records - 支持分页查询所有API Key的使用记录 - 自动关联API Key名称和账户名称 - 按时间倒序排列(最新的在前) - 新增仪表盘使用记录表格 - 显示时间、API Key、账户、模型、输入/输出/缓存创建/缓存读取tokens、成本 - 智能时间格式化(今天显示时分秒,昨天显示时间) - 支持加载更多记录,分页展示 - 响应式设计,支持暗黑模式 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/routes/admin/dashboard.js | 102 +++++++++ web/admin-spa/src/views/DashboardView.vue | 245 +++++++++++++++++++++- 2 files changed, 346 insertions(+), 1 deletion(-) diff --git a/src/routes/admin/dashboard.js b/src/routes/admin/dashboard.js index fe2cb440..56a4718a 100644 --- a/src/routes/admin/dashboard.js +++ b/src/routes/admin/dashboard.js @@ -704,4 +704,106 @@ router.post('/cleanup', authenticateAdmin, async (req, res) => { } }) +// 📊 获取最近的使用记录 +router.get('/usage-records', authenticateAdmin, async (req, res) => { + try { + const { limit = 100, offset = 0 } = req.query + const limitNum = Math.min(parseInt(limit) || 100, 500) // 最多500条 + const offsetNum = Math.max(parseInt(offset) || 0, 0) + + // 获取所有API Keys + const apiKeys = await apiKeyService.getAllApiKeys() + if (!apiKeys || apiKeys.length === 0) { + return res.json({ success: true, data: { records: [], total: 0 } }) + } + + // 收集所有API Key的使用记录 + const allRecords = [] + for (const key of apiKeys) { + try { + const records = await redis.getUsageRecords(key.id, 100) // 每个key最多取100条 + if (records && records.length > 0) { + // 为每条记录添加API Key信息 + const enrichedRecords = records.map((record) => ({ + ...record, + apiKeyId: key.id, + apiKeyName: key.name || 'Unnamed Key' + })) + allRecords.push(...enrichedRecords) + } + } catch (error) { + logger.error(`Failed to get usage records for key ${key.id}:`, error) + continue + } + } + + // 按时间戳倒序排序(最新的在前) + allRecords.sort((a, b) => { + const timeA = new Date(a.timestamp).getTime() + const timeB = new Date(b.timestamp).getTime() + return timeB - timeA + }) + + // 分页 + const paginatedRecords = allRecords.slice(offsetNum, offsetNum + limitNum) + + // 获取账户名称映射 + const accountIds = [...new Set(paginatedRecords.map((r) => r.accountId).filter(Boolean))] + const accountNameMap = {} + + // 并发获取所有账户名称 + await Promise.all( + accountIds.map(async (accountId) => { + try { + // 尝试从不同类型的账户中获取 + const claudeAcc = await redis.getAccount(accountId) + if (claudeAcc && claudeAcc.name) { + accountNameMap[accountId] = claudeAcc.name + return + } + + const consoleAcc = await redis.getClaudeConsoleAccount(accountId) + if (consoleAcc && consoleAcc.name) { + accountNameMap[accountId] = consoleAcc.name + return + } + + const geminiAcc = await redis.getGeminiAccount(accountId) + if (geminiAcc && geminiAcc.name) { + accountNameMap[accountId] = geminiAcc.name + return + } + + // 其他平台账户... + accountNameMap[accountId] = accountId // 降级显示ID + } catch (error) { + accountNameMap[accountId] = accountId + } + }) + ) + + // 为记录添加账户名称 + const enrichedRecords = paginatedRecords.map((record) => ({ + ...record, + accountName: record.accountId ? accountNameMap[record.accountId] || record.accountId : '-' + })) + + return res.json({ + success: true, + data: { + records: enrichedRecords, + total: allRecords.length, + limit: limitNum, + offset: offsetNum + } + }) + } catch (error) { + logger.error('❌ Failed to get usage records:', error) + return res.status(500).json({ + error: 'Failed to get usage records', + message: error.message + }) + } +}) + module.exports = router diff --git a/web/admin-spa/src/views/DashboardView.vue b/web/admin-spa/src/views/DashboardView.vue index 61ac8124..8666b600 100644 --- a/web/admin-spa/src/views/DashboardView.vue +++ b/web/admin-spa/src/views/DashboardView.vue @@ -673,6 +673,158 @@
+ + +
+
+
+

+ 最近使用记录 +

+ +
+ +
+
+

正在加载使用记录...

+
+ +
+

暂无使用记录

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 时间 + + API Key + + 账户 + + 模型 + + 输入 + + 输出 + + 缓存创建 + + 缓存读取 + + 成本 +
+ {{ formatRecordTime(record.timestamp) }} + +
+ {{ record.apiKeyName }} +
+
+
+ {{ record.accountName }} +
+
+
+ {{ record.model }} +
+
+ {{ formatNumber(record.inputTokens) }} + + {{ formatNumber(record.outputTokens) }} + + {{ formatNumber(record.cacheCreateTokens) }} + + {{ formatNumber(record.cacheReadTokens) }} + + ${{ formatCost(record.cost) }} +
+ + +
+ +
+
+
+
@@ -681,12 +833,21 @@ import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue' import { storeToRefs } from 'pinia' import { useDashboardStore } from '@/stores/dashboard' import { useThemeStore } from '@/stores/theme' +import { apiClient } from '@/config/api' +import { showToast } from '@/utils/toast' import Chart from 'chart.js/auto' const dashboardStore = useDashboardStore() const themeStore = useThemeStore() const { isDarkMode } = storeToRefs(themeStore) +// 使用记录相关 +const usageRecords = ref([]) +const usageRecordsLoading = ref(false) +const usageRecordsTotal = ref(0) +const usageRecordsOffset = ref(0) +const usageRecordsLimit = ref(50) + const { dashboardData, costsData, @@ -1477,13 +1638,95 @@ watch(accountUsageTrendData, () => { nextTick(() => createAccountUsageTrendChart()) }) +// 加载使用记录 +async function loadUsageRecords(reset = true) { + if (usageRecordsLoading.value) return + + try { + usageRecordsLoading.value = true + if (reset) { + usageRecordsOffset.value = 0 + usageRecords.value = [] + } + + const response = await apiClient.get('/admin/dashboard/usage-records', { + params: { + limit: usageRecordsLimit.value, + offset: usageRecordsOffset.value + } + }) + + if (response.success && response.data) { + if (reset) { + usageRecords.value = response.data.records || [] + } else { + usageRecords.value = [...usageRecords.value, ...(response.data.records || [])] + } + usageRecordsTotal.value = response.data.total || 0 + } + } catch (error) { + console.error('Failed to load usage records:', error) + showToast('加载使用记录失败', 'error') + } finally { + usageRecordsLoading.value = false + } +} + +// 加载更多使用记录 +async function loadMoreUsageRecords() { + usageRecordsOffset.value += usageRecordsLimit.value + await loadUsageRecords(false) +} + +// 格式化记录时间 +function formatRecordTime(timestamp) { + if (!timestamp) return '-' + const date = new Date(timestamp) + const now = new Date() + const diff = now - date + + // 如果是今天 + if (diff < 86400000 && date.getDate() === now.getDate()) { + return date.toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }) + } + + // 如果是昨天 + if (diff < 172800000 && date.getDate() === now.getDate() - 1) { + return '昨天 ' + date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) + } + + // 其他日期 + return date.toLocaleString('zh-CN', { + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }) +} + +// 格式化数字(添加千分位) +function formatNumber(num) { + if (!num || num === 0) return '0' + return num.toLocaleString('en-US') +} + +// 格式化成本 +function formatCost(cost) { + if (!cost || cost === 0) return '0.000000' + return cost.toFixed(6) +} + // 刷新所有数据 async function refreshAllData() { if (isRefreshing.value) return isRefreshing.value = true try { - await Promise.all([loadDashboardData(), refreshChartsData()]) + await Promise.all([loadDashboardData(), refreshChartsData(), loadUsageRecords()]) } finally { isRefreshing.value = false } From 3db268fff7dddf78b537e5c1f213da216a351fdc Mon Sep 17 00:00:00 2001 From: IanShaw027 Date: Wed, 3 Dec 2025 23:08:44 -0800 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E8=B4=A6?= =?UTF-8?q?=E6=88=B7=E7=AE=A1=E7=90=86=E5=92=8C=E4=BB=AA=E8=A1=A8=E7=9B=98?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改使用记录API路由路径为 /dashboard/usage-records - 增加对更多账户类型的支持(Bedrock、Azure、Droid、CCR等) - 修复Codex模型识别逻辑,避免 gpt-5-codex 系列被错误归一化 - 在账户管理页面添加状态过滤器(正常/异常) - 在账户管理页面添加限流时间过滤器(≤1h/5h/12h/1d) - 增加账户统计汇总弹窗,按平台分类展示 - 完善仪表盘使用记录展示功能,支持分页加载 - 将 logs1/ 目录添加到 .gitignore 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .gitignore | 1 + src/routes/admin/dashboard.js | 42 +++- src/routes/openaiRoutes.js | 6 +- web/admin-spa/src/views/AccountsView.vue | 273 +++++++++++++--------- web/admin-spa/src/views/DashboardView.vue | 258 +++++++++++++------- 5 files changed, 374 insertions(+), 206 deletions(-) diff --git a/.gitignore b/.gitignore index 10594f73..e4c9e9c1 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ redis_data/ # Logs directory logs/ +logs1/ *.log startup.log app.log diff --git a/src/routes/admin/dashboard.js b/src/routes/admin/dashboard.js index 56a4718a..6bcba72e 100644 --- a/src/routes/admin/dashboard.js +++ b/src/routes/admin/dashboard.js @@ -705,7 +705,7 @@ router.post('/cleanup', authenticateAdmin, async (req, res) => { }) // 📊 获取最近的使用记录 -router.get('/usage-records', authenticateAdmin, async (req, res) => { +router.get('/dashboard/usage-records', authenticateAdmin, async (req, res) => { try { const { limit = 100, offset = 0 } = req.query const limitNum = Math.min(parseInt(limit) || 100, 500) // 最多500条 @@ -774,8 +774,44 @@ router.get('/usage-records', authenticateAdmin, async (req, res) => { return } - // 其他平台账户... - accountNameMap[accountId] = accountId // 降级显示ID + const bedrockAcc = await redis.getBedrockAccount(accountId) + if (bedrockAcc && bedrockAcc.name) { + accountNameMap[accountId] = bedrockAcc.name + return + } + + const azureAcc = await redis.getAzureOpenaiAccount(accountId) + if (azureAcc && azureAcc.name) { + accountNameMap[accountId] = azureAcc.name + return + } + + const openaiResponsesAcc = await redis.getOpenaiResponsesAccount(accountId) + if (openaiResponsesAcc && openaiResponsesAcc.name) { + accountNameMap[accountId] = openaiResponsesAcc.name + return + } + + const droidAcc = await redis.getDroidAccount(accountId) + if (droidAcc && droidAcc.name) { + accountNameMap[accountId] = droidAcc.name + return + } + + const ccrAcc = await redis.getCcrAccount(accountId) + if (ccrAcc && ccrAcc.name) { + accountNameMap[accountId] = ccrAcc.name + return + } + + const openaiAcc = await redis.getOpenaiAccount(accountId) + if (openaiAcc && openaiAcc.name) { + accountNameMap[accountId] = openaiAcc.name + return + } + + // 降级显示ID + accountNameMap[accountId] = accountId } catch (error) { accountNameMap[accountId] = accountId } diff --git a/src/routes/openaiRoutes.js b/src/routes/openaiRoutes.js index 41fa1977..7faf9e87 100644 --- a/src/routes/openaiRoutes.js +++ b/src/routes/openaiRoutes.js @@ -247,9 +247,11 @@ const handleResponses = async (req, res) => { // 从请求体中提取模型和流式标志 let requestedModel = req.body?.model || null + const isCodexModel = + typeof requestedModel === 'string' && requestedModel.toLowerCase().includes('codex') - // 如果模型是 gpt-5 开头且后面还有内容(如 gpt-5-2025-08-07),则覆盖为 gpt-5 - if (requestedModel && requestedModel.startsWith('gpt-5-') && requestedModel !== 'gpt-5-codex') { + // 如果模型是 gpt-5 开头且后面还有内容(如 gpt-5-2025-08-07),并且不是 Codex 系列,则覆盖为 gpt-5 + if (requestedModel && requestedModel.startsWith('gpt-5-') && !isCodexModel) { logger.info(`📝 Model ${requestedModel} detected, normalizing to gpt-5 for Codex API`) requestedModel = 'gpt-5' req.body.model = 'gpt-5' // 同时更新请求体中的模型 diff --git a/web/admin-spa/src/views/AccountsView.vue b/web/admin-spa/src/views/AccountsView.vue index 2f6f132a..7a868b26 100644 --- a/web/admin-spa/src/views/AccountsView.vue +++ b/web/admin-spa/src/views/AccountsView.vue @@ -72,20 +72,6 @@ /> - -
-
- -
-
- +
+ + + + + + @@ -2038,8 +2050,7 @@ const bindingCounts = ref({}) // 轻量级绑定计数,用于显示"绑定: X const accountGroups = ref([]) const groupFilter = ref('all') const platformFilter = ref('all') -const statusFilter = ref('normal') // 新增:状态过滤 (normal/abnormal/all) -const rateLimitFilter = ref('all') // 新增:限流时间过滤 (all/1h/5h/12h/1d) +const statusFilter = ref('normal') // 状态过滤 (normal/rateLimited/other/all) const searchKeyword = ref('') const PAGE_SIZE_STORAGE_KEY = 'accountsPageSize' const getInitialPageSize = () => { @@ -2109,7 +2120,8 @@ const sortOptions = ref([ { value: 'dailyTokens', label: '按今日Token排序', icon: 'fa-coins' }, { value: 'dailyRequests', label: '按今日请求数排序', icon: 'fa-chart-line' }, { value: 'totalTokens', label: '按总Token排序', icon: 'fa-database' }, - { value: 'lastUsed', label: '按最后使用排序', icon: 'fa-clock' } + { value: 'lastUsed', label: '按最后使用排序', icon: 'fa-clock' }, + { value: 'rateLimitTime', label: '按限流时间排序', icon: 'fa-hourglass' } ]) const platformOptions = ref([ @@ -2128,18 +2140,12 @@ const platformOptions = ref([ const statusOptions = ref([ { value: 'normal', label: '正常', icon: 'fa-check-circle' }, - { value: 'abnormal', label: '异常', icon: 'fa-exclamation-triangle' }, + { value: 'unschedulable', label: '不可调度', icon: 'fa-ban' }, + { value: 'rateLimited', label: '限流', icon: 'fa-hourglass-half' }, + { value: 'other', label: '其他', icon: 'fa-exclamation-triangle' }, { value: 'all', label: '全部状态', icon: 'fa-list' } ]) -const rateLimitOptions = ref([ - { value: 'all', label: '全部限流', icon: 'fa-infinity' }, - { value: '1h', label: '限流≤1小时', icon: 'fa-hourglass-start' }, - { value: '5h', label: '限流≤5小时', icon: 'fa-hourglass-half' }, - { value: '12h', label: '限流≤12小时', icon: 'fa-hourglass-end' }, - { value: '1d', label: '限流≤1天', icon: 'fa-calendar-day' } -]) - const groupOptions = computed(() => { const options = [ { value: 'all', label: '所有账户', icon: 'fa-globe' }, @@ -2376,47 +2382,33 @@ const sortedAccounts = computed(() => { ) } - // 状态过滤 (normal/abnormal/all) + // 状态过滤 (normal/unschedulable/rateLimited/other/all) + // 限流: isActive && rate-limited (最高优先级) + // 正常: isActive && !rate-limited && !blocked && schedulable + // 不可调度: isActive && !rate-limited && !blocked && schedulable === false + // 其他: 非限流的(未激活 || 被阻止) if (statusFilter.value !== 'all') { sourceAccounts = sourceAccounts.filter((account) => { - const isNormal = - account.isActive && - account.status !== 'blocked' && - account.status !== 'unauthorized' && - account.schedulable !== false && - !isAccountRateLimited(account) + const isRateLimited = isAccountRateLimited(account) + const isBlocked = account.status === 'blocked' || account.status === 'unauthorized' - if (statusFilter.value === 'normal') { - return isNormal - } else if (statusFilter.value === 'abnormal') { - return !isNormal + if (statusFilter.value === 'rateLimited') { + // 限流: 激活且限流中(优先判断) + return account.isActive && isRateLimited + } else if (statusFilter.value === 'normal') { + // 正常: 激活且非限流且非阻止且可调度 + return account.isActive && !isRateLimited && !isBlocked && account.schedulable !== false + } else if (statusFilter.value === 'unschedulable') { + // 不可调度: 激活且非限流且非阻止但不可调度 + return account.isActive && !isRateLimited && !isBlocked && account.schedulable === false + } else if (statusFilter.value === 'other') { + // 其他: 非限流的异常账户(未激活或被阻止) + return !isRateLimited && (!account.isActive || isBlocked) } return true }) } - // 限流时间过滤 (all/1h/5h/12h/1d) - if (rateLimitFilter.value !== 'all') { - sourceAccounts = sourceAccounts.filter((account) => { - const rateLimitMinutes = getRateLimitRemainingMinutes(account) - if (!rateLimitMinutes || rateLimitMinutes <= 0) return false - - const minutes = Math.floor(rateLimitMinutes) - switch (rateLimitFilter.value) { - case '1h': - return minutes <= 60 - case '5h': - return minutes <= 300 - case '12h': - return minutes <= 720 - case '1d': - return minutes <= 1440 - default: - return true - } - }) - } - if (!accountsSortBy.value) return sourceAccounts const sorted = [...sourceAccounts].sort((a, b) => { @@ -2447,6 +2439,23 @@ const sortedAccounts = computed(() => { bVal = b.isActive ? 1 : 0 } + // 处理限流时间排序: 未限流优先,然后按剩余时间从小到大 + if (accountsSortBy.value === 'rateLimitTime') { + const aIsRateLimited = isAccountRateLimited(a) + const bIsRateLimited = isAccountRateLimited(b) + const aMinutes = aIsRateLimited ? getRateLimitRemainingMinutes(a) : 0 + const bMinutes = bIsRateLimited ? getRateLimitRemainingMinutes(b) : 0 + + // 未限流的排在前面 + if (!aIsRateLimited && bIsRateLimited) return -1 + if (aIsRateLimited && !bIsRateLimited) return 1 + + // 都未限流或都限流时,按剩余时间升序 + if (aMinutes < bMinutes) return -1 + if (aMinutes > bMinutes) return 1 + return 0 + } + if (aVal < bVal) return accountsSortOrder.value === 'asc' ? -1 : 1 if (aVal > bVal) return accountsSortOrder.value === 'asc' ? 1 : -1 return 0 @@ -2479,51 +2488,66 @@ const accountStats = computed(() => { .map((p) => { const platformAccounts = accounts.value.filter((acc) => acc.platform === p.value) - const normal = platformAccounts.filter((acc) => { - return ( - acc.isActive && - acc.status !== 'blocked' && - acc.status !== 'unauthorized' && - acc.schedulable !== false && - !isAccountRateLimited(acc) - ) - }).length - - const abnormal = platformAccounts.filter((acc) => { - return !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized' - }).length - + // 先筛选限流账户(优先级最高) const rateLimitedAccounts = platformAccounts.filter((acc) => isAccountRateLimited(acc)) - const rateLimit1h = rateLimitedAccounts.filter((acc) => { + // 正常: 非限流 && 激活 && 非阻止 && 可调度 + const normal = platformAccounts.filter((acc) => { + const isRateLimited = isAccountRateLimited(acc) + const isBlocked = acc.status === 'blocked' || acc.status === 'unauthorized' + return !isRateLimited && acc.isActive && !isBlocked && acc.schedulable !== false + }).length + + // 不可调度: 非限流 && 激活 && 非阻止 && 不可调度 + const unschedulable = platformAccounts.filter((acc) => { + const isRateLimited = isAccountRateLimited(acc) + const isBlocked = acc.status === 'blocked' || acc.status === 'unauthorized' + return !isRateLimited && acc.isActive && !isBlocked && acc.schedulable === false + }).length + + // 其他: 非限流的异常账户(未激活或被阻止) + const other = platformAccounts.filter((acc) => { + const isRateLimited = isAccountRateLimited(acc) + const isBlocked = acc.status === 'blocked' || acc.status === 'unauthorized' + return !isRateLimited && (!acc.isActive || isBlocked) + }).length + + const rateLimit0_1h = rateLimitedAccounts.filter((acc) => { const minutes = getRateLimitRemainingMinutes(acc) return minutes > 0 && minutes <= 60 }).length - const rateLimit5h = rateLimitedAccounts.filter((acc) => { + const rateLimit1_5h = rateLimitedAccounts.filter((acc) => { const minutes = getRateLimitRemainingMinutes(acc) - return minutes > 0 && minutes <= 300 + return minutes > 60 && minutes <= 300 }).length - const rateLimit12h = rateLimitedAccounts.filter((acc) => { + const rateLimit5_12h = rateLimitedAccounts.filter((acc) => { const minutes = getRateLimitRemainingMinutes(acc) - return minutes > 0 && minutes <= 720 + return minutes > 300 && minutes <= 720 }).length - const rateLimit1d = rateLimitedAccounts.filter((acc) => { + const rateLimit12_24h = rateLimitedAccounts.filter((acc) => { const minutes = getRateLimitRemainingMinutes(acc) - return minutes > 0 && minutes <= 1440 + return minutes > 720 && minutes <= 1440 + }).length + + const rateLimitOver24h = rateLimitedAccounts.filter((acc) => { + const minutes = getRateLimitRemainingMinutes(acc) + return minutes > 1440 }).length return { platform: p.value, platformLabel: p.label, normal, - rateLimit1h, - rateLimit5h, - rateLimit12h, - rateLimit1d, - abnormal, + unschedulable, + rateLimit0_1h, + rateLimit1_5h, + rateLimit5_12h, + rateLimit12_24h, + rateLimitOver24h, + other, total: platformAccounts.length } }) @@ -2535,21 +2559,25 @@ const accountStatsTotal = computed(() => { return accountStats.value.reduce( (total, stat) => { total.normal += stat.normal - total.rateLimit1h += stat.rateLimit1h - total.rateLimit5h += stat.rateLimit5h - total.rateLimit12h += stat.rateLimit12h - total.rateLimit1d += stat.rateLimit1d - total.abnormal += stat.abnormal + total.unschedulable += stat.unschedulable + total.rateLimit0_1h += stat.rateLimit0_1h + total.rateLimit1_5h += stat.rateLimit1_5h + total.rateLimit5_12h += stat.rateLimit5_12h + total.rateLimit12_24h += stat.rateLimit12_24h + total.rateLimitOver24h += stat.rateLimitOver24h + total.other += stat.other total.total += stat.total return total }, { normal: 0, - rateLimit1h: 0, - rateLimit5h: 0, - rateLimit12h: 0, - rateLimit1d: 0, - abnormal: 0, + unschedulable: 0, + rateLimit0_1h: 0, + rateLimit1_5h: 0, + rateLimit5_12h: 0, + rateLimit12_24h: 0, + rateLimitOver24h: 0, + other: 0, total: 0 } ) @@ -3351,8 +3379,21 @@ const isAccountRateLimited = (account) => { const getRateLimitRemainingMinutes = (account) => { if (!account || !account.rateLimitStatus) return 0 - if (typeof account.rateLimitStatus === 'object' && account.rateLimitStatus.remainingMinutes) { - return account.rateLimitStatus.remainingMinutes + if (typeof account.rateLimitStatus === 'object') { + const status = account.rateLimitStatus + if (Number.isFinite(status.minutesRemaining)) { + return Math.max(0, Math.ceil(status.minutesRemaining)) + } + if (Number.isFinite(status.remainingMinutes)) { + return Math.max(0, Math.ceil(status.remainingMinutes)) + } + if (Number.isFinite(status.remainingSeconds)) { + return Math.max(0, Math.ceil(status.remainingSeconds / 60)) + } + if (status.rateLimitResetAt) { + const diffMs = new Date(status.rateLimitResetAt).getTime() - Date.now() + return diffMs > 0 ? Math.ceil(diffMs / 60000) : 0 + } } // 如果有 rateLimitUntil 字段,计算剩余时间 diff --git a/web/admin-spa/src/views/DashboardView.vue b/web/admin-spa/src/views/DashboardView.vue index 8666b600..f27c0c00 100644 --- a/web/admin-spa/src/views/DashboardView.vue +++ b/web/admin-spa/src/views/DashboardView.vue @@ -676,21 +676,19 @@
-
-
-

- 最近使用记录 -

- -
+
+

最近使用记录

+ +
+

正在加载使用记录...

@@ -701,110 +699,94 @@
-
@@ -1914,19 +1900,25 @@ 正常 - 限流≤1h + 不可调度 - 限流≤5h + 限流0-1h - 限流≤12h + 限流1-5h - 限流≤1d + 限流5-12h - 异常 + 限流12-24h + + 限流>24h + + 其他 {{ stat.normal }} - {{ stat.rateLimit1h }} + {{ stat.unschedulable }} - {{ stat.rateLimit5h }} + {{ stat.rateLimit0_1h }} - {{ stat.rateLimit12h }} + {{ stat.rateLimit1_5h }} - {{ stat.rateLimit1d }} + {{ + stat.rateLimit5_12h + }} - {{ stat.abnormal }} + {{ + stat.rateLimit12_24h + }} + + {{ + stat.rateLimitOver24h + }} + + {{ stat.other }} - {{ - accountStatsTotal.rateLimit1h + {{ + accountStatsTotal.unschedulable }} {{ - accountStatsTotal.rateLimit5h + accountStatsTotal.rateLimit0_1h }} {{ - accountStatsTotal.rateLimit12h + accountStatsTotal.rateLimit1_5h }} {{ - accountStatsTotal.rateLimit1d + accountStatsTotal.rateLimit5_12h }} - {{ - accountStatsTotal.abnormal + {{ + accountStatsTotal.rateLimit12_24h }} + {{ + accountStatsTotal.rateLimitOver24h + }} + + {{ accountStatsTotal.other }} + {{ accountStatsTotal.total }}
+
- + - - - - - - - - @@ -813,14 +795,120 @@
时间 API Key 账户 模型 输入 输出 缓存创建 缓存读取 成本
+ {{ formatRecordTime(record.timestamp) }} -
+
+
{{ record.apiKeyName }}
-
+
+
{{ record.accountName }}
-
+
+
{{ record.model }}
+ {{ formatNumber(record.inputTokens) }} + {{ formatNumber(record.outputTokens) }} + {{ formatNumber(record.cacheCreateTokens) }} + {{ formatNumber(record.cacheReadTokens) }} ${{ formatCost(record.cost) }}
-
- +
+ +
+
+ 显示 {{ (usageRecordsCurrentPage - 1) * usageRecordsPageSize + 1 }} - + {{ Math.min(usageRecordsCurrentPage * usageRecordsPageSize, usageRecordsTotal) }} + 条,共 {{ usageRecordsTotal }} 条 +
+
+ 每页显示: + +
+
+ + +
+ + + + + + + + + +
@@ -845,8 +933,9 @@ const { isDarkMode } = storeToRefs(themeStore) const usageRecords = ref([]) const usageRecordsLoading = ref(false) const usageRecordsTotal = ref(0) -const usageRecordsOffset = ref(0) -const usageRecordsLimit = ref(50) +const usageRecordsCurrentPage = ref(1) +const usageRecordsPageSize = ref(20) +const usageRecordsPageSizeOptions = [10, 20, 50, 100] const { dashboardData, @@ -1639,29 +1728,22 @@ watch(accountUsageTrendData, () => { }) // 加载使用记录 -async function loadUsageRecords(reset = true) { +async function loadUsageRecords() { if (usageRecordsLoading.value) return try { usageRecordsLoading.value = true - if (reset) { - usageRecordsOffset.value = 0 - usageRecords.value = [] - } + const offset = (usageRecordsCurrentPage.value - 1) * usageRecordsPageSize.value const response = await apiClient.get('/admin/dashboard/usage-records', { params: { - limit: usageRecordsLimit.value, - offset: usageRecordsOffset.value + limit: usageRecordsPageSize.value, + offset: offset } }) if (response.success && response.data) { - if (reset) { - usageRecords.value = response.data.records || [] - } else { - usageRecords.value = [...usageRecords.value, ...(response.data.records || [])] - } + usageRecords.value = response.data.records || [] usageRecordsTotal.value = response.data.total || 0 } } catch (error) { @@ -1672,12 +1754,24 @@ async function loadUsageRecords(reset = true) { } } -// 加载更多使用记录 -async function loadMoreUsageRecords() { - usageRecordsOffset.value += usageRecordsLimit.value - await loadUsageRecords(false) +// 切换页码 +function handleUsageRecordsPageChange(page) { + usageRecordsCurrentPage.value = page + loadUsageRecords() } +// 切换每页数量 +function handleUsageRecordsPageSizeChange(size) { + usageRecordsPageSize.value = size + usageRecordsCurrentPage.value = 1 // 重置到第一页 + loadUsageRecords() +} + +// 计算总页数 +const usageRecordsTotalPages = computed(() => { + return Math.ceil(usageRecordsTotal.value / usageRecordsPageSize.value) || 1 +}) + // 格式化记录时间 function formatRecordTime(timestamp) { if (!timestamp) return '-' @@ -1708,12 +1802,6 @@ function formatRecordTime(timestamp) { }) } -// 格式化数字(添加千分位) -function formatNumber(num) { - if (!num || num === 0) return '0' - return num.toLocaleString('en-US') -} - // 格式化成本 function formatCost(cost) { if (!cost || cost === 0) return '0.000000' From 0b3cf5112bc7819b2e67817133daa7442be31cab Mon Sep 17 00:00:00 2001 From: IanShaw027 Date: Wed, 3 Dec 2025 23:37:17 -0800 Subject: [PATCH 4/6] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E4=BB=AA?= =?UTF-8?q?=E8=A1=A8=E7=9B=98=E4=BD=BF=E7=94=A8=E8=AE=B0=E5=BD=95=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E4=BB=A5=E9=81=BF=E5=85=8D=E4=B8=8EPR=20#753=E9=87=8D?= =?UTF-8?q?=E5=8F=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除了仪表盘中的使用记录展示功能,避免与PR #753的API Key详细使用记录功能重叠: - 移除DashboardView.vue中的使用记录表格UI及相关函数 - 移除dashboard.js中的/dashboard/usage-records接口 - 保留核心账户管理功能(账户过滤、限流状态、统计模态框等) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/routes/admin/dashboard.js | 138 --------- web/admin-spa/src/views/DashboardView.vue | 333 +--------------------- 2 files changed, 1 insertion(+), 470 deletions(-) diff --git a/src/routes/admin/dashboard.js b/src/routes/admin/dashboard.js index 6bcba72e..fe2cb440 100644 --- a/src/routes/admin/dashboard.js +++ b/src/routes/admin/dashboard.js @@ -704,142 +704,4 @@ router.post('/cleanup', authenticateAdmin, async (req, res) => { } }) -// 📊 获取最近的使用记录 -router.get('/dashboard/usage-records', authenticateAdmin, async (req, res) => { - try { - const { limit = 100, offset = 0 } = req.query - const limitNum = Math.min(parseInt(limit) || 100, 500) // 最多500条 - const offsetNum = Math.max(parseInt(offset) || 0, 0) - - // 获取所有API Keys - const apiKeys = await apiKeyService.getAllApiKeys() - if (!apiKeys || apiKeys.length === 0) { - return res.json({ success: true, data: { records: [], total: 0 } }) - } - - // 收集所有API Key的使用记录 - const allRecords = [] - for (const key of apiKeys) { - try { - const records = await redis.getUsageRecords(key.id, 100) // 每个key最多取100条 - if (records && records.length > 0) { - // 为每条记录添加API Key信息 - const enrichedRecords = records.map((record) => ({ - ...record, - apiKeyId: key.id, - apiKeyName: key.name || 'Unnamed Key' - })) - allRecords.push(...enrichedRecords) - } - } catch (error) { - logger.error(`Failed to get usage records for key ${key.id}:`, error) - continue - } - } - - // 按时间戳倒序排序(最新的在前) - allRecords.sort((a, b) => { - const timeA = new Date(a.timestamp).getTime() - const timeB = new Date(b.timestamp).getTime() - return timeB - timeA - }) - - // 分页 - const paginatedRecords = allRecords.slice(offsetNum, offsetNum + limitNum) - - // 获取账户名称映射 - const accountIds = [...new Set(paginatedRecords.map((r) => r.accountId).filter(Boolean))] - const accountNameMap = {} - - // 并发获取所有账户名称 - await Promise.all( - accountIds.map(async (accountId) => { - try { - // 尝试从不同类型的账户中获取 - const claudeAcc = await redis.getAccount(accountId) - if (claudeAcc && claudeAcc.name) { - accountNameMap[accountId] = claudeAcc.name - return - } - - const consoleAcc = await redis.getClaudeConsoleAccount(accountId) - if (consoleAcc && consoleAcc.name) { - accountNameMap[accountId] = consoleAcc.name - return - } - - const geminiAcc = await redis.getGeminiAccount(accountId) - if (geminiAcc && geminiAcc.name) { - accountNameMap[accountId] = geminiAcc.name - return - } - - const bedrockAcc = await redis.getBedrockAccount(accountId) - if (bedrockAcc && bedrockAcc.name) { - accountNameMap[accountId] = bedrockAcc.name - return - } - - const azureAcc = await redis.getAzureOpenaiAccount(accountId) - if (azureAcc && azureAcc.name) { - accountNameMap[accountId] = azureAcc.name - return - } - - const openaiResponsesAcc = await redis.getOpenaiResponsesAccount(accountId) - if (openaiResponsesAcc && openaiResponsesAcc.name) { - accountNameMap[accountId] = openaiResponsesAcc.name - return - } - - const droidAcc = await redis.getDroidAccount(accountId) - if (droidAcc && droidAcc.name) { - accountNameMap[accountId] = droidAcc.name - return - } - - const ccrAcc = await redis.getCcrAccount(accountId) - if (ccrAcc && ccrAcc.name) { - accountNameMap[accountId] = ccrAcc.name - return - } - - const openaiAcc = await redis.getOpenaiAccount(accountId) - if (openaiAcc && openaiAcc.name) { - accountNameMap[accountId] = openaiAcc.name - return - } - - // 降级显示ID - accountNameMap[accountId] = accountId - } catch (error) { - accountNameMap[accountId] = accountId - } - }) - ) - - // 为记录添加账户名称 - const enrichedRecords = paginatedRecords.map((record) => ({ - ...record, - accountName: record.accountId ? accountNameMap[record.accountId] || record.accountId : '-' - })) - - return res.json({ - success: true, - data: { - records: enrichedRecords, - total: allRecords.length, - limit: limitNum, - offset: offsetNum - } - }) - } catch (error) { - logger.error('❌ Failed to get usage records:', error) - return res.status(500).json({ - error: 'Failed to get usage records', - message: error.message - }) - } -}) - module.exports = router diff --git a/web/admin-spa/src/views/DashboardView.vue b/web/admin-spa/src/views/DashboardView.vue index f27c0c00..61ac8124 100644 --- a/web/admin-spa/src/views/DashboardView.vue +++ b/web/admin-spa/src/views/DashboardView.vue @@ -673,246 +673,6 @@
- - -
-
-

最近使用记录

- -
- -
-
-
-

正在加载使用记录...

-
- -
-

暂无使用记录

-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- 时间 - - API Key - - 账户 - - 模型 - - 输入 - - 输出 - - 缓存创建 - - 缓存读取 - - 成本 -
- {{ formatRecordTime(record.timestamp) }} - -
- {{ record.apiKeyName }} -
-
-
- {{ record.accountName }} -
-
-
- {{ record.model }} -
-
- {{ formatNumber(record.inputTokens) }} - - {{ formatNumber(record.outputTokens) }} - - {{ formatNumber(record.cacheCreateTokens) }} - - {{ formatNumber(record.cacheReadTokens) }} - - ${{ formatCost(record.cost) }} -
- - -
- -
-
- 显示 {{ (usageRecordsCurrentPage - 1) * usageRecordsPageSize + 1 }} - - {{ Math.min(usageRecordsCurrentPage * usageRecordsPageSize, usageRecordsTotal) }} - 条,共 {{ usageRecordsTotal }} 条 -
-
- 每页显示: - -
-
- - -
- - - - - - - - - -
-
-
-
-
@@ -921,22 +681,12 @@ import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue' import { storeToRefs } from 'pinia' import { useDashboardStore } from '@/stores/dashboard' import { useThemeStore } from '@/stores/theme' -import { apiClient } from '@/config/api' -import { showToast } from '@/utils/toast' import Chart from 'chart.js/auto' const dashboardStore = useDashboardStore() const themeStore = useThemeStore() const { isDarkMode } = storeToRefs(themeStore) -// 使用记录相关 -const usageRecords = ref([]) -const usageRecordsLoading = ref(false) -const usageRecordsTotal = ref(0) -const usageRecordsCurrentPage = ref(1) -const usageRecordsPageSize = ref(20) -const usageRecordsPageSizeOptions = [10, 20, 50, 100] - const { dashboardData, costsData, @@ -1727,94 +1477,13 @@ watch(accountUsageTrendData, () => { nextTick(() => createAccountUsageTrendChart()) }) -// 加载使用记录 -async function loadUsageRecords() { - if (usageRecordsLoading.value) return - - try { - usageRecordsLoading.value = true - const offset = (usageRecordsCurrentPage.value - 1) * usageRecordsPageSize.value - - const response = await apiClient.get('/admin/dashboard/usage-records', { - params: { - limit: usageRecordsPageSize.value, - offset: offset - } - }) - - if (response.success && response.data) { - usageRecords.value = response.data.records || [] - usageRecordsTotal.value = response.data.total || 0 - } - } catch (error) { - console.error('Failed to load usage records:', error) - showToast('加载使用记录失败', 'error') - } finally { - usageRecordsLoading.value = false - } -} - -// 切换页码 -function handleUsageRecordsPageChange(page) { - usageRecordsCurrentPage.value = page - loadUsageRecords() -} - -// 切换每页数量 -function handleUsageRecordsPageSizeChange(size) { - usageRecordsPageSize.value = size - usageRecordsCurrentPage.value = 1 // 重置到第一页 - loadUsageRecords() -} - -// 计算总页数 -const usageRecordsTotalPages = computed(() => { - return Math.ceil(usageRecordsTotal.value / usageRecordsPageSize.value) || 1 -}) - -// 格式化记录时间 -function formatRecordTime(timestamp) { - if (!timestamp) return '-' - const date = new Date(timestamp) - const now = new Date() - const diff = now - date - - // 如果是今天 - if (diff < 86400000 && date.getDate() === now.getDate()) { - return date.toLocaleTimeString('zh-CN', { - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }) - } - - // 如果是昨天 - if (diff < 172800000 && date.getDate() === now.getDate() - 1) { - return '昨天 ' + date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) - } - - // 其他日期 - return date.toLocaleString('zh-CN', { - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit' - }) -} - -// 格式化成本 -function formatCost(cost) { - if (!cost || cost === 0) return '0.000000' - return cost.toFixed(6) -} - // 刷新所有数据 async function refreshAllData() { if (isRefreshing.value) return isRefreshing.value = true try { - await Promise.all([loadDashboardData(), refreshChartsData(), loadUsageRecords()]) + await Promise.all([loadDashboardData(), refreshChartsData()]) } finally { isRefreshing.value = false } From 827c0f62075ac5d394d0d60960603ffa63a8b846 Mon Sep 17 00:00:00 2001 From: IanShaw027 <131567472+IanShaw027@users.noreply.github.com> Date: Fri, 5 Dec 2025 03:31:13 +0800 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=B8=A4?= =?UTF-8?q?=E7=BA=A7=E5=B9=B3=E5=8F=B0=E7=AD=9B=E9=80=89=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=88=E6=94=AF=E6=8C=81=E5=B9=B3=E5=8F=B0=E5=88=86=E7=BB=84?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 platformHierarchy 定义平台层级结构(Claude全部、OpenAI全部、Gemini全部、Droid) - 添加 platformGroupMap 映射平台组到具体平台 - 添加 platformRequestHandlers 动态处理平台请求 - 将 platformOptions 从 ref 改为 computed 支持缩进显示 - 优化 loadAccounts 使用动态平台加载替代大型 switch 语句 - 新增 getPlatformsForFilter 辅助函数 功能说明: - 支持选择"Claude(全部)"同时筛选 claude + claude-console + bedrock + ccr - 支持选择"OpenAI(全部)"同时筛选 openai + openai-responses + azure_openai - 支持选择"Gemini(全部)"同时筛选 gemini + gemini-api - 保持向后兼容,仍支持单独选择具体平台 --- web/admin-spa/src/views/AccountsView.vue | 516 ++++++++++------------- 1 file changed, 212 insertions(+), 304 deletions(-) diff --git a/web/admin-spa/src/views/AccountsView.vue b/web/admin-spa/src/views/AccountsView.vue index 7a868b26..01734bc8 100644 --- a/web/admin-spa/src/views/AccountsView.vue +++ b/web/admin-spa/src/views/AccountsView.vue @@ -2124,19 +2124,91 @@ const sortOptions = ref([ { value: 'rateLimitTime', label: '按限流时间排序', icon: 'fa-hourglass' } ]) -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: 'fab fa-google' }, - { value: 'gemini-api', label: 'Gemini API', icon: 'fa-key' }, - { value: 'openai', label: 'OpenAi', icon: 'fa-openai' }, - { value: 'azure_openai', label: 'Azure OpenAI', icon: 'fab fa-microsoft' }, - { value: 'bedrock', label: 'Bedrock', icon: 'fab fa-aws' }, - { value: 'openai-responses', label: 'OpenAI-Responses', icon: 'fa-server' }, - { value: 'ccr', label: 'CCR', icon: 'fa-code-branch' }, - { value: 'droid', label: 'Droid', icon: 'fa-robot' } -]) +// 平台层级结构定义 +const platformHierarchy = [ + { + value: 'group-claude', + label: 'Claude(全部)', + icon: 'fa-brain', + children: [ + { value: 'claude', label: 'Claude 官方/OAuth', icon: 'fa-brain' }, + { value: 'claude-console', label: 'Claude Console', icon: 'fa-terminal' }, + { value: 'bedrock', label: 'Bedrock', icon: 'fab fa-aws' }, + { value: 'ccr', label: 'CCR Relay', icon: 'fa-code-branch' } + ] + }, + { + value: 'group-openai', + label: 'Codex / OpenAI(全部)', + icon: 'fa-openai', + children: [ + { value: 'openai', label: 'OpenAI 官方', icon: 'fa-openai' }, + { value: 'openai-responses', label: 'OpenAI-Responses (Codex)', icon: 'fa-server' }, + { value: 'azure_openai', label: 'Azure OpenAI', icon: 'fab fa-microsoft' } + ] + }, + { + value: 'group-gemini', + label: 'Gemini(全部)', + icon: 'fab fa-google', + children: [ + { value: 'gemini', label: 'Gemini OAuth', icon: 'fab fa-google' }, + { value: 'gemini-api', label: 'Gemini API', icon: 'fa-key' } + ] + }, + { + value: 'group-droid', + label: 'Droid(全部)', + icon: 'fa-robot', + children: [{ value: 'droid', label: 'Droid', icon: 'fa-robot' }] + } +] + +// 平台分组映射 +const platformGroupMap = { + 'group-claude': ['claude', 'claude-console', 'bedrock', 'ccr'], + 'group-openai': ['openai', 'openai-responses', 'azure_openai'], + 'group-gemini': ['gemini', 'gemini-api'], + 'group-droid': ['droid'] +} + +// 平台请求处理器 +const platformRequestHandlers = { + claude: (params) => apiClient.get('/admin/claude-accounts', { params }), + 'claude-console': (params) => apiClient.get('/admin/claude-console-accounts', { params }), + bedrock: (params) => apiClient.get('/admin/bedrock-accounts', { params }), + gemini: (params) => apiClient.get('/admin/gemini-accounts', { params }), + openai: (params) => apiClient.get('/admin/openai-accounts', { params }), + azure_openai: (params) => apiClient.get('/admin/azure-openai-accounts', { params }), + 'openai-responses': (params) => apiClient.get('/admin/openai-responses-accounts', { params }), + ccr: (params) => apiClient.get('/admin/ccr-accounts', { params }), + droid: (params) => apiClient.get('/admin/droid-accounts', { params }), + 'gemini-api': (params) => apiClient.get('/admin/gemini-api-accounts', { params }) +} + +const allPlatformKeys = Object.keys(platformRequestHandlers) + +// 根据过滤器获取需要加载的平台列表 +const getPlatformsForFilter = (filter) => { + if (filter === 'all') return allPlatformKeys + if (platformGroupMap[filter]) return platformGroupMap[filter] + if (allPlatformKeys.includes(filter)) return [filter] + return allPlatformKeys +} + +// 平台选项(两级结构) +const platformOptions = computed(() => { + const options = [{ value: 'all', label: '所有平台', icon: 'fa-globe', indent: 0 }] + + platformHierarchy.forEach((group) => { + options.push({ ...group, indent: 0, isGroup: true }) + group.children?.forEach((child) => { + options.push({ ...child, indent: 1, parent: group.value }) + }) + }) + + return options +}) const statusOptions = ref([ { value: 'normal', label: '正常', icon: 'fa-check-circle' }, @@ -2696,190 +2768,14 @@ const loadAccounts = async (forceReload = false) => { try { // 构建查询参数(用于其他筛选情况) const params = {} - if (platformFilter.value !== 'all') { + if (platformFilter.value !== 'all' && !platformGroupMap[platformFilter.value]) { params.platform = platformFilter.value } if (groupFilter.value !== 'all') { params.groupId = groupFilter.value } - // 根据平台筛选决定需要请求哪些接口 - const requests = [] - - 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 }), - apiClient.get('/admin/openai-accounts', { params }), - apiClient.get('/admin/azure-openai-accounts', { params }), - apiClient.get('/admin/openai-responses-accounts', { params }), - apiClient.get('/admin/ccr-accounts', { params }), - apiClient.get('/admin/droid-accounts', { params }), - apiClient.get('/admin/gemini-api-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 占位 - Promise.resolve({ success: true, data: [] }), // openai 占位 - Promise.resolve({ success: true, data: [] }), // azure-openai 占位 - Promise.resolve({ success: true, data: [] }), // openai-responses 占位 - Promise.resolve({ success: true, data: [] }), // ccr 占位 - Promise.resolve({ success: true, data: [] }), // droid 占位 - Promise.resolve({ success: true, data: [] }) // gemini-api 占位 - ) - 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 占位 - Promise.resolve({ success: true, data: [] }), // openai 占位 - Promise.resolve({ success: true, data: [] }), // azure-openai 占位 - Promise.resolve({ success: true, data: [] }), // openai-responses 占位 - Promise.resolve({ success: true, data: [] }), // ccr 占位 - Promise.resolve({ success: true, data: [] }), // droid 占位 - Promise.resolve({ success: true, data: [] }) // gemini-api 占位 - ) - 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 占位 - Promise.resolve({ success: true, data: [] }), // openai 占位 - Promise.resolve({ success: true, data: [] }), // azure-openai 占位 - Promise.resolve({ success: true, data: [] }), // openai-responses 占位 - Promise.resolve({ success: true, data: [] }), // ccr 占位 - Promise.resolve({ success: true, data: [] }), // droid 占位 - Promise.resolve({ success: true, data: [] }) // gemini-api 占位 - ) - 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 }), - Promise.resolve({ success: true, data: [] }), // openai 占位 - Promise.resolve({ success: true, data: [] }), // azure-openai 占位 - Promise.resolve({ success: true, data: [] }), // openai-responses 占位 - Promise.resolve({ success: true, data: [] }), // ccr 占位 - Promise.resolve({ success: true, data: [] }), // droid 占位 - Promise.resolve({ success: true, data: [] }) // gemini-api 占位 - ) - break - case 'openai': - requests.push( - Promise.resolve({ success: true, data: [] }), // claude 占位 - Promise.resolve({ success: true, data: [] }), // claude-console 占位 - Promise.resolve({ success: true, data: [] }), // bedrock 占位 - Promise.resolve({ success: true, data: [] }), // gemini 占位 - apiClient.get('/admin/openai-accounts', { params }), - Promise.resolve({ success: true, data: [] }), // azure-openai 占位 - Promise.resolve({ success: true, data: [] }), // openai-responses 占位 - Promise.resolve({ success: true, data: [] }), // ccr 占位 - Promise.resolve({ success: true, data: [] }), // droid 占位 - Promise.resolve({ success: true, data: [] }) // gemini-api 占位 - ) - break - case 'azure_openai': - requests.push( - Promise.resolve({ success: true, data: [] }), // claude 占位 - Promise.resolve({ success: true, data: [] }), // claude-console 占位 - Promise.resolve({ success: true, data: [] }), // bedrock 占位 - Promise.resolve({ success: true, data: [] }), // gemini 占位 - Promise.resolve({ success: true, data: [] }), // openai 占位 - apiClient.get('/admin/azure-openai-accounts', { params }), - Promise.resolve({ success: true, data: [] }), // openai-responses 占位 - Promise.resolve({ success: true, data: [] }), // ccr 占位 - Promise.resolve({ success: true, data: [] }), // droid 占位 - Promise.resolve({ success: true, data: [] }) // gemini-api 占位 - ) - break - case 'openai-responses': - requests.push( - Promise.resolve({ success: true, data: [] }), // claude 占位 - Promise.resolve({ success: true, data: [] }), // claude-console 占位 - Promise.resolve({ success: true, data: [] }), // bedrock 占位 - Promise.resolve({ success: true, data: [] }), // gemini 占位 - Promise.resolve({ success: true, data: [] }), // openai 占位 - Promise.resolve({ success: true, data: [] }), // azure-openai 占位 - apiClient.get('/admin/openai-responses-accounts', { params }), - Promise.resolve({ success: true, data: [] }), // ccr 占位 - Promise.resolve({ success: true, data: [] }), // droid 占位 - Promise.resolve({ success: true, data: [] }) // gemini-api 占位 - ) - break - case 'ccr': - requests.push( - Promise.resolve({ success: true, data: [] }), // claude 占位 - Promise.resolve({ success: true, data: [] }), // claude-console 占位 - Promise.resolve({ success: true, data: [] }), // bedrock 占位 - Promise.resolve({ success: true, data: [] }), // gemini 占位 - Promise.resolve({ success: true, data: [] }), // openai 占位 - Promise.resolve({ success: true, data: [] }), // azure 占位 - Promise.resolve({ success: true, data: [] }), // openai-responses 占位 - apiClient.get('/admin/ccr-accounts', { params }), - Promise.resolve({ success: true, data: [] }), // droid 占位 - Promise.resolve({ success: true, data: [] }) // gemini-api 占位 - ) - break - case 'droid': - requests.push( - Promise.resolve({ success: true, data: [] }), // claude 占位 - Promise.resolve({ success: true, data: [] }), // claude-console 占位 - Promise.resolve({ success: true, data: [] }), // bedrock 占位 - Promise.resolve({ success: true, data: [] }), // gemini 占位 - Promise.resolve({ success: true, data: [] }), // openai 占位 - Promise.resolve({ success: true, data: [] }), // azure 占位 - Promise.resolve({ success: true, data: [] }), // openai-responses 占位 - Promise.resolve({ success: true, data: [] }), // ccr 占位 - apiClient.get('/admin/droid-accounts', { params }), - Promise.resolve({ success: true, data: [] }) // gemini-api 占位 - ) - break - case 'gemini-api': - requests.push( - Promise.resolve({ success: true, data: [] }), // claude 占位 - Promise.resolve({ success: true, data: [] }), // claude-console 占位 - Promise.resolve({ success: true, data: [] }), // bedrock 占位 - Promise.resolve({ success: true, data: [] }), // gemini 占位 - Promise.resolve({ success: true, data: [] }), // openai 占位 - Promise.resolve({ success: true, data: [] }), // azure-openai 占位 - Promise.resolve({ success: true, data: [] }), // openai-responses 占位 - Promise.resolve({ success: true, data: [] }), // ccr 占位 - Promise.resolve({ success: true, data: [] }), // droid 占位 - apiClient.get('/admin/gemini-api-accounts', { params }) - ) - break - default: - // 默认情况下返回空数组 - requests.push( - Promise.resolve({ success: true, data: [] }), - Promise.resolve({ success: true, data: [] }), - Promise.resolve({ success: true, data: [] }), - Promise.resolve({ success: true, data: [] }), - Promise.resolve({ success: true, data: [] }), - Promise.resolve({ success: true, data: [] }), - Promise.resolve({ success: true, data: [] }), - Promise.resolve({ success: true, data: [] }), - Promise.resolve({ success: true, data: [] }), - Promise.resolve({ success: true, data: [] }) - ) - break - } - } + const platformsToFetch = getPlatformsForFilter(platformFilter.value) // 使用缓存机制加载绑定计数和分组数据(不再加载完整的 API Keys 数据) await Promise.all([loadBindingCounts(forceReload), loadAccountGroups(forceReload)]) @@ -2887,125 +2783,137 @@ const loadAccounts = async (forceReload = false) => { // 后端账户API已经包含分组信息,不需要单独加载分组成员关系 // await loadGroupMembers(forceReload) - const [ - claudeData, - claudeConsoleData, - bedrockData, - geminiData, - openaiData, - azureOpenaiData, - openaiResponsesData, - ccrData, - droidData, - geminiApiData - ] = await Promise.all(requests) + const platformResults = await Promise.all( + platformsToFetch.map(async (platform) => { + const handler = platformRequestHandlers[platform] + if (!handler) { + return { platform, success: true, data: [] } + } - const allAccounts = [] - - // 获取绑定计数数据 - const counts = bindingCounts.value - - if (claudeData.success) { - const claudeAccounts = (claudeData.data || []).map((acc) => { - // 从绑定计数缓存获取数量 - const boundApiKeysCount = counts.claudeAccountId?.[acc.id] || 0 - // 后端已经包含了groupInfos,直接使用 - return { ...acc, platform: 'claude', boundApiKeysCount } - }) - allAccounts.push(...claudeAccounts) - } - - if (claudeConsoleData.success) { - const claudeConsoleAccounts = (claudeConsoleData.data || []).map((acc) => { - // 从绑定计数缓存获取数量 - const boundApiKeysCount = counts.claudeConsoleAccountId?.[acc.id] || 0 - // 后端已经包含了groupInfos,直接使用 - return { ...acc, platform: 'claude-console', boundApiKeysCount } - }) - allAccounts.push(...claudeConsoleAccounts) - } - - if (bedrockData.success) { - const bedrockAccounts = (bedrockData.data || []).map((acc) => { - // Bedrock账户暂时不支持直接绑定 - // 后端已经包含了groupInfos,直接使用 - return { ...acc, platform: 'bedrock', boundApiKeysCount: 0 } - }) - allAccounts.push(...bedrockAccounts) - } - - if (geminiData.success) { - const geminiAccounts = (geminiData.data || []).map((acc) => { - // 从绑定计数缓存获取数量 - const boundApiKeysCount = counts.geminiAccountId?.[acc.id] || 0 - // 后端已经包含了groupInfos,直接使用 - return { ...acc, platform: 'gemini', boundApiKeysCount } - }) - allAccounts.push(...geminiAccounts) - } - if (openaiData.success) { - const openaiAccounts = (openaiData.data || []).map((acc) => { - // 从绑定计数缓存获取数量 - const boundApiKeysCount = counts.openaiAccountId?.[acc.id] || 0 - // 后端已经包含了groupInfos,直接使用 - return { ...acc, platform: 'openai', boundApiKeysCount } - }) - allAccounts.push(...openaiAccounts) - } - if (azureOpenaiData && azureOpenaiData.success) { - const azureOpenaiAccounts = (azureOpenaiData.data || []).map((acc) => { - // 从绑定计数缓存获取数量 - const boundApiKeysCount = counts.azureOpenaiAccountId?.[acc.id] || 0 - // 后端已经包含了groupInfos,直接使用 - return { ...acc, platform: 'azure_openai', boundApiKeysCount } - }) - allAccounts.push(...azureOpenaiAccounts) - } - - if (openaiResponsesData && openaiResponsesData.success) { - const openaiResponsesAccounts = (openaiResponsesData.data || []).map((acc) => { - // 从绑定计数缓存获取数量 - // OpenAI-Responses账户使用 responses: 前缀 - const boundApiKeysCount = counts.openaiAccountId?.[`responses:${acc.id}`] || 0 - // 后端已经包含了groupInfos,直接使用 - return { ...acc, platform: 'openai-responses', boundApiKeysCount } - }) - allAccounts.push(...openaiResponsesAccounts) - } - - // CCR 账户 - if (ccrData && ccrData.success) { - const ccrAccounts = (ccrData.data || []).map((acc) => { - // CCR 不支持 API Key 绑定,固定为 0 - return { ...acc, platform: 'ccr', boundApiKeysCount: 0 } - }) - allAccounts.push(...ccrAccounts) - } - - // Droid 账户 - if (droidData && droidData.success) { - const droidAccounts = (droidData.data || []).map((acc) => { - // 从绑定计数缓存获取数量 - const boundApiKeysCount = counts.droidAccountId?.[acc.id] || acc.boundApiKeysCount || 0 - return { - ...acc, - platform: 'droid', - boundApiKeysCount + try { + const res = await handler(params) + return { platform, success: res?.success, data: res?.data } + } catch (error) { + console.debug(`Failed to load ${platform} accounts:`, error) + return { platform, success: false, data: [] } } }) - allAccounts.push(...droidAccounts) + ) + + const allAccounts = [] + const counts = bindingCounts.value || {} + let openaiResponsesRaw = [] + + const appendAccounts = (platform, data) => { + if (!data || data.length === 0) return + + switch (platform) { + case 'claude': { + const items = data.map((acc) => { + const boundApiKeysCount = counts.claudeAccountId?.[acc.id] || 0 + return { ...acc, platform: 'claude', boundApiKeysCount } + }) + allAccounts.push(...items) + break + } + case 'claude-console': { + const items = data.map((acc) => { + const boundApiKeysCount = counts.claudeConsoleAccountId?.[acc.id] || 0 + return { ...acc, platform: 'claude-console', boundApiKeysCount } + }) + allAccounts.push(...items) + break + } + case 'bedrock': { + const items = data.map((acc) => ({ ...acc, platform: 'bedrock', boundApiKeysCount: 0 })) + allAccounts.push(...items) + break + } + case 'gemini': { + const items = data.map((acc) => { + const boundApiKeysCount = counts.geminiAccountId?.[acc.id] || 0 + return { ...acc, platform: 'gemini', boundApiKeysCount } + }) + allAccounts.push(...items) + break + } + case 'openai': { + const items = data.map((acc) => { + const boundApiKeysCount = counts.openaiAccountId?.[acc.id] || 0 + return { ...acc, platform: 'openai', boundApiKeysCount } + }) + allAccounts.push(...items) + break + } + case 'azure_openai': { + const items = data.map((acc) => { + const boundApiKeysCount = counts.azureOpenaiAccountId?.[acc.id] || 0 + return { ...acc, platform: 'azure_openai', boundApiKeysCount } + }) + allAccounts.push(...items) + break + } + case 'openai-responses': { + openaiResponsesRaw = data + break + } + case 'ccr': { + const items = data.map((acc) => ({ ...acc, platform: 'ccr', boundApiKeysCount: 0 })) + allAccounts.push(...items) + break + } + case 'droid': { + const items = data.map((acc) => { + const boundApiKeysCount = counts.droidAccountId?.[acc.id] || acc.boundApiKeysCount || 0 + return { ...acc, platform: 'droid', boundApiKeysCount } + }) + allAccounts.push(...items) + break + } + case 'gemini-api': { + const items = data.map((acc) => { + const boundApiKeysCount = counts.geminiAccountId?.[`api:${acc.id}`] || 0 + return { ...acc, platform: 'gemini-api', boundApiKeysCount } + }) + allAccounts.push(...items) + break + } + default: + break + } } - // Gemini API 账户 - if (geminiApiData && geminiApiData.success) { - const geminiApiAccounts = (geminiApiData.data || []).map((acc) => { - // 从绑定计数缓存获取数量 - // Gemini-API账户使用 api: 前缀 - const boundApiKeysCount = counts.geminiAccountId?.[`api:${acc.id}`] || 0 - // 后端已经包含了groupInfos,直接使用 - return { ...acc, platform: 'gemini-api', boundApiKeysCount } + platformResults.forEach(({ platform, success, data }) => { + if (success) { + appendAccounts(platform, data || []) + } + }) + + if (openaiResponsesRaw.length > 0) { + let autoRecoveryConfigMap = {} + try { + const configsRes = await apiClient.get( + '/admin/openai-responses-accounts/auto-recovery-configs' + ) + if (configsRes.success && Array.isArray(configsRes.data)) { + autoRecoveryConfigMap = configsRes.data.reduce((map, config) => { + if (config?.accountId) { + map[config.accountId] = config + } + return map + }, {}) + } + } catch (error) { + console.debug('Failed to load auto-recovery configs:', error) + } + + const responsesAccounts = openaiResponsesRaw.map((acc) => { + const boundApiKeysCount = counts.openaiAccountId?.[`responses:${acc.id}`] || 0 + const autoRecoveryConfig = autoRecoveryConfigMap[acc.id] || acc.autoRecoveryConfig || null + return { ...acc, platform: 'openai-responses', boundApiKeysCount, autoRecoveryConfig } }) - allAccounts.push(...geminiApiAccounts) + + allAccounts.push(...responsesAccounts) } // 根据分组筛选器过滤账户 From a03753030c79964b3a4a586709fde7d6c762497c Mon Sep 17 00:00:00 2001 From: IanShaw027 <131567472+IanShaw027@users.noreply.github.com> Date: Fri, 5 Dec 2025 12:44:11 +0800 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20CustomDropdown=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=B1=82=E7=BA=A7=E7=BB=93=E6=9E=84=E6=98=BE?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加动态padding支持indent属性(每级缩进16px) - 添加isGroup属性支持,分组项显示为粗体带背景 - 修复暗黑模式下选中图标颜色 - 支持二级平台分类的视觉层级展示 --- .../src/components/common/CustomDropdown.vue | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/web/admin-spa/src/components/common/CustomDropdown.vue b/web/admin-spa/src/components/common/CustomDropdown.vue index 16ae391d..58d729be 100644 --- a/web/admin-spa/src/components/common/CustomDropdown.vue +++ b/web/admin-spa/src/components/common/CustomDropdown.vue @@ -41,19 +41,25 @@
{{ option.label }}