-
{t('auth_files.health_status_label')}
@@ -254,12 +256,14 @@ export function AuthFileCard(props: AuthFileCardProps) {
variant="secondary"
size="sm"
onClick={() => onShowModels(file)}
- className={styles.primaryActionButton}
+ className={`${styles.primaryActionButton} ${styles.modelsActionButton}`}
title={t('auth_files.models_button', { defaultValue: '模型' })}
disabled={disableControls}
>
<>
-
+
+
+
{t('auth_files.models_button', { defaultValue: '模型' })}
diff --git a/src/features/authFiles/hooks/useAuthFilesData.ts b/src/features/authFiles/hooks/useAuthFilesData.ts
index 1ad410d..571397c 100644
--- a/src/features/authFiles/hooks/useAuthFilesData.ts
+++ b/src/features/authFiles/hooks/useAuthFilesData.ts
@@ -40,7 +40,9 @@ export type UseAuthFilesDataResult = {
handleStatusToggle: (item: AuthFileItem, enabled: boolean) => Promise
;
toggleSelect: (name: string) => void;
selectAllVisible: (visibleFiles: AuthFileItem[]) => void;
+ invertVisibleSelection: (visibleFiles: AuthFileItem[]) => void;
deselectAll: () => void;
+ batchDownload: (names: string[]) => Promise;
batchSetStatus: (names: string[], enabled: boolean) => Promise;
batchDelete: (names: string[]) => void;
};
@@ -81,7 +83,31 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles
const nextSelected = visibleFiles
.filter((file) => !isRuntimeOnlyAuthFile(file))
.map((file) => file.name);
- setSelectedFiles(new Set(nextSelected));
+ if (nextSelected.length === 0) return;
+ setSelectedFiles((prev) => {
+ const next = new Set(prev);
+ nextSelected.forEach((name) => next.add(name));
+ return next;
+ });
+ }, []);
+
+ const invertVisibleSelection = useCallback((visibleFiles: AuthFileItem[]) => {
+ const visibleNames = visibleFiles
+ .filter((file) => !isRuntimeOnlyAuthFile(file))
+ .map((file) => file.name);
+ if (visibleNames.length === 0) return;
+
+ setSelectedFiles((prev) => {
+ const next = new Set(prev);
+ visibleNames.forEach((name) => {
+ if (next.has(name)) {
+ next.delete(name);
+ } else {
+ next.add(name);
+ }
+ });
+ return next;
+ });
}, []);
const deselectAll = useCallback(() => {
@@ -220,7 +246,7 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles
} finally {
setDeleting(null);
}
- }
+ },
});
},
[showConfirmation, showNotification, t]
@@ -348,7 +374,7 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles
} finally {
setDeletingAll(false);
}
- }
+ },
});
},
[deselectAll, files, showConfirmation, showNotification, t]
@@ -470,6 +496,43 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles
[deselectAll, showNotification, t]
);
+ const batchDownload = useCallback(
+ async (names: string[]) => {
+ const uniqueNames = Array.from(new Set(names));
+ if (uniqueNames.length === 0) return;
+
+ let successCount = 0;
+ let failCount = 0;
+
+ for (const name of uniqueNames) {
+ try {
+ const response = await apiClient.getRaw(
+ `/auth-files/download?name=${encodeURIComponent(name)}`,
+ { responseType: 'blob' }
+ );
+ const blob = new Blob([response.data]);
+ downloadBlob({ filename: name, blob });
+ successCount++;
+ } catch {
+ failCount++;
+ }
+ }
+
+ if (failCount === 0) {
+ showNotification(
+ t('auth_files.batch_download_success', { count: successCount }),
+ 'success'
+ );
+ } else {
+ showNotification(
+ t('auth_files.batch_download_partial', { success: successCount, failed: failCount }),
+ 'warning'
+ );
+ }
+ },
+ [showNotification, t]
+ );
+
const batchDelete = useCallback(
(names: string[]) => {
const uniqueNames = Array.from(new Set(names));
@@ -516,18 +579,21 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles
});
if (failCount === 0) {
- showNotification(`${t('auth_files.delete_all_success')} (${deleted.length})`, 'success');
+ showNotification(
+ `${t('auth_files.delete_all_success')} (${deleted.length})`,
+ 'success'
+ );
} else {
showNotification(
t('auth_files.delete_filtered_partial', {
success: deleted.length,
failed: failCount,
- type: t('auth_files.filter_all')
+ type: t('auth_files.filter_all'),
}),
'warning'
);
}
- }
+ },
});
},
[showConfirmation, showNotification, t]
@@ -553,8 +619,10 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles
handleStatusToggle,
toggleSelect,
selectAllVisible,
+ invertVisibleSelection,
deselectAll,
+ batchDownload,
batchSetStatus,
- batchDelete
+ batchDelete,
};
}
diff --git a/src/features/authFiles/uiState.ts b/src/features/authFiles/uiState.ts
index edc4c28..fd9ca4d 100644
--- a/src/features/authFiles/uiState.ts
+++ b/src/features/authFiles/uiState.ts
@@ -5,6 +5,7 @@ export type AuthFilesSortMode = (typeof AUTH_FILES_SORT_MODES)[number];
export type AuthFilesUiState = {
filter?: string;
problemOnly?: boolean;
+ compactMode?: boolean;
search?: string;
page?: number;
pageSize?: number;
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 3714b14..4280366 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -495,6 +495,8 @@
"search_placeholder": "Filter by name, type, or provider",
"problem_filter_label": "Problem Filter",
"problem_filter_only": "Only show problematic credentials",
+ "display_options_label": "Display options",
+ "compact_mode_label": "Compact mode",
"sort_label": "Sort",
"sort_default": "Default",
"sort_az": "A-Z Name",
@@ -549,7 +551,13 @@
"batch_delete_confirm": "Are you sure you want to delete {{count}} files?",
"batch_selected": "{{count}} selected",
"batch_select_all": "Select All",
+ "batch_select_page": "Select page",
+ "batch_select_filtered": "Select filtered",
+ "batch_invert_page": "Invert page",
"batch_deselect": "Deselect",
+ "batch_download": "Download selected",
+ "batch_download_success": "Started downloading {{count}} files",
+ "batch_download_partial": "Download finished: {{success}} succeeded, {{failed}} failed",
"batch_enable": "Enable",
"batch_disable": "Disable",
"prefix_proxy_button": "Auth File Details / Edit",
diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json
index d1b34bd..bd5a43d 100644
--- a/src/i18n/locales/ru.json
+++ b/src/i18n/locales/ru.json
@@ -495,6 +495,8 @@
"search_placeholder": "Фильтр по имени, типу или провайдеру",
"problem_filter_label": "Фильтр проблем",
"problem_filter_only": "Показывать только проблемные учётные данные",
+ "display_options_label": "Параметры отображения",
+ "compact_mode_label": "Компактный режим",
"sort_label": "Сортировка",
"sort_default": "По умолчанию",
"sort_az": "A-Z Имя",
@@ -549,7 +551,13 @@
"batch_delete_confirm": "Удалить {{count}} файлов?",
"batch_selected": "{{count}} выбрано",
"batch_select_all": "Выбрать все",
+ "batch_select_page": "Выбрать страницу",
+ "batch_select_filtered": "Выбрать по фильтру",
+ "batch_invert_page": "Инвертировать страницу",
"batch_deselect": "Отменить",
+ "batch_download": "Скачать выбранные",
+ "batch_download_success": "Запущена загрузка {{count}} файлов",
+ "batch_download_partial": "Загрузка завершена: успешно {{success}}, ошибок {{failed}}",
"batch_enable": "Включить",
"batch_disable": "Отключить",
"prefix_proxy_button": "Просмотр / редактирование файла авторизации",
diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json
index 3428d10..f7af907 100644
--- a/src/i18n/locales/zh-CN.json
+++ b/src/i18n/locales/zh-CN.json
@@ -495,6 +495,8 @@
"search_placeholder": "输入名称、类型或提供方关键字",
"problem_filter_label": "问题筛选",
"problem_filter_only": "仅显示有问题凭证",
+ "display_options_label": "显示选项",
+ "compact_mode_label": "简略模式",
"sort_label": "排序",
"sort_default": "默认",
"sort_az": "A-Z 名称",
@@ -549,7 +551,13 @@
"batch_delete_confirm": "确定要删除 {{count}} 个文件吗?",
"batch_selected": "已选 {{count}} 项",
"batch_select_all": "全选",
+ "batch_select_page": "全选当前页",
+ "batch_select_filtered": "全选筛选结果",
+ "batch_invert_page": "反选当前页",
"batch_deselect": "取消选择",
+ "batch_download": "下载选中",
+ "batch_download_success": "已开始下载 {{count}} 个文件",
+ "batch_download_partial": "下载完成:成功 {{success}} 个,失败 {{failed}} 个",
"batch_enable": "启用",
"batch_disable": "禁用",
"prefix_proxy_button": "认证文件详情 / 编辑",
diff --git a/src/pages/AuthFilesPage.module.scss b/src/pages/AuthFilesPage.module.scss
index 5953d38..f7debdd 100644
--- a/src/pages/AuthFilesPage.module.scss
+++ b/src/pages/AuthFilesPage.module.scss
@@ -160,6 +160,45 @@
color: var(--text-primary);
}
+.filterAllIconWrap {
+ position: relative;
+ overflow: hidden;
+ border-color: color-mix(in srgb, var(--primary-color) 20%, var(--border-color));
+ background:
+ radial-gradient(
+ circle at 30% 28%,
+ color-mix(in srgb, var(--primary-color) 22%, #ffffff),
+ transparent 58%
+ ),
+ linear-gradient(
+ 145deg,
+ color-mix(in srgb, var(--bg-secondary) 94%, var(--primary-color) 8%),
+ color-mix(in srgb, var(--bg-primary) 92%, var(--primary-color) 5%)
+ );
+ box-shadow:
+ inset 0 1px 0 color-mix(in srgb, #fff 54%, transparent),
+ 0 10px 22px -18px color-mix(in srgb, var(--primary-color) 50%, transparent);
+}
+
+.filterAllIconWrap::after {
+ content: '';
+ position: absolute;
+ inset: auto 7px 7px auto;
+ width: 10px;
+ height: 10px;
+ border-radius: 999px;
+ background: color-mix(in srgb, var(--primary-color) 18%, transparent);
+ filter: blur(1px);
+ opacity: 0.9;
+}
+
+.filterAllIcon {
+ position: relative;
+ z-index: 1;
+ display: block;
+ color: color-mix(in srgb, var(--primary-color) 70%, var(--text-primary));
+}
+
.filterRailHeroText {
display: flex;
flex-direction: column;
@@ -189,9 +228,8 @@
}
.filterRailCount {
- display: inline-flex;
- align-items: center;
- justify-content: center;
+ display: inline-grid;
+ place-items: center;
min-width: 38px;
height: 30px;
padding: 0 10px;
@@ -201,6 +239,8 @@
font-size: 13px;
font-weight: 700;
font-variant-numeric: tabular-nums;
+ line-height: 1;
+ text-align: center;
}
.filterRailMode {
@@ -333,9 +373,8 @@
}
.filterTagCount {
- display: inline-flex;
- align-items: center;
- justify-content: center;
+ display: inline-grid;
+ place-items: center;
min-width: 34px;
height: 28px;
padding: 0 10px;
@@ -346,6 +385,8 @@
font-weight: 700;
font-variant-numeric: tabular-nums;
flex: 0 0 auto;
+ line-height: 1;
+ text-align: center;
}
.filterTagActive .filterTagCount {
@@ -411,6 +452,23 @@
}
}
+.filterToggleGroup {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 8px;
+ min-height: 40px;
+}
+
+.filterToggleCard {
+ display: flex;
+ align-items: center;
+ min-height: 40px;
+ padding: 0 2px;
+ 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);
+}
+
.filterToggle {
display: flex;
align-items: center;
@@ -473,6 +531,18 @@
}
}
+.fileGridCompact {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+
+ @include tablet {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ }
+
+ @include mobile {
+ grid-template-columns: 1fr;
+ }
+}
+
.antigravityGrid {
display: grid;
gap: $spacing-md;
@@ -840,6 +910,108 @@
}
}
+.fileCardCompact {
+ padding: 14px;
+ border-radius: 14px;
+
+ .fileCardMain {
+ gap: 10px;
+ }
+
+ .cardHeader {
+ gap: 10px;
+ }
+
+ .cardSelection {
+ margin-top: 7px;
+ }
+
+ .providerAvatar {
+ width: 38px;
+ height: 38px;
+ border-radius: 12px;
+ }
+
+ .providerAvatarImage {
+ width: 18px;
+ height: 18px;
+ }
+
+ .providerAvatarFallback {
+ font-size: 16px;
+ }
+
+ .cardHeaderContent {
+ gap: 4px;
+ }
+
+ .cardBadgeRow {
+ gap: 6px;
+ }
+
+ .typeBadge,
+ .stateBadge {
+ padding: 4px 8px;
+ }
+
+ .fileName {
+ font-size: 14px;
+ line-height: 1.35;
+ -webkit-line-clamp: 1;
+ }
+
+ .healthStatusMessage {
+ padding: 8px 10px;
+ border-radius: 10px;
+ font-size: 11px;
+ }
+
+ .quotaSection {
+ display: none;
+ }
+
+ .cardActions {
+ gap: 8px;
+ padding-top: 0;
+ }
+
+ .cardActionsMain {
+ width: 100%;
+ gap: 8px;
+ flex-wrap: nowrap;
+ }
+
+ .primaryActionButton:global(.btn.btn-sm) {
+ flex: 1 1 auto;
+ min-width: 0;
+ padding-inline: 10px;
+ }
+
+ .cardUtilityActions {
+ gap: 4px;
+ padding: 3px;
+ border-radius: 10px;
+ }
+
+ .iconButton:global(.btn.btn-sm) {
+ width: 34px;
+ height: 34px;
+ min-width: 34px;
+ border-radius: 9px;
+ }
+
+ .statusToggle {
+ width: 100%;
+ justify-content: space-between;
+ gap: 8px;
+ padding: 5px 8px;
+ }
+
+ .statusToggleLabel {
+ font-size: 11px;
+ }
+}
+
.fileCardLayout {
display: flex;
align-items: stretch;
@@ -976,6 +1148,32 @@
}
}
+.cardMetaCompact {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.cardMetaCompact .metaItem {
+ flex-direction: row;
+ align-items: center;
+ gap: 6px;
+ padding: 7px 10px;
+ border-radius: 999px;
+ flex: 0 1 auto;
+}
+
+.cardMetaCompact .metaLabel {
+ font-size: 10px;
+ white-space: nowrap;
+}
+
+.cardMetaCompact .metaValue {
+ font-size: 12px;
+ line-height: 1.2;
+ white-space: nowrap;
+}
+
.metaItem {
display: flex;
flex-direction: column;
@@ -1066,6 +1264,12 @@
background: color-mix(in srgb, var(--bg-secondary) 88%, transparent);
}
+.cardInsightsCompact {
+ gap: 10px;
+ padding: 10px 12px;
+ border-radius: 14px;
+}
+
.cardStats {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -1076,6 +1280,26 @@
}
}
+.cardStatsCompact {
+ gap: 8px;
+}
+
+.cardStatsCompact .statPill {
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ padding: 8px 10px;
+}
+
+.cardStatsCompact .statLabel {
+ font-size: 11px;
+}
+
+.cardStatsCompact .statValue {
+ font-size: 18px;
+}
+
.statPill {
display: flex;
flex-direction: column;
@@ -1117,6 +1341,23 @@
gap: 8px;
}
+.statusPanelCompact {
+ gap: 6px;
+}
+
+.statusPanelCompact .statusPanelLabel {
+ display: none;
+}
+
+.statusPanelCompact .statusBar {
+ gap: 8px;
+}
+
+.statusPanelCompact .statusRate {
+ padding: 5px 8px;
+ font-size: 11px;
+}
+
.statusPanelLabel {
display: inline-flex;
align-items: center;
@@ -1394,6 +1635,42 @@
padding-inline: 12px;
}
+.modelsActionButton:global(.btn.btn-sm) {
+ background: linear-gradient(
+ 135deg,
+ color-mix(in srgb, var(--primary-color) 15%, var(--bg-secondary)),
+ color-mix(in srgb, var(--primary-color) 7%, var(--bg-primary))
+ );
+ border-color: color-mix(in srgb, var(--primary-color) 24%, var(--border-color));
+}
+
+.modelsActionButton:global(.btn.btn-sm):hover {
+ border-color: color-mix(in srgb, var(--primary-color) 38%, var(--border-color));
+ background: linear-gradient(
+ 135deg,
+ color-mix(in srgb, var(--primary-color) 18%, var(--bg-secondary)),
+ color-mix(in srgb, var(--primary-color) 9%, var(--bg-primary))
+ );
+}
+
+.modelsActionButton:global(.btn.btn-sm) > span {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+}
+
+.modelsActionIconWrap {
+ width: 22px;
+ height: 22px;
+ flex: 0 0 auto;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--primary-color) 16%, transparent);
+}
+
.actionButtonLabel {
@include text-ellipsis;
}
@@ -1461,6 +1738,20 @@
width: 100%;
justify-content: space-between;
}
+
+ .fileCardCompact {
+ .cardActionsMain {
+ flex-wrap: wrap;
+ }
+
+ .primaryActionButton:global(.btn.btn-sm) {
+ flex: 1 1 auto;
+ }
+
+ .cardUtilityActions {
+ margin-left: auto;
+ }
+ }
}
// 分页
diff --git a/src/pages/AuthFilesPage.tsx b/src/pages/AuthFilesPage.tsx
index d0d91b9..15943db 100644
--- a/src/pages/AuthFilesPage.tsx
+++ b/src/pages/AuthFilesPage.tsx
@@ -20,6 +20,7 @@ import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { Select } from '@/components/ui/Select';
+import { IconFilterAll } from '@/components/ui/icons';
import { EmptyState } from '@/components/ui/EmptyState';
import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
import { copyToClipboard } from '@/utils/clipboard';
@@ -74,6 +75,7 @@ export function AuthFilesPage() {
const [filter, setFilter] = useState<'all' | string>('all');
const [problemOnly, setProblemOnly] = useState(false);
+ const [compactMode, setCompactMode] = useState(false);
const [search, setSearch] = useState('');
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(9);
@@ -107,7 +109,9 @@ export function AuthFilesPage() {
handleStatusToggle,
toggleSelect,
selectAllVisible,
+ invertVisibleSelection,
deselectAll,
+ batchDownload,
batchSetStatus,
batchDelete,
} = useAuthFilesData({ refreshKeyStats });
@@ -174,6 +178,9 @@ export function AuthFilesPage() {
if (typeof persisted.problemOnly === 'boolean') {
setProblemOnly(persisted.problemOnly);
}
+ if (typeof persisted.compactMode === 'boolean') {
+ setCompactMode(persisted.compactMode);
+ }
if (typeof persisted.search === 'string') {
setSearch(persisted.search);
}
@@ -189,8 +196,8 @@ export function AuthFilesPage() {
}, []);
useEffect(() => {
- writeAuthFilesUiState({ filter, problemOnly, search, page, pageSize, sortMode });
- }, [filter, problemOnly, search, page, pageSize, sortMode]);
+ writeAuthFilesUiState({ filter, problemOnly, compactMode, search, page, pageSize, sortMode });
+ }, [filter, problemOnly, compactMode, search, page, pageSize, sortMode]);
useEffect(() => {
setPageSizeInput(String(pageSize));
@@ -344,6 +351,10 @@ export function AuthFilesPage() {
() => pageItems.filter((file) => !isRuntimeOnlyAuthFile(file)),
[pageItems]
);
+ const selectableFilteredItems = useMemo(
+ () => sorted.filter((file) => !isRuntimeOnlyAuthFile(file)),
+ [sorted]
+ );
const selectedNames = useMemo(() => Array.from(selectedFiles), [selectedFiles]);
const copyTextWithNotification = useCallback(
@@ -487,15 +498,21 @@ export function AuthFilesPage() {
-
- {activeFilterIcon ? (
-

- ) : (
-
- {activeFilterLabel.slice(0, 1).toUpperCase()}
-
- )}
-
+ {filter === 'all' ? (
+
+
+
+ ) : (
+
+ {activeFilterIcon ? (
+

+ ) : (
+
+ {activeFilterLabel.slice(0, 1).toUpperCase()}
+
+ )}
+
+ )}
{activeFilterLabel}
{t('auth_files.title_section')}
@@ -535,15 +552,21 @@ export function AuthFilesPage() {
}}
>
-
- {iconSrc ? (
-
- ) : (
-
- {getTypeLabel(t, type).slice(0, 1).toUpperCase()}
-
- )}
-
+ {type === 'all' ? (
+
+
+
+ ) : (
+
+ {iconSrc ? (
+
+ ) : (
+
+ {getTypeLabel(t, type).slice(0, 1).toUpperCase()}
+
+ )}
+
+ )}
{getTypeLabel(t, type)}
{typeCounts[type] ?? 0}
@@ -667,21 +690,35 @@ export function AuthFilesPage() {
/>
-
-
-
{
- setProblemOnly(value);
- setPage(1);
- }}
- ariaLabel={t('auth_files.problem_filter_only')}
- label={
-
- {t('auth_files.problem_filter_only')}
-
- }
- />
+
+
+
+ {
+ setProblemOnly(value);
+ setPage(1);
+ }}
+ ariaLabel={t('auth_files.problem_filter_only')}
+ label={
+
+ {t('auth_files.problem_filter_only')}
+
+ }
+ />
+
+
+ setCompactMode(value)}
+ ariaLabel={t('auth_files.compact_mode_label')}
+ label={
+
+ {t('auth_files.compact_mode_label')}
+
+ }
+ />
+
@@ -696,12 +733,13 @@ export function AuthFilesPage() {
/>
) : (
{pageItems.map((file) => (
selectAllVisible(pageItems)}
disabled={selectablePageItems.length === 0}
>
- {t('auth_files.batch_select_all')}
+ {t('auth_files.batch_select_page')}
+
+
+
+