diff --git a/src/components/FileBrowser.vue b/src/components/FileBrowser.vue index 6b4d4036..32d96598 100644 --- a/src/components/FileBrowser.vue +++ b/src/components/FileBrowser.vue @@ -31,6 +31,10 @@ const props = defineProps({ type: Array as PropType, default: () => [], }, + active: { + type: Boolean, + default: true, + }, }) // 对外事件 @@ -308,6 +312,7 @@ function stopDrag() { :refreshpending="refreshPending" :sort="sort" :showTree="showDirTree" + :active="active" :style="{ flex: 1 }" @pathchanged="pathChanged" @loading="loadingChanged" diff --git a/src/components/filebrowser/FileList.vue b/src/components/filebrowser/FileList.vue index 7a5cb4bf..9d7df57e 100644 --- a/src/components/filebrowser/FileList.vue +++ b/src/components/filebrowser/FileList.vue @@ -14,6 +14,7 @@ import { useI18n } from 'vue-i18n' import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization' import { usePWA } from '@/composables/usePWA' import { useAvailableHeight } from '@/composables/useAvailableHeight' +import { useKeepAliveRefresh } from '@/composables/useKeepAliveRefresh' // 国际化 const { t } = useI18n() @@ -43,6 +44,10 @@ const inProps = defineProps({ }, sort: String, showTree: Boolean, + active: { + type: Boolean, + default: true, + }, }) // 对外事件 @@ -235,28 +240,33 @@ async function list_files() { const prevURI = takeURISnapshot(); emit('loading', true) - // 参数 - const url = inProps.endpoints?.list.url.replace(/{sort}/g, inProps.sort || 'name') + try { + // 参数 + const url = inProps.endpoints?.list.url.replace(/{sort}/g, inProps.sort || 'name') - const config: AxiosRequestConfig = { - url, - method: inProps.endpoints?.list.method || 'get', - data: inProps.item, + const config: AxiosRequestConfig = { + url, + method: inProps.endpoints?.list.method || 'get', + data: inProps.item, + } + + // 加载数据 + const data = ((await inProps.axios.request(config))) ?? [] + // 如果当前路径已经变化,则放弃此次加载结果 + if (prevURI !== takeURISnapshot()) { + return + } + items.value = data + syncSelectedItems(data) + + // 通知父组件文件列表更新 + emit('items-updated', items.value) + } catch (error) { + console.error(error) + } finally { + emit('loading', false) + loading.value = false } - - // 加载数据 - const data = ((await inProps.axios.request(config))) ?? [] - // 如果当前路径已经变化,则放弃此次加载结果 - if (prevURI !== takeURISnapshot()) { - return; - } - items.value = data - syncSelectedItems(data) - emit('loading', false) - loading.value = false - - // 通知父组件文件列表更新 - emit('items-updated', items.value) } // 删除项目 @@ -667,6 +677,10 @@ onMounted(() => { list_files() }) +useKeepAliveRefresh(list_files, { + active: computed(() => inProps.active), +}) + onUnmounted(() => { revokeCurrentImgLink() stopLoadingProgress() diff --git a/src/composables/useKeepAliveRefresh.ts b/src/composables/useKeepAliveRefresh.ts new file mode 100644 index 00000000..fbeb6030 --- /dev/null +++ b/src/composables/useKeepAliveRefresh.ts @@ -0,0 +1,84 @@ +import { nextTick, onActivated, onMounted, toValue, watch, type MaybeRefOrGetter } from 'vue' + +type RefreshHandler = () => void | Promise + +interface KeepAliveRefreshOptions { + /** + * 当前内容是否处于可见状态。 + * keep-alive 会激活整棵缓存树,tab 内组件需要用它避免后台标签页也刷新。 + */ + active?: MaybeRefOrGetter + /** 是否在 keep-alive 页面重新进入时刷新。 */ + refreshOnActivated?: boolean + /** 是否在 tab 从隐藏切回可见时刷新。 */ + refreshOnTabActivated?: boolean +} + +/** + * keep-alive 页面复用实例时不会重新 mounted,这里统一补上重新进入和重新选中 tab 的刷新。 + */ +export function useKeepAliveRefresh(refresh: RefreshHandler, options: KeepAliveRefreshOptions = {}) { + let mounted = false + let activatedCount = 0 + let refreshing = false + let pendingRefresh = false + + const isActive = () => options.active === undefined || Boolean(toValue(options.active)) + + async function runRefresh() { + if (!isActive()) return + + // 避免路由激活和 tab 激活在同一轮里叠加出并发请求。 + if (refreshing) { + pendingRefresh = true + return + } + + refreshing = true + try { + await refresh() + } finally { + refreshing = false + + if (pendingRefresh) { + pendingRefresh = false + await runRefresh() + } + } + } + + function requestRefresh() { + void nextTick(runRefresh) + } + + onMounted(() => { + mounted = true + }) + + if (options.refreshOnActivated !== false) { + onActivated(() => { + activatedCount += 1 + + // KeepAlive 首次挂载也会触发 activated,初始加载交给页面自己的 mounted 逻辑。 + if (activatedCount === 1) return + + requestRefresh() + }) + } + + if (options.active !== undefined && options.refreshOnTabActivated !== false) { + watch( + () => Boolean(toValue(options.active)), + (active, oldActive) => { + if (!mounted || !active || oldActive !== false) return + + requestRefresh() + }, + { flush: 'post' }, + ) + } + + return { + refresh: runRefresh, + } +} diff --git a/src/pages/subscribe.vue b/src/pages/subscribe.vue index 6dfa7942..434d5c9f 100644 --- a/src/pages/subscribe.vue +++ b/src/pages/subscribe.vue @@ -287,6 +287,7 @@ onMounted(() => { :keyword="subscribeFilter" :status-filter="subscribeStatusFilter ?? ''" :sort-mode="subscribeSortMode" + :active="activeTab === 'mysub'" @update:sort-mode="subscribeSortMode = $event" /> @@ -295,14 +296,14 @@ onMounted(() => {
- +
- +
diff --git a/src/views/plugin/PluginCardListView.vue b/src/views/plugin/PluginCardListView.vue index 4a925f24..cd8b6c73 100644 --- a/src/views/plugin/PluginCardListView.vue +++ b/src/views/plugin/PluginCardListView.vue @@ -13,6 +13,7 @@ import PluginMixedSortCard from '@/components/cards/PluginMixedSortCard.vue' import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue' import { usePWA } from '@/composables/usePWA' import { useDynamicHeaderTab } from '@/composables/useDynamicHeaderTab' +import { useKeepAliveRefresh } from '@/composables/useKeepAliveRefresh' // 国际化 const { t } = useI18n() @@ -361,6 +362,9 @@ const draggableFolderPlugins = ref([]) // 是否正在拖拽排序中 const isDraggingSortMode = ref(false) +// 插件市场分页 key,重置后让 VInfiniteScroll 重新触发首屏加载。 +const marketInfiniteKey = ref(0) + // 显示的文件夹列表(按排序显示) const displayedFolders = computed(() => { if (currentFolder.value) return [] // 在文件夹内不显示其他文件夹 @@ -749,10 +753,11 @@ async function fetchInstalledPlugins() { }) // 排序 sortPluginOrder() - loading.value = false isRefreshed.value = true } catch (error) { console.error(error) + } finally { + loading.value = false } } @@ -776,7 +781,6 @@ async function fetchUninstalledPlugins(force: boolean = false) { } } } - loading.value = false isRefreshed.value = true // 更新插件市场列表 // 排除已安装且有更新的,上面的问题在于"本地存在未安装的旧版本插件且云端有更新时"不会在插件市场展示 @@ -788,6 +792,8 @@ async function fetchUninstalledPlugins(force: boolean = false) { isAppMarketLoaded.value = true } catch (error) { console.error(error) + } finally { + loading.value = false } } @@ -804,6 +810,7 @@ async function getPluginStatistics() { async function refreshData() { await fetchInstalledPlugins() await fetchUninstalledPlugins() + marketInfiniteKey.value++ getPluginStatistics() // 重新加载文件夹配置,确保分身插件能正确显示在文件夹中 await loadPluginFolders() @@ -877,6 +884,7 @@ async function refreshMarket() { try { await fetchUninstalledPlugins(true) getPluginStatistics() + marketInfiniteKey.value++ } catch (error) { console.error(error) } finally { @@ -884,6 +892,25 @@ async function refreshMarket() { } } +async function refreshActiveTabData() { + if (sortMode.value || isDraggingSortMode.value) return + + if (activeTab.value === 'market') { + isAppMarketLoaded.value = false + await fetchInstalledPlugins() + await fetchUninstalledPlugins() + getPluginStatistics() + marketInfiniteKey.value++ + return + } + + await fetchInstalledPlugins() + await fetchUninstalledPlugins() + getPluginStatistics() + // 文件夹配置可能在其它入口被插件操作改变,重新进入时同步一次。 + await loadPluginFolders() +} + function parseLocalRepoPath(repoUrl: string | undefined) { if (!repoUrl?.startsWith('local://')) return '' @@ -947,6 +974,14 @@ onMounted(async () => { } }) +const { refresh: refreshKeepAliveData } = useKeepAliveRefresh(refreshActiveTabData) + +watch(activeTab, (newTab, oldTab) => { + if (!oldTab || newTab === oldTab) return + + refreshKeepAliveData() +}) + function openPluginSearchDialog() { SearchDialog.value = true } @@ -1681,6 +1716,7 @@ function onDragStartPlugin(evt: any) { mode="intersect" side="end" :items="displayUninstalledList" + :key="marketInfiniteKey" @load="loadMarketMore" class="overflow-visible" > diff --git a/src/views/subscribe/SubscribeListView.vue b/src/views/subscribe/SubscribeListView.vue index 8d06e6aa..b07153e6 100644 --- a/src/views/subscribe/SubscribeListView.vue +++ b/src/views/subscribe/SubscribeListView.vue @@ -10,6 +10,7 @@ import { useUserStore } from '@/stores' import { useI18n } from 'vue-i18n' import { useToast } from 'vue-toastification' import { useConfirm } from '@/composables/useConfirm' +import { useKeepAliveRefresh } from '@/composables/useKeepAliveRefresh' // 国际化 const { t } = useI18n() @@ -37,6 +38,10 @@ const props = defineProps({ type: Boolean, default: false, }, + active: { + type: Boolean, + default: true, + }, }) const emit = defineEmits<{ @@ -413,10 +418,8 @@ onUnmounted(() => { window.removeEventListener('toggle-batch-mode', toggleBatchMode) }) -onActivated(async () => { - if (!loading.value) { - fetchData() - } +useKeepAliveRefresh(fetchData, { + active: computed(() => props.active), }) defineExpose({ diff --git a/src/views/subscribe/SubscribePopularView.vue b/src/views/subscribe/SubscribePopularView.vue index 19ca6f56..2636f400 100644 --- a/src/views/subscribe/SubscribePopularView.vue +++ b/src/views/subscribe/SubscribePopularView.vue @@ -4,6 +4,7 @@ import type { MediaInfo } from '@/api/types' import MediaCard from '@/components/cards/MediaCard.vue' import NoDataFound from '@/components/NoDataFound.vue' import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue' +import { useKeepAliveRefresh } from '@/composables/useKeepAliveRefresh' import { useI18n } from 'vue-i18n' // 国际化 @@ -12,6 +13,10 @@ const { t } = useI18n() // 输入参数 const props = defineProps({ type: String, + active: { + type: Boolean, + default: true, + }, }) // 判断是否有滚动条 @@ -47,6 +52,13 @@ const filterParams = reactive({ // 当前Key(用于重新加载数据) const currentKey = ref(0) +function resetData() { + dataList.value = [] + page.value = 1 + isRefreshed.value = false + currentKey.value++ +} + // TMDB电影风格字典 const tmdbMovieGenreDict: Record = { '28': t('tmdb.genreType.action'), @@ -99,15 +111,15 @@ const currentGenreDict = computed(() => { watch( filterParams, () => { - // 重置数据 - dataList.value = [] - page.value = 1 - isRefreshed.value = false - currentKey.value++ + resetData() }, { deep: true }, ) +useKeepAliveRefresh(resetData, { + active: computed(() => props.active), +}) + // 拼装参数 function getParams() { let params: { [key: string]: any } = { diff --git a/src/views/subscribe/SubscribeShareView.vue b/src/views/subscribe/SubscribeShareView.vue index ec931454..198ef2d4 100644 --- a/src/views/subscribe/SubscribeShareView.vue +++ b/src/views/subscribe/SubscribeShareView.vue @@ -4,6 +4,7 @@ import type { SubscribeShare } from '@/api/types' import NoDataFound from '@/components/NoDataFound.vue' import SubscribeShareCard from '@/components/cards/SubscribeShareCard.vue' import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue' +import { useKeepAliveRefresh } from '@/composables/useKeepAliveRefresh' import { useI18n } from 'vue-i18n' // 国际化 @@ -13,6 +14,10 @@ const { t } = useI18n() const props = defineProps({ // 过滤关键字 keyword: String, + active: { + type: Boolean, + default: true, + }, }) // 判断是否有滚动条 @@ -40,6 +45,13 @@ const filterParams = reactive({ // 当前Key(用于重新加载数据) const currentKey = ref(0) +function resetData() { + dataList.value = [] + page.value = 1 + isRefreshed.value = false + currentKey.value++ +} + // TMDB电影风格字典 const tmdbMovieGenreDict: Record = { '28': t('tmdb.genreType.action'), @@ -94,11 +106,7 @@ watch( () => props.keyword, newKeyword => { keyword.value = newKeyword || '' - // 重置页码和数据 - page.value = 1 - dataList.value = [] - isRefreshed.value = false - currentKey.value++ + resetData() }, ) @@ -106,11 +114,7 @@ watch( watch( filterParams, () => { - // 重置数据 - dataList.value = [] - page.value = 1 - isRefreshed.value = false - currentKey.value++ + resetData() }, { deep: true }, ) @@ -125,6 +129,10 @@ const isRefreshed = ref(false) const dataList = ref([]) const currData = ref([]) +useKeepAliveRefresh(resetData, { + active: computed(() => props.active), +}) + // 拼装参数 function getParams() { let params: { [key: string]: any } = {