From 69a1006f4c63661b8a0ad56078654204a3cd7836 Mon Sep 17 00:00:00 2001 From: IanShaw027 Date: Wed, 3 Dec 2025 19:35:29 -0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E8=B4=A6=E6=88=B7?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E9=A1=B5=E9=9D=A2=E7=9A=84=E8=BF=87=E6=BB=A4?= =?UTF-8?q?=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 // 重置选择的平台