diff --git a/app.js b/app.js index a4b6998..2b9e615 100644 --- a/app.js +++ b/app.js @@ -3071,6 +3071,17 @@ class CLIProxyManager { // 获取使用统计,按 source 聚合 const stats = await this.getKeyStats(); + // 收集所有文件类型(使用API返回的type字段) + const existingTypes = new Set(['all']); // 'all' 总是存在 + files.forEach(file => { + if (file.type) { + existingTypes.add(file.type); + } + }); + + // 更新筛选按钮显示 + this.updateFilterButtons(existingTypes); + container.innerHTML = files.map(file => { // 认证文件的统计匹配逻辑: // 1. 首先尝试完整文件名匹配 @@ -3118,32 +3129,145 @@ class CLIProxyManager { } } + // 使用API返回的文件类型 + const fileType = file.type || 'unknown'; + // 首字母大写显示类型,特殊处理 iFlow + let typeDisplay; + if (fileType === 'iflow') { + typeDisplay = 'iFlow'; + } else { + typeDisplay = fileType.charAt(0).toUpperCase() + fileType.slice(1); + } + const typeBadge = `${typeDisplay}`; + return ` -
+
-
${file.name}
-
${i18n.t('auth_files.file_size')}: ${this.formatFileSize(file.size)}
-
${i18n.t('auth_files.file_modified')}: ${new Date(file.modtime).toLocaleString(i18n.currentLanguage === 'zh-CN' ? 'zh-CN' : 'en-US')}
-
- - ${i18n.t('stats.success')}: ${fileStats.success} - - - ${i18n.t('stats.failure')}: ${fileStats.failure} - +
${typeBadge}${file.name}
+
+ ${i18n.t('auth_files.file_modified')}: ${new Date(file.modtime).toLocaleString(i18n.currentLanguage === 'zh-CN' ? 'zh-CN' : 'en-US')} + ${i18n.t('auth_files.file_size')}: ${this.formatFileSize(file.size)} +
+ -
-
- -
`; }).join(''); + + // 绑定筛选按钮事件 + this.bindAuthFileFilterEvents(); + } + + // 更新筛选按钮显示 + updateFilterButtons(existingTypes) { + const filterContainer = document.querySelector('.auth-file-filter'); + if (!filterContainer) return; + + // 预定义的按钮顺序和显示文本 + const predefinedTypes = [ + { type: 'all', label: 'All' }, + { type: 'qwen', label: 'Qwen' }, + { type: 'gemini', label: 'Gemini' }, + { type: 'claude', label: 'Claude' }, + { type: 'codex', label: 'Codex' }, + { type: 'iflow', label: 'iFlow' }, + { type: 'empty', label: 'Empty' } + ]; + + // 获取现有按钮 + const existingButtons = filterContainer.querySelectorAll('.filter-btn'); + const existingButtonTypes = new Set(); + existingButtons.forEach(btn => { + existingButtonTypes.add(btn.dataset.type); + }); + + // 显示/隐藏预定义按钮 + existingButtons.forEach(btn => { + const btnType = btn.dataset.type; + if (existingTypes.has(btnType)) { + btn.style.display = 'inline-block'; + } else { + btn.style.display = 'none'; + } + }); + + // 为未知类型添加新按钮 + const predefinedTypeSet = new Set(predefinedTypes.map(t => t.type)); + existingTypes.forEach(type => { + if (type !== 'all' && !predefinedTypeSet.has(type) && !existingButtonTypes.has(type)) { + // 创建新按钮 + const btn = document.createElement('button'); + btn.className = 'filter-btn'; + btn.dataset.type = type; + // 首字母大写 + btn.textContent = type.charAt(0).toUpperCase() + type.slice(1); + + // 插入到 Empty 按钮之前(如果存在) + const emptyBtn = filterContainer.querySelector('[data-type="empty"]'); + if (emptyBtn) { + filterContainer.insertBefore(btn, emptyBtn); + } else { + filterContainer.appendChild(btn); + } + + // 添加点击事件 + btn.addEventListener('click', (e) => { + this.handleFilterClick(e.target); + }); + } + }); + } + + // 处理筛选按钮点击 + handleFilterClick(clickedBtn) { + 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; + + // 筛选文件 + const fileItems = document.querySelectorAll('.file-item'); + fileItems.forEach(item => { + if (filterType === 'all' || item.dataset.fileType === filterType) { + item.style.display = 'flex'; + } else { + item.style.display = 'none'; + } + }); + } + + // 绑定认证文件筛选事件 + bindAuthFileFilterEvents() { + const filterBtns = document.querySelectorAll('.auth-file-filter .filter-btn'); + filterBtns.forEach(btn => { + btn.addEventListener('click', (e) => { + this.handleFilterClick(e.target); + }); + }); } // 格式化文件大小 @@ -3198,6 +3322,104 @@ class CLIProxyManager { event.target.value = ''; } + // 显示认证文件详细信息 + async showAuthFileDetails(filename) { + try { + const response = await fetch(`${this.apiUrl}/auth-files/download?name=${encodeURIComponent(filename)}`, { + headers: { + 'Authorization': `Bearer ${this.managementKey}` + } + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + const jsonData = await response.json(); + + // 格式化JSON数据 + const formattedJson = JSON.stringify(jsonData, null, 2); + + // 显示模态框 + this.showJsonModal(filename, formattedJson); + } catch (error) { + this.showNotification(`读取文件详情失败: ${error.message}`, 'error'); + } + } + + // 显示JSON模态框 + showJsonModal(filename, jsonContent) { + // 创建模态框HTML + const modalHtml = ` +
+
+
+

${filename}

+ +
+
+
${this.escapeHtml(jsonContent)}
+
+ +
+
+ `; + + // 移除旧的模态框(如果存在) + const oldModal = document.getElementById('json-modal'); + if (oldModal) { + oldModal.remove(); + } + + // 添加新模态框 + document.body.insertAdjacentHTML('beforeend', modalHtml); + + // 添加点击背景关闭功能 + const modal = document.getElementById('json-modal'); + modal.addEventListener('click', (e) => { + if (e.target === modal) { + this.closeJsonModal(); + } + }); + } + + // 关闭JSON模态框 + closeJsonModal() { + const modal = document.getElementById('json-modal'); + if (modal) { + modal.remove(); + } + } + + // 复制JSON内容 + copyJsonContent() { + const jsonContent = document.querySelector('.json-content'); + if (jsonContent) { + const text = jsonContent.textContent; + navigator.clipboard.writeText(text).then(() => { + this.showNotification('内容已复制到剪贴板', 'success'); + }).catch(() => { + this.showNotification('复制失败', 'error'); + }); + } + } + + // HTML转义函数 + escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + // 下载认证文件 async downloadAuthFile(filename) { try { diff --git a/i18n.js b/i18n.js index c9580de..391f4ec 100644 --- a/i18n.js +++ b/i18n.js @@ -40,6 +40,7 @@ const i18n = { 'common.alias': '别名', 'common.failure': '失败', 'common.unknown_error': '未知错误', + 'common.copy': '复制', // 页面标题 'title.main': 'CLI Proxy API Management Center', @@ -466,6 +467,7 @@ const i18n = { 'common.alias': 'Alias', 'common.failure': 'Failure', 'common.unknown_error': 'Unknown error', + 'common.copy': 'Copy', // Page titles 'title.main': 'CLI Proxy API Management Center', diff --git a/index.html b/index.html index 885105b..223804c 100644 --- a/index.html +++ b/index.html @@ -418,9 +418,21 @@
-
-

认证文件

+
+
+

认证文件

+ +
+ + + + + + + +
+
-
+
diff --git a/styles.css b/styles.css index 2b71f64..1d87d29 100644 --- a/styles.css +++ b/styles.css @@ -1195,6 +1195,76 @@ body { font-size: 14px; } +/* 小按钮样式 - 与stat-badge高度一致 */ +.btn-small { + padding: 4px 12px; + border: 1px solid transparent; + border-radius: 4px; + cursor: pointer; + font-size: 13px; + font-weight: 500; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 5px; + min-height: 28px; + white-space: nowrap; +} + +.btn-small.btn-primary { + background: var(--primary-color); + color: white; + border-color: var(--primary-color); +} + +.btn-small.btn-primary:hover { + background: var(--primary-hover); + box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3); + transform: translateY(-1px); +} + +.btn-small.btn-danger { + background: #ef4444; + color: white; + border-color: #ef4444; +} + +.btn-small.btn-danger:hover { + background: #dc2626; + box-shadow: 0 2px 4px rgba(239, 68, 68, 0.3); + transform: translateY(-1px); +} + +.btn-small.btn-info { + background: #06b6d4; + color: white; + border-color: #06b6d4; +} + +.btn-small.btn-info:hover { + background: #0891b2; + box-shadow: 0 2px 4px rgba(6, 182, 212, 0.3); + transform: translateY(-1px); +} + +.btn-small i { + font-size: 13px; +} + +/* 响应式调整 */ +@media (max-width: 768px) { + .btn-small { + padding: 3px 10px; + font-size: 12px; + min-height: 26px; + } + + .btn-small i { + font-size: 12px; + } +} + /* 表单元素 */ .form-group { margin-bottom: 18px; @@ -1454,10 +1524,168 @@ input:checked+.slider:before { /* 认证文件列表填满页面,保留版本信息空间 */ max-height: calc(100vh - 300px); /* 减去导航栏、padding和版本信息的高度 */ overflow-y: auto; + margin-right: -12px; /* 向右扩展 */ + padding-right: 12px; /* 滚动条不贴着最右侧卡片,保持左右边距一致 */ +} + +/* 认证文件Grid布局 */ +.file-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 15px; +} + +/* 响应式:中等屏幕2列 */ +@media (max-width: 1400px) { + .file-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +/* 响应式:小屏幕单列 */ +@media (max-width: 768px) { + .file-grid { + grid-template-columns: 1fr; + } +} + +/* 带筛选器的卡片头部布局 */ +.card-header-with-filter { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 15px; +} + +.card-header-with-filter .header-left { + display: flex; + align-items: center; + gap: 20px; + flex: 1; + flex-wrap: wrap; +} + +.card-header-with-filter .header-left h3 { + margin: 0; + white-space: nowrap; +} + +/* 认证文件筛选按钮 */ +.auth-file-filter { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.filter-btn { + padding: 6px 16px; + border: 1px solid var(--border-primary); + background: var(--bg-quaternary); + color: var(--text-secondary); + border-radius: 6px; + cursor: pointer; + font-size: 0.9rem; + transition: all 0.2s ease; +} + +.filter-btn:hover { + background: var(--bg-tertiary); + border-color: var(--border-secondary); +} + +.filter-btn.active { + background: var(--primary-color); + color: white; + border-color: var(--primary-color); +} + +/* 文件类型标签 */ +.file-type-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; + margin-right: 8px; +} + +.file-type-badge.qwen { + background: #e8f5e9; + color: #2e7d32; +} + +[data-theme="dark"] .file-type-badge.qwen { + background: #1b5e20; + color: #81c784; +} + +.file-type-badge.gemini { + background: #e3f2fd; + color: #1565c0; +} + +[data-theme="dark"] .file-type-badge.gemini { + background: #0d47a1; + color: #64b5f6; +} + +.file-type-badge.claude { + background: #fce4ec; + color: #c2185b; +} + +[data-theme="dark"] .file-type-badge.claude { + background: #880e4f; + color: #f48fb1; +} + +.file-type-badge.codex { + background: #fff3e0; + color: #ef6c00; +} + +[data-theme="dark"] .file-type-badge.codex { + background: #e65100; + color: #ffb74d; +} + +.file-type-badge.iflow { + background: #f3e5f5; + color: #7b1fa2; +} + +[data-theme="dark"] .file-type-badge.iflow { + background: #4a148c; + color: #ce93d8; +} + +.file-type-badge.empty { + background: #f5f5f5; + color: #616161; +} + +[data-theme="dark"] .file-type-badge.empty { + background: #424242; + color: #bdbdbd; +} + +/* 未知类型通用样式 */ +.file-type-badge:not(.qwen):not(.gemini):not(.claude):not(.codex):not(.iflow):not(.empty) { + background: #f0f0f0; + color: #666666; + border: 1px dashed #999999; +} + +[data-theme="dark"] .file-type-badge:not(.qwen):not(.gemini):not(.claude):not(.codex):not(.iflow):not(.empty) { + background: #3a3a3a; + color: #aaaaaa; + border: 1px dashed #666666; } .provider-list { /* 默认不限制高度,动态设置 */ + min-height: 0; } .key-item, @@ -1469,8 +1697,7 @@ input:checked+.slider:before { padding: 15px; margin-bottom: 10px; display: flex; - justify-content: space-between; - align-items: center; + flex-direction: column; transition: all 0.3s ease; } @@ -1484,16 +1711,42 @@ input:checked+.slider:before { .item-content { flex: 1; + width: 100%; +} + +.item-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 12px; + flex-wrap: wrap; + gap: 10px; +} + +.item-stats { + display: flex; + gap: 8px; + flex-wrap: wrap; + align-items: center; } .item-actions { display: flex; - gap: 8px; + gap: 6px; + flex-wrap: wrap; + align-items: center; } .item-title { font-weight: 600; color: var(--text-secondary); + margin-bottom: 6px; +} + +.item-meta { + display: flex; + gap: 16px; + flex-wrap: wrap; margin-bottom: 4px; } @@ -1572,6 +1825,150 @@ input:checked+.slider:before { color: var(--text-tertiary); } +/* JSON模态框 */ +.json-modal { + position: fixed; + z-index: 2000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: var(--bg-modal); + backdrop-filter: blur(5px); + display: flex; + align-items: center; + justify-content: center; + animation: fadeIn 0.2s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.json-modal-content { + background-color: var(--bg-secondary); + border-radius: 12px; + width: 90%; + max-width: 800px; + max-height: 90vh; + box-shadow: var(--shadow-modal); + display: flex; + flex-direction: column; + animation: slideUp 0.3s ease; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.json-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 24px; + border-bottom: 1px solid var(--border-primary); +} + +.json-modal-header h3 { + margin: 0; + color: var(--text-secondary); + font-size: 1.1rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 10px; +} + +.json-modal-close { + background: none; + border: none; + color: var(--text-tertiary); + font-size: 24px; + cursor: pointer; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: all 0.2s ease; +} + +.json-modal-close:hover { + background: var(--bg-tertiary); + color: var(--text-secondary); +} + +.json-modal-body { + flex: 1; + overflow-y: auto; + padding: 20px 24px; + min-height: 200px; + max-height: calc(90vh - 160px); +} + +.json-content { + background: var(--bg-tertiary); + border: 1px solid var(--border-primary); + border-radius: 8px; + padding: 16px; + margin: 0; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Courier New', monospace; + font-size: 13px; + line-height: 1.6; + color: var(--text-secondary); + overflow-x: auto; + white-space: pre; +} + +.json-modal-footer { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 10px; + padding: 16px 24px; + border-top: 1px solid var(--border-primary); +} + +@media (max-width: 768px) { + .json-modal-content { + width: 95%; + max-height: 95vh; + } + + .json-modal-header, + .json-modal-body, + .json-modal-footer { + padding: 16px; + } + + .json-content { + font-size: 12px; + padding: 12px; + } + + .json-modal-footer { + flex-wrap: wrap; + } + + .json-modal-footer .btn { + flex: 1; + min-width: 80px; + } +} + /* 模态框 */ .modal { display: none; @@ -2987,7 +3384,6 @@ input:checked+.slider:before { .item-stats { display: flex; gap: 8px; - margin-top: 10px; flex-wrap: wrap; }