From 09c17c03b9b325b8c4814e9c14ca791673a66da7 Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Sun, 14 Dec 2025 20:05:59 +0800 Subject: [PATCH] feat: unify page layout with consistent page titles and structure - Add page title (h1) to all main pages for consistent hierarchy - Wrap page content in container/content div structure - Handle 404 error for unsupported OAuth excluded models API - Add cache price input field in usage page model pricing - Add upgrade required i18n messages for older CPA versions - Import mixins in page-level SCSS modules --- src/i18n/locales/en.json | 5 +- src/i18n/locales/zh-CN.json | 5 +- src/pages/AiProvidersPage.tsx | 7 +- src/pages/ApiKeysPage.module.scss | 2 + src/pages/ApiKeysPage.tsx | 149 +++++++------- src/pages/AuthFilesPage.module.scss | 20 +- src/pages/AuthFilesPage.tsx | 42 +++- src/pages/LogsPage.module.scss | 2 + src/pages/LogsPage.tsx | 8 +- src/pages/OAuthPage.module.scss | 2 + src/pages/OAuthPage.tsx | 247 ++++++++++++------------ src/pages/Settings/Settings.module.scss | 2 + src/pages/SettingsPage.tsx | 9 +- src/pages/SystemPage.tsx | 7 +- src/pages/UsagePage.tsx | 19 +- 15 files changed, 316 insertions(+), 210 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 32a4cff..5cd407a 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -370,7 +370,10 @@ "load_failed": "Failed to load exclusion list", "provider_required": "Please enter a provider first", "scope_all": "Scope: All providers", - "scope_provider": "Scope: {provider}" + "scope_provider": "Scope: {provider}", + "upgrade_required": "This feature requires a newer CLI Proxy API (CPA) version. Please upgrade.", + "upgrade_required_title": "Please upgrade CLI Proxy API", + "upgrade_required_desc": "The current server does not support the OAuth excluded models API. Please upgrade to the latest CLI Proxy API (CPA) version." }, "auth_login": { "codex_oauth_title": "Codex OAuth", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index fe1e9d6..77b466b 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -380,7 +380,10 @@ "load_failed": "加载排除列表失败", "provider_required": "请先填写提供商名称", "scope_all": "当前范围:全局(显示所有提供商)", - "scope_provider": "当前范围:{{provider}}" + "scope_provider": "当前范围:{{provider}}", + "upgrade_required": "当前 CPA 版本不支持模型排除列表,请升级 CPA 版本", + "upgrade_required_title": "需要升级 CPA 版本", + "upgrade_required_desc": "当前服务器版本不支持获取模型排除列表功能,请升级到最新版本的 CPA(CLI Proxy API)后重试。" }, "auth_login": { "codex_oauth_title": "Codex OAuth", diff --git a/src/pages/AiProvidersPage.tsx b/src/pages/AiProvidersPage.tsx index f2486e9..c5519b7 100644 --- a/src/pages/AiProvidersPage.tsx +++ b/src/pages/AiProvidersPage.tsx @@ -1072,8 +1072,10 @@ export function AiProvidersPage() { }; return ( -
- {error &&
{error}
} +
+

{t('ai_providers.title')}

+
+ {error &&
{error}
} )} +
); } diff --git a/src/pages/ApiKeysPage.module.scss b/src/pages/ApiKeysPage.module.scss index b30bb31..224958f 100644 --- a/src/pages/ApiKeysPage.module.scss +++ b/src/pages/ApiKeysPage.module.scss @@ -1,3 +1,5 @@ +@use '../styles/mixins' as *; + .container { width: 100%; } diff --git a/src/pages/ApiKeysPage.tsx b/src/pages/ApiKeysPage.tsx index 9bf69fa..348ebd1 100644 --- a/src/pages/ApiKeysPage.tsx +++ b/src/pages/ApiKeysPage.tsx @@ -9,6 +9,7 @@ import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores'; import { apiKeysApi } from '@/services/api'; import { maskApiKey } from '@/utils/format'; +import styles from './ApiKeysPage.module.scss'; export function ApiKeysPage() { const { t } = useTranslation(); @@ -138,80 +139,84 @@ export function ApiKeysPage() { ); return ( - - {error &&
{error}
} +
+

{t('api_keys.title')}

