From cc24f8419f60299b79918c9a284761912e00e124 Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Sat, 3 Oct 2026 22:53:32 +0800 Subject: [PATCH] feat(auth-files): redesign credential management as a row-based list - Display credentials, plans, status, request statistics, quotas, and actions - Add pagination, enable/disable switches, expandable details, and compact quota views - Preserve model cooldown details, countdowns, and reset controls - Support responsive layouts, light/dark themes, and localization - Add component and UI regression tests --- src/App.tsx | 8 + src/components/AuthFileHealthStatus.css | 11 ++ src/components/AuthFileHealthStatus.tsx | 33 ++-- src/components/AuthFileQuotaPanel.css | 67 +++++++++ src/components/AuthFileQuotaPanel.tsx | 56 ++++--- src/components/AuthFileRequestStatus.css | 30 ++++ src/components/AuthFileRequestStatus.tsx | 13 +- src/components/AuthFileUsageSummary.css | 59 ++++++++ src/components/AuthFileUsageSummary.tsx | 39 +++++ src/i18n/authFileList.ts | 74 +++++++++ src/i18n/ja.ts | 11 ++ src/i18n/locales/en.ts | 11 ++ src/i18n/locales/zh-CN.ts | 11 ++ src/i18n/resources.ts | 9 ++ src/oauthNavigation.ts | 1 - src/pages/AuthFileManagementPage.css | 92 +++++++++--- src/pages/AuthFileManagementPage.tsx | 184 +++++++++++++++++++---- src/pages/HomeDashboard.css | 60 ++++++-- src/pages/ManagementPages.tsx | 2 - src/pages/QuotaPage.tsx | 3 + src/services/authFileHealth.ts | 13 +- src/services/authFiles.ts | 32 +++- tests/auth-file-cooldown-ui.cjs | 152 +++++++++++++++++++ tests/auth-file-list-ui.cjs | 146 ++++++++++++++++++ tests/authFileCompactPanels.test.tsx | 79 ++++++++++ tests/authFileCooldowns.test.ts | 135 +++++++++++++++++ tests/authFileHealthStatus.test.tsx | 49 ++++++ tests/authFileUsageSummary.test.tsx | 38 +++++ tests/fixtures/auth-file-cooldown.html | 2 + tests/fixtures/auth-file-cooldown.tsx | 93 ++++++++++++ tests/fixtures/auth-file-list.html | 2 + tests/fixtures/auth-file-list.tsx | 89 +++++++++++ tests/navigationAndUpdate.test.ts | 6 +- tests/operationFeedback.test.ts | 2 +- tests/responsive-layout-ui.cjs | 8 +- 35 files changed, 1504 insertions(+), 116 deletions(-) create mode 100644 src/components/AuthFileQuotaPanel.css create mode 100644 src/components/AuthFileUsageSummary.css create mode 100644 src/components/AuthFileUsageSummary.tsx create mode 100644 src/i18n/authFileList.ts create mode 100644 tests/auth-file-cooldown-ui.cjs create mode 100644 tests/auth-file-list-ui.cjs create mode 100644 tests/authFileCompactPanels.test.tsx create mode 100644 tests/authFileCooldowns.test.ts create mode 100644 tests/authFileHealthStatus.test.tsx create mode 100644 tests/authFileUsageSummary.test.tsx create mode 100644 tests/fixtures/auth-file-cooldown.html create mode 100644 tests/fixtures/auth-file-cooldown.tsx create mode 100644 tests/fixtures/auth-file-list.html create mode 100644 tests/fixtures/auth-file-list.tsx diff --git a/src/App.tsx b/src/App.tsx index 2c94018..3a7ef59 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import { Check, ChevronUp, ExternalLink, + Gauge, History, House, Languages, @@ -28,6 +29,7 @@ import { ApiAccessPage } from './pages/ApiAccessPage'; import { KernelPage } from './pages/Kernel'; import { VersionManagementPage } from './pages/VersionManagementPage'; import { OAuthManagementPage } from './pages/ManagementPages'; +import { QuotaPage } from './pages/QuotaPage'; import { AgentsPage } from './pages/AgentsPage'; import { EasyModePage } from './pages/EasyModePage'; import { UsageRecordsPage } from './pages/UsageRecordsPage'; @@ -67,6 +69,12 @@ const pages = [ icon: LogIn, component: OAuthManagementPage, }, + { + id: 'quota', + labelKey: 'app.nav.quota', + icon: Gauge, + component: QuotaPage, + }, { id: 'agents', labelKey: 'app.nav.agents', diff --git a/src/components/AuthFileHealthStatus.css b/src/components/AuthFileHealthStatus.css index 827b395..2082b04 100644 --- a/src/components/AuthFileHealthStatus.css +++ b/src/components/AuthFileHealthStatus.css @@ -83,6 +83,17 @@ .auth-health-record-heading strong { min-width: 0; color: var(--text-primary); overflow-wrap: anywhere; } .auth-health-record-meta { margin: 3px 0; } .auth-health-hint { display: block; margin: 3px 0 0; color: var(--text-tertiary); font-size: var(--font-size-meta); overflow-wrap: anywhere; } +.auth-health-actions { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 8px 12px; + padding: 8px 12px; + border-top: 1px solid var(--border-color); +} +.auth-health-actions .auth-health-hint { flex: 1; min-width: 140px; margin: 0; } +.auth-health-reset { flex-shrink: 0; margin-left: auto; } @media (max-width: 760px) { .auth-health-summary-content { flex-basis: calc(100% - 30px); } diff --git a/src/components/AuthFileHealthStatus.tsx b/src/components/AuthFileHealthStatus.tsx index b6e6d28..4901106 100644 --- a/src/components/AuthFileHealthStatus.tsx +++ b/src/components/AuthFileHealthStatus.tsx @@ -1,5 +1,5 @@ import { useMemo, useSyncExternalStore } from 'react'; -import { ChevronDown, CircleHelp, Clock3, Info, PauseCircle, ShieldAlert } from 'lucide-react'; +import { ChevronDown, CircleHelp, Clock3, Info, LoaderCircle, PauseCircle, RotateCcw, ShieldAlert } from 'lucide-react'; import { useI18n } from '../i18n'; import { authFileHealth, @@ -35,10 +35,12 @@ const subscribeClock = (listener: () => void) => { type HealthProps = { file: Record; snapshot?: AuthFileCooldownSnapshot; + resetting?: boolean; + resetDisabled?: boolean; + onReset?: () => void; }; -export function AuthFileHealthStatus({ file, receivedAtMs, observedAt }: { - file: Record; +export function AuthFileHealthStatus({ file, receivedAtMs, observedAt, ...resetProps }: Omit & { receivedAtMs: number; observedAt?: string; }) { @@ -47,8 +49,8 @@ export function AuthFileHealthStatus({ file, receivedAtMs, observedAt }: { [file, receivedAtMs, observedAt], ); return snapshot?.records?.length - ? - : ; + ? + : ; } function TimedHealthStatus(props: HealthProps) { @@ -56,7 +58,7 @@ function TimedHealthStatus(props: HealthProps) { return ; } -function HealthStatus({ file, snapshot, nowMs }: HealthProps & { nowMs: number }) { +function HealthStatus({ file, snapshot, nowMs, resetting = false, resetDisabled = false, onReset }: HealthProps & { nowMs: number }) { const { t, formatNumber, formatDate } = useI18n(); const health = authFileHealth(file); const cooldown = summarizeAuthFileCooldowns(snapshot, nowMs); @@ -67,10 +69,6 @@ function HealthStatus({ file, snapshot, nowMs }: HealthProps & { nowMs: number } const reasonSet = new Set(cooldown.active.map(({ record }) => cooldownReasonKey(record.reason))); if (reasonSet.has('authFiles.health.reason.credentialQuota')) reasonSet.delete('authFiles.health.reason.quota'); const reasons = Array.from(reasonSet); - const title = health.disabled ? t(health.label) - : cooldown.elapsed ? t('authFiles.health.elapsed') - : cooldown.active.length ? reasons.map((key) => t(key)).join(' · ') - : healthy ? t('authFiles.health.unknownCooldown') : t(health.label); const tone = health.disabled || (healthy && !hasTimers) ? 'neutral' : hasTimers ? 'warning' : health.tone; const separateState = hasTimers && !health.disabled && ( health.status === 'pending' || health.status === 'refreshing' @@ -89,13 +87,17 @@ function HealthStatus({ file, snapshot, nowMs }: HealthProps & { nowMs: number } ? cooldown.modelCount ? t('authFiles.health.credentialModels', { count: cooldown.modelCount }) : t('authFiles.health.credential') : cooldown.modelCount ? t('authFiles.health.models', { count: cooldown.modelCount }) : ''; + const title = health.disabled ? t(health.label) + : cooldown.elapsed ? t('authFiles.health.elapsed') + : scope || (healthy ? t('authFiles.health.unknownCooldown') : t(health.label)); + const subtitle = health.disabled ? scope : reasons.map((key) => t(key)).join(' · '); const formatTimestamp = (value: string) => formatDate(value, { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }); const summary = <>