diff --git a/src/components/quota/QuotaSection.tsx b/src/components/quota/QuotaSection.tsx index 98c7d4e..9852b4f 100644 --- a/src/components/quota/QuotaSection.tsx +++ b/src/components/quota/QuotaSection.tsx @@ -13,17 +13,17 @@ import { QuotaCard } from './QuotaCard'; import type { QuotaStatusState } from './QuotaCard'; import { useQuotaLoader } from './useQuotaLoader'; import type { QuotaConfig } from './quotaConfigs'; +import { useGridColumns } from './useGridColumns'; +import { IconRefreshCw } from '@/components/ui/icons'; import styles from '@/pages/QuotaPage.module.scss'; type QuotaUpdater = T | ((prev: T) => T); type QuotaSetter = (updater: QuotaUpdater) => void; -const MIN_CARD_PAGE_SIZE = 3; -const MAX_CARD_PAGE_SIZE = 30; +type ViewMode = 'paged' | 'all'; -const clampCardPageSize = (value: number) => - Math.min(MAX_CARD_PAGE_SIZE, Math.max(MIN_CARD_PAGE_SIZE, Math.round(value))); +const MAX_SHOW_ALL_THRESHOLD = 30; interface QuotaPaginationState { pageSize: number; @@ -40,7 +40,7 @@ interface QuotaPaginationState { const useQuotaPagination = (items: T[], defaultPageSize = 6): QuotaPaginationState => { const [page, setPage] = useState(1); - const [pageSize, setPageSizeState] = useState(() => clampCardPageSize(defaultPageSize)); + const [pageSize, setPageSizeState] = useState(defaultPageSize); const [loading, setLoadingState] = useState(false); const [loadingScope, setLoadingScope] = useState<'page' | 'all' | null>(null); @@ -57,7 +57,7 @@ const useQuotaPagination = (items: T[], defaultPageSize = 6): QuotaPaginatio }, [items, currentPage, pageSize]); const setPageSize = useCallback((size: number) => { - setPageSizeState(clampCardPageSize(size)); + setPageSizeState(size); setPage(1); }, []); @@ -107,6 +107,11 @@ export function QuotaSection({ Record >; + /* Removed useRef */ + const [columns, gridRef] = useGridColumns(380); // Min card width 380px matches SCSS + const [viewMode, setViewMode] = useState('paged'); + const [showTooManyWarning, setShowTooManyWarning] = useState(false); + const filteredFiles = useMemo(() => files.filter((file) => config.filterFn(file)), [ files, config.filterFn @@ -125,15 +130,25 @@ export function QuotaSection({ setLoading } = useQuotaPagination(filteredFiles); + // Update page size based on view mode and columns + useEffect(() => { + if (viewMode === 'all') { + setPageSize(Math.max(1, filteredFiles.length)); + } else { + // Paged mode: 3 rows * columns + setPageSize(columns * 3); + } + }, [viewMode, columns, filteredFiles.length, setPageSize]); + const { quota, loadQuota } = useQuotaLoader(config); - const handleRefreshPage = useCallback(() => { - loadQuota(pageItems, 'page', setLoading); - }, [loadQuota, pageItems, setLoading]); - - const handleRefreshAll = useCallback(() => { - loadQuota(filteredFiles, 'all', setLoading); - }, [loadQuota, filteredFiles, setLoading]); + const handleRefresh = useCallback(() => { + if (viewMode === 'all') { + loadQuota(filteredFiles, 'all', setLoading); + } else { + loadQuota(pageItems, 'page', setLoading); + } + }, [loadQuota, filteredFiles, pageItems, viewMode, setLoading]); useEffect(() => { if (loading) return; @@ -153,28 +168,53 @@ export function QuotaSection({ }); }, [filteredFiles, loading, setQuota]); + const titleNode = ( +
+ {t(`${config.i18nPrefix}.title`)} + {filteredFiles.length > 0 && ( + + {filteredFiles.length} + + )} +
+ ); + return ( +
+ + +
- } @@ -186,31 +226,7 @@ export function QuotaSection({ /> ) : ( <> -
-
- - { - const value = e.currentTarget.valueAsNumber; - if (!Number.isFinite(value)) return; - setPageSize(value); - }} - /> -
-
- -
- {filteredFiles.length} {t('auth_files.files_count')} -
-
-
-
+
{pageItems.map((item) => ( ({ /> ))}
- {filteredFiles.length > pageSize && ( + {filteredFiles.length > pageSize && viewMode === 'paged' && (
+
+
+ )}
); } diff --git a/src/components/quota/useGridColumns.ts b/src/components/quota/useGridColumns.ts new file mode 100644 index 0000000..206ccd8 --- /dev/null +++ b/src/components/quota/useGridColumns.ts @@ -0,0 +1,40 @@ +import { useState, useEffect, useCallback } from 'react'; + +/** + * Hook to calculate the number of grid columns based on container width and item min-width. + * Returns [columns, refCallback]. + */ +export function useGridColumns( + itemMinWidth: number, + gap: number = 16 +): [number, (node: HTMLDivElement | null) => void] { + const [columns, setColumns] = useState(1); + const [element, setElement] = useState(null); + + const refCallback = useCallback((node: HTMLDivElement | null) => { + setElement(node); + }, []); + + useEffect(() => { + if (!element) return; + + const updateColumns = () => { + const containerWidth = element.clientWidth; + const effectiveItemWidth = itemMinWidth + gap; + const count = Math.floor((containerWidth + gap) / effectiveItemWidth); + setColumns(Math.max(1, count)); + }; + + updateColumns(); + + const observer = new ResizeObserver(() => { + updateColumns(); + }); + + observer.observe(element); + + return () => observer.disconnect(); + }, [element, itemMinWidth, gap]); + + return [columns, refCallback]; +} diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 34fbb2c..743a595 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -327,6 +327,9 @@ "search_placeholder": "输入名称、类型或提供方关键字", "page_size_label": "单页数量", "page_size_unit": "个/页", + "view_mode_paged": "按页显示", + "view_mode_all": "显示全部", + "too_many_files_warning": "您的凭证总数过多,全部加载会导致页面卡顿,请保持单页浏览。", "filter_all": "全部", "filter_qwen": "Qwen", "filter_gemini": "Gemini", @@ -835,4 +838,4 @@ "version": "管理中心版本", "author": "作者" } -} +} \ No newline at end of file diff --git a/src/pages/QuotaPage.module.scss b/src/pages/QuotaPage.module.scss index 46857b3..c8715ae 100644 --- a/src/pages/QuotaPage.module.scss +++ b/src/pages/QuotaPage.module.scss @@ -30,6 +30,28 @@ display: flex; gap: $spacing-sm; flex-wrap: wrap; + align-items: center; +} + +.titleWrapper { + display: flex; + align-items: center; + gap: $spacing-sm; +} + +.countBadge { + display: inline-flex; + align-items: center; + justify-content: center; + height: 24px; + min-width: 24px; + padding: 0 8px; + border-radius: 999px; + font-size: 13px; + font-weight: 600; + color: #0284c7; // sky-600 + background-color: #e0f2fe; // sky-100 + box-sizing: border-box; } .errorBox { @@ -76,11 +98,7 @@ .geminiCliGrid { display: grid; gap: $spacing-md; - grid-template-columns: repeat(3, minmax(0, 1fr)); - - @include tablet { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } + grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); @include mobile { grid-template-columns: 1fr; @@ -112,28 +130,30 @@ } } +.viewModeToggle { + display: flex; + gap: 8px; + background-color: var(--bg-secondary); + padding: 4px; + border-radius: $radius-md; +} + .antigravityCard { - background-image: linear-gradient( - 180deg, - rgba(224, 247, 250, 0.12), - rgba(224, 247, 250, 0) - ); + background-image: linear-gradient(180deg, + rgba(224, 247, 250, 0.12), + rgba(224, 247, 250, 0)); } .codexCard { - background-image: linear-gradient( - 180deg, - rgba(255, 243, 224, 0.18), - rgba(255, 243, 224, 0) - ); + background-image: linear-gradient(180deg, + rgba(255, 243, 224, 0.18), + rgba(255, 243, 224, 0)); } .geminiCliCard { - background-image: linear-gradient( - 180deg, - rgba(231, 239, 255, 0.2), - rgba(231, 239, 255, 0) - ); + background-image: linear-gradient(180deg, + rgba(231, 239, 255, 0.2), + rgba(231, 239, 255, 0)); } .quotaSection { @@ -331,3 +351,32 @@ background-color: var(--bg-secondary); border-radius: $radius-md; } + +.warningOverlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.warningModal { + background-color: var(--bg-primary); + border-radius: $radius-lg; + padding: $spacing-lg; + max-width: 400px; + text-align: center; + box-shadow: $shadow-lg; + + p { + margin: 0 0 $spacing-md 0; + color: var(--text-primary); + font-size: 14px; + line-height: 1.6; + } +} \ No newline at end of file