diff --git a/web/admin-spa/src/views/AccountsView.vue b/web/admin-spa/src/views/AccountsView.vue index ddfd4a95..21c0dd6e 100644 --- a/web/admin-spa/src/views/AccountsView.vue +++ b/web/admin-spa/src/views/AccountsView.vue @@ -7,7 +7,7 @@ 账户管理

- 管理您的 Claude、Gemini、OpenAI、Azure OpenAI、OpenAI-Responses 与 CCR 账户及代理配置 + 管理 Claude、Gemini、OpenAI 等账户与代理配置

@@ -58,6 +58,31 @@ />
+ +
+
+
+ + + +
+
+ + +
-
- - + + + @@ -284,7 +309,7 @@ - +
@@ -1148,6 +1173,94 @@
+
+
+ + 共 {{ sortedAccounts.length }} 条记录 + +
+ 每页显示 + + +
+
+ +
+ + +
+ + + + + + + + + +
+ + +
+
+ { + const saved = localStorage.getItem(PAGE_SIZE_STORAGE_KEY) + if (saved) { + const parsedSize = parseInt(saved, 10) + if ([10, 20, 50, 100].includes(parsedSize)) { + return parsedSize + } + } + return 10 +} +const pageSizeOptions = [10, 20, 50, 100] +const pageSize = ref(getInitialPageSize()) +const currentPage = ref(1) // 缓存状态标志 const apiKeysLoaded = ref(false) @@ -1265,9 +1393,78 @@ const newAccountPlatform = ref(null) // 跟踪新建账户选择的平台 const showEditAccountModal = ref(false) const editingAccount = ref(null) +const collectAccountSearchableStrings = (account) => { + const values = new Set() + + const baseFields = [ + account?.name, + account?.email, + account?.accountName, + account?.owner, + account?.ownerName, + account?.ownerDisplayName, + account?.displayName, + account?.username, + account?.identifier, + account?.alias, + account?.title, + account?.label + ] + + baseFields.forEach((field) => { + if (typeof field === 'string') { + const trimmed = field.trim() + if (trimmed) { + values.add(trimmed) + } + } + }) + + if (Array.isArray(account?.groupInfos)) { + account.groupInfos.forEach((group) => { + if (group && typeof group.name === 'string') { + const trimmed = group.name.trim() + if (trimmed) { + values.add(trimmed) + } + } + }) + } + + Object.entries(account || {}).forEach(([key, value]) => { + if (typeof value === 'string') { + const lowerKey = key.toLowerCase() + if (lowerKey.includes('name') || lowerKey.includes('email')) { + const trimmed = value.trim() + if (trimmed) { + values.add(trimmed) + } + } + } + }) + + return Array.from(values) +} + +const accountMatchesKeyword = (account, normalizedKeyword) => { + if (!normalizedKeyword) return true + return collectAccountSearchableStrings(account).some((value) => + value.toLowerCase().includes(normalizedKeyword) + ) +} + // 计算排序后的账户列表 const sortedAccounts = computed(() => { - const sourceAccounts = accounts.value + let sourceAccounts = accounts.value + + const keyword = searchKeyword.value.trim() + if (keyword) { + const normalizedKeyword = keyword.toLowerCase() + sourceAccounts = sourceAccounts.filter((account) => + accountMatchesKeyword(account, normalizedKeyword) + ) + } + if (!accountsSortBy.value) return sourceAccounts const sorted = [...sourceAccounts].sort((a, b) => { @@ -1306,6 +1503,68 @@ const sortedAccounts = computed(() => { return sorted }) +const totalPages = computed(() => { + const total = sortedAccounts.value.length + return Math.ceil(total / pageSize.value) || 0 +}) + +const pageNumbers = computed(() => { + const total = totalPages.value + const current = currentPage.value + const pages = [] + + if (total <= 7) { + for (let i = 1; i <= total; i++) { + pages.push(i) + } + } else { + let start = Math.max(1, current - 2) + let end = Math.min(total, current + 2) + + if (current <= 3) { + end = 5 + } else if (current >= total - 2) { + start = total - 4 + } + + for (let i = start; i <= end; i++) { + pages.push(i) + } + } + + return pages +}) + +const shouldShowFirstPage = computed(() => { + const pages = pageNumbers.value + if (pages.length === 0) return false + return pages[0] > 1 +}) + +const shouldShowLastPage = computed(() => { + const pages = pageNumbers.value + if (pages.length === 0) return false + return pages[pages.length - 1] < totalPages.value +}) + +const showLeadingEllipsis = computed(() => { + const pages = pageNumbers.value + if (pages.length === 0) return false + return shouldShowFirstPage.value && pages[0] > 2 +}) + +const showTrailingEllipsis = computed(() => { + const pages = pageNumbers.value + if (pages.length === 0) return false + return shouldShowLastPage.value && pages[pages.length - 1] < totalPages.value - 1 +}) + +const paginatedAccounts = computed(() => { + const start = (currentPage.value - 1) * pageSize.value + const end = start + pageSize.value + return sortedAccounts.value.slice(start, end) +}) + // 加载账户列表 const loadAccounts = async (forceReload = false) => { accountsLoading.value = true @@ -1628,6 +1887,11 @@ const formatLastUsed = (dateString) => { return date.toLocaleDateString('zh-CN') } +const clearSearch = () => { + searchKeyword.value = '' + currentPage.value = 1 +} + // 加载API Keys列表(缓存版本) const loadApiKeys = async (forceReload = false) => { if (!forceReload && apiKeysLoaded.value) { @@ -1672,11 +1936,13 @@ const clearCache = () => { // 按平台筛选账户 const filterByPlatform = () => { + currentPage.value = 1 loadAccounts() } // 按分组筛选账户 const filterByGroup = () => { + currentPage.value = 1 loadAccounts() } @@ -2405,6 +2671,23 @@ const calculateDailyCost = (account) => { // await toggleSchedulable(account) // } +watch(searchKeyword, () => { + currentPage.value = 1 +}) + +watch(pageSize, (newSize) => { + localStorage.setItem(PAGE_SIZE_STORAGE_KEY, newSize.toString()) +}) + +watch( + () => sortedAccounts.value.length, + () => { + if (currentPage.value > totalPages.value) { + currentPage.value = totalPages.value || 1 + } + } +) + // 监听排序选择变化 watch(accountSortBy, (newVal) => { const fieldMap = { diff --git a/web/admin-spa/src/views/ApiKeysView.vue b/web/admin-spa/src/views/ApiKeysView.vue index 36ad6642..dad79779 100644 --- a/web/admin-spa/src/views/ApiKeysView.vue +++ b/web/admin-spa/src/views/ApiKeysView.vue @@ -125,7 +125,7 @@