diff --git a/frontend/package.json b/frontend/package.json index 1b380b176b..d2a6deded7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "@lobehub/icons": "^4.0.2", + "@tanstack/vue-virtual": "^3.13.23", "@vueuse/core": "^10.7.0", "axios": "^1.13.5", "chart.js": "^4.4.1", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 37c384b431..505b72f388 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@lobehub/icons': specifier: ^4.0.2 version: 4.0.2(@lobehub/ui@4.9.2)(@types/react@19.2.7)(antd@6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/vue-virtual': + specifier: ^3.13.23 + version: 3.13.23(vue@3.5.26(typescript@5.6.3)) '@vueuse/core': specifier: ^10.7.0 version: 10.11.1(vue@3.5.26(typescript@5.6.3)) @@ -1376,6 +1379,14 @@ packages: peerDependencies: react: '>= 16.3.0' + '@tanstack/virtual-core@3.13.23': + resolution: {integrity: sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==} + + '@tanstack/vue-virtual@3.13.23': + resolution: {integrity: sha512-b5jPluAR6U3eOq6GWAYSpj3ugnAIZgGR0e6aGAgyRse0Yu6MVQQ0ZWm9SArSXWtageogn6bkVD8D//c4IjW3xQ==} + peerDependencies: + vue: ^2.7.0 || ^3.0.0 + '@types/d3-array@3.2.2': resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} @@ -5808,6 +5819,13 @@ snapshots: dependencies: react: 19.2.3 + '@tanstack/virtual-core@3.13.23': {} + + '@tanstack/vue-virtual@3.13.23(vue@3.5.26(typescript@5.6.3))': + dependencies: + '@tanstack/virtual-core': 3.13.23 + vue: 3.5.26(typescript@5.6.3) + '@types/d3-array@3.2.2': {} '@types/d3-axis@3.0.6': diff --git a/frontend/src/components/account/AccountUsageCell.vue b/frontend/src/components/account/AccountUsageCell.vue index 57de3ed687..f8ae5af250 100644 --- a/frontend/src/components/account/AccountUsageCell.vue +++ b/frontend/src/components/account/AccountUsageCell.vue @@ -451,6 +451,10 @@ import { formatCompactNumber } from '@/utils/format' import UsageProgressBar from './UsageProgressBar.vue' import AccountQuotaInfo from './AccountQuotaInfo.vue' +// Module-level cache shared across all AccountUsageCell instances +const _usageCache = new Map() +const USAGE_CACHE_TTL = 5 * 60 * 1000 // 5 minutes + const props = withDefaults( defineProps<{ account: Account @@ -946,16 +950,29 @@ const copyValidationURL = async () => { } } -const loadUsage = async () => { +const loadUsage = async (bypassCache = false) => { if (!shouldFetchUsage.value) return + // Check cache + if (!bypassCache) { + const cached = _usageCache.get(props.account.id) + if (cached && Date.now() - cached.ts < USAGE_CACHE_TTL) { + usageInfo.value = cached.data + loading.value = false + return + } + } + loading.value = true error.value = null try { const fetchFn = () => adminAPI.accounts.getUsage(props.account.id) const result = await enqueueUsageRequest(props.account, fetchFn) - if (!unmounted.value) usageInfo.value = result + if (!unmounted.value) { + usageInfo.value = result + _usageCache.set(props.account.id, { data: result, ts: Date.now() }) + } } catch (e: any) { if (!unmounted.value) { error.value = t('common.error') @@ -1077,7 +1094,8 @@ watch( if (nextToken === prevToken) return if (!shouldFetchUsage.value) return - loadUsage().catch((e) => { + _usageCache.delete(props.account.id) + loadUsage(true).catch((e) => { console.error('Failed to refresh usage after manual refresh:', e) }) } diff --git a/frontend/src/components/common/DataTable.vue b/frontend/src/components/common/DataTable.vue index 16aea10739..57f800a034 100644 --- a/frontend/src/components/common/DataTable.vue +++ b/frontend/src/components/common/DataTable.vue @@ -33,7 +33,7 @@