feat: introduce useKeepAliveRefresh composable to manage tab data synchronization and lifecycle refresh logic

This commit is contained in:
jxxghp
2026-05-17 07:43:42 +08:00
parent 587f06eb9f
commit bbe3368c69
8 changed files with 206 additions and 43 deletions

View File

@@ -31,6 +31,10 @@ const props = defineProps({
type: Array as PropType<FileItem[]>,
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"

View File

@@ -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<FileItem> = {
url,
method: inProps.endpoints?.list.method || 'get',
data: inProps.item,
const config: AxiosRequestConfig<FileItem> = {
url,
method: inProps.endpoints?.list.method || 'get',
data: inProps.item,
}
// 加载数据
const data = ((await inProps.axios.request<FileItem[], FileItem[]>(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<FileItem[], FileItem[]>(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()

View File

@@ -0,0 +1,84 @@
import { nextTick, onActivated, onMounted, toValue, watch, type MaybeRefOrGetter } from 'vue'
type RefreshHandler = () => void | Promise<void>
interface KeepAliveRefreshOptions {
/**
* 当前内容是否处于可见状态。
* keep-alive 会激活整棵缓存树tab 内组件需要用它避免后台标签页也刷新。
*/
active?: MaybeRefOrGetter<boolean>
/** 是否在 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,
}
}

View File

@@ -287,6 +287,7 @@ onMounted(() => {
:keyword="subscribeFilter"
:status-filter="subscribeStatusFilter ?? ''"
:sort-mode="subscribeSortMode"
:active="activeTab === 'mysub'"
@update:sort-mode="subscribeSortMode = $event"
/>
</div>
@@ -295,14 +296,14 @@ onMounted(() => {
<VWindowItem value="popular">
<transition name="fade-slide" appear>
<div>
<SubscribePopularView :type="subType" />
<SubscribePopularView :type="subType" :active="activeTab === 'popular'" />
</div>
</transition>
</VWindowItem>
<VWindowItem value="share">
<transition name="fade-slide" appear>
<div>
<SubscribeShareView :keyword="shareKeyword" />
<SubscribeShareView :keyword="shareKeyword" :active="activeTab === 'share'" />
</div>
</transition>
</VWindowItem>

View File

@@ -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<Plugin[]>([])
// 是否正在拖拽排序中
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"
>

View File

@@ -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({

View File

@@ -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<string, string> = {
'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 } = {

View File

@@ -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<string, string> = {
'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<SubscribeShare[]>([])
const currData = ref<SubscribeShare[]>([])
useKeepAliveRefresh(resetData, {
active: computed(() => props.active),
})
// 拼装参数
function getParams() {
let params: { [key: string]: any } = {