mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-07-02 21:11:51 +08:00
feat: introduce useKeepAliveRefresh composable to manage tab data synchronization and lifecycle refresh logic
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
84
src/composables/useKeepAliveRefresh.ts
Normal file
84
src/composables/useKeepAliveRefresh.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 } = {
|
||||
|
||||
@@ -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 } = {
|
||||
|
||||
Reference in New Issue
Block a user