Merge pull request #10 from coulsontl/new_ui

feat(ui): 添加类型筛选和详细信息视图来优化认证文件管理,并优化布局结构
This commit is contained in:
Supra4E8C
2025-10-26 17:23:09 +08:00
committed by GitHub
4 changed files with 727 additions and 27 deletions

324
app.js
View File

@@ -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,189 @@ 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 = `<span class="file-type-badge ${fileType}">${typeDisplay}</span>`;
return `
<div class="file-item">
<div class="file-item" data-file-type="${fileType}">
<div class="item-content">
<div class="item-title">${file.name}</div>
<div class="item-subtitle">${i18n.t('auth_files.file_size')}: ${this.formatFileSize(file.size)}</div>
<div class="item-subtitle">${i18n.t('auth_files.file_modified')}: ${new Date(file.modtime).toLocaleString(i18n.currentLanguage === 'zh-CN' ? 'zh-CN' : 'en-US')}</div>
<div class="item-stats">
<span class="stat-badge stat-success">
<i class="fas fa-check-circle"></i> ${i18n.t('stats.success')}: ${fileStats.success}
</span>
<span class="stat-badge stat-failure">
<i class="fas fa-times-circle"></i> ${i18n.t('stats.failure')}: ${fileStats.failure}
</span>
<div class="item-title">${typeBadge}${file.name}</div>
<div class="item-meta">
<span class="item-subtitle">${i18n.t('auth_files.file_modified')}: ${new Date(file.modtime).toLocaleString(i18n.currentLanguage === 'zh-CN' ? 'zh-CN' : 'en-US')}</span>
<span class="item-subtitle">${i18n.t('auth_files.file_size')}: ${this.formatFileSize(file.size)}</span>
</div>
<div class="item-footer">
<div class="item-stats">
<span class="stat-badge stat-success">
<i class="fas fa-check-circle"></i> ${i18n.t('stats.success')}: ${fileStats.success}
</span>
<span class="stat-badge stat-failure">
<i class="fas fa-times-circle"></i> ${i18n.t('stats.failure')}: ${fileStats.failure}
</span>
</div>
<div class="item-actions" data-filename="${file.name}">
<button class="btn-small btn-info" data-action="showDetails" title="详细信息">
<i class="fas fa-info-circle"></i>
</button>
<button class="btn-small btn-primary" data-action="download" title="下载">
<i class="fas fa-download"></i>
</button>
<button class="btn-small btn-danger" data-action="delete" title="删除">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
<div class="item-actions">
<button class="btn btn-primary" onclick="manager.downloadAuthFile('${file.name}')">
<i class="fas fa-download"></i>
</button>
<button class="btn btn-danger" onclick="manager.deleteAuthFile('${file.name}')">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`;
}).join('');
// 绑定筛选按钮事件
this.bindAuthFileFilterEvents();
// 绑定认证文件操作按钮事件(使用事件委托)
this.bindAuthFileActionEvents();
}
// 更新筛选按钮显示
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);
}
}
});
}
// 处理筛选按钮点击
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.classList.remove('hidden');
} else {
item.classList.add('hidden');
}
});
}
// 绑定认证文件筛选事件
bindAuthFileFilterEvents() {
const filterBtns = document.querySelectorAll('.auth-file-filter .filter-btn');
filterBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
this.handleFilterClick(e.target);
});
});
}
// 绑定认证文件操作按钮事件(使用事件委托)
bindAuthFileActionEvents() {
const container = document.getElementById('auth-files-list');
if (!container) return;
// 移除旧的事件监听器(如果存在)
const oldListener = container._authFileActionListener;
if (oldListener) {
container.removeEventListener('click', oldListener);
}
// 创建新的事件监听器
const listener = (e) => {
// 查找最近的按钮元素
const button = e.target.closest('button[data-action]');
if (!button) return;
// 获取操作类型和文件名
const action = button.dataset.action;
const actionsContainer = button.closest('.item-actions');
if (!actionsContainer) return;
const filename = actionsContainer.dataset.filename;
if (!filename) return;
// 根据操作类型调用相应的方法
switch (action) {
case 'showDetails':
this.showAuthFileDetails(filename);
break;
case 'download':
this.downloadAuthFile(filename);
break;
case 'delete':
this.deleteAuthFile(filename);
break;
}
};
// 保存监听器引用以便后续移除
container._authFileActionListener = listener;
// 添加事件监听器
container.addEventListener('click', listener);
}
// 格式化文件大小
@@ -3198,6 +3366,124 @@ 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 = `
<div id="json-modal" class="json-modal">
<div class="json-modal-content">
<div class="json-modal-header">
<h3><i class="fas fa-file-code"></i> ${filename}</h3>
<button class="json-modal-close" data-action="close">
<i class="fas fa-times"></i>
</button>
</div>
<div class="json-modal-body">
<pre class="json-content">${this.escapeHtml(jsonContent)}</pre>
</div>
<div class="json-modal-footer">
<button class="btn btn-secondary" data-action="copy">
<i class="fas fa-copy"></i> ${i18n.t('common.copy')}
</button>
<button class="btn btn-secondary" data-action="close">
${i18n.t('common.close')}
</button>
</div>
</div>
</div>
`;
// 移除旧的模态框(如果存在)
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();
}
});
// 绑定模态框按钮事件(使用事件委托)
this.bindJsonModalEvents(modal);
}
// 绑定JSON模态框按钮事件使用事件委托
bindJsonModalEvents(modal) {
modal.addEventListener('click', (e) => {
// 查找最近的按钮元素
const button = e.target.closest('button[data-action]');
if (!button) return;
// 获取操作类型
const action = button.dataset.action;
// 根据操作类型调用相应的方法
switch (action) {
case 'copy':
this.copyJsonContent();
break;
case 'close':
this.closeJsonModal();
break;
}
});
}
// 关闭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');
});
}
}
// 下载认证文件
async downloadAuthFile(filename) {
try {

View File

@@ -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',

View File

@@ -418,9 +418,21 @@
<!-- 认证文件 -->
<div class="card">
<div class="card-header">
<h3><i class="fas fa-file-alt"></i> <span
data-i18n="auth_files.title_section">认证文件</span></h3>
<div class="card-header card-header-with-filter">
<div class="header-left">
<h3><i class="fas fa-file-alt"></i> <span
data-i18n="auth_files.title_section">认证文件</span></h3>
<!-- 类型筛选 -->
<div class="auth-file-filter">
<button class="filter-btn active" data-type="all">All</button>
<button class="filter-btn" data-type="qwen">Qwen</button>
<button class="filter-btn" data-type="gemini">Gemini</button>
<button class="filter-btn" data-type="claude">Claude</button>
<button class="filter-btn" data-type="codex">Codex</button>
<button class="filter-btn" data-type="iflow">iFlow</button>
<button class="filter-btn" data-type="empty">Empty</button>
</div>
</div>
<div class="header-actions">
<button id="upload-auth-file" class="btn btn-primary">
<i class="fas fa-upload"></i> <span
@@ -433,7 +445,7 @@
</div>
</div>
<div class="card-content">
<div id="auth-files-list" class="file-list"></div>
<div id="auth-files-list" class="file-list file-grid"></div>
<input type="file" id="auth-file-input" accept=".json" style="display: none;">
</div>
</div>

View File

@@ -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,11 +1697,14 @@ 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;
}
.file-item.hidden {
display: none;
}
.key-item:hover,
.provider-item:hover,
.file-item:hover {
@@ -1484,16 +1715,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 +1829,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 +3388,6 @@ input:checked+.slider:before {
.item-stats {
display: flex;
gap: 8px;
margin-top: 10px;
flex-wrap: wrap;
}