diff --git a/app.js b/app.js
index 1e5b807..1da18c1 100644
--- a/app.js
+++ b/app.js
@@ -30,6 +30,12 @@ class CLIProxyManager {
// Auth file filter state cache
this.currentAuthFileFilter = 'all';
this.cachedAuthFiles = [];
+ this.authFilesPagination = {
+ pageSize: 9,
+ currentPage: 1,
+ totalPages: 1
+ };
+ this.authFileStatsCache = {};
// Vertex AI credential import state
this.vertexImportState = {
@@ -616,6 +622,7 @@ class CLIProxyManager {
if (authFileInput) {
authFileInput.addEventListener('change', (e) => this.handleFileUpload(e));
}
+ this.bindAuthFilesPaginationEvents();
// Vertex AI credential import
const vertexSelectFile = document.getElementById('vertex-select-file');
@@ -3874,25 +3881,21 @@ class CLIProxyManager {
// 渲染认证文件列表
async renderAuthFiles(files, keyStats = null) {
const container = document.getElementById('auth-files-list');
- const isRuntimeOnlyFile = (file) => {
- if (!file) return false;
- const runtimeValue = file.runtime_only;
- return runtimeValue === true || runtimeValue === 'true';
- };
- const shouldDisplayDisabledGeminiCli = (file) => {
- if (!file) return false;
- const provider = typeof file.provider === 'string' ? file.provider.toLowerCase() : '';
- const type = typeof file.type === 'string' ? file.type.toLowerCase() : '';
- const isGeminiCli = provider === 'gemini-cli' || type === 'gemini-cli';
- return isGeminiCli && !isRuntimeOnlyFile(file);
- };
- const visibleFiles = Array.isArray(files) ? files.filter(file => {
- if (!file) return false;
- return shouldDisplayDisabledGeminiCli(file) || file.disabled !== true;
- }) : [];
- this.cachedAuthFiles = visibleFiles.map(file => ({ ...file }));
+ if (!container) {
+ return;
+ }
- if (visibleFiles.length === 0) {
+ const allFiles = Array.isArray(files) ? files : [];
+ const visibleFiles = allFiles.filter(file => {
+ if (!file) return false;
+ return this.shouldDisplayDisabledGeminiCli(file) || file.disabled !== true;
+ });
+ const stats = keyStats || await this.getKeyStats();
+
+ this.cachedAuthFiles = visibleFiles.map(file => ({ ...file }));
+ this.authFileStatsCache = stats || {};
+
+ if (this.cachedAuthFiles.length === 0) {
container.innerHTML = `
@@ -3902,125 +3905,130 @@ class CLIProxyManager {
`;
this.updateFilterButtons(new Set(['all']));
this.bindAuthFileFilterEvents();
- this.applyAuthFileFilterState();
+ this.applyAuthFileFilterState(false);
+ this.authFilesPagination.currentPage = 1;
+ this.authFilesPagination.totalPages = 1;
+ this.updatePaginationControls(0);
return;
}
- // 使用传入的keyStats,如果没有则获取一次
- if (!keyStats) {
- keyStats = await this.getKeyStats();
- }
- const stats = keyStats;
-
- // 收集所有文件类型(使用API返回的type字段)
- const existingTypes = new Set(['all']); // 'all' 总是存在
- visibleFiles.forEach(file => {
+ const existingTypes = new Set(['all']);
+ this.cachedAuthFiles.forEach(file => {
if (file.type) {
existingTypes.add(file.type);
}
});
- // 更新筛选按钮显示
this.updateFilterButtons(existingTypes);
+ this.bindAuthFileFilterEvents();
+ this.applyAuthFileFilterState(false);
+ this.renderAuthFilesPage(this.authFilesPagination.currentPage);
+ this.bindAuthFileActionEvents();
+ }
- container.innerHTML = visibleFiles.map(file => {
- const rawFileName = typeof file.name === 'string' ? file.name : '';
- const safeFileName = this.escapeHtml(rawFileName);
- // 认证文件的统计匹配逻辑:
- // 1. 首先尝试完整文件名匹配
- // 2. 如果没有匹配,尝试脱敏文件名匹配(去掉扩展名后的脱敏版本)
- let fileStats = stats[rawFileName] || { success: 0, failure: 0 };
+ isRuntimeOnlyAuthFile(file) {
+ if (!file) return false;
+ const runtimeValue = file.runtime_only;
+ return runtimeValue === true || runtimeValue === 'true';
+ }
- // 如果完整文件名没有统计,尝试基于文件名的脱敏版本匹配
- if (fileStats.success === 0 && fileStats.failure === 0) {
- const nameWithoutExt = rawFileName.replace(/\.[^/.]+$/, ""); // 去掉扩展名
+ shouldDisplayDisabledGeminiCli(file) {
+ if (!file) return false;
+ const provider = typeof file.provider === 'string' ? file.provider.toLowerCase() : '';
+ const type = typeof file.type === 'string' ? file.type.toLowerCase() : '';
+ const isGeminiCli = provider === 'gemini-cli' || type === 'gemini-cli';
+ return isGeminiCli && !this.isRuntimeOnlyAuthFile(file);
+ }
- const possibleSources = [];
+ resolveAuthFileStats(file, stats = {}) {
+ const rawFileName = typeof file?.name === 'string' ? file.name : '';
+ const defaultStats = { success: 0, failure: 0 };
+ if (!rawFileName) {
+ return defaultStats;
+ }
- // 规则1:尝试完整描述脱敏
- const match = nameWithoutExt.match(/^([^@]+@[^-]+)-(.+)$/);
- if (match) {
- const email = match[1];
- const projectName = match[2];
+ const fromName = stats[rawFileName];
+ if (fromName && (fromName.success > 0 || fromName.failure > 0)) {
+ return fromName;
+ }
- // 组合成完整的描述格式
- const fullDescription = `${email} (${projectName})`;
+ let fileStats = fromName || defaultStats;
+ if (fileStats.success === 0 && fileStats.failure === 0) {
+ const nameWithoutExt = rawFileName.replace(/\.[^/.]+$/, "");
+ const possibleSources = [];
- // 对完整描述进行脱敏
- const maskedDescription = this.maskApiKey(fullDescription);
- possibleSources.push(maskedDescription);
- }
-
- // 规则2:类型-个人标识.json 格式,去掉类型前缀后脱敏
- const typeMatch = nameWithoutExt.match(/^[^-]+-(.+)$/);
- if (typeMatch) {
- const personalId = typeMatch[1]; // 个人标识部分
- const maskedPersonalId = this.maskApiKey(personalId);
- possibleSources.push(maskedPersonalId);
- }
-
- // 规则3:AI Studio 特殊处理 - 对完整文件名脱敏
- if (nameWithoutExt.startsWith('aistudio-')) {
- const maskedFullName = this.maskApiKey(nameWithoutExt);
- possibleSources.push(maskedFullName);
- }
-
- // 查找第一个有统计数据的匹配
- for (const source of possibleSources) {
- if (stats[source] && (stats[source].success > 0 || stats[source].failure > 0)) {
- fileStats = stats[source];
- break;
- }
- }
+ const match = nameWithoutExt.match(/^([^@]+@[^-]+)-(.+)$/);
+ if (match) {
+ const email = match[1];
+ const projectName = match[2];
+ const fullDescription = `${email} (${projectName})`;
+ possibleSources.push(this.maskApiKey(fullDescription));
}
- // 使用API返回的文件类型
- const fileType = file.type || 'unknown';
- // 首字母大写显示类型,特殊处理 iFlow
- let typeDisplayKey;
- switch (fileType) {
- case 'qwen':
- typeDisplayKey = 'auth_files.type_qwen';
- break;
- case 'gemini':
- typeDisplayKey = 'auth_files.type_gemini';
- break;
- case 'gemini-cli':
- typeDisplayKey = 'auth_files.type_gemini-cli';
- break;
- case 'aistudio':
- typeDisplayKey = 'auth_files.type_aistudio';
- break;
- case 'claude':
- typeDisplayKey = 'auth_files.type_claude';
- break;
- case 'codex':
- typeDisplayKey = 'auth_files.type_codex';
- break;
- case 'iflow':
- typeDisplayKey = 'auth_files.type_iflow';
- break;
- case 'vertex':
- typeDisplayKey = 'auth_files.type_vertex';
- break;
- case 'empty':
- typeDisplayKey = 'auth_files.type_empty';
- break;
- default:
- typeDisplayKey = 'auth_files.type_unknown';
- break;
+ const typeMatch = nameWithoutExt.match(/^[^-]+-(.+)$/);
+ if (typeMatch) {
+ possibleSources.push(this.maskApiKey(typeMatch[1]));
}
- const typeBadge = `
${i18n.t(typeDisplayKey)}`;
- // Determine whether the entry is runtime-only
- const isRuntimeOnly = isRuntimeOnlyFile(file);
+ if (nameWithoutExt.startsWith('aistudio-')) {
+ possibleSources.push(this.maskApiKey(nameWithoutExt));
+ }
- const shouldShowMainFlag = shouldDisplayDisabledGeminiCli(file);
- const mainFlagButton = shouldShowMainFlag ? `
+ for (const source of possibleSources) {
+ if (stats[source] && (stats[source].success > 0 || stats[source].failure > 0)) {
+ return stats[source];
+ }
+ }
+ }
+
+ return fileStats;
+ }
+
+ buildAuthFileItemHtml(file) {
+ const rawFileName = typeof file?.name === 'string' ? file.name : '';
+ const safeFileName = this.escapeHtml(rawFileName);
+ const stats = this.authFileStatsCache || {};
+ const fileStats = this.resolveAuthFileStats(file, stats);
+ const fileType = file.type || 'unknown';
+ let typeDisplayKey;
+ switch (fileType) {
+ case 'qwen':
+ typeDisplayKey = 'auth_files.type_qwen';
+ break;
+ case 'gemini':
+ typeDisplayKey = 'auth_files.type_gemini';
+ break;
+ case 'gemini-cli':
+ typeDisplayKey = 'auth_files.type_gemini-cli';
+ break;
+ case 'aistudio':
+ typeDisplayKey = 'auth_files.type_aistudio';
+ break;
+ case 'claude':
+ typeDisplayKey = 'auth_files.type_claude';
+ break;
+ case 'codex':
+ typeDisplayKey = 'auth_files.type_codex';
+ break;
+ case 'iflow':
+ typeDisplayKey = 'auth_files.type_iflow';
+ break;
+ case 'vertex':
+ typeDisplayKey = 'auth_files.type_vertex';
+ break;
+ case 'empty':
+ typeDisplayKey = 'auth_files.type_empty';
+ break;
+ default:
+ typeDisplayKey = 'auth_files.type_unknown';
+ break;
+ }
+ const typeBadge = `
${i18n.t(typeDisplayKey)}`;
+ const isRuntimeOnly = this.isRuntimeOnlyAuthFile(file);
+ const shouldShowMainFlag = this.shouldDisplayDisabledGeminiCli(file);
+ const mainFlagButton = shouldShowMainFlag ? `
` : '';
-
- // Build action buttons; runtime-only entries display placeholder badge
- const actionsHtml = isRuntimeOnly ? `
+ const actionsHtml = isRuntimeOnly ? `
虚拟认证文件
` : `
@@ -4037,7 +4045,7 @@ class CLIProxyManager {
`;
- return `
+ return `
${typeBadge}${safeFileName}
@@ -4059,19 +4067,110 @@ class CLIProxyManager {
`;
- }).join('');
-
- // 绑定筛选按钮事件
- this.bindAuthFileFilterEvents();
-
- // 绑定认证文件操作按钮事件(使用事件委托)
- this.bindAuthFileActionEvents();
-
- // Reapply current filter state
- this.applyAuthFileFilterState();
}
- // 更新筛选按钮显示
+ getFilteredAuthFiles(filterType = this.currentAuthFileFilter) {
+ const files = Array.isArray(this.cachedAuthFiles) ? this.cachedAuthFiles : [];
+ const filterValue = (filterType || 'all').toLowerCase();
+ if (filterValue === 'all') {
+ return files;
+ }
+ return files.filter(file => {
+ const type = (file?.type || 'unknown').toLowerCase();
+ return type === filterValue;
+ });
+ }
+
+ renderAuthFilesPage(page = null) {
+ const container = document.getElementById('auth-files-list');
+ if (!container) return;
+
+ const pageSize = this.authFilesPagination?.pageSize || 9;
+ const filteredFiles = this.getFilteredAuthFiles();
+ const totalItems = filteredFiles.length;
+
+ if (totalItems === 0) {
+ container.innerHTML = `
+
+
+
${i18n.t('auth_files.empty_title')}
+
${i18n.t('auth_files.empty_desc')}
+
+ `;
+ this.authFilesPagination.currentPage = 1;
+ this.authFilesPagination.totalPages = 1;
+ this.updatePaginationControls(0);
+ return;
+ }
+
+ const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));
+ let targetPage = typeof page === 'number' ? page : (this.authFilesPagination.currentPage || 1);
+ targetPage = Math.max(1, Math.min(targetPage, totalPages));
+
+ this.authFilesPagination.currentPage = targetPage;
+ this.authFilesPagination.totalPages = totalPages;
+
+ const startIndex = (targetPage - 1) * pageSize;
+ const pageFiles = filteredFiles.slice(startIndex, startIndex + pageSize);
+ container.innerHTML = pageFiles.map(file => this.buildAuthFileItemHtml(file)).join('');
+
+ this.updatePaginationControls(totalItems);
+ }
+
+ updatePaginationControls(totalItems = 0) {
+ const paginationContainer = document.getElementById('auth-files-pagination');
+ const infoEl = document.getElementById('auth-files-pagination-info');
+ if (!paginationContainer || !infoEl) return;
+
+ const prevBtn = paginationContainer.querySelector('button[data-action="prev"]');
+ const nextBtn = paginationContainer.querySelector('button[data-action="next"]');
+ const pageSize = this.authFilesPagination?.pageSize || 9;
+ const totalPages = this.authFilesPagination?.totalPages || 1;
+ const currentPage = Math.min(this.authFilesPagination?.currentPage || 1, totalPages);
+ const shouldShow = totalItems > pageSize;
+
+ paginationContainer.style.display = shouldShow ? 'flex' : 'none';
+
+ const infoParams = totalItems === 0
+ ? { current: 0, total: 0, count: 0 }
+ : { current: currentPage, total: totalPages, count: totalItems };
+ infoEl.textContent = i18n.t('auth_files.pagination_info', infoParams);
+
+ if (prevBtn) {
+ prevBtn.disabled = currentPage <= 1;
+ }
+ if (nextBtn) {
+ nextBtn.disabled = currentPage >= totalPages;
+ }
+ }
+
+ bindAuthFilesPaginationEvents() {
+ const container = document.getElementById('auth-files-pagination');
+ if (!container) return;
+
+ const oldListener = container._paginationListener;
+ if (oldListener) {
+ container.removeEventListener('click', oldListener);
+ }
+
+ const listener = (event) => {
+ const button = event.target.closest('button[data-action]');
+ if (!button || !container.contains(button)) return;
+ event.preventDefault();
+ const action = button.dataset.action;
+ const currentPage = this.authFilesPagination?.currentPage || 1;
+ if (action === 'prev') {
+ this.renderAuthFilesPage(currentPage - 1);
+ } else if (action === 'next') {
+ this.renderAuthFilesPage(currentPage + 1);
+ }
+ };
+
+ container._paginationListener = listener;
+ container.addEventListener('click', listener);
+ }
+
+// 更新筛选按钮显示
updateFilterButtons(existingTypes) {
const filterContainer = document.querySelector('.auth-file-filter');
if (!filterContainer) return;
@@ -4143,28 +4242,22 @@ class CLIProxyManager {
}
// 处理筛选按钮点击
- handleFilterClick(clickedBtn) {
+ handleFilterClick(clickedBtn, options = {}) {
+ if (!clickedBtn) return;
+ const { skipRender = false } = options;
const filterBtns = document.querySelectorAll('.auth-file-filter .filter-btn');
-
- // 更新按钮状态
+
filterBtns.forEach(b => b.classList.remove('active'));
clickedBtn.classList.add('active');
- // 获取筛选类型
const filterType = clickedBtn.dataset.type;
this.currentAuthFileFilter = filterType || 'all';
-
- // 筛选文件
- const fileItems = document.querySelectorAll('.file-item');
- fileItems.forEach(item => {
- if (filterType === 'all' || item.dataset.fileType === filterType) {
- item.classList.remove('hidden');
- } else {
- item.classList.add('hidden');
- }
- });
- // 更新筛选按钮文本(以防语言切换后新按钮未刷新)
+ if (!skipRender) {
+ this.authFilesPagination.currentPage = 1;
+ this.renderAuthFilesPage(1);
+ }
+
this.refreshFilterButtonTexts();
}
@@ -4205,7 +4298,7 @@ class CLIProxyManager {
}
// Apply current filter selection to the list
- applyAuthFileFilterState() {
+ applyAuthFileFilterState(shouldRender = false) {
const filterContainer = document.querySelector('.auth-file-filter');
if (!filterContainer) return;
@@ -4228,7 +4321,7 @@ class CLIProxyManager {
}
if (targetButton) {
- this.handleFilterClick(targetButton);
+ this.handleFilterClick(targetButton, { skipRender: !shouldRender });
}
}
@@ -4241,26 +4334,10 @@ class CLIProxyManager {
const removalSet = new Set(filenames);
this.cachedAuthFiles = (this.cachedAuthFiles || []).filter(file => file && !removalSet.has(file.name));
- const container = document.getElementById('auth-files-list');
- if (!container) return;
-
- const fileItems = container.querySelectorAll('.file-item');
- fileItems.forEach(item => {
- const fileNameAttr = item.getAttribute('data-file-name');
- if (fileNameAttr && removalSet.has(fileNameAttr)) {
- item.remove();
- }
- });
-
- if (!container.querySelector('.file-item')) {
- container.innerHTML = `
-
-
-
${i18n.t('auth_files.empty_title')}
-
${i18n.t('auth_files.empty_desc')}
-
- `;
+ if (!this.cachedAuthFiles.length) {
+ this.authFilesPagination.currentPage = 1;
}
+ this.renderAuthFilesPage(this.authFilesPagination.currentPage);
}
// 刷新筛选按钮文本(根据 data-i18n-text)
diff --git a/i18n.js b/i18n.js
index d185495..fc2ed59 100644
--- a/i18n.js
+++ b/i18n.js
@@ -244,6 +244,9 @@ const i18n = {
'auth_files.delete_filtered_partial': '{type} 认证文件删除完成,成功 {success} 个,失败 {failed} 个',
'auth_files.delete_filtered_none': '当前筛选类型 ({type}) 下没有可删除的认证文件',
'auth_files.files_count': '个文件',
+ 'auth_files.pagination_prev': '上一页',
+ 'auth_files.pagination_next': '下一页',
+ 'auth_files.pagination_info': '第 {current} / {total} 页 · 共 {count} 个文件',
'auth_files.filter_all': '全部',
'auth_files.filter_qwen': 'Qwen',
'auth_files.filter_gemini': 'Gemini',
@@ -737,6 +740,9 @@ const i18n = {
'auth_files.delete_filtered_partial': '{type} auth files deletion finished: {success} succeeded, {failed} failed',
'auth_files.delete_filtered_none': 'No deletable auth files under the current filter ({type})',
'auth_files.files_count': 'files',
+ 'auth_files.pagination_prev': 'Previous',
+ 'auth_files.pagination_next': 'Next',
+ 'auth_files.pagination_info': 'Page {current} / {total} · {count} files',
'auth_files.filter_all': 'All',
'auth_files.filter_qwen': 'Qwen',
'auth_files.filter_gemini': 'Gemini',
diff --git a/index.html b/index.html
index 2f9ddd2..97c1077 100644
--- a/index.html
+++ b/index.html
@@ -520,6 +520,17 @@
diff --git a/styles.css b/styles.css
index 6c5e3bd..06e318c 100644
--- a/styles.css
+++ b/styles.css
@@ -1567,6 +1567,33 @@ input:checked+.slider:before {
gap: 15px;
}
+.pagination-controls {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 12px;
+ margin-top: 20px;
+ flex-wrap: wrap;
+}
+
+.pagination-btn {
+ gap: 6px;
+ min-width: 120px;
+ justify-content: center;
+ display: inline-flex;
+ align-items: center;
+}
+
+.pagination-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.pagination-info {
+ font-size: 0.95rem;
+ color: var(--text-secondary);
+}
+
/* 响应式:中等屏幕2列 */
@media (max-width: 1400px) {
.file-grid {