diff --git a/src/App.tsx b/src/App.tsx index af9163b..504610d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,8 +6,6 @@ import { Check, ChevronUp, ExternalLink, - FileKey, - Gauge, History, House, Languages, @@ -23,10 +21,8 @@ import appLogo from './assets/logo.jpg'; import { CoreRuntimeProvider, useCoreRuntime } from './coreRuntime'; import { ConfigPanelPage } from './pages/ConfigPanel'; import { ApiAccessPage } from './pages/ApiAccessPage'; -import { AuthFileManagementPage } from './pages/AuthFileManagementPage'; import { KernelPage } from './pages/Kernel'; -import { OAuthLoginPage } from './pages/ManagementPages'; -import { QuotaPage } from './pages/QuotaPage'; +import { OAuthManagementPage } from './pages/ManagementPages'; import { AgentsPage } from './pages/AgentsPage'; import { UsageRecordsPage } from './pages/UsageRecordsPage'; import { languageOptions, useI18n } from './i18n'; @@ -60,7 +56,7 @@ const pages = [ id: 'oauth', labelKey: 'app.nav.oauth', icon: LogIn, - component: OAuthLoginPage, + component: OAuthManagementPage, }, { id: 'api', @@ -68,18 +64,6 @@ const pages = [ icon: Network, component: ApiAccessPage, }, - { - id: 'auth-files', - labelKey: 'app.nav.authFiles', - icon: FileKey, - component: AuthFileManagementPage, - }, - { - id: 'quota', - labelKey: 'app.nav.quota', - icon: Gauge, - component: QuotaPage, - }, { id: 'usage-records', labelKey: 'app.nav.usageRecords', diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index 9b7c611..49987fb 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -60,6 +60,7 @@ export const jaOverrides = { 'quota.confirm.expiry': '最も早い有効期限:{time}', 'quota.confirm.warning': '「確認」を押した場合のみ実行されます。「キャンセル」ではリセット回数を消費しません。', 'oauth.title': 'OAuth ログイン', + 'oauth.tabs.label': 'OAuth 管理ページ', 'oauth.loginSuccess': '{provider} へのログインに成功しました', 'oauth.priorityApplyFailed': 'OAuth ログインは成功しましたが、新しい認証情報の優先度を 0 に設定できませんでした:{error}', 'oauth.authFailed': '認証に失敗しました', diff --git a/src/i18n/resources.ts b/src/i18n/resources.ts index 4142ca4..3658bdc 100644 --- a/src/i18n/resources.ts +++ b/src/i18n/resources.ts @@ -63,6 +63,7 @@ export const zhCN = { 'quota.confirm.expiry': '最早过期:{time}', 'quota.confirm.warning': '只有点击“确定”后才会执行;点击“取消”不会消耗重置机会。', 'oauth.title': 'OAuth 登录', + 'oauth.tabs.label': 'OAuth 管理子页面', 'oauth.loginSuccess': '{provider} 登录成功', 'oauth.priorityApplyFailed': 'OAuth 登录成功,但无法将新凭证的优先级设为 0:{error}', 'oauth.authFailed': '认证失败', @@ -951,6 +952,7 @@ export const en: Record = { 'quota.confirm.expiry': 'Earliest expiry: {time}', 'quota.confirm.warning': 'The reset runs only after you confirm. Canceling will not consume a reset credit.', 'oauth.title': 'OAuth Sign-In', + 'oauth.tabs.label': 'OAuth management sections', 'oauth.loginSuccess': '{provider} sign-in succeeded', 'oauth.priorityApplyFailed': 'OAuth sign-in succeeded, but the new credential priority could not be set to 0: {error}', 'oauth.authFailed': 'Authentication failed', diff --git a/src/oauthNavigation.ts b/src/oauthNavigation.ts new file mode 100644 index 0000000..4c8ca9e --- /dev/null +++ b/src/oauthNavigation.ts @@ -0,0 +1,7 @@ +export const oauthSubpages = [ + { id: 'login', labelKey: 'oauth.title' }, + { id: 'authFiles', labelKey: 'authFiles.title' }, + { id: 'quota', labelKey: 'quota.title' }, +] as const; + +export type OAuthSubpage = (typeof oauthSubpages)[number]['id']; diff --git a/src/pages/ManagementPages.tsx b/src/pages/ManagementPages.tsx index 6db0fc2..0b69ea3 100644 --- a/src/pages/ManagementPages.tsx +++ b/src/pages/ManagementPages.tsx @@ -13,12 +13,15 @@ import codexIcon from '../assets/icons/codex.svg'; import grokIcon from '../assets/icons/grok.svg'; import kimiIcon from '../assets/icons/kimi-light.svg'; import { useI18n } from '../i18n'; +import { oauthSubpages, type OAuthSubpage } from '../oauthNavigation'; import { changedOAuthAuthFileNames, snapshotAuthFiles, type AuthFileSnapshot, } from '../services/authFiles'; import { managementApi, responseList } from '../services/managementApi'; +import { AuthFileManagementPage } from './AuthFileManagementPage'; +import { QuotaPage } from './QuotaPage'; type OAuthProviderId = 'codex' | 'claude' | 'antigravity' | 'kimi' | 'xai'; type OAuthFlowStatus = 'idle' | 'waiting' | 'success' | 'error'; @@ -65,6 +68,51 @@ const XAI_CALLBACK_URL = 'http://127.0.0.1:56121/callback'; const OAUTH_SUCCESS_RESET_MS = 5000; const OAUTH_POLL_INTERVAL_MS = 3000; +export function OAuthManagementPage() { + const { t } = useI18n(); + const [activeSubpage, setActiveSubpage] = useState('login'); + + return ( +
+
+ {oauthSubpages.map((subpage) => { + const active = activeSubpage === subpage.id; + return ( + + ); + })} +
+ +
+ {activeSubpage === 'login' ? : null} + {activeSubpage === 'authFiles' ? : null} + {activeSubpage === 'quota' ? : null} +
+
+ ); +} + export function OAuthLoginPage() { const { t } = useI18n(); const [states, setStates] = useState>>({}); diff --git a/src/styles.css b/src/styles.css index 133ceb3..3961673 100644 --- a/src/styles.css +++ b/src/styles.css @@ -4498,6 +4498,13 @@ textarea:focus-visible { box-shadow: 0 1px 4px rgba(45, 42, 38, 0.14); } +.oauth-management-page, +.oauth-subpage-panel { + display: grid; + min-width: 0; + gap: 18px; +} + .agent-core-config { display: grid; min-width: 0; diff --git a/tests/navigationAndUpdate.test.ts b/tests/navigationAndUpdate.test.ts index df52750..4c15c10 100644 --- a/tests/navigationAndUpdate.test.ts +++ b/tests/navigationAndUpdate.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from 'bun:test'; import { appUpdateIndicatorState } from '../src/appUpdateModel'; import { canOpenAppPage, isAlwaysAvailablePage } from '../src/navigation'; +import { oauthSubpages } from '../src/oauthNavigation'; describe('首页、配置与版本管理导航', () => { test('内核停止时首页、配置和版本管理始终可进入', () => { @@ -19,6 +20,17 @@ describe('首页、配置与版本管理导航', () => { }); }); +describe('OAuth 子页面导航', () => { + test('认证文件和额度查询收纳在 OAuth 页面内', () => { + expect(oauthSubpages.map((page) => page.id)).toEqual(['login', 'authFiles', 'quota']); + expect(oauthSubpages.map((page) => page.labelKey)).toEqual([ + 'oauth.title', + 'authFiles.title', + 'quota.title', + ]); + }); +}); + describe('软件更新导航提示点', () => { test('有新版显示橙点,处理中的蓝点优先', () => { expect(appUpdateIndicatorState(true, false)).toBe('available');