- {loading ? ( -
- -
- ) : apiKeys.length === 0 ? ( - - {t('api_keys.add_button')} - - } - /> - ) : ( -
- {apiKeys.map((key, index) => ( -
-
-
#{index + 1}
-
{t('api_keys.item_title')}
-
{maskApiKey(String(key || ''))}
-
-
- - -
-
- ))} -
- )} + + {error &&
{error}
} - - - - - } - > - + +
+ ) : apiKeys.length === 0 ? ( + + {t('api_keys.add_button')} + + } + /> + ) : ( +
+ {apiKeys.map((key, index) => ( +
+
+
#{index + 1}
+
{t('api_keys.item_title')}
+
{maskApiKey(String(key || ''))}
+
+
+ + +
+
+ ))} +
+ )} + + + + + } - placeholder={ - editingIndex !== null - ? t('api_keys.edit_modal_key_label') - : t('api_keys.add_modal_key_placeholder') - } - value={inputValue} - onChange={(e) => setInputValue(e.target.value)} - disabled={saving} - /> - -
+ > + setInputValue(e.target.value)} + disabled={saving} + /> + + +
); } diff --git a/src/pages/AuthFilesPage.module.scss b/src/pages/AuthFilesPage.module.scss index 3d0e106..fc8807f 100644 --- a/src/pages/AuthFilesPage.module.scss +++ b/src/pages/AuthFilesPage.module.scss @@ -7,6 +7,25 @@ gap: $spacing-lg; } +.pageHeader { + display: flex; + flex-direction: column; + gap: $spacing-sm; +} + +.pageTitle { + font-size: 28px; + font-weight: 700; + color: var(--text-primary); + margin: 0; +} + +.description { + font-size: 14px; + color: var(--text-secondary); + margin: 0; +} + .headerActions { display: flex; gap: $spacing-sm; @@ -472,4 +491,3 @@ border: 1px solid var(--danger-color); flex-shrink: 0; } - diff --git a/src/pages/AuthFilesPage.tsx b/src/pages/AuthFilesPage.tsx index a135a5e..c1f7252 100644 --- a/src/pages/AuthFilesPage.tsx +++ b/src/pages/AuthFilesPage.tsx @@ -150,11 +150,13 @@ export function AuthFilesPage() { // OAuth 排除模型相关 const [excluded, setExcluded] = useState>({}); + const [excludedError, setExcludedError] = useState<'unsupported' | null>(null); const [excludedModalOpen, setExcludedModalOpen] = useState(false); const [excludedForm, setExcludedForm] = useState({ provider: '', modelsText: '' }); const [savingExcluded, setSavingExcluded] = useState(false); const fileInputRef = useRef(null); + const excludedUnsupportedRef = useRef(false); const disableControls = connectionStatus !== 'connected'; @@ -199,11 +201,27 @@ export function AuthFilesPage() { const loadExcluded = useCallback(async () => { try { const res = await authFilesApi.getOauthExcludedModels(); + excludedUnsupportedRef.current = false; setExcluded(res || {}); - } catch { + setExcludedError(null); + } catch (err: unknown) { + const status = + typeof err === 'object' && err !== null && 'status' in err + ? (err as { status?: unknown }).status + : undefined; + + if (status === 404) { + setExcluded({}); + setExcludedError('unsupported'); + if (!excludedUnsupportedRef.current) { + excludedUnsupportedRef.current = true; + showNotification(t('oauth_excluded.upgrade_required'), 'warning'); + } + return; + } // 静默失败 } - }, []); + }, [showNotification, t]); useEffect(() => { loadFiles(); @@ -651,8 +669,13 @@ export function AuthFilesPage() { return (
+
+

{t('auth_files.title')}

+

{t('auth_files.description')}

+
+ } > - {Object.keys(excluded).length === 0 ? ( + {excludedError === 'unsupported' ? ( + + ) : Object.keys(excluded).length === 0 ? ( ) : (
diff --git a/src/pages/LogsPage.module.scss b/src/pages/LogsPage.module.scss index 55a585e..d7bebee 100644 --- a/src/pages/LogsPage.module.scss +++ b/src/pages/LogsPage.module.scss @@ -1,3 +1,5 @@ +@use '../styles/mixins' as *; + .container { width: 100%; } diff --git a/src/pages/LogsPage.tsx b/src/pages/LogsPage.tsx index ddc71d1..3ba8c8a 100644 --- a/src/pages/LogsPage.tsx +++ b/src/pages/LogsPage.tsx @@ -5,6 +5,7 @@ import { Button } from '@/components/ui/Button'; import { EmptyState } from '@/components/ui/EmptyState'; import { useNotificationStore, useAuthStore } from '@/stores'; import { logsApi } from '@/services/api/logs'; +import styles from './LogsPage.module.scss'; interface ErrorLogItem { name: string; @@ -197,9 +198,11 @@ export function LogsPage() { const logsText = logLines.join('\n'); return ( -
+
+

{t('logs.title')}

+
)} +
); } diff --git a/src/pages/OAuthPage.module.scss b/src/pages/OAuthPage.module.scss index c0e468c..0bc426f 100644 --- a/src/pages/OAuthPage.module.scss +++ b/src/pages/OAuthPage.module.scss @@ -1,3 +1,5 @@ +@use '../styles/mixins' as *; + .container { width: 100%; } diff --git a/src/pages/OAuthPage.tsx b/src/pages/OAuthPage.tsx index 0aa943b..92abf57 100644 --- a/src/pages/OAuthPage.tsx +++ b/src/pages/OAuthPage.tsx @@ -6,6 +6,7 @@ import { Input } from '@/components/ui/Input'; import { useNotificationStore } from '@/stores'; import { oauthApi, type OAuthProvider, type IFlowCookieAuthResponse } from '@/services/api/oauth'; import { isLocalhost } from '@/utils/connection'; +import styles from './OAuthPage.module.scss'; interface ProviderState { url?: string; @@ -157,130 +158,134 @@ export function OAuthPage() { }; return ( -
- {PROVIDERS.map((provider) => { - const state = states[provider.id] || {}; - // 非本地访问时禁用所有 OAuth 登录方式 - const isDisabled = !isLocal; - return ( -
- startAuth(provider.id)} - loading={state.polling} - disabled={isDisabled} - > - {t('common.login')} - - } - > -
{t(provider.hintKey)}
- {isDisabled && ( -
- {t('auth_login.remote_access_disabled')} -
- )} - {!isDisabled && state.url && ( -
-
{t(provider.urlLabelKey)}
-
{state.url}
-
- - -
-
- )} - {!isDisabled && state.status && state.status !== 'idle' && ( -
- {state.status === 'success' - ? t('auth_login.codex_oauth_status_success') - : state.status === 'error' - ? `${t('auth_login.codex_oauth_status_error')} ${state.error || ''}` - : t('auth_login.codex_oauth_status_waiting')} -
- )} -
-
- ); - })} +
+

{t('nav.oauth', { defaultValue: 'OAuth' })}

- {/* iFlow Cookie 登录 */} - - {t('auth_login.iflow_cookie_button')} - - } - > -
{t('auth_login.iflow_cookie_hint')}
-
- {t('auth_login.iflow_cookie_key_hint')} -
-
- - setIflowCookie((prev) => ({ ...prev, cookie: e.target.value }))} - placeholder={t('auth_login.iflow_cookie_placeholder')} - /> -
- {iflowCookie.error && ( -
- {iflowCookie.errorType === 'warning' - ? t('auth_login.iflow_cookie_status_duplicate') - : t('auth_login.iflow_cookie_status_error')}{' '} - {iflowCookie.error} -
- )} - {iflowCookie.result && iflowCookie.result.status === 'ok' && ( -
-
{t('auth_login.iflow_cookie_result_title')}
-
- {iflowCookie.result.email && ( -
- {t('auth_login.iflow_cookie_result_email')} - {iflowCookie.result.email} -
- )} - {iflowCookie.result.expired && ( -
- {t('auth_login.iflow_cookie_result_expired')} - {iflowCookie.result.expired} -
- )} - {iflowCookie.result.saved_path && ( -
- {t('auth_login.iflow_cookie_result_path')} - {iflowCookie.result.saved_path} -
- )} - {iflowCookie.result.type && ( -
- {t('auth_login.iflow_cookie_result_type')} - {iflowCookie.result.type} -
- )} +
+ {PROVIDERS.map((provider) => { + const state = states[provider.id] || {}; + // 非本地访问时禁用所有 OAuth 登录方式 + const isDisabled = !isLocal; + return ( +
+ startAuth(provider.id)} + loading={state.polling} + disabled={isDisabled} + > + {t('common.login')} + + } + > +
{t(provider.hintKey)}
+ {isDisabled && ( +
+ {t('auth_login.remote_access_disabled')} +
+ )} + {!isDisabled && state.url && ( +
+
{t(provider.urlLabelKey)}
+
{state.url}
+
+ + +
+
+ )} + {!isDisabled && state.status && state.status !== 'idle' && ( +
+ {state.status === 'success' + ? t('auth_login.codex_oauth_status_success') + : state.status === 'error' + ? `${t('auth_login.codex_oauth_status_error')} ${state.error || ''}` + : t('auth_login.codex_oauth_status_waiting')} +
+ )} +
+ ); + })} + + {/* iFlow Cookie 登录 */} + + {t('auth_login.iflow_cookie_button')} + + } + > +
{t('auth_login.iflow_cookie_hint')}
+
+ {t('auth_login.iflow_cookie_key_hint')}
- )} -
+
+ + setIflowCookie((prev) => ({ ...prev, cookie: e.target.value }))} + placeholder={t('auth_login.iflow_cookie_placeholder')} + /> +
+ {iflowCookie.error && ( +
+ {iflowCookie.errorType === 'warning' + ? t('auth_login.iflow_cookie_status_duplicate') + : t('auth_login.iflow_cookie_status_error')}{' '} + {iflowCookie.error} +
+ )} + {iflowCookie.result && iflowCookie.result.status === 'ok' && ( +
+
{t('auth_login.iflow_cookie_result_title')}
+
+ {iflowCookie.result.email && ( +
+ {t('auth_login.iflow_cookie_result_email')} + {iflowCookie.result.email} +
+ )} + {iflowCookie.result.expired && ( +
+ {t('auth_login.iflow_cookie_result_expired')} + {iflowCookie.result.expired} +
+ )} + {iflowCookie.result.saved_path && ( +
+ {t('auth_login.iflow_cookie_result_path')} + {iflowCookie.result.saved_path} +
+ )} + {iflowCookie.result.type && ( +
+ {t('auth_login.iflow_cookie_result_type')} + {iflowCookie.result.type} +
+ )} +
+
+ )} + +
); } diff --git a/src/pages/Settings/Settings.module.scss b/src/pages/Settings/Settings.module.scss index bd5d42a..5aa5060 100644 --- a/src/pages/Settings/Settings.module.scss +++ b/src/pages/Settings/Settings.module.scss @@ -1,3 +1,5 @@ +@use '../../styles/mixins' as *; + .container { width: 100%; } diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 606e8d1..5af860a 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -7,6 +7,7 @@ import { Input } from '@/components/ui/Input'; import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores'; import { configApi } from '@/services/api'; import type { Config } from '@/types'; +import styles from './Settings/Settings.module.scss'; type PendingKey = | 'debug' @@ -168,8 +169,11 @@ export function SettingsPage() { const quotaSwitchPreview = config?.quotaExceeded?.switchPreviewModel ?? false; return ( -
- +
+

