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();
- }
- }}
- />
-
-
-
-
-
-
-
-
-
{
- setProblemOnly(value);
- setPage(1);
- }}
- ariaLabel={t('auth_files.problem_filter_only')}
- label={
-
- {t('auth_files.problem_filter_only')}
-
- }
- />
+
+
+
+
+
+ {
+ 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();
+ }
+ }}
+ />
+
+
+
+
+
+
+
+
+ {
+ setProblemOnly(value);
+ setPage(1);
+ }}
+ ariaLabel={t('auth_files.problem_filter_only')}
+ label={
+
+ {t('auth_files.problem_filter_only')}
+
+ }
+ />
+
+
+
+ {loading ? (
+
{t('common.loading')}
+ ) : pageItems.length === 0 ? (
+
+ ) : (
+
+ {pageItems.map((file) => (
+
+ ))}
+
+ )}
+
+ {!loading && sorted.length > pageSize && (
+
+
+
+ {t('auth_files.pagination_info', {
+ current: currentPage,
+ total: totalPages,
+ count: sorted.length,
+ })}
+
+
+
+ )}
-
- {loading ? (
-
{t('common.loading')}
- ) : pageItems.length === 0 ? (
-
- ) : (
-
- {pageItems.map((file) => (
-
- ))}
-
- )}
-
- {!loading && sorted.length > pageSize && (
-
-
-
- {t('auth_files.pagination_info', {
- current: currentPage,
- total: totalPages,
- count: sorted.length,
- })}
-
-
-
- )}