diff --git a/src/pages/AiProvidersPage.module.scss b/src/pages/AiProvidersPage.module.scss index 9947223..484105b 100644 --- a/src/pages/AiProvidersPage.module.scss +++ b/src/pages/AiProvidersPage.module.scss @@ -1,3 +1,6 @@ +@use '../styles/variables' as *; +@use '../styles/mixins' as *; + .container { width: 100%; } @@ -44,3 +47,37 @@ grid-template-columns: 1fr; } } + +// 成功失败次数统计样式 +.cardStats { + display: flex; + gap: 8px; + padding: 8px 0 0; + margin-top: 8px; +} + +.statSuccess { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 12px; + font-weight: 600; + color: #fff; + background-color: #22c55e; + padding: 4px 12px; + border-radius: 14px; + white-space: nowrap; +} + +.statFailure { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 12px; + font-weight: 600; + color: #fff; + background-color: #ef4444; + padding: 4px 12px; + border-radius: 14px; + white-space: nowrap; +} diff --git a/src/pages/AiProvidersPage.tsx b/src/pages/AiProvidersPage.tsx index 5c8d29a..02f4ba0 100644 --- a/src/pages/AiProvidersPage.tsx +++ b/src/pages/AiProvidersPage.tsx @@ -66,9 +66,37 @@ const parseExcludedModels = (text: string): string[] => const excludedModelsToText = (models?: string[]) => (Array.isArray(models) ? models.join('\n') : ''); -// 根据 auth_index 获取统计数据 -const getStatsByAuthIndex = (authIndex: string, keyStats: KeyStats): KeyStatBucket => { - return keyStats.byAuthIndex?.[authIndex] ?? { success: 0, failure: 0 }; +// 根据 source (apiKey) 获取统计数据 - 与旧版逻辑一致 +const getStatsBySource = ( + apiKey: string, + keyStats: KeyStats, + maskFn: (key: string) => string +): KeyStatBucket => { + const bySource = keyStats.bySource ?? {}; + const masked = maskFn(apiKey); + return bySource[apiKey] || bySource[masked] || { success: 0, failure: 0 }; +}; + +// 对于 OpenAI 提供商,汇总所有 apiKeyEntries 的统计 - 与旧版逻辑一致 +const getOpenAIProviderStats = ( + apiKeyEntries: ApiKeyEntry[] | undefined, + keyStats: KeyStats, + maskFn: (key: string) => string +): KeyStatBucket => { + const bySource = keyStats.bySource ?? {}; + let totalSuccess = 0; + let totalFailure = 0; + + (apiKeyEntries || []).forEach((entry) => { + const key = entry?.apiKey || ''; + if (!key) return; + const masked = maskFn(key); + const stats = bySource[key] || bySource[masked] || { success: 0, failure: 0 }; + totalSuccess += stats.success; + totalFailure += stats.failure; + }); + + return { success: totalSuccess, failure: totalFailure }; }; const buildApiKeyEntry = (input?: Partial): ApiKeyEntry => ({ @@ -505,20 +533,31 @@ export function AiProvidersPage() { {renderList( geminiKeys, (item) => item.apiKey, - (item, index) => ( - -
- {t('ai_providers.gemini_item_title')} #{index + 1} -
-
{maskApiKey(item.apiKey)}
- {item.baseUrl &&
{item.baseUrl}
} - {item.excludedModels?.length ? ( -
- {t('ai_providers.excluded_models_count', { count: item.excludedModels.length })} + (item, index) => { + const stats = getStatsBySource(item.apiKey, keyStats, maskApiKey); + return ( + +
+ {t('ai_providers.gemini_item_title')} #{index + 1}
- ) : null} -
- ), +
{maskApiKey(item.apiKey)}
+ {item.baseUrl &&
{item.baseUrl}
} + {item.excludedModels?.length ? ( +
+ {t('ai_providers.excluded_models_count', { count: item.excludedModels.length })} +
+ ) : null} +
+ + {t('stats.success')}:{stats.success}次 + + + {t('stats.failure')}:{stats.failure}次 + +
+ + ); + }, (index) => openGeminiModal(index), (item) => deleteGemini(item.apiKey), t('ai_providers.gemini_add_button') @@ -536,13 +575,24 @@ export function AiProvidersPage() { {renderList( codexConfigs, (item) => item.apiKey, - (item) => ( - -
{item.baseUrl || t('ai_providers.codex_item_title')}
-
{maskApiKey(item.apiKey)}
- {item.proxyUrl &&
{item.proxyUrl}
} -
- ), + (item, _index) => { + const stats = getStatsBySource(item.apiKey, keyStats, maskApiKey); + return ( + +
{item.baseUrl || t('ai_providers.codex_item_title')}
+
{maskApiKey(item.apiKey)}
+ {item.proxyUrl &&
{item.proxyUrl}
} +
+ + {t('stats.success')}:{stats.success}次 + + + {t('stats.failure')}:{stats.failure}次 + +
+
+ ); + }, (index) => openProviderModal('codex', index), (item) => deleteProviderEntry('codex', item.apiKey), t('ai_providers.codex_add_button') @@ -560,18 +610,29 @@ export function AiProvidersPage() { {renderList( claudeConfigs, (item) => item.apiKey, - (item) => ( - -
{item.baseUrl || t('ai_providers.claude_item_title')}
-
{maskApiKey(item.apiKey)}
- {item.proxyUrl &&
{item.proxyUrl}
} - {item.models?.length ? ( -
- {t('ai_providers.claude_models_count')}: {item.models.length} + (item, _index) => { + const stats = getStatsBySource(item.apiKey, keyStats, maskApiKey); + return ( + +
{item.baseUrl || t('ai_providers.claude_item_title')}
+
{maskApiKey(item.apiKey)}
+ {item.proxyUrl &&
{item.proxyUrl}
} + {item.models?.length ? ( +
+ {t('ai_providers.claude_models_count')}: {item.models.length} +
+ ) : null} +
+ + {t('stats.success')}:{stats.success}次 + + + {t('stats.failure')}:{stats.failure}次 +
- ) : null} -
- ), + + ); + }, (index) => openProviderModal('claude', index), (item) => deleteProviderEntry('claude', item.apiKey), t('ai_providers.claude_add_button') @@ -589,19 +650,30 @@ export function AiProvidersPage() { {renderList( openaiProviders, (item) => item.name, - (item) => ( - -
{item.name}
-
{item.baseUrl}
-
- {t('ai_providers.openai_keys_count')}: {item.apiKeyEntries?.length || 0} -
-
- {t('ai_providers.openai_models_count')}: {item.models?.length || 0} -
- {item.testModel &&
{item.testModel}
} -
- ), + (item, _index) => { + const stats = getOpenAIProviderStats(item.apiKeyEntries, keyStats, maskApiKey); + return ( + +
{item.name}
+
{item.baseUrl}
+
+ {t('ai_providers.openai_keys_count')}: {item.apiKeyEntries?.length || 0} +
+
+ {t('ai_providers.openai_models_count')}: {item.models?.length || 0} +
+ {item.testModel &&
{item.testModel}
} +
+ + {t('stats.success')}:{stats.success}次 + + + {t('stats.failure')}:{stats.failure}次 + +
+
+ ); + }, (index) => openOpenaiModal(index), (item) => deleteOpenai(item.name), t('ai_providers.openai_add_button')