{t('basic_settings.title')}

+ +
+ {error &&
{error}
}
+
); } diff --git a/src/pages/SystemPage.tsx b/src/pages/SystemPage.tsx index 3f5242c..2a4ec18 100644 --- a/src/pages/SystemPage.tsx +++ b/src/pages/SystemPage.tsx @@ -115,9 +115,11 @@ export function SystemPage() { }, [auth.connectionStatus, auth.apiBase]); return ( -
+
+

{t('system_info.title')}

+
fetchConfig(undefined, true)}> {t('common.refresh')} @@ -186,6 +188,7 @@ export function SystemPage() {
)} +
); } diff --git a/src/pages/UsagePage.tsx b/src/pages/UsagePage.tsx index 1dbd0ef..ace30c8 100644 --- a/src/pages/UsagePage.tsx +++ b/src/pages/UsagePage.tsx @@ -72,6 +72,7 @@ export function UsagePage() { const [selectedModel, setSelectedModel] = useState(''); const [promptPrice, setPromptPrice] = useState(''); const [completionPrice, setCompletionPrice] = useState(''); + const [cachePrice, setCachePrice] = useState(''); // Expanded sections const [expandedApis, setExpandedApis] = useState>(new Set()); @@ -340,12 +341,14 @@ export function UsagePage() { if (!selectedModel) return; const prompt = parseFloat(promptPrice) || 0; const completion = parseFloat(completionPrice) || 0; - const newPrices = { ...modelPrices, [selectedModel]: { prompt, completion } }; + const cache = cachePrice.trim() === '' ? prompt : parseFloat(cachePrice) || 0; + const newPrices = { ...modelPrices, [selectedModel]: { prompt, completion, cache } }; setModelPrices(newPrices); saveModelPrices(newPrices); setSelectedModel(''); setPromptPrice(''); setCompletionPrice(''); + setCachePrice(''); }; // Handle model price delete @@ -362,6 +365,7 @@ export function UsagePage() { setSelectedModel(model); setPromptPrice(price?.prompt?.toString() || ''); setCompletionPrice(price?.completion?.toString() || ''); + setCachePrice(price?.cache?.toString() || ''); }; // Toggle API expansion @@ -771,9 +775,11 @@ export function UsagePage() { if (price) { setPromptPrice(price.prompt.toString()); setCompletionPrice(price.completion.toString()); + setCachePrice(price.cache.toString()); } else { setPromptPrice(''); setCompletionPrice(''); + setCachePrice(''); } }} className={styles.select} @@ -804,6 +810,16 @@ export function UsagePage() { step="0.0001" />
+
+ + setCachePrice(e.target.value)} + placeholder="0.00" + step="0.0001" + /> +