feat(auth-files): improve credential cards, bulk actions, and dedupe auth file list

This commit is contained in:
Supra4E8C
2026-03-22 14:41:57 +08:00
parent 975d0471f7
commit e44a9fc099
10 changed files with 670 additions and 65 deletions
@@ -4,11 +4,10 @@ import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { SelectionCheckbox } from '@/components/ui/SelectionCheckbox';
import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
import {
IconBot,
IconDownload,
IconInfo,
IconModelCluster,
IconSettings,
IconShield,
IconTrash2,
} from '@/components/ui/icons';
import { ProviderStatusBar } from '@/components/providers/ProviderStatusBar';
@@ -37,6 +36,7 @@ const HEALTHY_STATUS_MESSAGES = new Set(['ok', 'healthy', 'ready', 'success', 'a
export type AuthFileCardProps = {
file: AuthFileItem;
compact: boolean;
selected: boolean;
resolvedTheme: ResolvedTheme;
disableControls: boolean;
@@ -63,6 +63,7 @@ export function AuthFileCard(props: AuthFileCardProps) {
const { t } = useTranslation();
const {
file,
compact,
selected,
resolvedTheme,
disableControls,
@@ -90,7 +91,7 @@ export function AuthFileCard(props: AuthFileCardProps) {
const quotaType =
quotaFilterType && resolveQuotaType(file) === quotaFilterType ? quotaFilterType : null;
const showQuotaLayout = Boolean(quotaType) && !isRuntimeOnly;
const showQuotaLayout = Boolean(quotaType) && !isRuntimeOnly && !compact;
const providerCardClass =
quotaType === 'antigravity'
@@ -134,7 +135,7 @@ export function AuthFileCard(props: AuthFileCardProps) {
return (
<div
className={`${styles.fileCard} ${providerCardClass} ${selected ? styles.fileCardSelected : ''} ${file.disabled ? styles.fileCardDisabled : ''}`}
className={`${styles.fileCard} ${compact ? styles.fileCardCompact : ''} ${providerCardClass} ${selected ? styles.fileCardSelected : ''} ${file.disabled ? styles.fileCardDisabled : ''}`}
>
<div className={styles.fileCardLayout}>
<div className={styles.fileCardMain}>
@@ -180,8 +181,10 @@ export function AuthFileCard(props: AuthFileCardProps) {
</span>
<span className={`${styles.stateBadge} ${stateBadgeClass}`}>{stateLabel}</span>
</div>
<span className={styles.fileName}>{file.name}</span>
{noteValue && (
<span className={styles.fileName} title={file.name}>
{file.name}
</span>
{!compact && noteValue && (
<div className={styles.noteText} title={noteValue}>
<span className={styles.noteLabel}>{t('auth_files.note_display')}</span>
<span className={styles.noteValue}>{noteValue}</span>
@@ -190,7 +193,7 @@ export function AuthFileCard(props: AuthFileCardProps) {
</div>
</div>
<div className={styles.cardMeta}>
<div className={`${styles.cardMeta} ${compact ? styles.cardMetaCompact : ''}`}>
<div className={styles.metaItem}>
<span className={styles.metaLabel}>{t('auth_files.file_size')}</span>
<span className={styles.metaValue}>
@@ -218,8 +221,8 @@ export function AuthFileCard(props: AuthFileCardProps) {
</div>
)}
<div className={styles.cardInsights}>
<div className={styles.cardStats}>
<div className={`${styles.cardInsights} ${compact ? styles.cardInsightsCompact : ''}`}>
<div className={`${styles.cardStats} ${compact ? styles.cardStatsCompact : ''}`}>
<div className={`${styles.statPill} ${styles.statSuccess}`}>
<span className={styles.statLabel}>{t('stats.success')}</span>
<span className={styles.statValue}>{fileStats.success}</span>
@@ -230,9 +233,8 @@ export function AuthFileCard(props: AuthFileCardProps) {
</div>
</div>
<div className={styles.statusPanel}>
<div className={`${styles.statusPanel} ${compact ? styles.statusPanelCompact : ''}`}>
<div className={styles.statusPanelLabel}>
<IconShield size={14} />
<span>{t('auth_files.health_status_label')}</span>
</div>
<ProviderStatusBar statusData={statusData} styles={styles} />
@@ -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}
>
<>
<IconBot className={styles.actionIcon} size={16} />
<span className={styles.modelsActionIconWrap}>
<IconModelCluster className={styles.actionIcon} size={16} />
</span>
<span className={styles.actionButtonLabel}>
{t('auth_files.models_button', { defaultValue: '模型' })}
</span>
@@ -40,7 +40,9 @@ export type UseAuthFilesDataResult = {
handleStatusToggle: (item: AuthFileItem, enabled: boolean) => Promise<void>;
toggleSelect: (name: string) => void;
selectAllVisible: (visibleFiles: AuthFileItem[]) => void;
invertVisibleSelection: (visibleFiles: AuthFileItem[]) => void;
deselectAll: () => void;
batchDownload: (names: string[]) => Promise<void>;
batchSetStatus: (names: string[], enabled: boolean) => Promise<void>;
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,
};
}
+1
View File
@@ -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;