From 975d0471f7775ca920b95169e5d8776848dbea3a Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Sun, 22 Mar 2026 13:30:53 +0800 Subject: [PATCH] feat(auth-files): revamp Auth Files page filters and card layout --- .../authFiles/components/AuthFileCard.tsx | 249 ++++-- src/features/authFiles/constants.ts | 68 +- src/pages/AuthFilesPage.module.scss | 785 ++++++++++++++---- src/pages/AuthFilesPage.tsx | 384 +++++---- 4 files changed, 1041 insertions(+), 445 deletions(-) diff --git a/src/features/authFiles/components/AuthFileCard.tsx b/src/features/authFiles/components/AuthFileCard.tsx index 142a278..1bdf3f2 100644 --- a/src/features/authFiles/components/AuthFileCard.tsx +++ b/src/features/authFiles/components/AuthFileCard.tsx @@ -3,7 +3,14 @@ import { Button } from '@/components/ui/Button'; import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; import { SelectionCheckbox } from '@/components/ui/SelectionCheckbox'; import { ToggleSwitch } from '@/components/ui/ToggleSwitch'; -import { IconBot, IconDownload, IconSettings, IconTrash2 } from '@/components/ui/icons'; +import { + IconBot, + IconDownload, + IconInfo, + IconSettings, + IconShield, + IconTrash2, +} from '@/components/ui/icons'; import { ProviderStatusBar } from '@/components/providers/ProviderStatusBar'; import type { AuthFileItem } from '@/types'; import { resolveAuthProvider } from '@/utils/quota'; @@ -12,6 +19,7 @@ import { formatFileSize } from '@/utils/format'; import { QUOTA_PROVIDER_TYPES, formatModified, + getAuthFileIcon, getAuthFileStatusMessage, getTypeColor, getTypeLabel, @@ -76,6 +84,8 @@ export function AuthFileCard(props: AuthFileCardProps) { const isAistudio = (file.type || '').toLowerCase() === 'aistudio'; const showModelsButton = !isRuntimeOnly || isAistudio; const typeColor = getTypeColor(file.type || 'unknown', resolvedTheme); + const typeLabel = getTypeLabel(t, file.type || 'unknown'); + const providerIcon = getAuthFileIcon(file.type || 'unknown', resolvedTheme); const quotaType = quotaFilterType && resolveQuotaType(file) === quotaFilterType ? quotaFilterType : null; @@ -105,6 +115,22 @@ export function AuthFileCard(props: AuthFileCardProps) { const priorityValue = parsePriorityValue(file.priority ?? file['priority']); const noteValue = typeof file.note === 'string' ? file.note.trim() : ''; + const stateLabel = isRuntimeOnly + ? t('auth_files.type_virtual') || '虚拟认证文件' + : file.disabled + ? t('auth_files.health_status_disabled') + : hasStatusWarning + ? t('auth_files.health_status_warning') + : rawStatusMessage + ? t('auth_files.health_status_healthy') + : t('auth_files.status_toggle_label'); + const stateBadgeClass = isRuntimeOnly + ? styles.stateBadgeVirtual + : file.disabled + ? styles.stateBadgeDisabled + : hasStatusWarning + ? styles.stateBadgeWarning + : styles.stateBadgeActive; return (
onToggleSelect(file.name)} + className={styles.cardSelection} aria-label={ selected ? t('auth_files.batch_deselect') : t('auth_files.batch_select_all') } title={selected ? t('auth_files.batch_deselect') : t('auth_files.batch_select_all')} /> )} - - {getTypeLabel(t, file.type || 'unknown')} - - {file.name} + {providerIcon ? ( + + ) : ( + + {typeLabel.slice(0, 1).toUpperCase()} + + )} +
+
+
+ + {typeLabel} + + {stateLabel} +
+ {file.name} + {noteValue && ( +
+ {t('auth_files.note_display')} + {noteValue} +
+ )} +
- - {t('auth_files.file_size')}: {file.size ? formatFileSize(file.size) : '-'} - - - {t('auth_files.file_modified')}: {formatModified(file)} - - {priorityValue !== undefined && ( - - {t('auth_files.priority_display')}:{' '} - {priorityValue} +
+ {t('auth_files.file_size')} + + {file.size ? formatFileSize(file.size) : '-'} +
+
+ {t('auth_files.file_modified')} + {formatModified(file)} +
+ {priorityValue !== undefined && ( +
+ {t('auth_files.priority_display')} + + {priorityValue} + +
)}
- {noteValue && ( -
- {t('auth_files.note_display')}: - {noteValue} -
- )} - {rawStatusMessage && hasStatusWarning && (
- {rawStatusMessage} + + {rawStatusMessage}
)} -
- - {t('stats.success')}: {fileStats.success} - - - {t('stats.failure')}: {fileStats.failure} - +
+
+
+ {t('stats.success')} + {fileStats.success} +
+
+ {t('stats.failure')} + {fileStats.failure} +
+
+ +
+
+ + {t('auth_files.health_status_label')} +
+ +
+ + {showQuotaLayout && quotaType && ( + + )}
- - - {showQuotaLayout && quotaType && ( - - )} -
- {showModelsButton && ( - - )} - {!isRuntimeOnly && ( - <> +
+ {showModelsButton && ( - - - - )} + )} + {!isRuntimeOnly && ( +
+ + + +
+ )} +
{!isRuntimeOnly && (
+ + {t('auth_files.status_toggle_label')} +
)} - {isRuntimeOnly && ( -
- {t('auth_files.type_virtual') || '虚拟认证文件'} -
- )}
diff --git a/src/features/authFiles/constants.ts b/src/features/authFiles/constants.ts index 2fb30cf..276b123 100644 --- a/src/features/authFiles/constants.ts +++ b/src/features/authFiles/constants.ts @@ -1,16 +1,31 @@ import type { TFunction } from 'i18next'; +import iconAntigravity from '@/assets/icons/antigravity.svg'; +import iconClaude from '@/assets/icons/claude.svg'; +import iconCodex from '@/assets/icons/codex.svg'; +import iconGemini from '@/assets/icons/gemini.svg'; +import iconIflow from '@/assets/icons/iflow.svg'; +import iconKimiDark from '@/assets/icons/kimi-dark.svg'; +import iconKimiLight from '@/assets/icons/kimi-light.svg'; +import iconQwen from '@/assets/icons/qwen.svg'; +import iconVertex from '@/assets/icons/vertex.svg'; import type { AuthFileItem } from '@/types'; import { normalizeAuthIndex, normalizeUsageSourceId, type KeyStatBucket, - type KeyStats + type KeyStats, } from '@/utils/usage'; export type ThemeColors = { bg: string; text: string; border?: string }; export type TypeColorSet = { light: ThemeColors; dark?: ThemeColors }; export type ResolvedTheme = 'light' | 'dark'; -export type AuthFileModelItem = { id: string; display_name?: string; type?: string; owned_by?: string }; +export type AuthFileModelItem = { + id: string; + display_name?: string; + type?: string; + owned_by?: string; +}; +export type AuthFileIconAsset = string | { light: string; dark: string }; export type QuotaProviderType = 'antigravity' | 'claude' | 'codex' | 'gemini-cli' | 'kimi'; @@ -19,7 +34,7 @@ export const QUOTA_PROVIDER_TYPES = new Set([ 'claude', 'codex', 'gemini-cli', - 'kimi' + 'kimi', ]); export const MIN_CARD_PAGE_SIZE = 3; @@ -34,48 +49,61 @@ export const FALSY_TEXT_VALUES = new Set(['false', '0', 'no', 'n', 'off']); export const TYPE_COLORS: Record = { qwen: { light: { bg: '#e8f5e9', text: '#2e7d32' }, - dark: { bg: '#1b5e20', text: '#81c784' } + dark: { bg: '#1b5e20', text: '#81c784' }, }, kimi: { light: { bg: '#fff4e5', text: '#ad6800' }, - dark: { bg: '#7c4a03', text: '#ffd591' } + dark: { bg: '#7c4a03', text: '#ffd591' }, }, gemini: { light: { bg: '#e3f2fd', text: '#1565c0' }, - dark: { bg: '#0d47a1', text: '#64b5f6' } + dark: { bg: '#0d47a1', text: '#64b5f6' }, }, 'gemini-cli': { light: { bg: '#e7efff', text: '#1e4fa3' }, - dark: { bg: '#1c3f73', text: '#a8c7ff' } + dark: { bg: '#1c3f73', text: '#a8c7ff' }, }, aistudio: { light: { bg: '#f0f2f5', text: '#2f343c' }, - dark: { bg: '#373c42', text: '#cfd3db' } + dark: { bg: '#373c42', text: '#cfd3db' }, }, claude: { light: { bg: '#fce4ec', text: '#c2185b' }, - dark: { bg: '#880e4f', text: '#f48fb1' } + dark: { bg: '#880e4f', text: '#f48fb1' }, }, codex: { light: { bg: '#fff3e0', text: '#ef6c00' }, - dark: { bg: '#e65100', text: '#ffb74d' } + dark: { bg: '#e65100', text: '#ffb74d' }, }, antigravity: { light: { bg: '#e0f7fa', text: '#006064' }, - dark: { bg: '#004d40', text: '#80deea' } + dark: { bg: '#004d40', text: '#80deea' }, }, iflow: { light: { bg: '#f3e5f5', text: '#7b1fa2' }, - dark: { bg: '#4a148c', text: '#ce93d8' } + dark: { bg: '#4a148c', text: '#ce93d8' }, }, empty: { light: { bg: '#f5f5f5', text: '#616161' }, - dark: { bg: '#424242', text: '#bdbdbd' } + dark: { bg: '#424242', text: '#bdbdbd' }, }, unknown: { light: { bg: '#f0f0f0', text: '#666666', border: '1px dashed #999999' }, - dark: { bg: '#3a3a3a', text: '#aaaaaa', border: '1px dashed #666666' } - } + dark: { bg: '#3a3a3a', text: '#aaaaaa', border: '1px dashed #666666' }, + }, +}; + +export const AUTH_FILE_ICONS: Record = { + antigravity: iconAntigravity, + aistudio: iconGemini, + claude: iconClaude, + codex: iconCodex, + gemini: iconGemini, + 'gemini-cli': iconGemini, + iflow: iconIflow, + kimi: { light: iconKimiLight, dark: iconKimiDark }, + qwen: iconQwen, + vertex: iconVertex, }; export const clampCardPageSize = (value: number) => @@ -116,6 +144,16 @@ export const getTypeColor = (type: string, resolvedTheme: ResolvedTheme): ThemeC return resolvedTheme === 'dark' && set.dark ? set.dark : set.light; }; +export const getAuthFileIcon = (type: string, resolvedTheme: ResolvedTheme): string | null => { + const iconEntry = AUTH_FILE_ICONS[normalizeProviderKey(type)]; + if (!iconEntry) return null; + return typeof iconEntry === 'string' + ? iconEntry + : resolvedTheme === 'dark' + ? iconEntry.dark + : iconEntry.light; +}; + export const parsePriorityValue = (value: unknown): number | undefined => { if (typeof value === 'number') { return Number.isInteger(value) ? value : undefined; diff --git a/src/pages/AuthFilesPage.module.scss b/src/pages/AuthFilesPage.module.scss index e14a778..5953d38 100644 --- a/src/pages/AuthFilesPage.module.scss +++ b/src/pages/AuthFilesPage.module.scss @@ -5,7 +5,9 @@ display: flex; flex-direction: column; gap: $spacing-lg; - padding-bottom: calc(var(--auth-files-action-bar-height, 0px) + 16px + env(safe-area-inset-bottom)); + padding-bottom: calc( + var(--auth-files-action-bar-height, 0px) + 16px + env(safe-area-inset-bottom) + ); } .pageHeader { @@ -67,70 +69,303 @@ // 筛选区域 .filterSection { + display: grid; + grid-template-columns: 248px minmax(0, 1fr); + gap: 20px; + align-items: start; + margin-bottom: $spacing-lg; + + @include tablet { + grid-template-columns: 220px minmax(0, 1fr); + } + + @include mobile { + grid-template-columns: 1fr; + gap: $spacing-md; + } +} + +.filterRail { + position: sticky; + top: 16px; + display: flex; + flex-direction: column; + gap: 14px; + padding: 16px; + border-radius: 18px; + border: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent); + background: + linear-gradient(180deg, color-mix(in srgb, var(--bg-secondary) 94%, transparent), transparent), + color-mix(in srgb, var(--bg-primary) 90%, transparent); + box-shadow: + inset 0 1px 0 color-mix(in srgb, #fff 52%, transparent), + 0 18px 32px -30px rgba(0, 0, 0, 0.35); + align-self: start; + + @include mobile { + position: static; + padding: 14px; + } +} + +.filterRailHeader { + display: flex; + flex-direction: column; + gap: 12px; +} + +.filterRailEyebrow { + font-size: 11px; + font-weight: 700; + color: var(--text-tertiary); + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.filterRailHero { + display: flex; + flex-direction: column; + gap: 12px; +} + +.filterRailHeroTitle { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; +} + +.filterRailHeroIcon { + width: 42px; + height: 42px; + flex: 0 0 auto; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 14px; + border: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent); + background: color-mix(in srgb, var(--bg-secondary) 88%, transparent); +} + +.filterRailHeroIconImage { + width: 22px; + height: 22px; + object-fit: contain; +} + +.filterRailHeroIconFallback { + font-size: 18px; + font-weight: 700; + line-height: 1; + color: var(--text-primary); +} + +.filterRailHeroText { + display: flex; + flex-direction: column; + gap: 3px; + min-width: 0; +} + +.filterRailTitle { + font-size: 16px; + font-weight: 700; + color: var(--text-primary); + line-height: 1.2; + @include text-ellipsis; +} + +.filterRailDescription { + font-size: 12px; + color: var(--text-secondary); + line-height: 1.35; +} + +.filterRailMeta { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.filterRailCount { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 38px; + height: 30px; + padding: 0 10px; + border-radius: 999px; + background: color-mix(in srgb, var(--primary-color) 16%, transparent); + color: var(--text-primary); + font-size: 13px; + font-weight: 700; + font-variant-numeric: tabular-nums; +} + +.filterRailMode { + display: inline-flex; + align-items: center; + min-height: 30px; + padding: 6px 10px; + border-radius: 999px; + border: 1px solid var(--warning-border); + background: var(--warning-bg); + color: var(--warning-text); + font-size: 11px; + font-weight: 700; + line-height: 1.2; +} + +.filterContent { display: flex; flex-direction: column; gap: $spacing-md; - margin-bottom: $spacing-lg; + min-width: 0; } .filterTags { display: flex; - flex-wrap: wrap; - gap: $spacing-xs; + flex-direction: column; + gap: 8px; + + @include mobile { + flex-direction: row; + overflow-x: auto; + padding-bottom: 2px; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } + } } .filterTag { display: inline-flex; align-items: center; - gap: 8px; - padding: 6px 14px; - border-radius: 20px; + justify-content: space-between; + gap: 12px; + width: 100%; + padding: 10px 12px; + border-radius: 14px; font-size: 13px; - font-weight: 500; - line-height: 1; - border: 1px solid transparent; + font-weight: 600; + line-height: 1.2; + border: 1px solid color-mix(in srgb, var(--filter-color) 18%, var(--border-color)); + background: color-mix(in srgb, var(--filter-surface) 56%, var(--bg-secondary)); + color: var(--text-primary); cursor: pointer; - transition: all $transition-fast; + transition: + transform $transition-fast, + border-color $transition-fast, + background-color $transition-fast, + box-shadow $transition-fast; + text-align: left; &:hover { - transform: translateY(-1px); - box-shadow: $shadow-sm; + transform: translateX(2px); + border-color: color-mix(in srgb, var(--filter-color) 52%, var(--border-color)); + background: color-mix(in srgb, var(--filter-surface) 72%, var(--bg-secondary)); + box-shadow: 0 14px 24px -24px color-mix(in srgb, var(--filter-color) 65%, transparent); + } + + @include mobile { + width: auto; + min-width: max-content; + flex: 0 0 auto; + + &:hover { + transform: translateY(-1px); + } } } .filterTagActive { - font-weight: 600; + border-color: color-mix(in srgb, var(--filter-color) 72%, transparent); + background: linear-gradient( + 135deg, + color-mix(in srgb, var(--filter-surface) 74%, var(--bg-primary)), + color-mix(in srgb, var(--filter-surface) 44%, var(--bg-secondary)) + ); + box-shadow: + inset 3px 0 0 var(--filter-color), + 0 16px 30px -26px color-mix(in srgb, var(--filter-color) 58%, transparent); } .filterTagLabel { display: inline-flex; align-items: center; - gap: 6px; - white-space: nowrap; + gap: 10px; + min-width: 0; +} + +.filterTagIconWrap { + width: 34px; + height: 34px; + flex: 0 0 auto; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 12px; + border: 1px solid color-mix(in srgb, var(--filter-color) 16%, var(--border-color)); + background: color-mix(in srgb, var(--filter-surface) 74%, var(--bg-primary)); } .filterTagIcon { - width: 15px; - height: 15px; + width: 18px; + height: 18px; flex: 0 0 auto; object-fit: contain; } +.filterTagIconFallback { + color: var(--filter-color); + font-size: 14px; + font-weight: 700; + line-height: 1; +} + +.filterTagText { + min-width: 0; + color: inherit; + @include text-ellipsis; +} + .filterTagCount { display: inline-flex; align-items: center; - justify-content: flex-end; - min-width: 2ch; + justify-content: center; + min-width: 34px; + height: 28px; + padding: 0 10px; + border-radius: 999px; + background: color-mix(in srgb, var(--filter-color) 14%, transparent); + color: var(--filter-color); font-size: 12px; - font-weight: 600; + font-weight: 700; font-variant-numeric: tabular-nums; - opacity: 0.85; + flex: 0 0 auto; +} + +.filterTagActive .filterTagCount { + background: var(--filter-color); + color: var(--filter-active-text, #fff); +} + +.filterControlsPanel { + padding: 16px; + border-radius: 18px; + border: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent); + background: + linear-gradient(180deg, color-mix(in srgb, var(--bg-secondary) 92%, transparent), transparent), + color-mix(in srgb, var(--bg-primary) 88%, transparent); } .filterControls { - display: flex; + display: grid; + grid-template-columns: minmax(240px, 1.4fr) repeat(2, minmax(120px, 0.72fr)) minmax(220px, auto); gap: $spacing-md; - flex-wrap: wrap; align-items: flex-end; // 修复 Input 组件在 filterItem 中的嵌套样式 @@ -139,69 +374,75 @@ } :global(.input) { - height: 38px; + height: 40px; box-sizing: border-box; } + + @include tablet { + grid-template-columns: minmax(220px, 1fr) repeat(2, minmax(140px, 0.8fr)); + } + + @include mobile { + grid-template-columns: 1fr; + } } .filterItem { display: flex; flex-direction: column; - gap: 4px; + gap: 6px; + min-width: 0; label { - font-size: 12px; - color: var(--text-secondary); - font-weight: 500; + font-size: 11px; + color: var(--text-tertiary); + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; white-space: nowrap; } - - // 搜索输入框需要更宽以显示完整的 placeholder - &:first-child { - min-width: 220px; - flex: 1; - max-width: 320px; - } - - // 其他 filterItem 保持较小的宽度 - &:not(:first-child) { - min-width: auto; - } } .filterToggleItem { - min-width: 220px; + min-width: 0; + + @include tablet { + grid-column: 1 / -1; + } } .filterToggle { display: flex; align-items: center; - min-height: 38px; + min-height: 40px; + padding-inline: 2px; } .filterToggleLabel { display: inline-flex; align-items: center; color: var(--text-primary); - font-size: 14px; - font-weight: 500; + font-size: 13px; + font-weight: 600; white-space: nowrap; } .pageSizeSelect { + width: 100%; padding: 8px 12px; - border: 1px solid var(--border-color); - border-radius: $radius-md; - background-color: var(--bg-primary); + border: 1px solid color-mix(in srgb, var(--border-color) 88%, transparent); + border-radius: 12px; + background-color: var(--bg-secondary); color: var(--text-primary); font-size: 14px; cursor: text; - height: 38px; + height: 40px; box-sizing: border-box; &:focus { outline: none; border-color: var(--primary-color); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 14%, transparent); } } @@ -362,8 +603,8 @@ flex-direction: column; gap: $spacing-sm; padding-top: $spacing-sm; - margin-top: $spacing-xs; - border-top: 1px dashed var(--border-color); + margin-top: 2px; + border-top: 1px dashed color-mix(in srgb, var(--border-color) 82%, transparent); } .quotaRow { @@ -526,12 +767,14 @@ text-transform: capitalize; @media (dynamic-range: high) { - background: linear-gradient(135deg, + background: linear-gradient( + 135deg, color(display-p3 0.99 0.95 0.82), - color(display-p3 0.97 0.82 0.28)); - border-color: color(display-p3 0.90 0.73 0.12); - box-shadow: 0 1px 8px color(display-p3 1.0 0.84 0.0 / 0.25); - color: color(display-p3 0.50 0.38 0.0); + color(display-p3 0.97 0.82 0.28) + ); + border-color: color(display-p3 0.9 0.73 0.12); + box-shadow: 0 1px 8px color(display-p3 1 0.84 0 / 0.25); + color: color(display-p3 0.5 0.38 0); } } @@ -542,24 +785,28 @@ color: #e8c84c; @media (dynamic-range: high) { - background: linear-gradient(135deg, - color(display-p3 0.24 0.20 0.06), - color(display-p3 0.38 0.30 0.08)); + background: linear-gradient( + 135deg, + color(display-p3 0.24 0.2 0.06), + color(display-p3 0.38 0.3 0.08) + ); border-color: color(display-p3 0.58 0.46 0.15); - box-shadow: 0 1px 12px color(display-p3 0.80 0.65 0.0 / 0.2); + box-shadow: 0 1px 12px color(display-p3 0.8 0.65 0 / 0.2); color: color(display-p3 0.95 0.82 0.25); } } // 单个认证文件卡片 .fileCard { + position: relative; + overflow: hidden; background-color: var(--bg-primary); border: 1px solid var(--border-color); - border-radius: $radius-lg; - padding: $spacing-md; + border-radius: 16px; + padding: 18px; display: flex; flex-direction: column; - gap: $spacing-sm; + min-height: 100%; transition: transform $transition-fast, box-shadow $transition-fast, @@ -568,13 +815,15 @@ &:hover { transform: translateY(-2px); box-shadow: $shadow-md; - border-color: rgba(37, 99, 235, 0.2); + border-color: color-mix(in srgb, var(--primary-color) 36%, var(--border-color)); } } .fileCardSelected { border-color: var(--primary-color); - box-shadow: 0 0 0 1px color-mix(in srgb, var(--primary-color) 70%, transparent); + box-shadow: + 0 0 0 1px color-mix(in srgb, var(--primary-color) 70%, transparent), + 0 14px 30px -24px color-mix(in srgb, var(--primary-color) 45%, transparent); &:hover { border-color: var(--primary-color); @@ -582,7 +831,7 @@ } .fileCardDisabled { - opacity: 0.6; + opacity: 0.72; &:hover { transform: none; @@ -595,149 +844,315 @@ display: flex; align-items: stretch; gap: $spacing-md; + min-height: 100%; } .fileCardMain { display: flex; flex-direction: column; - gap: $spacing-sm; + gap: 14px; flex: 1; min-width: 0; } .cardHeader { display: flex; + align-items: flex-start; + gap: 12px; +} + +.cardSelection { + flex-shrink: 0; + margin-top: 8px; +} + +.providerAvatar { + width: 44px; + height: 44px; + flex: 0 0 auto; + display: inline-flex; align-items: center; - gap: $spacing-sm; - min-height: 28px; + justify-content: center; + border-radius: 14px; + border: 1px solid color-mix(in srgb, var(--border-color) 74%, transparent); + box-shadow: inset 0 1px 0 color-mix(in srgb, #fff 55%, transparent); +} + +.providerAvatarImage { + width: 22px; + height: 22px; + object-fit: contain; +} + +.providerAvatarFallback { + font-size: 18px; + font-weight: 700; + line-height: 1; +} + +.cardHeaderContent { + display: flex; + flex-direction: column; + gap: 6px; + flex: 1; + min-width: 0; +} + +.cardBadgeRow { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; } .typeBadge { - padding: 4px 10px; - border-radius: 12px; + display: inline-flex; + align-items: center; + padding: 5px 10px; + border-radius: 999px; font-size: 12px; font-weight: 600; + line-height: 1; + border: 1px solid transparent; white-space: nowrap; flex-shrink: 0; } +.stateBadge { + display: inline-flex; + align-items: center; + padding: 5px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + line-height: 1; + white-space: nowrap; + letter-spacing: 0.02em; +} + +.stateBadgeActive { + color: var(--success-badge-text, #065f46); + background: var(--success-badge-bg, #d1fae5); + border: 1px solid var(--success-badge-border, #6ee7b7); +} + +.stateBadgeWarning { + color: var(--warning-text); + background: var(--warning-bg); + border: 1px solid var(--warning-border); +} + +.stateBadgeDisabled { + color: var(--text-secondary); + background: color-mix(in srgb, var(--bg-tertiary) 86%, transparent); + border: 1px solid var(--border-color); +} + +.stateBadgeVirtual { + color: var(--text-secondary); + background: color-mix(in srgb, var(--bg-tertiary) 82%, transparent); + border: 1px dashed color-mix(in srgb, var(--border-primary) 90%, transparent); +} + .fileName { - font-size: 14px; - font-weight: 600; + font-size: 15px; + font-weight: 700; color: var(--text-primary); word-break: break-all; - line-height: 1.4; + line-height: 1.45; + display: -webkit-box; + overflow: hidden; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; } .cardMeta { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; + + @include mobile { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +.metaItem { display: flex; flex-direction: column; - gap: 2px; - font-size: 12px; - color: var(--text-secondary); - padding: $spacing-xs 0; - border-top: 1px solid var(--border-color); - border-bottom: 1px solid var(--border-color); + gap: 4px; + min-width: 0; + padding: 10px 12px; + border-radius: 12px; + border: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent); + background: color-mix(in srgb, var(--bg-secondary) 92%, transparent); +} + +.metaLabel { + font-size: 11px; + font-weight: 600; + color: var(--text-tertiary); + line-height: 1.2; +} + +.metaValue { + min-width: 0; + font-size: 13px; + font-weight: 600; + color: var(--text-primary); + line-height: 1.45; + word-break: break-word; } .priorityBadge { - display: inline-flex; - align-items: center; - gap: 4px; - font-size: 12px; - color: var(--text-secondary); + min-width: 0; +} - .priorityValue { - font-weight: 600; - color: var(--text-primary); - font-variant-numeric: tabular-nums; - } +.priorityValue { + font-variant-numeric: tabular-nums; } .noteText { font-size: 12px; color: var(--text-secondary); - line-height: 1.4; - overflow: hidden; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - word-break: break-word; + line-height: 1.5; + display: flex; + gap: 6px; + align-items: flex-start; + min-width: 0; +} - .noteLabel { - color: var(--text-tertiary); - } +.noteLabel { + flex: 0 0 auto; + color: var(--text-tertiary); + font-weight: 600; +} + +.noteValue { + @include text-ellipsis-multiline(2); + min-width: 0; + word-break: break-word; } .sortSelect { - min-width: 140px; + width: 100%; + min-width: 0; } .healthStatusMessage { + display: flex; + align-items: flex-start; + gap: 8px; font-size: 12px; color: var(--warning-text); background-color: var(--warning-bg); border: 1px solid var(--warning-border); - border-radius: $radius-sm; - padding: 5px 8px; + border-radius: 12px; + padding: 10px 12px; word-break: break-word; } -.cardStats { +.messageIcon { + flex-shrink: 0; + margin-top: 1px; +} + +.cardInsights { display: flex; - flex-wrap: wrap; - gap: $spacing-sm; - padding-top: $spacing-xs; - margin-top: $spacing-xs; + flex-direction: column; + gap: 12px; + padding: 14px; + border-radius: 16px; + border: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent); + background: color-mix(in srgb, var(--bg-secondary) 88%, transparent); +} + +.cardStats { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + + @include mobile { + grid-template-columns: 1fr; + } } .statPill { - display: inline-flex; - align-items: center; + display: flex; + flex-direction: column; gap: 6px; - padding: 6px 12px; - border-radius: 999px; - font-size: 13px; - font-weight: 600; - line-height: 1.1; + min-width: 0; + padding: 12px; + border-radius: 12px; border: 1px solid transparent; - background-color: var(--bg-tertiary); - color: var(--text-primary); - white-space: nowrap; +} + +.statLabel { + font-size: 12px; + font-weight: 600; + line-height: 1.2; +} + +.statValue { + font-size: 22px; + font-weight: 700; + line-height: 1; + font-variant-numeric: tabular-nums; } .statSuccess { - background-color: var(--success-badge-bg, #d1fae5); + background: var(--success-badge-bg, #d1fae5); color: var(--success-badge-text, #065f46); border-color: var(--success-badge-border, #6ee7b7); } .statFailure { - background-color: var(--failure-badge-bg); + background: var(--failure-badge-bg); color: var(--failure-badge-text); border-color: var(--failure-badge-border); } +.statusPanel { + display: flex; + flex-direction: column; + gap: 8px; +} + +.statusPanelLabel { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 12px; + font-weight: 600; + color: var(--text-secondary); +} + // 状态监测栏 .statusBar { display: flex; align-items: center; - gap: 8px; - padding: 4px 0; - max-width: 280px; + gap: 10px; + width: 100%; + min-width: 0; + padding: 0; + max-width: none; + + @include mobile { + flex-direction: column; + align-items: stretch; + } } .statusBlocks { display: flex; - gap: 2px; + gap: 3px; flex: 1; - min-width: 180px; + min-width: 0; position: relative; } .statusBlockWrapper { flex: 1; - min-width: 6px; + min-width: 7px; position: relative; cursor: pointer; -webkit-tap-highlight-color: transparent; @@ -745,13 +1160,15 @@ .statusBlock { width: 100%; - height: 8px; - border-radius: 2px; - transition: transform 0.15s ease, opacity 0.15s ease; + height: 10px; + border-radius: 999px; + transition: + transform 0.15s ease, + opacity 0.15s ease; .statusBlockWrapper:hover &, .statusBlockWrapper.statusBlockActive & { - transform: scaleY(1.8); + transform: scaleY(1.45); opacity: 0.9; } } @@ -765,14 +1182,14 @@ bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%); - background: var(--bg-primary, #fff); + background: var(--bg-quinary, var(--bg-primary, #fff)); border: 1px solid var(--border-secondary, #e5e7eb); - border-radius: 6px; - padding: 6px 10px; + border-radius: 10px; + padding: 8px 10px; font-size: 11px; line-height: 1.5; white-space: nowrap; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14); z-index: $z-dropdown; pointer-events: none; color: var(--text-primary); @@ -784,7 +1201,7 @@ left: 50%; transform: translateX(-50%); border: 5px solid transparent; - border-top-color: var(--bg-primary, #fff); + border-top-color: var(--bg-quinary, var(--bg-primary, #fff)); } &::before { @@ -852,11 +1269,12 @@ align-items: center; gap: 4px; font-size: 12px; - font-weight: 600; + font-weight: 700; white-space: nowrap; - padding: 4px 8px; - border-radius: 6px; + padding: 6px 10px; + border-radius: 999px; background: var(--bg-tertiary); + flex-shrink: 0; } .statusRateHigh { @@ -881,7 +1299,7 @@ } .statusBlocks { - min-width: 140px; + min-width: 0; } } @@ -955,27 +1373,65 @@ .cardActions { display: flex; - gap: $spacing-xs; - justify-content: flex-end; - align-items: center; + justify-content: space-between; + align-items: flex-end; + gap: 12px; + flex-wrap: wrap; margin-top: auto; - padding-top: $spacing-sm; + padding-top: 4px; +} + +.cardActionsMain { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + min-width: 0; +} + +.primaryActionButton:global(.btn.btn-sm) { + min-width: 96px; + padding-inline: 12px; +} + +.actionButtonLabel { + @include text-ellipsis; +} + +.cardUtilityActions { + display: flex; + align-items: center; + gap: 6px; + padding: 4px; + border-radius: 12px; + border: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent); + background: color-mix(in srgb, var(--bg-secondary) 90%, transparent); } .statusToggle { - display: flex; + display: inline-flex; align-items: center; + gap: 10px; flex-shrink: 0; - margin-left: $spacing-sm; + padding: 6px 10px; + border-radius: 12px; + border: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent); + background: color-mix(in srgb, var(--bg-secondary) 90%, transparent); +} + +.statusToggleLabel { + font-size: 12px; + font-weight: 600; + color: var(--text-secondary); } .iconButton:global(.btn.btn-sm) { - width: 34px; - height: 34px; - min-width: 34px; + width: 36px; + height: 36px; + min-width: 36px; padding: 0; box-sizing: border-box; - border-radius: 6px; + border-radius: 10px; gap: 0; } @@ -983,15 +1439,28 @@ display: block; } -.virtualBadge { - font-size: 12px; - color: var(--text-secondary); - background-color: var(--bg-tertiary); - padding: 4px 10px; - border-radius: $radius-sm; - font-style: italic; - display: inline-flex; - align-items: center; +@include mobile { + .cardActions { + align-items: stretch; + } + + .cardActionsMain { + width: 100%; + } + + .primaryActionButton:global(.btn.btn-sm) { + flex: 1 1 auto; + min-width: 0; + } + + .cardUtilityActions { + margin-left: auto; + } + + .statusToggle { + width: 100%; + justify-content: space-between; + } } // 分页 @@ -1410,7 +1879,7 @@ background-color: var(--bg-hover); } - input[type="checkbox"] { + input[type='checkbox'] { width: 16px; height: 16px; cursor: pointer; diff --git a/src/pages/AuthFilesPage.tsx b/src/pages/AuthFilesPage.tsx index afc4680..d0d91b9 100644 --- a/src/pages/AuthFilesPage.tsx +++ b/src/pages/AuthFilesPage.tsx @@ -1,5 +1,6 @@ import { useCallback, + type CSSProperties, useEffect, useLayoutEffect, useMemo, @@ -27,6 +28,7 @@ import { MIN_CARD_PAGE_SIZE, QUOTA_PROVIDER_TYPES, clampCardPageSize, + getAuthFileIcon, getTypeColor, getTypeLabel, hasAuthFileStatusMessage, @@ -41,15 +43,6 @@ import { AuthFileModelsModal } from '@/features/authFiles/components/AuthFileMod import { AuthFilesPrefixProxyEditorModal } from '@/features/authFiles/components/AuthFilesPrefixProxyEditorModal'; import { OAuthExcludedCard } from '@/features/authFiles/components/OAuthExcludedCard'; import { OAuthModelAliasCard } from '@/features/authFiles/components/OAuthModelAliasCard'; -import iconAntigravity from '@/assets/icons/antigravity.svg'; -import iconClaude from '@/assets/icons/claude.svg'; -import iconCodex from '@/assets/icons/codex.svg'; -import iconGemini from '@/assets/icons/gemini.svg'; -import iconIflow from '@/assets/icons/iflow.svg'; -import iconKimiDark from '@/assets/icons/kimi-dark.svg'; -import iconKimiLight from '@/assets/icons/kimi-light.svg'; -import iconQwen from '@/assets/icons/qwen.svg'; -import iconVertex from '@/assets/icons/vertex.svg'; import { useAuthFilesData } from '@/features/authFiles/hooks/useAuthFilesData'; import { useAuthFilesModels } from '@/features/authFiles/hooks/useAuthFilesModels'; import { useAuthFilesOauth } from '@/features/authFiles/hooks/useAuthFilesOauth'; @@ -69,28 +62,6 @@ const easePower3Out = (progress: number) => 1 - (1 - progress) ** 4; const easePower2In = (progress: number) => progress ** 3; const BATCH_BAR_BASE_TRANSFORM = 'translateX(-50%)'; const BATCH_BAR_HIDDEN_TRANSFORM = 'translateX(-50%) translateY(56px)'; -const AUTH_FILE_FILTER_ICONS: Record = { - antigravity: iconAntigravity, - aistudio: iconGemini, - claude: iconClaude, - codex: iconCodex, - gemini: iconGemini, - 'gemini-cli': iconGemini, - iflow: iconIflow, - kimi: { light: iconKimiLight, dark: iconKimiDark }, - qwen: iconQwen, - vertex: iconVertex, -}; - -const getFilterTagIcon = (type: string, resolvedTheme: ResolvedTheme): string | null => { - const iconEntry = AUTH_FILE_FILTER_ICONS[normalizeProviderKey(type)]; - if (!iconEntry) return null; - return typeof iconEntry === 'string' - ? iconEntry - : resolvedTheme === 'dark' - ? iconEntry.dark - : iconEntry.light; -}; export function AuthFilesPage() { const { t } = useTranslation(); @@ -325,6 +296,11 @@ export function AuthFilesPage() { return counts; }, [filesMatchingProblemFilter]); + const activeFilterLabel = + filter === 'all' ? t('auth_files.filter_all') : getTypeLabel(t, String(filter)); + const activeFilterCount = typeCounts[String(filter)] ?? 0; + const activeFilterIcon = getAuthFileIcon(String(filter), resolvedTheme); + const filtered = useMemo(() => { return filesMatchingProblemFilter.filter((item) => { const matchType = filter === 'all' || item.type === filter; @@ -504,38 +480,78 @@ export function AuthFilesPage() { ); const renderFilterTags = () => ( -
- {existingTypes.map((type) => { - const isActive = filter === type; - const iconSrc = getFilterTagIcon(type, resolvedTheme); - const color = - type === 'all' - ? { bg: 'var(--bg-tertiary)', text: 'var(--text-primary)' } - : getTypeColor(type, resolvedTheme); - const activeTextColor = resolvedTheme === 'dark' ? '#111827' : '#fff'; - return ( - - ); - })} -
+ ); const titleNode = ( @@ -607,129 +623,133 @@ export function AuthFilesPage() {
{renderFilterTags()} -
-
- - { - setSearch(e.target.value); - setPage(1); - }} - placeholder={t('auth_files.search_placeholder')} - /> -
-
- - commitPageSizeInput(e.currentTarget.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.currentTarget.blur(); - } - }} - /> -
-
- - { + setSearch(e.target.value); + setPage(1); + }} + placeholder={t('auth_files.search_placeholder')} + /> +
+
+ + commitPageSizeInput(e.currentTarget.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.currentTarget.blur(); + } + }} + /> +
+
+ +