From 4c4856794f57aeec814e120b72c0cbbd512c87c4 Mon Sep 17 00:00:00 2001 From: liucong2013 <10938633+liucong2013@users.noreply.github.com> Date: Tue, 14 Apr 2026 22:58:26 +0000 Subject: [PATCH 1/6] feat(openai): add provider sorting and model filters --- .../providers/ClaudeSection/ClaudeSection.tsx | 4 +- .../providers/CodexSection/CodexSection.tsx | 4 +- .../providers/GeminiSection/GeminiSection.tsx | 4 +- .../providers/OpenAISection/OpenAISection.tsx | 228 ++++++++++- src/components/providers/ProviderList.tsx | 8 +- .../providers/VertexSection/VertexSection.tsx | 4 +- src/i18n/locales/en.json | 25 +- src/i18n/locales/ru.json | 11 +- src/i18n/locales/zh-CN.json | 25 +- src/pages/AiProvidersPage.module.scss | 386 ++++++++++++++++++ src/styles/components.scss | 23 +- 11 files changed, 691 insertions(+), 31 deletions(-) diff --git a/src/components/providers/ClaudeSection/ClaudeSection.tsx b/src/components/providers/ClaudeSection/ClaudeSection.tsx index 63ceb32..4ecf668 100644 --- a/src/components/providers/ClaudeSection/ClaudeSection.tsx +++ b/src/components/providers/ClaudeSection/ClaudeSection.tsx @@ -89,8 +89,8 @@ export function ClaudeSection({ keyField={(item) => item.apiKey} emptyTitle={t('ai_providers.claude_empty_title')} emptyDescription={t('ai_providers.claude_empty_desc')} - onEdit={onEdit} - onDelete={onDelete} + onEdit={(_, index) => onEdit(index)} + onDelete={(_, index) => onDelete(index)} actionsDisabled={actionsDisabled} getRowDisabled={(item) => hasDisableAllModelsRule(item.excludedModels)} renderExtraActions={(item, index) => ( diff --git a/src/components/providers/CodexSection/CodexSection.tsx b/src/components/providers/CodexSection/CodexSection.tsx index ea31771..5328eb3 100644 --- a/src/components/providers/CodexSection/CodexSection.tsx +++ b/src/components/providers/CodexSection/CodexSection.tsx @@ -89,8 +89,8 @@ export function CodexSection({ keyField={(item) => item.apiKey} emptyTitle={t('ai_providers.codex_empty_title')} emptyDescription={t('ai_providers.codex_empty_desc')} - onEdit={onEdit} - onDelete={onDelete} + onEdit={(_, index) => onEdit(index)} + onDelete={(_, index) => onDelete(index)} actionsDisabled={actionsDisabled} getRowDisabled={(item) => hasDisableAllModelsRule(item.excludedModels)} renderExtraActions={(item, index) => ( diff --git a/src/components/providers/GeminiSection/GeminiSection.tsx b/src/components/providers/GeminiSection/GeminiSection.tsx index 6f28f3e..3fc6e5d 100644 --- a/src/components/providers/GeminiSection/GeminiSection.tsx +++ b/src/components/providers/GeminiSection/GeminiSection.tsx @@ -89,8 +89,8 @@ export function GeminiSection({ keyField={(item) => item.apiKey} emptyTitle={t('ai_providers.gemini_empty_title')} emptyDescription={t('ai_providers.gemini_empty_desc')} - onEdit={onEdit} - onDelete={onDelete} + onEdit={(_, index) => onEdit(index)} + onDelete={(_, index) => onDelete(index)} actionsDisabled={actionsDisabled} getRowDisabled={(item) => hasDisableAllModelsRule(item.excludedModels)} renderExtraActions={(item, index) => ( diff --git a/src/components/providers/OpenAISection/OpenAISection.tsx b/src/components/providers/OpenAISection/OpenAISection.tsx index 1782c72..59a9835 100644 --- a/src/components/providers/OpenAISection/OpenAISection.tsx +++ b/src/components/providers/OpenAISection/OpenAISection.tsx @@ -1,4 +1,4 @@ -import { Fragment, useMemo } from 'react'; +import { Fragment, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/Button'; import { Card } from '@/components/ui/Card'; @@ -18,6 +18,10 @@ import { ProviderList } from '../ProviderList'; import { ProviderStatusBar } from '../ProviderStatusBar'; import { getOpenAIProviderStats, getStatsBySource } from '../utils'; +type SortOption = 'name' | 'priority' | 'recent-success'; +type SortDirection = 'asc' | 'desc'; +type ToolbarPosition = 'top' | 'bottom'; + interface OpenAISectionProps { configs: OpenAIProviderConfig[]; keyStats: KeyStats; @@ -45,6 +49,38 @@ export function OpenAISection({ }: OpenAISectionProps) { const { t } = useTranslation(); const actionsDisabled = disableControls || loading || isSwitching; + const [sortOption, setSortOption] = useState('priority'); + const [sortDirection, setSortDirection] = useState('asc'); + const [selectedModels, setSelectedModels] = useState>(new Set()); + const [activeDropdown, setActiveDropdown] = useState(null); + const topDropdownRef = useRef(null); + const bottomDropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Node; + const clickedTop = topDropdownRef.current?.contains(target); + const clickedBottom = bottomDropdownRef.current?.contains(target); + + if (!clickedTop && !clickedBottom) { + setActiveDropdown(null); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const allModelNames = useMemo(() => { + const modelSet = new Set(); + configs.forEach((provider) => { + provider.models?.forEach((model) => { + if (model.name) { + modelSet.add(model.name); + } + }); + }); + return Array.from(modelSet).sort(); + }, [configs]); const statusBarCache = useMemo(() => { const cache = new Map>(); @@ -65,6 +101,171 @@ export function OpenAISection({ return cache; }, [configs, usageDetailsBySource]); + const sortedConfigs = useMemo(() => { + const filtered = configs.filter((provider) => { + if (selectedModels.size === 0) return true; + return provider.models?.some((model) => selectedModels.has(model.name)); + }); + + const sorted = [...filtered]; + const direction = sortDirection === 'desc' ? -1 : 1; + + switch (sortOption) { + case 'name': + sorted.sort((a, b) => direction * a.name.localeCompare(b.name)); + break; + case 'priority': + sorted.sort((a, b) => { + const priorityA = a.priority ?? Number.MAX_SAFE_INTEGER; + const priorityB = b.priority ?? Number.MAX_SAFE_INTEGER; + return direction * (priorityA - priorityB) || a.name.localeCompare(b.name); + }); + break; + case 'recent-success': + sorted.sort((a, b) => { + const statsA = getOpenAIProviderStats(a.apiKeyEntries, keyStats, a.prefix); + const statsB = getOpenAIProviderStats(b.apiKeyEntries, keyStats, b.prefix); + return direction * (statsA.success - statsB.success) || a.name.localeCompare(b.name); + }); + break; + default: + break; + } + + return sorted; + }, [configs, sortOption, sortDirection, keyStats, selectedModels]); + + const getProviderKey = (item: OpenAIProviderConfig) => `${item.name}-${item.prefix ?? ''}-${item.baseUrl}`; + + const getProviderIndex = (item: OpenAIProviderConfig) => + configs.findIndex((config) => config === item); + + const toggleModelSelection = (modelName: string) => { + setSelectedModels((prev) => { + const next = new Set(prev); + if (next.has(modelName)) { + next.delete(modelName); + } else { + next.add(modelName); + } + return next; + }); + }; + + const clearAllModels = () => { + setSelectedModels(new Set()); + }; + + const toggleSortDirection = () => { + setSortDirection((prev) => (prev === 'asc' ? 'desc' : 'asc')); + }; + + const toggleDropdown = (position: ToolbarPosition) => { + setActiveDropdown((prev) => (prev === position ? null : position)); + }; + + const renderSortControls = () => ( +
+ + +
+ ); + + const renderToolbar = (position: ToolbarPosition) => { + const isDropdownOpen = activeDropdown === position; + const dropdownRef = position === 'top' ? topDropdownRef : bottomDropdownRef; + const wrapperClassName = + position === 'top' ? styles.cardHeaderActions : `${styles.cardHeaderActions} ${styles.cardFooterActions}`; + + return ( +
+
+
toggleDropdown(position)}> + {selectedModels.size === 0 ? ( + + {t('ai_providers.model_search_placeholder')} + + ) : ( + <> + {Array.from(selectedModels).map((name) => ( + + {name} + + + ))} + + )} + +
+ + {isDropdownOpen && ( +
+
+ + {selectedModels.size > 0 && ( + + )} +
+ {allModelNames.map((name) => ( + + ))} +
+ )} +
+ {renderSortControls()} + +
+ ); + }; + return ( <> } - extra={ - - } + extra={renderToolbar('top')} > - items={configs} + items={sortedConfigs} loading={loading} - keyField={(_, index) => `openai-provider-${index}`} + keyField={(item) => getProviderKey(item)} emptyTitle={t('ai_providers.openai_empty_title')} emptyDescription={t('ai_providers.openai_empty_desc')} - onEdit={onEdit} - onDelete={onDelete} + onEdit={(item) => { + const index = getProviderIndex(item); + if (index >= 0) { + onEdit(index); + } + }} + onDelete={(item) => { + const index = getProviderIndex(item); + if (index >= 0) { + onDelete(index); + } + }} actionsDisabled={actionsDisabled} renderContent={(item) => { const stats = getOpenAIProviderStats(item.apiKeyEntries, keyStats, item.prefix); @@ -195,6 +402,7 @@ export function OpenAISection({ ); }} /> +
{renderToolbar('bottom')}
); diff --git a/src/components/providers/ProviderList.tsx b/src/components/providers/ProviderList.tsx index 9d1a1f7..4f2fada 100644 --- a/src/components/providers/ProviderList.tsx +++ b/src/components/providers/ProviderList.tsx @@ -8,8 +8,8 @@ interface ProviderListProps { loading: boolean; keyField: (item: T, index: number) => string; renderContent: (item: T, index: number) => ReactNode; - onEdit: (index: number) => void; - onDelete: (index: number) => void; + onEdit: (item: T, index: number) => void; + onDelete: (item: T, index: number) => void; emptyTitle: string; emptyDescription: string; deleteLabel?: string; @@ -57,7 +57,7 @@ export function ProviderList({ + + + + ); + }; + return ( <> - - - {t('ai_providers.openai_title')} - - } - extra={renderToolbar('top')} - > - - items={sortedConfigs} - loading={loading} - keyField={(item) => `openai-provider-${item.originalIndex}`} - emptyTitle={t('ai_providers.openai_empty_title')} - emptyDescription={t('ai_providers.openai_empty_desc')} - listClassName={styles.providerList} - rowClassName={styles.providerCard} - metaClassName={styles.providerMeta} - actionsClassName={styles.providerActions} - onEdit={(item) => { - onEdit(item.originalIndex); - }} - onDelete={(item) => { - onDelete(item.originalIndex); - }} - actionsDisabled={actionsDisabled} - renderContent={(item) => { - const provider = item.config; - const stats = getOpenAIProviderStats(provider.apiKeyEntries, keyStats, provider.prefix); - const headerEntries = Object.entries(provider.headers || {}); - const apiKeyEntries = provider.apiKeyEntries || []; - const statusData = statusBarCache.get(provider.name) || calculateStatusBarData([]); - - return ( - -
{provider.name}
- {provider.priority !== undefined && ( -
- {t('common.priority')}: - {provider.priority} -
- )} - {provider.prefix && ( -
- {t('common.prefix')}: - {provider.prefix} -
- )} -
- {t('common.base_url')}: - {provider.baseUrl} -
- {headerEntries.length > 0 && ( -
- {headerEntries.map(([key, value]) => ( - - {key}: {value} - - ))} -
- )} - {apiKeyEntries.length > 0 && ( -
-
- {t('ai_providers.openai_keys_count')}: {apiKeyEntries.length} -
-
- {apiKeyEntries.map((entry, entryIndex) => { - const entryStats = getStatsBySource(entry.apiKey, keyStats); - return ( -
- {entryIndex + 1} - {maskApiKey(entry.apiKey)} - {entry.proxyUrl && ( - {entry.proxyUrl} - )} -
- - {entryStats.success} - - - {entryStats.failure} - -
-
- ); - })} -
-
- )} -
- {t('ai_providers.openai_models_count')}: - {provider.models?.length || 0} -
- {provider.models?.length ? ( -
- {provider.models.map((model) => ( - - {model.name} - {model.alias && model.alias !== model.name && ( - {model.alias} - )} - - ))} -
- ) : null} - {provider.testModel && ( -
- Test Model: - {provider.testModel} -
- )} -
- - {t('stats.success')}: {stats.success} - - - {t('stats.failure')}: {stats.failure} - -
- -
- ); - }} - /> -
{renderToolbar('bottom')}
-
+
+ + {renderToolbar()} +
+ } + > + {loading && sortedConfigs.length === 0 ? ( +
{t('common.loading')}
+ ) : sortedConfigs.length === 0 ? ( + + ) : ( +
{sortedConfigs.map(renderProviderCard)}
+ )} + + + {typeof document !== 'undefined' && floatingToolbarStyle.visible + ? createPortal( +
+
+
{renderStaticTitle()}
+ {renderToolbar()} +
+
, + document.body + ) + : null} ); } diff --git a/src/pages/AiProvidersPage.module.scss b/src/pages/AiProvidersPage.module.scss index 05427e2..8b2d965 100644 --- a/src/pages/AiProvidersPage.module.scss +++ b/src/pages/AiProvidersPage.module.scss @@ -52,7 +52,7 @@ } } -.providerList { +.openaiProviderList { display: grid; gap: $spacing-md; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); @@ -66,7 +66,7 @@ } } -.providerCard { +.openaiProviderCard { border: 1px solid var(--border-color); border-radius: $radius-md; padding: $spacing-md; @@ -78,7 +78,7 @@ min-height: 0; } -.providerMeta { +.openaiProviderMeta { display: flex; flex-direction: column; gap: 6px; @@ -86,14 +86,14 @@ min-width: 0; } -.providerActions { +.openaiProviderActions { display: flex; gap: $spacing-sm; flex-wrap: wrap; justify-content: flex-end; } -.providerTitle { +.openaiProviderTitle { font-weight: 700; color: var(--text-primary); } @@ -166,6 +166,7 @@ } } + // 卡片头部操作区 .cardHeaderActions { display: flex; @@ -174,17 +175,27 @@ gap: $spacing-sm; } -.cardFooterActions { - justify-content: flex-end; - width: 100%; +.openaiToolbarAnchorHidden { + visibility: hidden; + pointer-events: none; } -.providerToolbarFooter { - margin-top: $spacing-md; - padding-top: $spacing-md; - border-top: 1px solid var(--border-primary); +.openaiFloatingToolbar { + position: fixed; + z-index: 20; + background: var(--bg-primary); + box-shadow: none; + overflow: visible; + padding: 0; } +.openaiFloatingToolbar :global(.card-header) { + margin-bottom: 0; + padding: $spacing-sm $spacing-lg; + border-radius: 0; +} + + // 模型多选下拉框 .modelMultiSelectWrapper { position: relative; @@ -448,6 +459,7 @@ display: inline-flex; align-items: center; gap: 4px; + flex-wrap: wrap; background: var(--bg-quinary, #f8f9fa); color: var(--text-secondary); border: 1px solid var(--border-secondary); @@ -465,11 +477,13 @@ .modelName { font-weight: 600; color: var(--text-primary); + overflow-wrap: anywhere; } .modelAlias { color: var(--text-tertiary); font-style: italic; + overflow-wrap: anywhere; &::before { content: '→ '; diff --git a/src/styles/components.scss b/src/styles/components.scss index 7b4e6de..297eb62 100644 --- a/src/styles/components.scss +++ b/src/styles/components.scss @@ -648,21 +648,20 @@ textarea { } .item-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: $spacing-md; border: 1px solid var(--border-color); border-radius: $radius-md; padding: $spacing-md; background: var(--bg-primary); + display: flex; + align-items: center; + justify-content: space-between; + gap: $spacing-md; + flex-wrap: wrap; .item-meta { display: flex; flex-direction: column; gap: 6px; - flex: 1; - min-width: 0; } .item-title { @@ -679,9 +678,7 @@ textarea { .item-actions { display: flex; - align-items: center; gap: $spacing-sm; - flex-shrink: 0; } } From b75314588796390db0b57c6aa82500e1bd4f6e10 Mon Sep 17 00:00:00 2001 From: liucong2013 <10938633+liucong2013@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:21:05 +0000 Subject: [PATCH 5/6] fix(openai): address provider UI review feedback --- .../providers/OpenAISection/OpenAISection.tsx | 25 +++++++++++++++++-- src/i18n/locales/en.json | 21 +++------------- src/i18n/locales/ru.json | 7 +++--- src/i18n/locales/zh-CN.json | 21 +++------------- src/pages/AiProvidersPage.module.scss | 18 ++++++++++--- 5 files changed, 47 insertions(+), 45 deletions(-) diff --git a/src/components/providers/OpenAISection/OpenAISection.tsx b/src/components/providers/OpenAISection/OpenAISection.tsx index c4e13f8..6c51ecc 100644 --- a/src/components/providers/OpenAISection/OpenAISection.tsx +++ b/src/components/providers/OpenAISection/OpenAISection.tsx @@ -325,7 +325,7 @@ export function OpenAISection({ onClick={toggleSortDirection} className={styles.sortDirectionButton} disabled={actionsDisabled} - title={sortDirection === 'asc' ? t('common.sort_ascending') : t('common.sort_descending')} + title={sortDirection === 'asc' ? t('ai_providers.sort_ascending') : t('ai_providers.sort_descending')} > {sortDirection === 'asc' ? '↑' : '↓'} @@ -356,7 +356,7 @@ export function OpenAISection({ ) : ( <> {Array.from(selectedModels).map((name) => ( - + {name} )} + {selectedModels.size > 0 && ( + + )} {renderSortControls()} + )} + /> ) : sortedConfigs.length === 0 ? ( Date: Wed, 22 Apr 2026 17:48:58 +0000 Subject: [PATCH 6/6] fix(openai): polish provider list overlays and i18n --- src/components/common/PageTransition.tsx | 5 +- src/components/common/PageTransitionLayer.ts | 7 +- .../providers/OpenAISection/OpenAISection.tsx | 91 ++++++++++--------- src/i18n/locales/en.json | 3 + src/i18n/locales/zh-CN.json | 3 + src/i18n/locales/zh-TW.json | 8 ++ src/pages/AiProvidersPage.module.scss | 6 +- src/styles/layout.scss | 2 +- 8 files changed, 76 insertions(+), 49 deletions(-) diff --git a/src/components/common/PageTransition.tsx b/src/components/common/PageTransition.tsx index 452d56a..5561275 100644 --- a/src/components/common/PageTransition.tsx +++ b/src/components/common/PageTransition.tsx @@ -391,7 +391,10 @@ export function PageTransition({ } > {render(layer.location)} diff --git a/src/components/common/PageTransitionLayer.ts b/src/components/common/PageTransitionLayer.ts index 98f5898..0ab66c3 100644 --- a/src/components/common/PageTransitionLayer.ts +++ b/src/components/common/PageTransitionLayer.ts @@ -5,15 +5,16 @@ export type LayerStatus = 'current' | 'exiting' | 'stacked'; export type PageTransitionLayerContextValue = { status: LayerStatus; isCurrentLayer: boolean; + isAnimating: boolean; }; export const PageTransitionLayerContext = createContext(null); export const PAGE_TRANSITION_LAYER_CONTEXT_VALUES: Record = { - current: { status: 'current', isCurrentLayer: true }, - stacked: { status: 'stacked', isCurrentLayer: false }, - exiting: { status: 'exiting', isCurrentLayer: false }, + current: { status: 'current', isCurrentLayer: true, isAnimating: false }, + stacked: { status: 'stacked', isCurrentLayer: false, isAnimating: false }, + exiting: { status: 'exiting', isCurrentLayer: false, isAnimating: false }, }; export function usePageTransitionLayer() { diff --git a/src/components/providers/OpenAISection/OpenAISection.tsx b/src/components/providers/OpenAISection/OpenAISection.tsx index 6c51ecc..7b7c766 100644 --- a/src/components/providers/OpenAISection/OpenAISection.tsx +++ b/src/components/providers/OpenAISection/OpenAISection.tsx @@ -16,6 +16,7 @@ import { import { type UsageDetailsByAuthIndex, type UsageDetailsBySource } from '@/utils/usageIndex'; import styles from '@/pages/AiProvidersPage.module.scss'; import { ProviderStatusBar } from '../ProviderStatusBar'; +import { usePageTransitionLayer } from '@/components/common/PageTransitionLayer'; import { collectOpenAIProviderUsageDetails, getOpenAIProviderKey, @@ -68,6 +69,8 @@ export function OpenAISection({ onDelete, }: OpenAISectionProps) { const { t } = useTranslation(); + const pageTransitionLayer = usePageTransitionLayer(); + const isTransitionAnimating = pageTransitionLayer?.isAnimating ?? false; const actionsDisabled = disableControls || loading || isSwitching; const [sortOption, setSortOption] = useState('priority'); const [sortDirection, setSortDirection] = useState('asc'); @@ -85,7 +88,13 @@ export function OpenAISection({ const topDropdownRef = useRef(null); const floatingDropdownRef = useRef(null); + const shouldRenderFloatingToolbar = !isTransitionAnimating && floatingToolbarStyle.visible; + useEffect(() => { + if (isTransitionAnimating) { + return; + } + const updateFloatingToolbar = () => { const section = sectionRef.current; const anchor = topToolbarAnchorRef.current; @@ -131,7 +140,7 @@ export function OpenAISection({ window.removeEventListener('resize', updateFloatingToolbar); window.removeEventListener('scroll', updateFloatingToolbar, true); }; - }, [configs.length, isDropdownOpen, selectedModels, sortDirection, sortOption]); + }, [configs.length, isDropdownOpen, isTransitionAnimating, selectedModels, sortDirection, sortOption]); useEffect(() => { if (!isDropdownOpen) { @@ -239,11 +248,11 @@ export function OpenAISection({ const providerStats = sortOption === 'recent-success' ? new Map( - sorted.map(({ config }) => [ - config, - getOpenAIProviderStats(config, keyStats), - ]) - ) + sorted.map(({ config }) => [ + config, + getOpenAIProviderStats(config, keyStats), + ]) + ) : null; switch (sortOption) { @@ -333,7 +342,7 @@ export function OpenAISection({ ); const renderToolbar = (isFloating = false) => { - const isActiveToolbar = isFloating === floatingToolbarStyle.visible; + const isActiveToolbar = isFloating === shouldRenderFloatingToolbar; const dropdownClassName = dropdownLayout.openAbove ? `${styles.modelDropdownList} ${styles.modelDropdownListAbove}` : styles.modelDropdownList; @@ -536,7 +545,7 @@ export function OpenAISection({ ) : null} {provider.testModel && (
- Test Model: + {t('ai_providers.openai_test_model')}: {provider.testModel}
)} @@ -570,7 +579,7 @@ export function OpenAISection({ extra={
{renderToolbar(false)}
@@ -579,42 +588,42 @@ export function OpenAISection({ {loading && sortedConfigs.length === 0 ? (
{t('common.loading')}
) : configs.length > 0 && sortedConfigs.length === 0 ? ( - - {t('ai_providers.model_search_clear')} - - )} - /> + + {t('ai_providers.model_search_clear')} + + )} + /> ) : sortedConfigs.length === 0 ? ( - - ) : ( -
{sortedConfigs.map(renderProviderCard)}
- )} + + ) : ( +
{sortedConfigs.map(renderProviderCard)}
+ )} - {typeof document !== 'undefined' && floatingToolbarStyle.visible + {typeof document !== 'undefined' && shouldRenderFloatingToolbar ? createPortal( -
-
-
{renderStaticTitle()}
- {renderToolbar(true)} -
-
, - document.body - ) +
+
+
{renderStaticTitle()}
+ {renderToolbar(true)} +
+
, + document.body + ) : null} ); diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index e5c5feb..2d3ddae 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -405,8 +405,11 @@ "openai_filtered_empty_title": "No matching providers", "openai_filtered_empty_desc": "No providers match the current model filter. Clear the filter and try again.", "sort_by_name": "Sort by Name", + "sort_ascending": "Sort ascending", "sort_by_priority": "Sort by Priority", "sort_by_recent_success": "Sort by Recent Success", + "sort_descending": "Sort descending", + "openai_test_model": "Test Model", "openai_add_modal_title": "Add OpenAI Compatible Provider", "openai_add_modal_name_label": "Provider Name:", "openai_add_modal_name_placeholder": "e.g.: openrouter", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 02046f6..756ba9f 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -405,8 +405,11 @@ "openai_filtered_empty_title": "没有匹配的提供商", "openai_filtered_empty_desc": "当前模型筛选下没有匹配的提供商,请清除筛选后重试。", "sort_by_name": "按名称排序", + "sort_ascending": "升序排序", "sort_by_priority": "按优先级排序", "sort_by_recent_success": "按最近成功数排序", + "sort_descending": "降序排序", + "openai_test_model": "测试模型", "openai_add_modal_title": "添加OpenAI兼容提供商", "openai_add_modal_name_label": "提供商名称:", "openai_add_modal_name_placeholder": "例如: openrouter", diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index 7db6685..fee12b4 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -402,6 +402,14 @@ "openai_add_button": "新增供應商", "openai_empty_title": "暫無 OpenAI 相容供應商", "openai_empty_desc": "點擊上方按鈕新增第一個供應商", + "openai_filtered_empty_title": "沒有匹配的供應商", + "openai_filtered_empty_desc": "目前模型篩選下沒有匹配的供應商,請清除篩選後再試一次。", + "sort_by_name": "依名稱排序", + "sort_ascending": "升冪排序", + "sort_by_priority": "依優先順序排序", + "sort_by_recent_success": "依最近成功排序", + "sort_descending": "降冪排序", + "openai_test_model": "測試模型", "openai_add_modal_title": "新增 OpenAI 相容供應商", "openai_add_modal_name_label": "供應商名稱:", "openai_add_modal_name_placeholder": "例如: openrouter", diff --git a/src/pages/AiProvidersPage.module.scss b/src/pages/AiProvidersPage.module.scss index ec09b26..46ed02a 100644 --- a/src/pages/AiProvidersPage.module.scss +++ b/src/pages/AiProvidersPage.module.scss @@ -255,8 +255,8 @@ height: 18px; padding: 0; border: none; - background: rgba(0, 0, 0, 0.15); - color: #000; + background: var(--bg-secondary); + color: var(--text-primary); font-size: 18px; line-height: 1; cursor: pointer; @@ -265,7 +265,7 @@ flex-shrink: 0; &:hover { - background: rgba(0, 0, 0, 0.3); + background: var(--bg-tertiary); } } diff --git a/src/styles/layout.scss b/src/styles/layout.scss index 7c38ab0..c2f3494 100644 --- a/src/styles/layout.scss +++ b/src/styles/layout.scss @@ -31,7 +31,7 @@ border-bottom: 1px solid var(--border-color); position: sticky; top: 0; - z-index: 10; + z-index: $z-dropdown + 1; width: 100%; @media (max-width: $breakpoint-mobile) {