mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 11:20:50 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8372906820 | ||
|
|
5feea2e345 | ||
|
|
825ad53c2c | ||
|
|
3e9413172c | ||
|
|
89099b58ff | ||
|
|
7509a1eddc |
@@ -10,7 +10,7 @@ Example URL:
|
||||
https://remote.router-for.me/
|
||||
|
||||
Minimum required version: ≥ 6.0.0
|
||||
Recommended version: ≥ 6.1.3
|
||||
Recommended version: ≥ 6.2.32
|
||||
|
||||
Since version 6.0.19, the WebUI has been rolled into the main program. You can access it by going to `/management.html` on the external port after firing up the main project.
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ https://remote.router-for.me/
|
||||
|
||||
最低可用版本 ≥ 6.0.0
|
||||
|
||||
推荐版本 ≥ 6.1.3
|
||||
推荐版本 ≥ 6.2.32
|
||||
|
||||
自6.0.19起WebUI已经集成在主程序中 可以通过主项目开启的外部端口的`/management.html`访问
|
||||
|
||||
|
||||
473
app.js
473
app.js
@@ -182,12 +182,12 @@ class CLIProxyManager {
|
||||
// 如果有完整的连接信息且之前已登录,尝试自动登录
|
||||
if (savedBase && savedKey && wasLoggedIn) {
|
||||
try {
|
||||
console.log('检测到本地连接数据,尝试自动登录...');
|
||||
console.log(i18n.t('auto_login.title'));
|
||||
this.showAutoLoginLoading();
|
||||
await this.attemptAutoLogin(savedBase, savedKey);
|
||||
return; // 自动登录成功,不显示登录页面
|
||||
} catch (error) {
|
||||
console.log('自动登录失败:', error.message);
|
||||
console.log(`${i18n.t('notification.login_failed')}: ${error.message}`);
|
||||
// 清除无效的登录状态
|
||||
localStorage.removeItem('isLoggedIn');
|
||||
this.hideAutoLoginLoading();
|
||||
@@ -232,7 +232,7 @@ class CLIProxyManager {
|
||||
this.hideAutoLoginLoading();
|
||||
this.showMainPage();
|
||||
|
||||
console.log('自动登录成功');
|
||||
console.log(i18n.t('auto_login.title'));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('自动登录失败:', error);
|
||||
@@ -807,7 +807,8 @@ class CLIProxyManager {
|
||||
// 更新按钮提示文本
|
||||
const toggleBtn = document.getElementById('sidebar-toggle-btn-desktop');
|
||||
if (toggleBtn) {
|
||||
toggleBtn.setAttribute('title', isCollapsed ? '展开侧边栏' : '收起侧边栏');
|
||||
toggleBtn.setAttribute('data-i18n-title', isCollapsed ? 'sidebar.toggle_expand' : 'sidebar.toggle_collapse');
|
||||
toggleBtn.title = i18n.t(isCollapsed ? 'sidebar.toggle_expand' : 'sidebar.toggle_collapse');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -828,7 +829,8 @@ class CLIProxyManager {
|
||||
// 更新按钮提示文本
|
||||
const toggleBtn = document.getElementById('sidebar-toggle-btn-desktop');
|
||||
if (toggleBtn) {
|
||||
toggleBtn.setAttribute('title', '展开侧边栏');
|
||||
toggleBtn.setAttribute('data-i18n-title', 'sidebar.toggle_expand');
|
||||
toggleBtn.title = i18n.t('sidebar.toggle_expand');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1420,7 +1422,7 @@ class CLIProxyManager {
|
||||
// 加载所有数据 - 使用新的 /config 端点一次性获取所有配置
|
||||
async loadAllData(forceRefresh = false) {
|
||||
try {
|
||||
console.log('使用新的 /config 端点加载所有配置...');
|
||||
console.log(i18n.t('system_info.real_time_data'));
|
||||
// 使用新的 /config 端点一次性获取所有配置
|
||||
const config = await this.getConfig(forceRefresh);
|
||||
|
||||
@@ -2213,10 +2215,10 @@ class CLIProxyManager {
|
||||
<div class="item-value">${this.maskApiKey(key)}</div>
|
||||
<div class="item-stats">
|
||||
<span class="stat-badge stat-success">
|
||||
<i class="fas fa-check-circle"></i> 成功: ${keyStats.success}
|
||||
<i class="fas fa-check-circle"></i> ${i18n.t('stats.success')}: ${keyStats.success}
|
||||
</span>
|
||||
<span class="stat-badge stat-failure">
|
||||
<i class="fas fa-times-circle"></i> 失败: ${keyStats.failure}
|
||||
<i class="fas fa-times-circle"></i> ${i18n.t('stats.failure')}: ${keyStats.failure}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2238,14 +2240,14 @@ class CLIProxyManager {
|
||||
const modalBody = document.getElementById('modal-body');
|
||||
|
||||
modalBody.innerHTML = `
|
||||
<h3>添加Gemini API密钥</h3>
|
||||
<h3>${i18n.t('ai_providers.gemini_add_modal_title')}</h3>
|
||||
<div class="form-group">
|
||||
<label for="new-gemini-key">API密钥:</label>
|
||||
<input type="text" id="new-gemini-key" placeholder="请输入Gemini API密钥">
|
||||
<label for="new-gemini-key">${i18n.t('ai_providers.gemini_add_modal_key_label')}</label>
|
||||
<input type="text" id="new-gemini-key" placeholder="${i18n.t('ai_providers.gemini_add_modal_key_placeholder')}">
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" onclick="manager.closeModal()">取消</button>
|
||||
<button class="btn btn-primary" onclick="manager.addGeminiKey()">添加</button>
|
||||
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
|
||||
<button class="btn btn-primary" onclick="manager.addGeminiKey()">${i18n.t('common.add')}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -2257,7 +2259,7 @@ class CLIProxyManager {
|
||||
const newKey = document.getElementById('new-gemini-key').value.trim();
|
||||
|
||||
if (!newKey) {
|
||||
this.showNotification('请输入Gemini API密钥', 'error');
|
||||
this.showNotification(i18n.t('notification.please_enter') + ' ' + i18n.t('notification.gemini_api_key'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2274,9 +2276,9 @@ class CLIProxyManager {
|
||||
this.clearCache(); // 清除缓存
|
||||
this.closeModal();
|
||||
this.loadGeminiKeys();
|
||||
this.showNotification('Gemini密钥添加成功', 'success');
|
||||
this.showNotification(i18n.t('notification.gemini_key_added'), 'success');
|
||||
} catch (error) {
|
||||
this.showNotification(`添加Gemini密钥失败: ${error.message}`, 'error');
|
||||
this.showNotification(`${i18n.t('notification.add_failed')}: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2286,14 +2288,14 @@ class CLIProxyManager {
|
||||
const modalBody = document.getElementById('modal-body');
|
||||
|
||||
modalBody.innerHTML = `
|
||||
<h3>编辑Gemini API密钥</h3>
|
||||
<h3>${i18n.t('ai_providers.gemini_edit_modal_title')}</h3>
|
||||
<div class="form-group">
|
||||
<label for="edit-gemini-key">API密钥:</label>
|
||||
<label for="edit-gemini-key">${i18n.t('ai_providers.gemini_edit_modal_key_label')}</label>
|
||||
<input type="text" id="edit-gemini-key" value="${currentKey}">
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" onclick="manager.closeModal()">取消</button>
|
||||
<button class="btn btn-primary" onclick="manager.updateGeminiKey('${currentKey}')">更新</button>
|
||||
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
|
||||
<button class="btn btn-primary" onclick="manager.updateGeminiKey('${currentKey}')">${i18n.t('common.update')}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -2305,7 +2307,7 @@ class CLIProxyManager {
|
||||
const newKey = document.getElementById('edit-gemini-key').value.trim();
|
||||
|
||||
if (!newKey) {
|
||||
this.showNotification('请输入Gemini API密钥', 'error');
|
||||
this.showNotification(i18n.t('notification.please_enter') + ' ' + i18n.t('notification.gemini_api_key'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2318,9 +2320,9 @@ class CLIProxyManager {
|
||||
this.clearCache(); // 清除缓存
|
||||
this.closeModal();
|
||||
this.loadGeminiKeys();
|
||||
this.showNotification('Gemini密钥更新成功', 'success');
|
||||
this.showNotification(i18n.t('notification.gemini_key_updated'), 'success');
|
||||
} catch (error) {
|
||||
this.showNotification(`更新Gemini密钥失败: ${error.message}`, 'error');
|
||||
this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2332,9 +2334,9 @@ class CLIProxyManager {
|
||||
await this.makeRequest(`/generative-language-api-key?value=${encodeURIComponent(key)}`, { method: 'DELETE' });
|
||||
this.clearCache(); // 清除缓存
|
||||
this.loadGeminiKeys();
|
||||
this.showNotification('Gemini密钥删除成功', 'success');
|
||||
this.showNotification(i18n.t('notification.gemini_key_deleted'), 'success');
|
||||
} catch (error) {
|
||||
this.showNotification(`删除Gemini密钥失败: ${error.message}`, 'error');
|
||||
this.showNotification(`${i18n.t('notification.delete_failed')}: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2381,10 +2383,10 @@ class CLIProxyManager {
|
||||
${config['proxy-url'] ? `<div class="item-subtitle">${i18n.t('common.proxy_url')}: ${this.escapeHtml(config['proxy-url'])}</div>` : ''}
|
||||
<div class="item-stats">
|
||||
<span class="stat-badge stat-success">
|
||||
<i class="fas fa-check-circle"></i> 成功: ${keyStats.success}
|
||||
<i class="fas fa-check-circle"></i> ${i18n.t('stats.success')}: ${keyStats.success}
|
||||
</span>
|
||||
<span class="stat-badge stat-failure">
|
||||
<i class="fas fa-times-circle"></i> 失败: ${keyStats.failure}
|
||||
<i class="fas fa-times-circle"></i> ${i18n.t('stats.failure')}: ${keyStats.failure}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2461,9 +2463,9 @@ class CLIProxyManager {
|
||||
this.clearCache(); // 清除缓存
|
||||
this.closeModal();
|
||||
this.loadCodexKeys();
|
||||
this.showNotification('Codex配置添加成功', 'success');
|
||||
this.showNotification(i18n.t('notification.codex_config_added'), 'success');
|
||||
} catch (error) {
|
||||
this.showNotification(`添加Codex配置失败: ${error.message}`, 'error');
|
||||
this.showNotification(`${i18n.t('notification.add_failed')}: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2523,9 +2525,9 @@ class CLIProxyManager {
|
||||
this.clearCache(); // 清除缓存
|
||||
this.closeModal();
|
||||
this.loadCodexKeys();
|
||||
this.showNotification('Codex配置更新成功', 'success');
|
||||
this.showNotification(i18n.t('notification.codex_config_updated'), 'success');
|
||||
} catch (error) {
|
||||
this.showNotification(`更新Codex配置失败: ${error.message}`, 'error');
|
||||
this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2537,9 +2539,9 @@ class CLIProxyManager {
|
||||
await this.makeRequest(`/codex-api-key?api-key=${encodeURIComponent(apiKey)}`, { method: 'DELETE' });
|
||||
this.clearCache(); // 清除缓存
|
||||
this.loadCodexKeys();
|
||||
this.showNotification('Codex配置删除成功', 'success');
|
||||
this.showNotification(i18n.t('notification.codex_config_deleted'), 'success');
|
||||
} catch (error) {
|
||||
this.showNotification(`删除Codex配置失败: ${error.message}`, 'error');
|
||||
this.showNotification(`${i18n.t('notification.delete_failed')}: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2586,10 +2588,10 @@ class CLIProxyManager {
|
||||
${config['proxy-url'] ? `<div class="item-subtitle">${i18n.t('common.proxy_url')}: ${this.escapeHtml(config['proxy-url'])}</div>` : ''}
|
||||
<div class="item-stats">
|
||||
<span class="stat-badge stat-success">
|
||||
<i class="fas fa-check-circle"></i> 成功: ${keyStats.success}
|
||||
<i class="fas fa-check-circle"></i> ${i18n.t('stats.success')}: ${keyStats.success}
|
||||
</span>
|
||||
<span class="stat-badge stat-failure">
|
||||
<i class="fas fa-times-circle"></i> 失败: ${keyStats.failure}
|
||||
<i class="fas fa-times-circle"></i> ${i18n.t('stats.failure')}: ${keyStats.failure}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2666,9 +2668,9 @@ class CLIProxyManager {
|
||||
this.clearCache(); // 清除缓存
|
||||
this.closeModal();
|
||||
this.loadClaudeKeys();
|
||||
this.showNotification('Claude配置添加成功', 'success');
|
||||
this.showNotification(i18n.t('notification.claude_config_added'), 'success');
|
||||
} catch (error) {
|
||||
this.showNotification(`添加Claude配置失败: ${error.message}`, 'error');
|
||||
this.showNotification(`${i18n.t('notification.add_failed')}: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2728,9 +2730,9 @@ class CLIProxyManager {
|
||||
this.clearCache(); // 清除缓存
|
||||
this.closeModal();
|
||||
this.loadClaudeKeys();
|
||||
this.showNotification('Claude配置更新成功', 'success');
|
||||
this.showNotification(i18n.t('notification.claude_config_updated'), 'success');
|
||||
} catch (error) {
|
||||
this.showNotification(`更新Claude配置失败: ${error.message}`, 'error');
|
||||
this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2742,9 +2744,9 @@ class CLIProxyManager {
|
||||
await this.makeRequest(`/claude-api-key?api-key=${encodeURIComponent(apiKey)}`, { method: 'DELETE' });
|
||||
this.clearCache(); // 清除缓存
|
||||
this.loadClaudeKeys();
|
||||
this.showNotification('Claude配置删除成功', 'success');
|
||||
this.showNotification(i18n.t('notification.claude_config_deleted'), 'success');
|
||||
} catch (error) {
|
||||
this.showNotification(`删除Claude配置失败: ${error.message}`, 'error');
|
||||
this.showNotification(`${i18n.t('notification.delete_failed')}: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2829,10 +2831,10 @@ class CLIProxyManager {
|
||||
${this.renderOpenAIModelBadges(models)}
|
||||
<div class="item-stats">
|
||||
<span class="stat-badge stat-success">
|
||||
<i class="fas fa-check-circle"></i> 成功: ${totalSuccess}
|
||||
<i class="fas fa-check-circle"></i> ${i18n.t('stats.success')}: ${totalSuccess}
|
||||
</span>
|
||||
<span class="stat-badge stat-failure">
|
||||
<i class="fas fa-times-circle"></i> 失败: ${totalFailure}
|
||||
<i class="fas fa-times-circle"></i> ${i18n.t('stats.failure')}: ${totalFailure}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2927,9 +2929,9 @@ class CLIProxyManager {
|
||||
this.clearCache(); // 清除缓存
|
||||
this.closeModal();
|
||||
this.loadOpenAIProviders();
|
||||
this.showNotification('OpenAI提供商添加成功', 'success');
|
||||
this.showNotification(i18n.t('notification.openai_provider_added'), 'success');
|
||||
} catch (error) {
|
||||
this.showNotification(`添加OpenAI提供商失败: ${error.message}`, 'error');
|
||||
this.showNotification(`${i18n.t('notification.add_failed')}: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3021,9 +3023,9 @@ class CLIProxyManager {
|
||||
this.clearCache(); // 清除缓存
|
||||
this.closeModal();
|
||||
this.loadOpenAIProviders();
|
||||
this.showNotification('OpenAI提供商更新成功', 'success');
|
||||
this.showNotification(i18n.t('notification.openai_provider_updated'), 'success');
|
||||
} catch (error) {
|
||||
this.showNotification(`更新OpenAI提供商失败: ${error.message}`, 'error');
|
||||
this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3035,9 +3037,9 @@ class CLIProxyManager {
|
||||
await this.makeRequest(`/openai-compatibility?name=${encodeURIComponent(name)}`, { method: 'DELETE' });
|
||||
this.clearCache(); // 清除缓存
|
||||
this.loadOpenAIProviders();
|
||||
this.showNotification('OpenAI提供商删除成功', 'success');
|
||||
this.showNotification(i18n.t('notification.openai_provider_deleted'), 'success');
|
||||
} catch (error) {
|
||||
this.showNotification(`删除OpenAI提供商失败: ${error.message}`, 'error');
|
||||
this.showNotification(`${i18n.t('notification.delete_failed')}: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3069,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. 首先尝试完整文件名匹配
|
||||
@@ -3116,32 +3129,200 @@ 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> 成功: ${fileStats.success}
|
||||
</span>
|
||||
<span class="stat-badge stat-failure">
|
||||
<i class="fas fa-times-circle"></i> 失败: ${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 filterContainer = document.querySelector('.auth-file-filter');
|
||||
if (!filterContainer) return;
|
||||
|
||||
// 清理旧的监听器,避免重复绑定
|
||||
if (filterContainer._filterListener) {
|
||||
filterContainer.removeEventListener('click', filterContainer._filterListener);
|
||||
}
|
||||
|
||||
const listener = (event) => {
|
||||
const button = event.target.closest('.filter-btn');
|
||||
if (!button || !filterContainer.contains(button)) return;
|
||||
event.preventDefault();
|
||||
this.handleFilterClick(button);
|
||||
};
|
||||
|
||||
filterContainer._filterListener = listener;
|
||||
filterContainer.addEventListener('click', listener);
|
||||
}
|
||||
|
||||
// 绑定认证文件操作按钮事件(使用事件委托)
|
||||
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);
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
@@ -3189,13 +3370,131 @@ class CLIProxyManager {
|
||||
this.loadAuthFiles();
|
||||
this.showNotification(i18n.t('auth_files.upload_success'), 'success');
|
||||
} catch (error) {
|
||||
this.showNotification(`文件上传失败: ${error.message}`, 'error');
|
||||
this.showNotification(`${i18n.t('notification.upload_failed')}: ${error.message}`, 'error');
|
||||
}
|
||||
|
||||
// 清空文件输入
|
||||
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 {
|
||||
@@ -3219,7 +3518,7 @@ class CLIProxyManager {
|
||||
|
||||
this.showNotification(i18n.t('auth_files.download_success'), 'success');
|
||||
} catch (error) {
|
||||
this.showNotification(`文件下载失败: ${error.message}`, 'error');
|
||||
this.showNotification(`${i18n.t('notification.download_failed')}: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3233,7 +3532,7 @@ class CLIProxyManager {
|
||||
this.loadAuthFiles();
|
||||
this.showNotification(i18n.t('auth_files.delete_success'), 'success');
|
||||
} catch (error) {
|
||||
this.showNotification(`文件删除失败: ${error.message}`, 'error');
|
||||
this.showNotification(`${i18n.t('notification.delete_failed')}: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3247,7 +3546,7 @@ class CLIProxyManager {
|
||||
this.loadAuthFiles();
|
||||
this.showNotification(`${i18n.t('auth_files.delete_all_success')} ${response.deleted} ${i18n.t('auth_files.files_count')}`, 'success');
|
||||
} catch (error) {
|
||||
this.showNotification(`删除文件失败: ${error.message}`, 'error');
|
||||
this.showNotification(`${i18n.t('notification.delete_failed')}: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3313,12 +3612,12 @@ class CLIProxyManager {
|
||||
if (urlInput && urlInput.value) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(urlInput.value);
|
||||
this.showNotification('链接已复制到剪贴板', 'success');
|
||||
this.showNotification(i18n.t('notification.link_copied'), 'success');
|
||||
} catch (error) {
|
||||
// 降级方案:使用传统的复制方法
|
||||
urlInput.select();
|
||||
document.execCommand('copy');
|
||||
this.showNotification('链接已复制到剪贴板', 'success');
|
||||
this.showNotification(i18n.t('notification.link_copied'), 'success');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3326,7 +3625,7 @@ class CLIProxyManager {
|
||||
// 开始轮询 OAuth 状态
|
||||
startCodexOAuthPolling(state) {
|
||||
if (!state) {
|
||||
this.showNotification('无法获取认证状态参数', 'error');
|
||||
this.showNotification(i18n.t('auth_login.missing_state'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3458,12 +3757,12 @@ class CLIProxyManager {
|
||||
if (urlInput && urlInput.value) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(urlInput.value);
|
||||
this.showNotification('链接已复制到剪贴板', 'success');
|
||||
this.showNotification(i18n.t('notification.link_copied'), 'success');
|
||||
} catch (error) {
|
||||
// 降级方案:使用传统的复制方法
|
||||
urlInput.select();
|
||||
document.execCommand('copy');
|
||||
this.showNotification('链接已复制到剪贴板', 'success');
|
||||
this.showNotification(i18n.t('notification.link_copied'), 'success');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3471,7 +3770,7 @@ class CLIProxyManager {
|
||||
// 开始轮询 Anthropic OAuth 状态
|
||||
startAnthropicOAuthPolling(state) {
|
||||
if (!state) {
|
||||
this.showNotification('无法获取认证状态参数', 'error');
|
||||
this.showNotification(i18n.t('auth_login.missing_state'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3612,12 +3911,12 @@ class CLIProxyManager {
|
||||
if (urlInput && urlInput.value) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(urlInput.value);
|
||||
this.showNotification('链接已复制到剪贴板', 'success');
|
||||
this.showNotification(i18n.t('notification.link_copied'), 'success');
|
||||
} catch (error) {
|
||||
// 降级方案:使用传统的复制方法
|
||||
urlInput.select();
|
||||
document.execCommand('copy');
|
||||
this.showNotification('链接已复制到剪贴板', 'success');
|
||||
this.showNotification(i18n.t('notification.link_copied'), 'success');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3625,7 +3924,7 @@ class CLIProxyManager {
|
||||
// 开始轮询 Gemini CLI OAuth 状态
|
||||
startGeminiCliOAuthPolling(state) {
|
||||
if (!state) {
|
||||
this.showNotification('无法获取认证状态参数', 'error');
|
||||
this.showNotification(i18n.t('auth_login.missing_state'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3757,12 +4056,12 @@ class CLIProxyManager {
|
||||
if (urlInput && urlInput.value) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(urlInput.value);
|
||||
this.showNotification('链接已复制到剪贴板', 'success');
|
||||
this.showNotification(i18n.t('notification.link_copied'), 'success');
|
||||
} catch (error) {
|
||||
// 降级方案:使用传统的复制方法
|
||||
urlInput.select();
|
||||
document.execCommand('copy');
|
||||
this.showNotification('链接已复制到剪贴板', 'success');
|
||||
this.showNotification(i18n.t('notification.link_copied'), 'success');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3770,7 +4069,7 @@ class CLIProxyManager {
|
||||
// 开始轮询 Qwen OAuth 状态
|
||||
startQwenOAuthPolling(state) {
|
||||
if (!state) {
|
||||
this.showNotification('无法获取认证状态参数', 'error');
|
||||
this.showNotification(i18n.t('auth_login.missing_state'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3902,12 +4201,12 @@ class CLIProxyManager {
|
||||
if (urlInput && urlInput.value) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(urlInput.value);
|
||||
this.showNotification('链接已复制到剪贴板', 'success');
|
||||
this.showNotification(i18n.t('notification.link_copied'), 'success');
|
||||
} catch (error) {
|
||||
// 降级方案:使用传统的复制方法
|
||||
urlInput.select();
|
||||
document.execCommand('copy');
|
||||
this.showNotification('链接已复制到剪贴板', 'success');
|
||||
this.showNotification(i18n.t('notification.link_copied'), 'success');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3915,7 +4214,7 @@ class CLIProxyManager {
|
||||
// 开始轮询 iFlow OAuth 状态
|
||||
startIflowOAuthPolling(state) {
|
||||
if (!state) {
|
||||
this.showNotification('无法获取认证状态参数', 'error');
|
||||
this.showNotification(i18n.t('auth_login.missing_state'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
48
i18n.js
48
i18n.js
@@ -38,6 +38,9 @@ const i18n = {
|
||||
'common.base_url': '地址',
|
||||
'common.proxy_url': '代理',
|
||||
'common.alias': '别名',
|
||||
'common.failure': '失败',
|
||||
'common.unknown_error': '未知错误',
|
||||
'common.copy': '复制',
|
||||
|
||||
// 页面标题
|
||||
'title.main': 'CLI Proxy API Management Center',
|
||||
@@ -275,6 +278,7 @@ const i18n = {
|
||||
'auth_login.qwen_oauth_status_error': '认证失败:',
|
||||
'auth_login.qwen_oauth_start_error': '启动 Qwen OAuth 失败:',
|
||||
'auth_login.qwen_oauth_polling_error': '检查认证状态失败:',
|
||||
'auth_login.missing_state': '无法获取认证状态参数',
|
||||
|
||||
// iFlow OAuth
|
||||
'auth_login.iflow_oauth_title': 'iFlow OAuth',
|
||||
@@ -308,6 +312,8 @@ const i18n = {
|
||||
'usage_stats.tokens_count': 'Token数量',
|
||||
'usage_stats.models': '模型统计',
|
||||
'usage_stats.success_rate': '成功率',
|
||||
'stats.success': '成功',
|
||||
'stats.failure': '失败',
|
||||
|
||||
// 日志查看
|
||||
'logs.title': '日志查看',
|
||||
@@ -347,6 +353,7 @@ const i18n = {
|
||||
'config_management.status_save_failed': '保存失败',
|
||||
'config_management.save_success': '配置已保存',
|
||||
'config_management.error_yaml_not_supported': '服务器未返回 YAML 格式,请确认 /config.yaml 接口可用',
|
||||
'config_management.editor_placeholder': 'key: value',
|
||||
|
||||
// 系统信息
|
||||
'system_info.title': '系统信息',
|
||||
@@ -402,6 +409,7 @@ const i18n = {
|
||||
'notification.gemini_api_key': 'Gemini API密钥',
|
||||
'notification.codex_api_key': 'Codex API密钥',
|
||||
'notification.claude_api_key': 'Claude API密钥',
|
||||
'notification.link_copied': '链接已复制到剪贴板',
|
||||
|
||||
// 语言切换
|
||||
'language.switch': '语言',
|
||||
@@ -416,6 +424,10 @@ const i18n = {
|
||||
'theme.switch_to_dark': '切换到暗色模式',
|
||||
'theme.auto': '跟随系统',
|
||||
|
||||
// 侧边栏
|
||||
'sidebar.toggle_expand': '展开侧边栏',
|
||||
'sidebar.toggle_collapse': '收起侧边栏',
|
||||
|
||||
// 页脚
|
||||
'footer.version': '版本',
|
||||
'footer.author': '作者'
|
||||
@@ -453,6 +465,9 @@ const i18n = {
|
||||
'common.base_url': 'Address',
|
||||
'common.proxy_url': 'Proxy',
|
||||
'common.alias': 'Alias',
|
||||
'common.failure': 'Failure',
|
||||
'common.unknown_error': 'Unknown error',
|
||||
'common.copy': 'Copy',
|
||||
|
||||
// Page titles
|
||||
'title.main': 'CLI Proxy API Management Center',
|
||||
@@ -689,6 +704,7 @@ const i18n = {
|
||||
'auth_login.qwen_oauth_status_error': 'Authentication failed:',
|
||||
'auth_login.qwen_oauth_start_error': 'Failed to start Qwen OAuth:',
|
||||
'auth_login.qwen_oauth_polling_error': 'Failed to check authentication status:',
|
||||
'auth_login.missing_state': 'Unable to retrieve authentication state parameter',
|
||||
|
||||
// iFlow OAuth
|
||||
'auth_login.iflow_oauth_title': 'iFlow OAuth',
|
||||
@@ -722,6 +738,8 @@ const i18n = {
|
||||
'usage_stats.tokens_count': 'Token Count',
|
||||
'usage_stats.models': 'Model Statistics',
|
||||
'usage_stats.success_rate': 'Success Rate',
|
||||
'stats.success': 'Success',
|
||||
'stats.failure': 'Failure',
|
||||
|
||||
// Logs viewer
|
||||
'logs.title': 'Logs Viewer',
|
||||
@@ -761,6 +779,7 @@ const i18n = {
|
||||
'config_management.status_save_failed': 'Save failed',
|
||||
'config_management.save_success': 'Configuration saved successfully',
|
||||
'config_management.error_yaml_not_supported': 'Server did not return YAML. Verify the /config.yaml endpoint is available.',
|
||||
'config_management.editor_placeholder': 'key: value',
|
||||
|
||||
// System info
|
||||
'system_info.title': 'System Information',
|
||||
@@ -816,6 +835,7 @@ const i18n = {
|
||||
'notification.gemini_api_key': 'Gemini API key',
|
||||
'notification.codex_api_key': 'Codex API key',
|
||||
'notification.claude_api_key': 'Claude API key',
|
||||
'notification.link_copied': 'Link copied to clipboard',
|
||||
|
||||
// Language switch
|
||||
'language.switch': 'Language',
|
||||
@@ -830,6 +850,10 @@ const i18n = {
|
||||
'theme.switch_to_dark': 'Switch to dark mode',
|
||||
'theme.auto': 'Follow system',
|
||||
|
||||
// Sidebar
|
||||
'sidebar.toggle_expand': 'Expand sidebar',
|
||||
'sidebar.toggle_collapse': 'Collapse sidebar',
|
||||
|
||||
// Footer
|
||||
'footer.version': 'Version',
|
||||
'footer.author': 'Author'
|
||||
@@ -879,6 +903,30 @@ const i18n = {
|
||||
}
|
||||
});
|
||||
|
||||
// 更新所有包含 data-i18n-placeholder 的输入框占位符
|
||||
document.querySelectorAll('[data-i18n-placeholder]').forEach(element => {
|
||||
const key = element.getAttribute('data-i18n-placeholder');
|
||||
element.placeholder = this.t(key);
|
||||
});
|
||||
|
||||
// 更新 data-i18n-title
|
||||
document.querySelectorAll('[data-i18n-title]').forEach(element => {
|
||||
const key = element.getAttribute('data-i18n-title');
|
||||
element.title = this.t(key);
|
||||
});
|
||||
|
||||
// 更新 data-i18n-tooltip
|
||||
document.querySelectorAll('[data-i18n-tooltip]').forEach(element => {
|
||||
const key = element.getAttribute('data-i18n-tooltip');
|
||||
element.setAttribute('data-tooltip', this.t(key));
|
||||
});
|
||||
|
||||
// 更新 data-i18n-text(常用于按钮或标签)
|
||||
document.querySelectorAll('[data-i18n-text]').forEach(element => {
|
||||
const key = element.getAttribute('data-i18n-text');
|
||||
element.textContent = this.t(key);
|
||||
});
|
||||
|
||||
// 更新所有带有 data-i18n-html 属性的元素(支持HTML)
|
||||
document.querySelectorAll('[data-i18n-html]').forEach(element => {
|
||||
const key = element.getAttribute('data-i18n-html');
|
||||
|
||||
61
index.html
61
index.html
@@ -77,8 +77,7 @@
|
||||
<div class="form-group">
|
||||
<label for="login-api-base" data-i18n="login.custom_connection_label">自定义连接地址:</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="login-api-base" data-i18n="login.custom_connection_placeholder"
|
||||
placeholder="例如: https://example.com:8317">
|
||||
<input type="text" id="login-api-base" data-i18n-placeholder="login.custom_connection_placeholder">
|
||||
<button type="button" id="login-reset-api-base"
|
||||
class="btn btn-secondary connection-reset-btn">
|
||||
<i class="fas fa-location-arrow"></i>
|
||||
@@ -91,8 +90,7 @@
|
||||
<div class="form-group">
|
||||
<label for="login-management-key" data-i18n="login.management_key_label">管理密钥:</label>
|
||||
<div class="input-group">
|
||||
<input type="password" id="login-management-key"
|
||||
data-i18n="login.management_key_placeholder" placeholder="请输入管理密钥" required>
|
||||
<input type="password" id="login-management-key" data-i18n-placeholder="login.management_key_placeholder" required>
|
||||
<button type="button" class="btn btn-secondary toggle-key-visibility">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
@@ -123,7 +121,7 @@
|
||||
<button class="mobile-menu-btn" id="mobile-menu-btn">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<button class="sidebar-toggle-btn-desktop" id="sidebar-toggle-btn-desktop" title="收起/展开侧边栏">
|
||||
<button class="sidebar-toggle-btn-desktop" id="sidebar-toggle-btn-desktop" data-i18n-title="sidebar.toggle_collapse">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<div class="top-navbar-brand">
|
||||
@@ -161,29 +159,29 @@
|
||||
<nav class="sidebar" id="sidebar">
|
||||
<!-- 导航菜单 -->
|
||||
<ul class="nav-menu">
|
||||
<li data-tooltip="基础设置"><a href="#basic-settings" class="nav-item active"
|
||||
<li data-i18n-tooltip="nav.basic_settings"><a href="#basic-settings" class="nav-item active"
|
||||
data-section="basic-settings">
|
||||
<i class="fas fa-sliders-h"></i> <span data-i18n="nav.basic_settings">基础设置</span>
|
||||
</a></li>
|
||||
<li data-tooltip="API 密钥"><a href="#api-keys" class="nav-item" data-section="api-keys">
|
||||
<li data-i18n-tooltip="nav.api_keys"><a href="#api-keys" class="nav-item" data-section="api-keys">
|
||||
<i class="fas fa-key"></i> <span data-i18n="nav.api_keys">API 密钥</span>
|
||||
</a></li>
|
||||
<li data-tooltip="AI 提供商"><a href="#ai-providers" class="nav-item" data-section="ai-providers">
|
||||
<li data-i18n-tooltip="nav.ai_providers"><a href="#ai-providers" class="nav-item" data-section="ai-providers">
|
||||
<i class="fas fa-robot"></i> <span data-i18n="nav.ai_providers">AI 提供商</span>
|
||||
</a></li>
|
||||
<li data-tooltip="认证文件"><a href="#auth-files" class="nav-item" data-section="auth-files">
|
||||
<li data-i18n-tooltip="nav.auth_files"><a href="#auth-files" class="nav-item" data-section="auth-files">
|
||||
<i class="fas fa-file-alt"></i> <span data-i18n="nav.auth_files">认证文件</span>
|
||||
</a></li>
|
||||
<li data-tooltip="使用统计"><a href="#usage-stats" class="nav-item" data-section="usage-stats">
|
||||
<li data-i18n-tooltip="nav.usage_stats"><a href="#usage-stats" class="nav-item" data-section="usage-stats">
|
||||
<i class="fas fa-chart-line"></i> <span data-i18n="nav.usage_stats">使用统计</span>
|
||||
</a></li>
|
||||
<li data-tooltip="配置管理"><a href="#config-management" class="nav-item" data-section="config-management">
|
||||
<li data-i18n-tooltip="nav.config_management"><a href="#config-management" class="nav-item" data-section="config-management">
|
||||
<i class="fas fa-cog"></i> <span data-i18n="nav.config_management">配置管理</span>
|
||||
</a></li>
|
||||
<li id="logs-nav-item" data-tooltip="日志查看" style="display: none;"><a href="#logs" class="nav-item" data-section="logs">
|
||||
<li id="logs-nav-item" data-i18n-tooltip="nav.logs" style="display: none;"><a href="#logs" class="nav-item" data-section="logs">
|
||||
<i class="fas fa-scroll"></i> <span data-i18n="nav.logs">日志查看</span>
|
||||
</a></li>
|
||||
<li data-tooltip="系统信息"><a href="#system-info" class="nav-item" data-section="system-info">
|
||||
<li data-i18n-tooltip="nav.system_info"><a href="#system-info" class="nav-item" data-section="system-info">
|
||||
<i class="fas fa-info-circle"></i> <span data-i18n="nav.system_info">系统信息</span>
|
||||
</a></li>
|
||||
</ul>
|
||||
@@ -230,9 +228,7 @@
|
||||
<label for="proxy-url" data-i18n="basic_settings.proxy_url_label">代理
|
||||
URL:</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="proxy-url"
|
||||
data-i18n="basic_settings.proxy_url_placeholder"
|
||||
placeholder="例如: socks5://user:pass@127.0.0.1:1080/">
|
||||
<input type="text" id="proxy-url" data-i18n-placeholder="basic_settings.proxy_url_placeholder">
|
||||
<button id="update-proxy" class="btn btn-primary"
|
||||
data-i18n="basic_settings.proxy_update">更新</button>
|
||||
<button id="clear-proxy" class="btn btn-danger"
|
||||
@@ -422,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
|
||||
@@ -437,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>
|
||||
@@ -461,7 +469,7 @@
|
||||
<div class="form-group">
|
||||
<label data-i18n="auth_login.codex_oauth_url_label">授权链接:</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="codex-oauth-url" readonly>
|
||||
<input type="text" id="codex-oauth-url" readonly>
|
||||
<button id="codex-open-link" class="btn btn-primary">
|
||||
<i class="fas fa-external-link-alt"></i> <span
|
||||
data-i18n="auth_login.codex_open_link">打开链接</span>
|
||||
@@ -533,11 +541,10 @@
|
||||
data-i18n="auth_login.gemini_cli_project_id_label">Google Cloud 项目 ID
|
||||
(可选):</label>
|
||||
<input type="text" id="gemini-cli-project-id"
|
||||
data-i18n="auth_login.gemini_cli_project_id_placeholder"
|
||||
placeholder="输入 Google Cloud 项目 ID (可选)">
|
||||
<div class="form-hint" data-i18n="auth_login.gemini_cli_project_id_hint">
|
||||
如果指定了项目 ID,将使用该项目的认证信息。
|
||||
</div>
|
||||
data-i18n-placeholder="auth_login.gemini_cli_project_id_placeholder">
|
||||
<div class="form-hint" data-i18n="auth_login.gemini_cli_project_id_hint">
|
||||
如果指定了项目 ID,将使用该项目的认证信息。
|
||||
</div>
|
||||
</div>
|
||||
<div id="gemini-cli-oauth-content" style="display: none;">
|
||||
<div class="form-group">
|
||||
@@ -794,7 +801,7 @@
|
||||
<div class="card-content">
|
||||
<p class="form-hint" data-i18n="config_management.description">查看并编辑服务器上的 config.yaml 配置文件。保存前请确认语法正确。</p>
|
||||
<div class="yaml-editor-container">
|
||||
<textarea id="config-editor" class="yaml-editor" spellcheck="false" placeholder="key: value"></textarea>
|
||||
<textarea id="config-editor" class="yaml-editor" spellcheck="false" data-i18n="config_management.editor_placeholder"></textarea>
|
||||
<div id="config-editor-status" class="editor-status" data-i18n="config_management.status_idle">等待操作</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
408
styles.css
408
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,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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user