mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-09-24 16:04:35 +08:00
feat(auth-files): improve credential cards, bulk actions, and dedupe auth file list
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user