Compare commits

...

4 Commits

Author SHA1 Message Date
hkfires
aa729914c5 fix(ui): position API Keys/Providers action buttons; prevent overlap 2025-11-03 16:31:29 +08:00
hkfires
f98f31f2ed fix(i18n, ui): correct syntax error in generateDynamicTypeLabel method
- Move generateDynamicTypeLabel method from inside updateAuthFileFilterButtons to proper class method location
- Fix JavaScript syntax error that prevented app from loading
- Preserve all internationalization functionality from 257260b

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 16:05:51 +08:00
hkfires
1e79f918e2 fix(ui): harden key list rendering and config handling 2025-10-31 16:24:43 +08:00
Supra4E8C
257260b1d2 feat(i18n, ui): enhance file type filtering with internationalization support
- Updated the file type display logic to utilize i18n for dynamic translations.
- Refactored filter button definitions to include data attributes for improved localization.
- Added new translation keys for various file types and filter options in both English and Chinese.
- Implemented a method to refresh button texts upon language change, ensuring consistent UI updates.
2025-10-26 17:46:57 +08:00
4 changed files with 235 additions and 58 deletions

161
app.js
View File

@@ -1498,24 +1498,16 @@ class CLIProxyManager {
}
// Gemini 密钥
if (config['generative-language-api-key']) {
await this.renderGeminiKeys(config['generative-language-api-key']);
}
await this.renderGeminiKeys(Array.isArray(config['generative-language-api-key']) ? config['generative-language-api-key'] : []);
// Codex 密钥
if (config['codex-api-key']) {
await this.renderCodexKeys(config['codex-api-key']);
}
await this.renderCodexKeys(Array.isArray(config['codex-api-key']) ? config['codex-api-key'] : []);
// Claude 密钥
if (config['claude-api-key']) {
await this.renderClaudeKeys(config['claude-api-key']);
}
await this.renderClaudeKeys(Array.isArray(config['claude-api-key']) ? config['claude-api-key'] : []);
// OpenAI 兼容提供商
if (config['openai-compatibility']) {
await this.renderOpenAIProviders(config['openai-compatibility']);
}
await this.renderOpenAIProviders(Array.isArray(config['openai-compatibility']) ? config['openai-compatibility'] : []);
}
// 回退方法:原来的逐个加载方式
@@ -2179,9 +2171,8 @@ class CLIProxyManager {
async loadGeminiKeys() {
try {
const config = await this.getConfig();
if (config['generative-language-api-key']) {
await this.renderGeminiKeys(config['generative-language-api-key']);
}
const keys = Array.isArray(config['generative-language-api-key']) ? config['generative-language-api-key'] : [];
await this.renderGeminiKeys(keys);
} catch (error) {
console.error('加载Gemini密钥失败:', error);
}
@@ -2190,8 +2181,12 @@ class CLIProxyManager {
// 渲染Gemini密钥列表
async renderGeminiKeys(keys) {
const container = document.getElementById('gemini-keys-list');
if (!container) {
return;
}
const list = Array.isArray(keys) ? keys : [];
if (keys.length === 0) {
if (list.length === 0) {
container.innerHTML = `
<div class="empty-state">
<i class="fab fa-google"></i>
@@ -2205,7 +2200,7 @@ class CLIProxyManager {
// 获取使用统计,按 source 聚合
const stats = await this.getKeyStats();
container.innerHTML = keys.map((key, index) => {
container.innerHTML = list.map((key, index) => {
const masked = this.maskApiKey(key);
const keyStats = stats[key] || stats[masked] || { success: 0, failure: 0 };
return `
@@ -2344,9 +2339,8 @@ class CLIProxyManager {
async loadCodexKeys() {
try {
const config = await this.getConfig();
if (config['codex-api-key']) {
await this.renderCodexKeys(config['codex-api-key']);
}
const keys = Array.isArray(config['codex-api-key']) ? config['codex-api-key'] : [];
await this.renderCodexKeys(keys);
} catch (error) {
console.error('加载Codex密钥失败:', error);
}
@@ -2355,8 +2349,12 @@ class CLIProxyManager {
// 渲染Codex密钥列表
async renderCodexKeys(keys) {
const container = document.getElementById('codex-keys-list');
if (!container) {
return;
}
const list = Array.isArray(keys) ? keys : [];
if (keys.length === 0) {
if (list.length === 0) {
container.innerHTML = `
<div class="empty-state">
<i class="fas fa-code"></i>
@@ -2370,7 +2368,7 @@ class CLIProxyManager {
// 获取使用统计,按 source 聚合
const stats = await this.getKeyStats();
container.innerHTML = keys.map((config, index) => {
container.innerHTML = list.map((config, index) => {
const rawKey = config['api-key'];
const masked = rawKey ? this.maskApiKey(rawKey) : '';
const keyStats = (rawKey && (stats[rawKey] || stats[masked])) || { success: 0, failure: 0 };
@@ -2549,9 +2547,8 @@ class CLIProxyManager {
async loadClaudeKeys() {
try {
const config = await this.getConfig();
if (config['claude-api-key']) {
await this.renderClaudeKeys(config['claude-api-key']);
}
const keys = Array.isArray(config['claude-api-key']) ? config['claude-api-key'] : [];
await this.renderClaudeKeys(keys);
} catch (error) {
console.error('加载Claude密钥失败:', error);
}
@@ -2560,8 +2557,12 @@ class CLIProxyManager {
// 渲染Claude密钥列表
async renderClaudeKeys(keys) {
const container = document.getElementById('claude-keys-list');
if (!container) {
return;
}
const list = Array.isArray(keys) ? keys : [];
if (keys.length === 0) {
if (list.length === 0) {
container.innerHTML = `
<div class="empty-state">
<i class="fas fa-brain"></i>
@@ -2575,7 +2576,7 @@ class CLIProxyManager {
// 获取使用统计,按 source 聚合
const stats = await this.getKeyStats();
container.innerHTML = keys.map((config, index) => {
container.innerHTML = list.map((config, index) => {
const rawKey = config['api-key'];
const masked = rawKey ? this.maskApiKey(rawKey) : '';
const keyStats = (rawKey && (stats[rawKey] || stats[masked])) || { success: 0, failure: 0 };
@@ -2754,9 +2755,8 @@ class CLIProxyManager {
async loadOpenAIProviders() {
try {
const config = await this.getConfig();
if (config['openai-compatibility']) {
await this.renderOpenAIProviders(config['openai-compatibility']);
}
const providers = Array.isArray(config['openai-compatibility']) ? config['openai-compatibility'] : [];
await this.renderOpenAIProviders(providers);
} catch (error) {
console.error('加载OpenAI提供商失败:', error);
}
@@ -2765,8 +2765,12 @@ class CLIProxyManager {
// 渲染OpenAI提供商列表
async renderOpenAIProviders(providers) {
const container = document.getElementById('openai-providers-list');
if (!container) {
return;
}
const list = Array.isArray(providers) ? providers : [];
if (!Array.isArray(providers) || providers.length === 0) {
if (list.length === 0) {
container.innerHTML = `
<div class="empty-state">
<i class="fas fa-plug"></i>
@@ -2781,7 +2785,7 @@ class CLIProxyManager {
}
// 根据提供商数量设置滚动条
if (providers.length > 5) {
if (list.length > 5) {
container.style.maxHeight = '400px';
container.style.overflowY = 'auto';
} else {
@@ -2792,7 +2796,7 @@ class CLIProxyManager {
// 获取使用统计,按 source 聚合
const stats = await this.getKeyStats();
container.innerHTML = providers.map((provider, index) => {
container.innerHTML = list.map((provider, index) => {
const item = typeof provider === 'object' && provider !== null ? provider : {};
// 处理两种API密钥格式新的 api-key-entries 和旧的 api-keys
@@ -3132,13 +3136,31 @@ 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);
let typeDisplayKey;
switch (fileType) {
case 'qwen':
typeDisplayKey = 'auth_files.type_qwen';
break;
case 'gemini':
typeDisplayKey = 'auth_files.type_gemini';
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 'empty':
typeDisplayKey = 'auth_files.type_empty';
break;
default:
typeDisplayKey = 'auth_files.type_unknown';
break;
}
const typeBadge = `<span class="file-type-badge ${fileType}">${typeDisplay}</span>`;
const typeBadge = `<span class="file-type-badge ${fileType}">${i18n.t(typeDisplayKey)}</span>`;
return `
<div class="file-item" data-file-type="${fileType}">
@@ -3188,13 +3210,13 @@ class CLIProxyManager {
// 预定义的按钮顺序和显示文本
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' }
{ type: 'all', labelKey: 'auth_files.filter_all' },
{ type: 'qwen', labelKey: 'auth_files.filter_qwen' },
{ type: 'gemini', labelKey: 'auth_files.filter_gemini' },
{ type: 'claude', labelKey: 'auth_files.filter_claude' },
{ type: 'codex', labelKey: 'auth_files.filter_codex' },
{ type: 'iflow', labelKey: 'auth_files.filter_iflow' },
{ type: 'empty', labelKey: 'auth_files.filter_empty' }
];
// 获取现有按钮
@@ -3209,6 +3231,11 @@ class CLIProxyManager {
const btnType = btn.dataset.type;
if (existingTypes.has(btnType)) {
btn.style.display = 'inline-block';
const match = predefinedTypes.find(item => item.type === btnType);
if (match) {
btn.textContent = i18n.t(match.labelKey);
btn.setAttribute('data-i18n-text', match.labelKey);
}
} else {
btn.style.display = 'none';
}
@@ -3222,9 +3249,17 @@ class CLIProxyManager {
const btn = document.createElement('button');
btn.className = 'filter-btn';
btn.dataset.type = type;
// 首字母大写
btn.textContent = type.charAt(0).toUpperCase() + type.slice(1);
const match = predefinedTypes.find(item => item.type === type);
if (match) {
btn.setAttribute('data-i18n-text', match.labelKey);
btn.textContent = i18n.t(match.labelKey);
} else {
const dynamicKey = `auth_files.filter_${type}`;
btn.setAttribute('data-i18n-text', dynamicKey);
btn.textContent = this.generateDynamicTypeLabel(type);
}
// 插入到 Empty 按钮之前(如果存在)
const emptyBtn = filterContainer.querySelector('[data-type="empty"]');
if (emptyBtn) {
@@ -3256,6 +3291,21 @@ class CLIProxyManager {
item.classList.add('hidden');
}
});
// 更新筛选按钮文本(以防语言切换后新按钮未刷新)
this.refreshFilterButtonTexts();
}
// 生成动态类型标签
generateDynamicTypeLabel(type) {
if (!type) return '';
const key = `auth_files.type_${type}`;
const translated = i18n.t(key);
if (translated && translated !== key) {
return translated;
}
if (type.toLowerCase() === 'iflow') return 'iFlow';
return type.charAt(0).toUpperCase() + type.slice(1);
}
// 绑定认证文件筛选事件
@@ -3277,6 +3327,19 @@ class CLIProxyManager {
filterContainer._filterListener = listener;
filterContainer.addEventListener('click', listener);
// 首次渲染时刷新按钮文本
this.refreshFilterButtonTexts();
}
// 刷新筛选按钮文本(根据 data-i18n-text
refreshFilterButtonTexts() {
document.querySelectorAll('.auth-file-filter .filter-btn[data-i18n-text]').forEach(btn => {
const key = btn.getAttribute('data-i18n-text');
if (key) {
btn.textContent = i18n.t(key);
}
});
}
// 绑定认证文件操作按钮事件(使用事件委托)

30
i18n.js
View File

@@ -222,6 +222,21 @@ const i18n = {
'auth_files.delete_success': '文件删除成功',
'auth_files.delete_all_success': '成功删除',
'auth_files.files_count': '个文件',
'auth_files.filter_all': '全部',
'auth_files.filter_qwen': 'Qwen',
'auth_files.filter_gemini': 'Gemini',
'auth_files.filter_claude': 'Claude',
'auth_files.filter_codex': 'Codex',
'auth_files.filter_iflow': 'iFlow',
'auth_files.filter_empty': '空文件',
'auth_files.filter_unknown': '其他',
'auth_files.type_qwen': 'Qwen',
'auth_files.type_gemini': 'Gemini',
'auth_files.type_claude': 'Claude',
'auth_files.type_codex': 'Codex',
'auth_files.type_iflow': 'iFlow',
'auth_files.type_empty': '空文件',
'auth_files.type_unknown': '其他',
// Codex OAuth
@@ -649,6 +664,21 @@ const i18n = {
'auth_files.delete_success': 'File deleted successfully',
'auth_files.delete_all_success': 'Successfully deleted',
'auth_files.files_count': 'files',
'auth_files.filter_all': 'All',
'auth_files.filter_qwen': 'Qwen',
'auth_files.filter_gemini': 'Gemini',
'auth_files.filter_claude': 'Claude',
'auth_files.filter_codex': 'Codex',
'auth_files.filter_iflow': 'iFlow',
'auth_files.filter_empty': 'Empty',
'auth_files.filter_unknown': 'Other',
'auth_files.type_qwen': 'Qwen',
'auth_files.type_gemini': 'Gemini',
'auth_files.type_claude': 'Claude',
'auth_files.type_codex': 'Codex',
'auth_files.type_iflow': 'iFlow',
'auth_files.type_empty': 'Empty',
'auth_files.type_unknown': 'Other',
// Codex OAuth
'auth_login.codex_oauth_title': 'Codex OAuth',

View File

@@ -424,13 +424,13 @@
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>
<button class="filter-btn active" data-type="all" data-i18n-text="auth_files.filter_all">All</button>
<button class="filter-btn" data-type="qwen" data-i18n-text="auth_files.filter_qwen">Qwen</button>
<button class="filter-btn" data-type="gemini" data-i18n-text="auth_files.filter_gemini">Gemini</button>
<button class="filter-btn" data-type="claude" data-i18n-text="auth_files.filter_claude">Claude</button>
<button class="filter-btn" data-type="codex" data-i18n-text="auth_files.filter_codex">Codex</button>
<button class="filter-btn" data-type="iflow" data-i18n-text="auth_files.filter_iflow">iFlow</button>
<button class="filter-btn" data-type="empty" data-i18n-text="auth_files.filter_empty">Empty</button>
</div>
</div>
<div class="header-actions">

View File

@@ -1699,6 +1699,7 @@ input:checked+.slider:before {
display: flex;
flex-direction: column;
transition: all 0.3s ease;
position: relative;
}
.file-item.hidden {
@@ -1718,6 +1719,13 @@ input:checked+.slider:before {
width: 100%;
}
/* 为 API Keys 和 AI Providers 的项优化按钮位置 */
.key-item .item-content,
.provider-item .item-content {
padding-right: 120px; /* 为按钮预留空间,防止内容重叠 */
min-width: 0; /* 确保内容可以正常换行 */
}
.item-footer {
display: flex;
justify-content: space-between;
@@ -1725,6 +1733,7 @@ input:checked+.slider:before {
margin-top: 12px;
flex-wrap: wrap;
gap: 10px;
align-items: flex-start;
}
.item-stats {
@@ -1736,9 +1745,46 @@ input:checked+.slider:before {
.item-actions {
display: flex;
gap: 6px;
flex-wrap: wrap;
gap: 8px;
flex-wrap: nowrap;
align-items: center;
justify-content: flex-end;
}
/* API Keys 和 AI Providers 的按钮组 - 绝对定位到右侧垂直居中 */
.key-item .item-actions,
.provider-item .item-actions {
position: absolute;
top: 50%;
right: 15px;
transform: translateY(-50%);
gap: 8px;
}
/* 按钮样式优化 */
.key-item .item-actions .btn,
.provider-item .item-actions .btn {
min-width: 40px;
height: 40px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.key-item .item-actions .btn:hover,
.provider-item .item-actions .btn:hover {
transform: scale(1.05);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.key-item .item-actions .btn i,
.provider-item .item-actions .btn i {
font-size: 16px;
margin: 0;
}
.item-title {
@@ -3460,6 +3506,44 @@ input:checked+.slider:before {
font-size: 11px !important;
}
}
/* 响应式设计 - API Keys 和 AI Providers 按钮优化 */
@media (max-width: 768px) {
/* 移动端按钮位置调整 */
.key-item .item-actions,
.provider-item .item-actions {
position: relative;
top: auto;
right: auto;
transform: none;
margin-top: 12px;
justify-content: flex-start;
}
/* 移动端移除内容右侧内边距 */
.key-item .item-content,
.provider-item .item-content {
padding-right: 0;
}
/* 移动端按钮尺寸调整 */
.key-item .item-actions .btn,
.provider-item .item-actions .btn {
min-width: 36px;
height: 36px;
}
.key-item .item-actions .btn:hover,
.provider-item .item-actions .btn:hover {
transform: scale(1.05);
}
.key-item .item-actions .btn i,
.provider-item .item-actions .btn i {
font-size: 14px;
}
}
#config-management .CodeMirror .CodeMirror-lines {
padding: 12px;
}