diff --git a/src/pages/AuthFilesPage.module.scss b/src/pages/AuthFilesPage.module.scss index 57f40a9..f46a759 100644 --- a/src/pages/AuthFilesPage.module.scss +++ b/src/pages/AuthFilesPage.module.scss @@ -1,58 +1,349 @@ +@use '../styles/variables' as *; +@use '../styles/mixins' as *; + .container { - width: 100%; -} - -.pageTitle { - font-size: 28px; - font-weight: 700; - color: var(--text-primary); - margin: 0 0 $spacing-md 0; -} - -.description { - font-size: 14px; - color: var(--text-secondary); - margin: 0 0 $spacing-xl 0; -} - -.content { display: flex; flex-direction: column; gap: $spacing-lg; } -.controls { - display: flex; - justify-content: space-between; - align-items: center; - gap: $spacing-md; - - @include mobile { - flex-direction: column; - align-items: stretch; - } -} - -.filters { +.headerActions { display: flex; gap: $spacing-sm; flex-wrap: wrap; } +.errorBox { + padding: $spacing-md; + background-color: rgba(239, 68, 68, 0.1); + border: 1px solid var(--danger-color); + border-radius: $radius-md; + color: var(--danger-color); + font-size: 14px; + margin-bottom: $spacing-md; +} + +// 筛选区域 +.filterSection { + display: flex; + flex-direction: column; + gap: $spacing-md; + margin-bottom: $spacing-lg; +} + +.filterTags { + display: flex; + flex-wrap: wrap; + gap: $spacing-xs; +} + +.filterTag { + padding: 6px 14px; + border-radius: 20px; + font-size: 13px; + font-weight: 500; + border: 1px solid transparent; + cursor: pointer; + transition: all $transition-fast; + + &:hover { + transform: translateY(-1px); + box-shadow: $shadow-sm; + } +} + +.filterTagActive { + font-weight: 600; +} + +.filterControls { + display: flex; + gap: $spacing-md; + flex-wrap: wrap; + align-items: flex-end; +} + +.filterItem { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 120px; + + label { + font-size: 12px; + color: var(--text-secondary); + font-weight: 500; + } +} + +.pageSizeSelect { + padding: 8px 12px; + border: 1px solid var(--border-color); + border-radius: $radius-md; + background-color: var(--bg-primary); + color: var(--text-primary); + font-size: 14px; + cursor: pointer; + + &:focus { + outline: none; + border-color: var(--primary-color); + } +} + +.statsInfo { + padding: 8px 12px; + background-color: var(--bg-secondary); + border-radius: $radius-md; + font-size: 13px; + color: var(--text-secondary); + white-space: nowrap; +} + +// 卡片网格 .fileGrid { display: grid; gap: $spacing-md; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + + @include tablet { + grid-template-columns: repeat(2, 1fr); + } @include mobile { grid-template-columns: 1fr; } } +// 单个认证文件卡片 +.fileCard { + background-color: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: $radius-lg; + padding: $spacing-md; + display: flex; + flex-direction: column; + gap: $spacing-sm; + transition: transform $transition-fast, box-shadow $transition-fast, border-color $transition-fast; + + &:hover { + transform: translateY(-2px); + box-shadow: $shadow-md; + border-color: rgba(37, 99, 235, 0.2); + } +} + +.cardHeader { + display: flex; + align-items: center; + gap: $spacing-sm; + min-height: 28px; +} + +.typeBadge { + padding: 4px 10px; + border-radius: 12px; + font-size: 12px; + font-weight: 600; + white-space: nowrap; + flex-shrink: 0; +} + +.fileName { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + word-break: break-all; + line-height: 1.4; +} + +.cardMeta { + 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); +} + +.cardStats { + display: flex; + gap: $spacing-md; + padding: $spacing-sm 0; +} + +.statSuccess { + display: flex; + align-items: center; + gap: 4px; + font-size: 13px; + color: var(--success-color, #22c55e); + font-weight: 500; +} + +.statFailure { + display: flex; + align-items: center; + gap: 4px; + font-size: 13px; + color: var(--danger-color, #ef4444); + font-weight: 500; +} + +.statIcon { + font-style: normal; + font-size: 12px; +} + +.cardActions { + display: flex; + gap: $spacing-xs; + justify-content: flex-end; + margin-top: auto; + padding-top: $spacing-sm; +} + +.actionIcon { + font-style: normal; + font-size: 14px; +} + +.virtualBadge { + font-size: 12px; + color: var(--text-secondary); + background-color: var(--bg-tertiary); + padding: 4px 10px; + border-radius: $radius-sm; + font-style: italic; +} + +// 分页 .pagination { display: flex; justify-content: center; align-items: center; gap: $spacing-md; margin-top: $spacing-lg; + padding-top: $spacing-md; + border-top: 1px solid var(--border-color); +} + +.pageInfo { + font-size: 13px; + color: var(--text-secondary); + padding: $spacing-xs $spacing-md; + background-color: var(--bg-secondary); + border-radius: $radius-md; +} + +// OAuth 排除列表 +.excludedList { + display: flex; + flex-direction: column; + gap: $spacing-sm; +} + +.excludedItem { + display: flex; + justify-content: space-between; + align-items: center; + padding: $spacing-md; + background-color: var(--bg-secondary); + border-radius: $radius-md; + border: 1px solid var(--border-color); + gap: $spacing-md; + + @include mobile { + flex-direction: column; + align-items: flex-start; + } +} + +.excludedInfo { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + flex: 1; +} + +.excludedProvider { + font-weight: 600; + color: var(--text-primary); + font-size: 14px; +} + +.excludedModels { + font-size: 12px; + color: var(--text-secondary); +} + +.excludedActions { + display: flex; + gap: $spacing-xs; + flex-shrink: 0; +} + +// 详情弹窗 +.detailContent { + max-height: 400px; + overflow: auto; +} + +.jsonContent { + background-color: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: $radius-md; + padding: $spacing-md; + font-family: monospace; + font-size: 12px; + white-space: pre-wrap; + word-break: break-all; + color: var(--text-primary); + margin: 0; +} + +// 表单 +.formGroup { + display: flex; + flex-direction: column; + gap: $spacing-xs; + margin-top: $spacing-md; + + label { + font-size: 14px; + font-weight: 500; + color: var(--text-primary); + } +} + +.textarea { + width: 100%; + padding: $spacing-sm $spacing-md; + border: 1px solid var(--border-color); + border-radius: $radius-md; + background-color: var(--bg-primary); + color: var(--text-primary); + font-size: 14px; + font-family: monospace; + resize: vertical; + + &:focus { + outline: none; + border-color: var(--primary-color); + } + + &::placeholder { + color: var(--text-tertiary); + } +} + +.hint { + font-size: 12px; + color: var(--text-tertiary); + font-style: italic; + text-align: center; + padding: $spacing-lg; } diff --git a/src/pages/AuthFilesPage.tsx b/src/pages/AuthFilesPage.tsx index 4c60111..01cf9c6 100644 --- a/src/pages/AuthFilesPage.tsx +++ b/src/pages/AuthFilesPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; @@ -6,16 +6,84 @@ import { Input } from '@/components/ui/Input'; import { Modal } from '@/components/ui/Modal'; import { EmptyState } from '@/components/ui/EmptyState'; import { useAuthStore, useNotificationStore } from '@/stores'; -import { authFilesApi } from '@/services/api'; +import { authFilesApi, usageApi } from '@/services/api'; import { apiClient } from '@/services/api/client'; import type { AuthFileItem } from '@/types'; +import type { KeyStats, KeyStatBucket } from '@/utils/usage'; import { formatFileSize } from '@/utils/format'; +import styles from './AuthFilesPage.module.scss'; + +// 标签类型颜色配置 +const TYPE_COLORS: Record = { + qwen: { bg: 'rgba(59, 130, 246, 0.15)', text: '#3b82f6' }, + gemini: { bg: 'rgba(34, 197, 94, 0.15)', text: '#22c55e' }, + 'gemini-cli': { bg: 'rgba(6, 182, 212, 0.15)', text: '#06b6d4' }, + aistudio: { bg: 'rgba(139, 92, 246, 0.15)', text: '#8b5cf6' }, + claude: { bg: 'rgba(249, 115, 22, 0.15)', text: '#f97316' }, + codex: { bg: 'rgba(236, 72, 153, 0.15)', text: '#ec4899' }, + antigravity: { bg: 'rgba(245, 158, 11, 0.15)', text: '#f59e0b' }, + iflow: { bg: 'rgba(132, 204, 22, 0.15)', text: '#84cc16' }, + vertex: { bg: 'rgba(239, 68, 68, 0.15)', text: '#ef4444' }, + empty: { bg: 'rgba(107, 114, 128, 0.15)', text: '#6b7280' }, + unknown: { bg: 'rgba(156, 163, 175, 0.15)', text: '#9ca3af' } +}; interface ExcludedFormState { provider: string; modelsText: string; } +// 标准化 auth_index 值(与 usage.ts 中的 normalizeAuthIndex 保持一致) +function normalizeAuthIndexValue(value: unknown): string | null { + if (typeof value === 'number' && Number.isFinite(value)) { + return value.toString(); + } + if (typeof value === 'string') { + const trimmed = value.trim(); + return trimmed ? trimmed : null; + } + return null; +} + +// 解析认证文件的统计数据 +function resolveAuthFileStats( + file: AuthFileItem, + stats: KeyStats +): KeyStatBucket { + const defaultStats: KeyStatBucket = { success: 0, failure: 0 }; + const rawFileName = file?.name || ''; + + // 兼容 auth_index 和 authIndex 两种字段名(API 返回的是 auth_index) + const rawAuthIndex = file['auth_index'] ?? file.authIndex; + const authIndexKey = normalizeAuthIndexValue(rawAuthIndex); + + // 尝试根据 authIndex 匹配 + if (authIndexKey && stats.byAuthIndex?.[authIndexKey]) { + return stats.byAuthIndex[authIndexKey]; + } + + // 尝试根据 source (文件名) 匹配 + if (rawFileName && stats.bySource?.[rawFileName]) { + const fromName = stats.bySource[rawFileName]; + if (fromName.success > 0 || fromName.failure > 0) { + return fromName; + } + } + + // 尝试去掉扩展名后匹配 + if (rawFileName) { + const nameWithoutExt = rawFileName.replace(/\.[^/.]+$/, ''); + if (nameWithoutExt && nameWithoutExt !== rawFileName) { + const fromNameWithoutExt = stats.bySource?.[nameWithoutExt]; + if (fromNameWithoutExt && (fromNameWithoutExt.success > 0 || fromNameWithoutExt.failure > 0)) { + return fromNameWithoutExt; + } + } + } + + return defaultStats; +} + export function AuthFilesPage() { const { t } = useTranslation(); const { showNotification } = useNotificationStore(); @@ -27,10 +95,17 @@ export function AuthFilesPage() { const [filter, setFilter] = useState<'all' | string>('all'); const [search, setSearch] = useState(''); const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(10); + const [pageSize, setPageSize] = useState(9); const [uploading, setUploading] = useState(false); const [deleting, setDeleting] = useState(null); + const [deletingAll, setDeletingAll] = useState(false); + const [keyStats, setKeyStats] = useState({ bySource: {}, byAuthIndex: {} }); + // 详情弹窗相关 + const [detailModalOpen, setDetailModalOpen] = useState(false); + const [selectedFile, setSelectedFile] = useState(null); + + // OAuth 排除模型相关 const [excluded, setExcluded] = useState>({}); const [excludedModalOpen, setExcludedModalOpen] = useState(false); const [excludedForm, setExcludedForm] = useState({ provider: '', modelsText: '' }); @@ -40,44 +115,71 @@ export function AuthFilesPage() { const disableControls = connectionStatus !== 'connected'; + // 格式化修改时间 const formatModified = (item: AuthFileItem): string => { - const raw = (item as any).modtime ?? item.modified; - if (!raw) return t('auth_files.file_modified'); + const raw = item['modtime'] ?? item.modified; + if (!raw) return '-'; const asNumber = Number(raw); const date = Number.isFinite(asNumber) && !Number.isNaN(asNumber) ? new Date(asNumber < 1e12 ? asNumber * 1000 : asNumber) : new Date(String(raw)); - return Number.isNaN(date.getTime()) ? t('auth_files.file_modified') : date.toLocaleString(); + return Number.isNaN(date.getTime()) ? '-' : date.toLocaleString(); }; - const loadFiles = async () => { + // 加载文件列表 + const loadFiles = useCallback(async () => { setLoading(true); setError(''); try { const data = await authFilesApi.list(); setFiles(data?.files || []); - } catch (err: any) { - setError(err?.message || t('notification.refresh_failed')); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : t('notification.refresh_failed'); + setError(errorMessage); } finally { setLoading(false); } - }; + }, [t]); - const loadExcluded = async () => { + // 加载 key 统计 + const loadKeyStats = useCallback(async () => { + try { + const stats = await usageApi.getKeyStats(); + setKeyStats(stats); + } catch { + // 静默失败 + } + }, []); + + // 加载 OAuth 排除列表 + const loadExcluded = useCallback(async () => { try { const res = await authFilesApi.getOauthExcludedModels(); setExcluded(res || {}); - } catch (err) { - // ignore silently + } catch { + // 静默失败 } - }; + }, []); useEffect(() => { loadFiles(); + loadKeyStats(); loadExcluded(); - }, []); + }, [loadFiles, loadKeyStats, loadExcluded]); + // 提取所有存在的类型 + const existingTypes = useMemo(() => { + const types = new Set(['all']); + files.forEach((file) => { + if (file.type) { + types.add(file.type); + } + }); + return Array.from(types); + }, [files]); + + // 过滤和搜索 const filtered = useMemo(() => { return files.filter((item) => { const matchType = filter === 'all' || item.type === filter; @@ -91,58 +193,159 @@ export function AuthFilesPage() { }); }, [files, filter, search]); + // 分页计算 const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize)); const currentPage = Math.min(page, totalPages); const start = (currentPage - 1) * pageSize; const pageItems = filtered.slice(start, start + pageSize); + // 统计信息 const totalSize = useMemo(() => files.reduce((sum, item) => sum + (item.size || 0), 0), [files]); + // 点击上传 const handleUploadClick = () => { fileInputRef.current?.click(); }; + // 处理文件上传(支持多选) const handleFileChange = async (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - if (!file) return; - setUploading(true); - try { - await authFilesApi.upload(file); - showNotification(t('auth_files.upload_success'), 'success'); - await loadFiles(); - } catch (err: any) { - showNotification(`${t('notification.upload_failed')}: ${err?.message || ''}`, 'error'); - } finally { - setUploading(false); - event.target.value = ''; + const fileList = event.target.files; + if (!fileList || fileList.length === 0) return; + + const filesToUpload = Array.from(fileList); + const validFiles: File[] = []; + const invalidFiles: string[] = []; + + filesToUpload.forEach((file) => { + if (file.name.endsWith('.json')) { + validFiles.push(file); + } else { + invalidFiles.push(file.name); + } + }); + + if (invalidFiles.length > 0) { + showNotification(t('auth_files.upload_error_json'), 'error'); } + + if (validFiles.length === 0) { + event.target.value = ''; + return; + } + + setUploading(true); + let successCount = 0; + const failed: { name: string; message: string }[] = []; + + for (const file of validFiles) { + try { + await authFilesApi.upload(file); + successCount++; + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error'; + failed.push({ name: file.name, message: errorMessage }); + } + } + + if (successCount > 0) { + const suffix = validFiles.length > 1 ? ` (${successCount}/${validFiles.length})` : ''; + showNotification(`${t('auth_files.upload_success')}${suffix}`, failed.length ? 'warning' : 'success'); + await loadFiles(); + await loadKeyStats(); + } + + if (failed.length > 0) { + const details = failed.map((item) => `${item.name}: ${item.message}`).join('; '); + showNotification(`${t('notification.upload_failed')}: ${details}`, 'error'); + } + + setUploading(false); + event.target.value = ''; }; + // 删除单个文件 const handleDelete = async (name: string) => { - if (!window.confirm(t('auth_files.delete_confirm'))) return; + if (!window.confirm(`${t('auth_files.delete_confirm')} "${name}" ?`)) return; setDeleting(name); try { await authFilesApi.deleteFile(name); showNotification(t('auth_files.delete_success'), 'success'); setFiles((prev) => prev.filter((item) => item.name !== name)); - } catch (err: any) { - showNotification(`${t('notification.delete_failed')}: ${err?.message || ''}`, 'error'); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : ''; + showNotification(`${t('notification.delete_failed')}: ${errorMessage}`, 'error'); } finally { setDeleting(null); } }; + // 删除全部(根据筛选类型) const handleDeleteAll = async () => { - if (!window.confirm(t('auth_files.delete_all_confirm'))) return; + const isFiltered = filter !== 'all'; + const typeLabel = isFiltered ? getTypeLabel(filter) : t('auth_files.filter_all'); + const confirmMessage = isFiltered + ? t('auth_files.delete_filtered_confirm', { type: typeLabel }) + : t('auth_files.delete_all_confirm'); + + if (!window.confirm(confirmMessage)) return; + + setDeletingAll(true); try { - await authFilesApi.deleteAll(); - showNotification(t('auth_files.delete_all_success'), 'success'); - setFiles([]); - } catch (err: any) { - showNotification(`${t('notification.delete_failed')}: ${err?.message || ''}`, 'error'); + if (!isFiltered) { + // 删除全部 + await authFilesApi.deleteAll(); + showNotification(t('auth_files.delete_all_success'), 'success'); + setFiles([]); + } else { + // 删除筛选类型的文件 + const filesToDelete = files.filter( + (f) => f.type === filter && !f['runtime_only'] + ); + + if (filesToDelete.length === 0) { + showNotification(t('auth_files.delete_filtered_none', { type: typeLabel }), 'info'); + setDeletingAll(false); + return; + } + + let success = 0; + let failed = 0; + const deletedNames: string[] = []; + + for (const file of filesToDelete) { + try { + await authFilesApi.deleteFile(file.name); + success++; + deletedNames.push(file.name); + } catch { + failed++; + } + } + + setFiles((prev) => prev.filter((f) => !deletedNames.includes(f.name))); + + if (failed === 0) { + showNotification( + t('auth_files.delete_filtered_success', { count: success, type: typeLabel }), + 'success' + ); + } else { + showNotification( + t('auth_files.delete_filtered_partial', { success, failed, type: typeLabel }), + 'warning' + ); + } + setFilter('all'); + } + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : ''; + showNotification(`${t('notification.delete_failed')}: ${errorMessage}`, 'error'); + } finally { + setDeletingAll(false); } }; + // 下载文件 const handleDownload = async (name: string) => { try { const response = await apiClient.getRaw(`/auth-files/download?name=${encodeURIComponent(name)}`, { @@ -156,11 +359,33 @@ export function AuthFilesPage() { a.click(); window.URL.revokeObjectURL(url); showNotification(t('auth_files.download_success'), 'success'); - } catch (err: any) { - showNotification(`${t('notification.download_failed')}: ${err?.message || ''}`, 'error'); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : ''; + showNotification(`${t('notification.download_failed')}: ${errorMessage}`, 'error'); } }; + // 显示详情弹窗 + const showDetails = (file: AuthFileItem) => { + setSelectedFile(file); + setDetailModalOpen(true); + }; + + // 获取类型标签显示文本 + const getTypeLabel = (type: string): string => { + const key = `auth_files.filter_${type}`; + const translated = t(key); + if (translated !== key) return translated; + if (type.toLowerCase() === 'iflow') return 'iFlow'; + return type.charAt(0).toUpperCase() + type.slice(1); + }; + + // 获取类型颜色 + const getTypeColor = (type: string) => { + return TYPE_COLORS[type] || TYPE_COLORS.unknown; + }; + + // OAuth 排除相关方法 const openExcludedModal = (provider?: string) => { const models = provider ? excluded[provider] : []; setExcludedForm({ @@ -190,8 +415,9 @@ export function AuthFilesPage() { await loadExcluded(); showNotification(t('oauth_excluded.save_success'), 'success'); setExcludedModalOpen(false); - } catch (err: any) { - showNotification(`${t('oauth_excluded.save_failed')}: ${err?.message || ''}`, 'error'); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : ''; + showNotification(`${t('oauth_excluded.save_failed')}: ${errorMessage}`, 'error'); } finally { setSavingExcluded(false); } @@ -203,156 +429,229 @@ export function AuthFilesPage() { await authFilesApi.deleteOauthExcludedEntry(provider); await loadExcluded(); showNotification(t('oauth_excluded.delete_success'), 'success'); - } catch (err: any) { - showNotification(`${t('oauth_excluded.delete_failed')}: ${err?.message || ''}`, 'error'); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : ''; + showNotification(`${t('oauth_excluded.delete_failed')}: ${errorMessage}`, 'error'); } }; - const typeOptions: { value: string; label: string }[] = [ - { value: 'all', label: t('auth_files.filter_all') }, - { value: 'qwen', label: t('auth_files.filter_qwen') }, - { value: 'gemini', label: t('auth_files.filter_gemini') }, - { value: 'gemini-cli', label: t('auth_files.filter_gemini-cli') }, - { value: 'aistudio', label: t('auth_files.filter_aistudio') }, - { value: 'claude', label: t('auth_files.filter_claude') }, - { value: 'codex', label: t('auth_files.filter_codex') }, - { value: 'antigravity', label: t('auth_files.filter_antigravity') }, - { value: 'iflow', label: t('auth_files.filter_iflow') }, - { value: 'vertex', label: t('auth_files.filter_vertex') }, - { value: 'empty', label: t('auth_files.filter_empty') }, - { value: 'unknown', label: t('auth_files.filter_unknown') } - ]; + // 渲染标签筛选器 + const renderFilterTags = () => ( +
+ {existingTypes.map((type) => { + const isActive = filter === type; + const color = type === 'all' ? { bg: 'var(--bg-tertiary)', text: 'var(--text-primary)' } : getTypeColor(type); + return ( + + ); + })} +
+ ); + + // 渲染单个认证文件卡片 + const renderFileCard = (item: AuthFileItem) => { + const fileStats = resolveAuthFileStats(item, keyStats); + const runtimeOnlyValue = item['runtime_only']; + const isRuntimeOnly = runtimeOnlyValue === true || runtimeOnlyValue === 'true'; + const typeColor = getTypeColor(item.type || 'unknown'); + + return ( +
+
+ + {getTypeLabel(item.type || 'unknown')} + + {item.name} +
+ +
+ {t('auth_files.file_size')}: {item.size ? formatFileSize(item.size) : '-'} + {t('auth_files.file_modified')}: {formatModified(item)} +
+ +
+ + + {t('stats.success')}: {fileStats.success} + + + + {t('stats.failure')}: {fileStats.failure} + +
+ +
+ {isRuntimeOnly ? ( + {t('auth_files.type_virtual') || '虚拟认证文件'} + ) : ( + <> + + + + + )} +
+
+ ); + }; return ( -
+
- - -
} > - {error &&
{error}
} + {error &&
{error}
} -
-
- - setSearch(e.target.value)} - placeholder={t('auth_files.search_placeholder')} - /> -
-
- - setPageSize(Number(e.target.value) || 10)} - /> -
-
- -
- {files.length} {t('auth_files.files_count')} · {formatFileSize(totalSize)} + {/* 筛选区域 */} +
+ {renderFilterTags()} + +
+
+ + { + setSearch(e.target.value); + setPage(1); + }} + placeholder={t('auth_files.search_placeholder')} + /> +
+
+ + +
+
+ +
+ {files.length} {t('auth_files.files_count')} · {formatFileSize(totalSize)} +
-
-
- -
+ {/* 卡片网格 */} {loading ? ( -
{t('common.loading')}
+
{t('common.loading')}
) : pageItems.length === 0 ? ( ) : ( -
-
-
{t('auth_files.title_section')}
-
{t('auth_files.file_size')}
-
{t('auth_files.file_modified')}
-
Actions
-
- {pageItems.map((item) => ( -
-
-
{item.name}
-
- {item.type || t('auth_files.type_unknown')} {item.provider ? `· ${item.provider}` : ''} -
-
-
{item.size ? formatFileSize(item.size) : '-'}
-
- {formatModified(item)} -
-
-
- - -
-
-
- ))} +
+ {pageItems.map(renderFileCard)}
)} -
- -
- {t('auth_files.pagination_info', { - current: currentPage, - total: totalPages, - count: filtered.length - })} + {/* 分页 */} + {!loading && filtered.length > pageSize && ( +
+ +
+ {t('auth_files.pagination_info', { + current: currentPage, + total: totalPages, + count: filtered.length + })} +
+
- -
+ )} + {/* OAuth 排除列表卡片 */} ) : ( -
+
{Object.entries(excluded).map(([provider, models]) => ( -
-
-
{provider}
-
+
+
+
{provider}
+
{models?.length ? t('oauth_excluded.model_count', { count: models.length }) : t('oauth_excluded.no_models')}
-
+
@@ -389,6 +688,39 @@ export function AuthFilesPage() { )} + {/* 详情弹窗 */} + setDetailModalOpen(false)} + title={selectedFile?.name || t('auth_files.title_section')} + footer={ + <> + + + + } + > + {selectedFile && ( +
+
{JSON.stringify(selectedFile, null, 2)}
+
+ )} +
+ + {/* OAuth 排除弹窗 */} setExcludedModalOpen(false)} @@ -410,16 +742,16 @@ export function AuthFilesPage() { value={excludedForm.provider} onChange={(e) => setExcludedForm((prev) => ({ ...prev, provider: e.target.value }))} /> -
+