From 0e005c3c7e590841ead3c1d68ae8294c1938472d Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sun, 17 May 2026 14:06:05 +0800 Subject: [PATCH] refactor: optimize Keep-Alive component rendering and data synchronization by introducing silent refresh states and fallback layout calculations. --- env.d.ts | 5 ++ src/components/filebrowser/FileList.vue | 26 ++++---- src/components/misc/ProgressiveCardGrid.vue | 39 ++++++++++-- src/components/slide/VirtualSlideView.vue | 12 +++- src/composables/useKeepAliveRefresh.ts | 22 ++++--- src/layouts/default.vue | 7 ++- src/pages/downloading.vue | 3 +- src/pages/resource.vue | 35 ++++++++--- src/router/index.ts | 3 + src/views/plugin/PluginCardListView.vue | 64 +++++++++++++------- src/views/reorganize/DownloadingListView.vue | 13 ++-- src/views/site/SiteCardListView.vue | 32 ++++++---- src/views/subscribe/SubscribeListView.vue | 14 +++-- 13 files changed, 197 insertions(+), 78 deletions(-) diff --git a/env.d.ts b/env.d.ts index 9341252a..27d15a17 100644 --- a/env.d.ts +++ b/env.d.ts @@ -4,8 +4,13 @@ declare module 'vue-router' { interface RouteMeta { action?: string subject?: string + keepAlive?: boolean + keepAliveKey?: string layoutWrapperClasses?: string navActiveLink?: RouteLocationRaw + requiresAuth?: boolean + subType?: string + hideFooter?: boolean } } diff --git a/src/components/filebrowser/FileList.vue b/src/components/filebrowser/FileList.vue index 1313fa31..61d4c878 100644 --- a/src/components/filebrowser/FileList.vue +++ b/src/components/filebrowser/FileList.vue @@ -14,7 +14,7 @@ import { useI18n } from 'vue-i18n' import { useBackground } from '@/composables/useBackground' import { usePWA } from '@/composables/usePWA' import { useAvailableHeight } from '@/composables/useAvailableHeight' -import { useKeepAliveRefresh } from '@/composables/useKeepAliveRefresh' +import { useKeepAliveRefresh, type KeepAliveRefreshContext } from '@/composables/useKeepAliveRefresh' // 国际化 const { t } = useI18n() @@ -234,11 +234,15 @@ function changeSelectMode() { } // 调API加载文件夹内的内容 -async function list_files() { - loading.value = true - const takeURISnapshot = () => [inProps.item.storage, inProps.item.path].join(':/'); - const prevURI = takeURISnapshot(); - emit('loading', true) +async function list_files(context: KeepAliveRefreshContext = {}) { + const silentRefresh = Boolean(context.silent && items.value.length > 0) + const takeURISnapshot = () => [inProps.item.storage, inProps.item.path].join(':/') + const prevURI = takeURISnapshot() + + if (!silentRefresh) { + loading.value = true + emit('loading', true) + } try { // 参数 @@ -264,8 +268,10 @@ async function list_files() { } catch (error) { console.error(error) } finally { - emit('loading', false) - loading.value = false + if (!silentRefresh) { + emit('loading', false) + loading.value = false + } } } @@ -673,10 +679,6 @@ function stopLoadingProgress() { progressSSE.stop() } -onMounted(() => { - list_files() -}) - useKeepAliveRefresh(list_files, { active: computed(() => inProps.active), }) diff --git a/src/components/misc/ProgressiveCardGrid.vue b/src/components/misc/ProgressiveCardGrid.vue index d2ec05ec..2e7d790e 100644 --- a/src/components/misc/ProgressiveCardGrid.vue +++ b/src/components/misc/ProgressiveCardGrid.vue @@ -260,6 +260,15 @@ function getComparableKey(item: any, index: number): ItemKey { return index } +function getFallbackLayoutWidth() { + if (typeof window === 'undefined') { + return safeMinItemWidth.value + } + + // keep-alive 激活首帧可能还拿不到网格宽度,先用视口宽度兜底,避免只渲染一小列。 + return Math.max(document.documentElement.clientWidth || window.innerWidth || 0, safeMinItemWidth.value) +} + function findFirstRowAtOrAfterOffset(offsets: number[], heights: number[], offset: number) { let low = 0 let high = heights.length - 1 @@ -547,19 +556,31 @@ function syncLayoutWidth() { const element = trackRef.value if (!element) { - layoutWidth.value = 0 + if (layoutWidth.value <= 0) { + layoutWidth.value = getFallbackLayoutWidth() + } return } - layoutWidth.value = element.clientWidth + const nextWidth = element.clientWidth + if (nextWidth > 0) { + layoutWidth.value = nextWidth + return + } + + if (layoutWidth.value <= 0) { + layoutWidth.value = getFallbackLayoutWidth() + } } function syncViewport() { const element = trackRef.value if (!element) { - viewportTop.value = 0 - viewportBottom.value = 0 + if (viewportBottom.value <= viewportTop.value) { + viewportTop.value = 0 + viewportBottom.value = typeof window === 'undefined' ? 0 : window.innerHeight + } return } @@ -572,8 +593,13 @@ function syncViewport() { top: 0, } - viewportTop.value = viewportRect.top - trackRect.top - viewportBottom.value = viewportRect.bottom - trackRect.top + const nextViewportTop = viewportRect.top - trackRect.top + const nextViewportBottom = viewportRect.bottom - trackRect.top + + if (nextViewportBottom > nextViewportTop) { + viewportTop.value = nextViewportTop + viewportBottom.value = nextViewportBottom + } } function queueLayoutSync() { @@ -800,6 +826,7 @@ onActivated(() => { mounted = true refreshScrollTarget() queueLayoutSync() + requestAnimationFrame(queueLayoutSync) }) onDeactivated(() => { diff --git a/src/components/slide/VirtualSlideView.vue b/src/components/slide/VirtualSlideView.vue index 612d90d6..0413ba49 100644 --- a/src/components/slide/VirtualSlideView.vue +++ b/src/components/slide/VirtualSlideView.vue @@ -60,6 +60,15 @@ const trailingSpaceWidth = computed(() => { return Math.max(totalContentWidth.value - leadingSpaceWidth.value - visibleItemsWidth.value, 0) }) +function getFallbackViewportWidth() { + if (typeof window === 'undefined') { + return itemStep.value * Math.max(props.overscanItems, 1) + } + + // keep-alive 激活的首帧偶尔测不到容器宽度,先按视口宽度渲染一屏,避免右侧短暂空白。 + return Math.max(window.innerWidth, itemStep.value * Math.max(props.overscanItems, 1)) +} + function resolveItemKey(item: any, index: number) { if (props.getItemKey) { return props.getItemKey(item, startIndex.value + index) @@ -87,7 +96,7 @@ function updateVisibleRange() { return } - const viewportWidth = element.clientWidth + const viewportWidth = element.clientWidth || getFallbackViewportWidth() if (!viewportWidth || !props.items.length) { startIndex.value = 0 endIndex.value = Math.min(props.items.length, props.overscanItems) @@ -185,6 +194,7 @@ onActivated(() => { } nextTick(syncLayoutState) + requestAnimationFrame(syncLayoutState) }) watch( diff --git a/src/composables/useKeepAliveRefresh.ts b/src/composables/useKeepAliveRefresh.ts index e741cf21..005c2f93 100644 --- a/src/composables/useKeepAliveRefresh.ts +++ b/src/composables/useKeepAliveRefresh.ts @@ -1,6 +1,12 @@ import { nextTick, onActivated, onMounted, toValue, watch, type MaybeRefOrGetter } from 'vue' -type RefreshHandler = () => void | Promise +export interface KeepAliveRefreshContext { + /** 重新进入页面时已有旧内容可用,刷新应尽量避免切换主 loading 或清空列表。 */ + silent?: boolean + source?: 'activated' | 'tab' | 'manual' +} + +type RefreshHandler = (context?: KeepAliveRefreshContext) => void | Promise interface KeepAliveRefreshOptions { /** @@ -26,7 +32,7 @@ export function useKeepAliveRefresh(refresh: RefreshHandler, options: KeepAliveR const isActive = () => options.active === undefined || Boolean(toValue(options.active)) - async function runRefresh() { + async function runRefresh(context: KeepAliveRefreshContext = { silent: true, source: 'manual' }) { if (!isActive()) return // 避免路由激活和 tab 激活在同一轮里叠加出并发请求。 @@ -37,25 +43,25 @@ export function useKeepAliveRefresh(refresh: RefreshHandler, options: KeepAliveR refreshing = true try { - await refresh() + await refresh(context) } finally { refreshing = false if (pendingRefresh) { pendingRefresh = false - await runRefresh() + await runRefresh(context) } } } - function requestRefresh() { + function requestRefresh(source: KeepAliveRefreshContext['source']) { // 同一轮激活里可能同时触发路由激活和 tab 激活,合并成一次静默刷新。 if (refreshScheduled) return refreshScheduled = true void nextTick(async () => { refreshScheduled = false - await runRefresh() + await runRefresh({ silent: true, source }) }) } @@ -70,7 +76,7 @@ export function useKeepAliveRefresh(refresh: RefreshHandler, options: KeepAliveR // KeepAlive 首次挂载也会触发 activated,初始加载交给页面自己的 mounted 逻辑。 if (activatedCount === 1) return - requestRefresh() + requestRefresh('activated') }) } @@ -80,7 +86,7 @@ export function useKeepAliveRefresh(refresh: RefreshHandler, options: KeepAliveR (active, oldActive) => { if (!mounted || !active || oldActive !== false) return - requestRefresh() + requestRefresh('tab') }, { flush: 'post' }, ) diff --git a/src/layouts/default.vue b/src/layouts/default.vue index 5019cc69..702ae04b 100644 --- a/src/layouts/default.vue +++ b/src/layouts/default.vue @@ -2,13 +2,16 @@ import DefaultLayout from './components/DefaultLayout.vue' const route = useRoute() + +// keep-alive 缓存按页面身份命中,避免 query 变化导致同一页面反复新建实例。 +const routeCacheKey = computed(() => route.meta.keepAliveKey?.toString() || route.path)