mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-07-12 16:01:35 +08:00
refactor: optimize Keep-Alive component rendering and data synchronization by introducing silent refresh states and fallback layout calculations.
This commit is contained in:
5
env.d.ts
vendored
5
env.d.ts
vendored
@@ -4,8 +4,13 @@ declare module 'vue-router' {
|
|||||||
interface RouteMeta {
|
interface RouteMeta {
|
||||||
action?: string
|
action?: string
|
||||||
subject?: string
|
subject?: string
|
||||||
|
keepAlive?: boolean
|
||||||
|
keepAliveKey?: string
|
||||||
layoutWrapperClasses?: string
|
layoutWrapperClasses?: string
|
||||||
navActiveLink?: RouteLocationRaw
|
navActiveLink?: RouteLocationRaw
|
||||||
|
requiresAuth?: boolean
|
||||||
|
subType?: string
|
||||||
|
hideFooter?: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import { useBackground } from '@/composables/useBackground'
|
import { useBackground } from '@/composables/useBackground'
|
||||||
import { usePWA } from '@/composables/usePWA'
|
import { usePWA } from '@/composables/usePWA'
|
||||||
import { useAvailableHeight } from '@/composables/useAvailableHeight'
|
import { useAvailableHeight } from '@/composables/useAvailableHeight'
|
||||||
import { useKeepAliveRefresh } from '@/composables/useKeepAliveRefresh'
|
import { useKeepAliveRefresh, type KeepAliveRefreshContext } from '@/composables/useKeepAliveRefresh'
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -234,11 +234,15 @@ function changeSelectMode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 调API加载文件夹内的内容
|
// 调API加载文件夹内的内容
|
||||||
async function list_files() {
|
async function list_files(context: KeepAliveRefreshContext = {}) {
|
||||||
loading.value = true
|
const silentRefresh = Boolean(context.silent && items.value.length > 0)
|
||||||
const takeURISnapshot = () => [inProps.item.storage, inProps.item.path].join(':/');
|
const takeURISnapshot = () => [inProps.item.storage, inProps.item.path].join(':/')
|
||||||
const prevURI = takeURISnapshot();
|
const prevURI = takeURISnapshot()
|
||||||
emit('loading', true)
|
|
||||||
|
if (!silentRefresh) {
|
||||||
|
loading.value = true
|
||||||
|
emit('loading', true)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 参数
|
// 参数
|
||||||
@@ -264,8 +268,10 @@ async function list_files() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
} finally {
|
} finally {
|
||||||
emit('loading', false)
|
if (!silentRefresh) {
|
||||||
loading.value = false
|
emit('loading', false)
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -673,10 +679,6 @@ function stopLoadingProgress() {
|
|||||||
progressSSE.stop()
|
progressSSE.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
list_files()
|
|
||||||
})
|
|
||||||
|
|
||||||
useKeepAliveRefresh(list_files, {
|
useKeepAliveRefresh(list_files, {
|
||||||
active: computed(() => inProps.active),
|
active: computed(() => inProps.active),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -260,6 +260,15 @@ function getComparableKey(item: any, index: number): ItemKey {
|
|||||||
return index
|
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) {
|
function findFirstRowAtOrAfterOffset(offsets: number[], heights: number[], offset: number) {
|
||||||
let low = 0
|
let low = 0
|
||||||
let high = heights.length - 1
|
let high = heights.length - 1
|
||||||
@@ -547,19 +556,31 @@ function syncLayoutWidth() {
|
|||||||
const element = trackRef.value
|
const element = trackRef.value
|
||||||
|
|
||||||
if (!element) {
|
if (!element) {
|
||||||
layoutWidth.value = 0
|
if (layoutWidth.value <= 0) {
|
||||||
|
layoutWidth.value = getFallbackLayoutWidth()
|
||||||
|
}
|
||||||
return
|
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() {
|
function syncViewport() {
|
||||||
const element = trackRef.value
|
const element = trackRef.value
|
||||||
|
|
||||||
if (!element) {
|
if (!element) {
|
||||||
viewportTop.value = 0
|
if (viewportBottom.value <= viewportTop.value) {
|
||||||
viewportBottom.value = 0
|
viewportTop.value = 0
|
||||||
|
viewportBottom.value = typeof window === 'undefined' ? 0 : window.innerHeight
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,8 +593,13 @@ function syncViewport() {
|
|||||||
top: 0,
|
top: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
viewportTop.value = viewportRect.top - trackRect.top
|
const nextViewportTop = viewportRect.top - trackRect.top
|
||||||
viewportBottom.value = viewportRect.bottom - trackRect.top
|
const nextViewportBottom = viewportRect.bottom - trackRect.top
|
||||||
|
|
||||||
|
if (nextViewportBottom > nextViewportTop) {
|
||||||
|
viewportTop.value = nextViewportTop
|
||||||
|
viewportBottom.value = nextViewportBottom
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function queueLayoutSync() {
|
function queueLayoutSync() {
|
||||||
@@ -800,6 +826,7 @@ onActivated(() => {
|
|||||||
mounted = true
|
mounted = true
|
||||||
refreshScrollTarget()
|
refreshScrollTarget()
|
||||||
queueLayoutSync()
|
queueLayoutSync()
|
||||||
|
requestAnimationFrame(queueLayoutSync)
|
||||||
})
|
})
|
||||||
|
|
||||||
onDeactivated(() => {
|
onDeactivated(() => {
|
||||||
|
|||||||
@@ -60,6 +60,15 @@ const trailingSpaceWidth = computed(() => {
|
|||||||
return Math.max(totalContentWidth.value - leadingSpaceWidth.value - visibleItemsWidth.value, 0)
|
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) {
|
function resolveItemKey(item: any, index: number) {
|
||||||
if (props.getItemKey) {
|
if (props.getItemKey) {
|
||||||
return props.getItemKey(item, startIndex.value + index)
|
return props.getItemKey(item, startIndex.value + index)
|
||||||
@@ -87,7 +96,7 @@ function updateVisibleRange() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewportWidth = element.clientWidth
|
const viewportWidth = element.clientWidth || getFallbackViewportWidth()
|
||||||
if (!viewportWidth || !props.items.length) {
|
if (!viewportWidth || !props.items.length) {
|
||||||
startIndex.value = 0
|
startIndex.value = 0
|
||||||
endIndex.value = Math.min(props.items.length, props.overscanItems)
|
endIndex.value = Math.min(props.items.length, props.overscanItems)
|
||||||
@@ -185,6 +194,7 @@ onActivated(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nextTick(syncLayoutState)
|
nextTick(syncLayoutState)
|
||||||
|
requestAnimationFrame(syncLayoutState)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { nextTick, onActivated, onMounted, toValue, watch, type MaybeRefOrGetter } from 'vue'
|
import { nextTick, onActivated, onMounted, toValue, watch, type MaybeRefOrGetter } from 'vue'
|
||||||
|
|
||||||
type RefreshHandler = () => void | Promise<void>
|
export interface KeepAliveRefreshContext {
|
||||||
|
/** 重新进入页面时已有旧内容可用,刷新应尽量避免切换主 loading 或清空列表。 */
|
||||||
|
silent?: boolean
|
||||||
|
source?: 'activated' | 'tab' | 'manual'
|
||||||
|
}
|
||||||
|
|
||||||
|
type RefreshHandler = (context?: KeepAliveRefreshContext) => void | Promise<void>
|
||||||
|
|
||||||
interface KeepAliveRefreshOptions {
|
interface KeepAliveRefreshOptions {
|
||||||
/**
|
/**
|
||||||
@@ -26,7 +32,7 @@ export function useKeepAliveRefresh(refresh: RefreshHandler, options: KeepAliveR
|
|||||||
|
|
||||||
const isActive = () => options.active === undefined || Boolean(toValue(options.active))
|
const isActive = () => options.active === undefined || Boolean(toValue(options.active))
|
||||||
|
|
||||||
async function runRefresh() {
|
async function runRefresh(context: KeepAliveRefreshContext = { silent: true, source: 'manual' }) {
|
||||||
if (!isActive()) return
|
if (!isActive()) return
|
||||||
|
|
||||||
// 避免路由激活和 tab 激活在同一轮里叠加出并发请求。
|
// 避免路由激活和 tab 激活在同一轮里叠加出并发请求。
|
||||||
@@ -37,25 +43,25 @@ export function useKeepAliveRefresh(refresh: RefreshHandler, options: KeepAliveR
|
|||||||
|
|
||||||
refreshing = true
|
refreshing = true
|
||||||
try {
|
try {
|
||||||
await refresh()
|
await refresh(context)
|
||||||
} finally {
|
} finally {
|
||||||
refreshing = false
|
refreshing = false
|
||||||
|
|
||||||
if (pendingRefresh) {
|
if (pendingRefresh) {
|
||||||
pendingRefresh = false
|
pendingRefresh = false
|
||||||
await runRefresh()
|
await runRefresh(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestRefresh() {
|
function requestRefresh(source: KeepAliveRefreshContext['source']) {
|
||||||
// 同一轮激活里可能同时触发路由激活和 tab 激活,合并成一次静默刷新。
|
// 同一轮激活里可能同时触发路由激活和 tab 激活,合并成一次静默刷新。
|
||||||
if (refreshScheduled) return
|
if (refreshScheduled) return
|
||||||
|
|
||||||
refreshScheduled = true
|
refreshScheduled = true
|
||||||
void nextTick(async () => {
|
void nextTick(async () => {
|
||||||
refreshScheduled = false
|
refreshScheduled = false
|
||||||
await runRefresh()
|
await runRefresh({ silent: true, source })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +76,7 @@ export function useKeepAliveRefresh(refresh: RefreshHandler, options: KeepAliveR
|
|||||||
// KeepAlive 首次挂载也会触发 activated,初始加载交给页面自己的 mounted 逻辑。
|
// KeepAlive 首次挂载也会触发 activated,初始加载交给页面自己的 mounted 逻辑。
|
||||||
if (activatedCount === 1) return
|
if (activatedCount === 1) return
|
||||||
|
|
||||||
requestRefresh()
|
requestRefresh('activated')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +86,7 @@ export function useKeepAliveRefresh(refresh: RefreshHandler, options: KeepAliveR
|
|||||||
(active, oldActive) => {
|
(active, oldActive) => {
|
||||||
if (!mounted || !active || oldActive !== false) return
|
if (!mounted || !active || oldActive !== false) return
|
||||||
|
|
||||||
requestRefresh()
|
requestRefresh('tab')
|
||||||
},
|
},
|
||||||
{ flush: 'post' },
|
{ flush: 'post' },
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,13 +2,16 @@
|
|||||||
import DefaultLayout from './components/DefaultLayout.vue'
|
import DefaultLayout from './components/DefaultLayout.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
|
// keep-alive 缓存按页面身份命中,避免 query 变化导致同一页面反复新建实例。
|
||||||
|
const routeCacheKey = computed(() => route.meta.keepAliveKey?.toString() || route.path)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DefaultLayout>
|
<DefaultLayout>
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
<keep-alive :max="12">
|
<keep-alive :max="24">
|
||||||
<component :is="Component" v-if="route.meta.keepAlive" :key="route.fullPath" />
|
<component :is="Component" v-if="route.meta.keepAlive" :key="routeCacheKey" />
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
<component :is="Component" v-if="!route.meta.keepAlive" :key="route.fullPath" />
|
<component :is="Component" v-if="!route.meta.keepAlive" :key="route.fullPath" />
|
||||||
</router-view>
|
</router-view>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import DownloadingListView from '@/views/reorganize/DownloadingListView.vue'
|
|||||||
import NoDataFound from '@/components/NoDataFound.vue'
|
import NoDataFound from '@/components/NoDataFound.vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useDynamicHeaderTab } from '@/composables/useDynamicHeaderTab'
|
import { useDynamicHeaderTab } from '@/composables/useDynamicHeaderTab'
|
||||||
|
import { useKeepAliveRefresh } from '@/composables/useKeepAliveRefresh'
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -52,7 +53,7 @@ onMounted(async () => {
|
|||||||
registerTabs()
|
registerTabs()
|
||||||
})
|
})
|
||||||
|
|
||||||
onActivated(async () => {
|
useKeepAliveRefresh(async () => {
|
||||||
await loadDownloaderSetting()
|
await loadDownloaderSetting()
|
||||||
registerTabs()
|
registerTabs()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { useTorrentFilter, type FilterState } from '@/composables/useTorrentFilt
|
|||||||
import { useDynamicButton } from '@/composables/useDynamicButton'
|
import { useDynamicButton } from '@/composables/useDynamicButton'
|
||||||
import { usePWA } from '@/composables/usePWA'
|
import { usePWA } from '@/composables/usePWA'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
|
import { useKeepAliveRefresh } from '@/composables/useKeepAliveRefresh'
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -611,6 +612,13 @@ function handleSearchStreamMessage(eventData: { [key: string]: any }) {
|
|||||||
|
|
||||||
// 按请求搜索
|
// 按请求搜索
|
||||||
async function searchByRequest(params: SearchParams, requestToken?: string) {
|
async function searchByRequest(params: SearchParams, requestToken?: string) {
|
||||||
|
const items = await requestSearchResults(params, requestToken)
|
||||||
|
streamTotalCount.value = items.length
|
||||||
|
setStreamResults(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 静默刷新使用普通请求,保留当前结果直到新数据完整返回,避免返回页面时露出搜索进度态。
|
||||||
|
async function requestSearchResults(params: SearchParams, requestToken?: string) {
|
||||||
let result: { [key: string]: any }
|
let result: { [key: string]: any }
|
||||||
// 如果keyword的格式是 xxxx:xxxxx 且:前面的xxxx为字符,则按照媒体ID格式搜索
|
// 如果keyword的格式是 xxxx:xxxxx 且:前面的xxxx为字符,则按照媒体ID格式搜索
|
||||||
if (/^[a-zA-Z]+:/.test(params.keyword)) {
|
if (/^[a-zA-Z]+:/.test(params.keyword)) {
|
||||||
@@ -637,13 +645,11 @@ async function searchByRequest(params: SearchParams, requestToken?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result && result.success) {
|
if (result && result.success) {
|
||||||
streamTotalCount.value = result.data?.length || 0
|
return (result.data || []) as Context[]
|
||||||
setStreamResults(result.data || [])
|
|
||||||
} else {
|
|
||||||
errorDescription.value = result?.message || t('resource.noResourceFound')
|
|
||||||
streamTotalCount.value = 0
|
|
||||||
setStreamResults([])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errorDescription.value = result?.message || t('resource.noResourceFound')
|
||||||
|
throw new Error(errorDescription.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按流搜索
|
// 按流搜索
|
||||||
@@ -714,13 +720,14 @@ function changeViewType(newType: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取搜索列表数据
|
// 获取搜索列表数据
|
||||||
async function fetchData(options: { force?: boolean; params?: SearchParams } = {}) {
|
async function fetchData(options: { force?: boolean; params?: SearchParams; silent?: boolean } = {}) {
|
||||||
const currentSearchParams = { ...(options.params ?? activeSearchParams.value) }
|
const currentSearchParams = { ...(options.params ?? activeSearchParams.value) }
|
||||||
if (hasSearchKeyword(currentSearchParams)) {
|
if (hasSearchKeyword(currentSearchParams)) {
|
||||||
activeSearchParams.value = { ...currentSearchParams }
|
activeSearchParams.value = { ...currentSearchParams }
|
||||||
rememberSearchParams(currentSearchParams)
|
rememberSearchParams(currentSearchParams)
|
||||||
}
|
}
|
||||||
const requestToken = options.force || Boolean(currentSearchParams.keyword) ? createSearchRequestToken() : undefined
|
const requestToken = options.force || Boolean(currentSearchParams.keyword) ? createSearchRequestToken() : undefined
|
||||||
|
const silentRefresh = Boolean(options.silent && isRefreshed.value && rawDataList.value.length > 0)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
enableFilterAnimation.value = true
|
enableFilterAnimation.value = true
|
||||||
@@ -728,6 +735,11 @@ async function fetchData(options: { force?: boolean; params?: SearchParams } = {
|
|||||||
// 查询上次搜索结果,并同步可重放的搜索参数
|
// 查询上次搜索结果,并同步可重放的搜索参数
|
||||||
const results = await fetchLastSearchContext()
|
const results = await fetchLastSearchContext()
|
||||||
setStreamResults(results || [])
|
setStreamResults(results || [])
|
||||||
|
} else if (silentRefresh) {
|
||||||
|
// keep-alive 重新进入时后台刷新,旧结果继续显示,等新结果完整返回后一次性替换。
|
||||||
|
const results = await requestSearchResults(currentSearchParams, requestToken)
|
||||||
|
streamTotalCount.value = results.length
|
||||||
|
setStreamResults(results)
|
||||||
} else {
|
} else {
|
||||||
resetSearchResults()
|
resetSearchResults()
|
||||||
startLoadingProgress()
|
startLoadingProgress()
|
||||||
@@ -1067,6 +1079,15 @@ onMounted(async () => {
|
|||||||
void fetchData()
|
void fetchData()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useKeepAliveRefresh(async () => {
|
||||||
|
if (progressActive.value || isRefreshing.value || isRecommending.value || showingAiResults.value) return
|
||||||
|
|
||||||
|
const refreshParams = await resolveRefreshSearchParams()
|
||||||
|
if (!refreshParams) return
|
||||||
|
|
||||||
|
await fetchData({ force: true, params: refreshParams, silent: true })
|
||||||
|
})
|
||||||
|
|
||||||
// 卸载时停止轮询
|
// 卸载时停止轮询
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
closeSearchEventSource()
|
closeSearchEventSource()
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ const router = createRouter({
|
|||||||
path: '/resource',
|
path: '/resource',
|
||||||
component: () => import('../pages/resource.vue'),
|
component: () => import('../pages/resource.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
|
keepAlive: true,
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -56,6 +57,7 @@ const router = createRouter({
|
|||||||
component: () => import('../pages/subscribe.vue'),
|
component: () => import('../pages/subscribe.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
|
keepAliveKey: 'subscribe-movie',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
subType: '电影',
|
subType: '电影',
|
||||||
},
|
},
|
||||||
@@ -65,6 +67,7 @@ const router = createRouter({
|
|||||||
component: () => import('../pages/subscribe.vue'),
|
component: () => import('../pages/subscribe.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
|
keepAliveKey: 'subscribe-tv',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
subType: '电视剧',
|
subType: '电视剧',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import PluginMixedSortCard from '@/components/cards/PluginMixedSortCard.vue'
|
|||||||
import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue'
|
import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue'
|
||||||
import { usePWA } from '@/composables/usePWA'
|
import { usePWA } from '@/composables/usePWA'
|
||||||
import { useDynamicHeaderTab } from '@/composables/useDynamicHeaderTab'
|
import { useDynamicHeaderTab } from '@/composables/useDynamicHeaderTab'
|
||||||
import { useKeepAliveRefresh } from '@/composables/useKeepAliveRefresh'
|
import { useKeepAliveRefresh, type KeepAliveRefreshContext } from '@/composables/useKeepAliveRefresh'
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -740,9 +740,13 @@ const filterPlugins = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 获取插件列表数据
|
// 获取插件列表数据
|
||||||
async function fetchInstalledPlugins() {
|
async function fetchInstalledPlugins(context: KeepAliveRefreshContext = {}) {
|
||||||
|
const showLoading = !context.silent || !isRefreshed.value
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
if (showLoading) {
|
||||||
|
loading.value = true
|
||||||
|
}
|
||||||
dataList.value = await api.get('plugin/', {
|
dataList.value = await api.get('plugin/', {
|
||||||
params: {
|
params: {
|
||||||
state: 'installed',
|
state: 'installed',
|
||||||
@@ -754,14 +758,20 @@ async function fetchInstalledPlugins() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
if (showLoading) {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取未安装插件列表数据
|
// 获取未安装插件列表数据
|
||||||
async function fetchUninstalledPlugins(force: boolean = false) {
|
async function fetchUninstalledPlugins(force: boolean = false, context: KeepAliveRefreshContext = {}) {
|
||||||
|
const showLoading = !context.silent || !isAppMarketLoaded.value
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
if (showLoading) {
|
||||||
|
loading.value = true
|
||||||
|
}
|
||||||
uninstalledList.value = await api.get('plugin/', {
|
uninstalledList.value = await api.get('plugin/', {
|
||||||
params: {
|
params: {
|
||||||
state: 'market',
|
state: 'market',
|
||||||
@@ -790,7 +800,9 @@ async function fetchUninstalledPlugins(force: boolean = false) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
if (showLoading) {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -804,10 +816,10 @@ async function getPluginStatistics() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 加载所有数据
|
// 加载所有数据
|
||||||
async function refreshData() {
|
async function refreshData(context: KeepAliveRefreshContext = {}) {
|
||||||
await fetchInstalledPlugins()
|
await fetchInstalledPlugins(context)
|
||||||
await fetchUninstalledPlugins()
|
await fetchUninstalledPlugins(false, context)
|
||||||
getPluginStatistics()
|
await getPluginStatistics()
|
||||||
// 重新加载文件夹配置,确保分身插件能正确显示在文件夹中
|
// 重新加载文件夹配置,确保分身插件能正确显示在文件夹中
|
||||||
await loadPluginFolders()
|
await loadPluginFolders()
|
||||||
}
|
}
|
||||||
@@ -876,26 +888,33 @@ function marketSettingDone() {
|
|||||||
|
|
||||||
// 手动刷新插件市场
|
// 手动刷新插件市场
|
||||||
async function refreshMarket() {
|
async function refreshMarket() {
|
||||||
isMarketRefreshing.value = true
|
const showMarketLoading = !isAppMarketLoaded.value
|
||||||
|
if (showMarketLoading) {
|
||||||
|
isMarketRefreshing.value = true
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await fetchUninstalledPlugins(true)
|
await fetchUninstalledPlugins(true, { silent: isAppMarketLoaded.value, source: 'manual' })
|
||||||
getPluginStatistics()
|
await getPluginStatistics()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
} finally {
|
} finally {
|
||||||
isMarketRefreshing.value = false
|
if (showMarketLoading) {
|
||||||
|
isMarketRefreshing.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshActiveTabData() {
|
async function refreshActiveTabData(context: KeepAliveRefreshContext = {}) {
|
||||||
if (sortMode.value || isDraggingSortMode.value) return
|
if (sortMode.value || isDraggingSortMode.value) return
|
||||||
|
|
||||||
if (activeTab.value === 'market') {
|
if (activeTab.value === 'market') {
|
||||||
|
await fetchUninstalledPlugins(false, context)
|
||||||
|
await getPluginStatistics()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await fetchInstalledPlugins()
|
await fetchInstalledPlugins(context)
|
||||||
getPluginStatistics()
|
await getPluginStatistics()
|
||||||
// 文件夹配置可能在其它入口被插件操作改变,重新进入时同步一次。
|
// 文件夹配置可能在其它入口被插件操作改变,重新进入时同步一次。
|
||||||
await loadPluginFolders()
|
await loadPluginFolders()
|
||||||
}
|
}
|
||||||
@@ -968,7 +987,7 @@ const { refresh: refreshKeepAliveData } = useKeepAliveRefresh(refreshActiveTabDa
|
|||||||
watch(activeTab, (newTab, oldTab) => {
|
watch(activeTab, (newTab, oldTab) => {
|
||||||
if (!oldTab || newTab === oldTab) return
|
if (!oldTab || newTab === oldTab) return
|
||||||
|
|
||||||
refreshKeepAliveData()
|
refreshKeepAliveData({ silent: true, source: 'tab' })
|
||||||
})
|
})
|
||||||
|
|
||||||
function openPluginSearchDialog() {
|
function openPluginSearchDialog() {
|
||||||
@@ -1698,10 +1717,13 @@ function onDragStartPlugin(evt: any) {
|
|||||||
<VWindowItem value="market">
|
<VWindowItem value="market">
|
||||||
<transition name="fade-slide" appear>
|
<transition name="fade-slide" appear>
|
||||||
<div>
|
<div>
|
||||||
<LoadingBanner v-if="!isAppMarketLoaded || isMarketRefreshing" class="mt-12" />
|
<LoadingBanner
|
||||||
|
v-if="!isAppMarketLoaded || (isMarketRefreshing && displayUninstalledList.length === 0)"
|
||||||
|
class="mt-12"
|
||||||
|
/>
|
||||||
<!-- 资源列表 -->
|
<!-- 资源列表 -->
|
||||||
<VInfiniteScroll
|
<VInfiniteScroll
|
||||||
v-if="isAppMarketLoaded && !isMarketRefreshing"
|
v-if="isAppMarketLoaded && !(isMarketRefreshing && displayUninstalledList.length === 0)"
|
||||||
mode="intersect"
|
mode="intersect"
|
||||||
side="end"
|
side="end"
|
||||||
:items="displayUninstalledList"
|
:items="displayUninstalledList"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue'
|
|||||||
import { useUserStore } from '@/stores'
|
import { useUserStore } from '@/stores'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useBackground } from '@/composables/useBackground'
|
import { useBackground } from '@/composables/useBackground'
|
||||||
import { useKeepAliveRefresh } from '@/composables/useKeepAliveRefresh'
|
import { useKeepAliveRefresh, type KeepAliveRefreshContext } from '@/composables/useKeepAliveRefresh'
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -30,7 +30,7 @@ const dataList = ref<DownloadingInfo[]>([])
|
|||||||
const isRefreshed = ref(false)
|
const isRefreshed = ref(false)
|
||||||
|
|
||||||
// 获取订阅列表数据
|
// 获取订阅列表数据
|
||||||
async function fetchData() {
|
async function fetchData(_context: KeepAliveRefreshContext = {}) {
|
||||||
try {
|
try {
|
||||||
dataList.value = await api.get('download/', { params: { name: props.name } })
|
dataList.value = await api.get('download/', { params: { name: props.name } })
|
||||||
isRefreshed.value = true
|
isRefreshed.value = true
|
||||||
@@ -45,8 +45,9 @@ const loading = ref(false)
|
|||||||
// 下拉刷新
|
// 下拉刷新
|
||||||
function onRefresh() {
|
function onRefresh() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
fetchData()
|
void fetchData().finally(() => {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 过滤数据,管理员用户显示全部,非管理员只显示自己的订阅
|
// 过滤数据,管理员用户显示全部,非管理员只显示自己的订阅
|
||||||
@@ -63,9 +64,11 @@ const { loading: dataLoading } = useDataRefresh(
|
|||||||
'downloading-list',
|
'downloading-list',
|
||||||
fetchData,
|
fetchData,
|
||||||
3000, // 3秒间隔
|
3000, // 3秒间隔
|
||||||
true // 立即执行
|
false // 初始加载交给 keep-alive 页面自身,避免同时发起两次请求
|
||||||
)
|
)
|
||||||
|
|
||||||
|
onMounted(fetchData)
|
||||||
|
|
||||||
useKeepAliveRefresh(fetchData, {
|
useKeepAliveRefresh(fetchData, {
|
||||||
active: computed(() => props.active !== false),
|
active: computed(() => props.active !== false),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useDynamicButton } from '@/composables/useDynamicButton'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { usePWA } from '@/composables/usePWA'
|
import { usePWA } from '@/composables/usePWA'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
|
import { useKeepAliveRefresh, type KeepAliveRefreshContext } from '@/composables/useKeepAliveRefresh'
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -118,17 +119,27 @@ const currentFilter = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 获取站点列表数据
|
// 获取站点列表数据
|
||||||
async function fetchData() {
|
async function fetchData(context: KeepAliveRefreshContext = {}) {
|
||||||
|
const showLoading = !context.silent || !isRefreshed.value
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
if (showLoading) {
|
||||||
siteList.value = await api.get('site/')
|
loading.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const [sites] = await Promise.all([
|
||||||
|
api.get<Site[], Site[]>('site/'),
|
||||||
|
// 站点统计在列表请求期间并行预取,减少刷新时卡片分两轮明显重绘。
|
||||||
|
fetchSiteStats(),
|
||||||
|
])
|
||||||
|
siteList.value = sites
|
||||||
isRefreshed.value = true
|
isRefreshed.value = true
|
||||||
// 获取站点列表后,获取统计数据
|
|
||||||
await fetchSiteStats()
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
if (showLoading) {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,11 +311,10 @@ onBeforeMount(() => {
|
|||||||
fetchUserData()
|
fetchUserData()
|
||||||
})
|
})
|
||||||
|
|
||||||
onActivated(() => {
|
useKeepAliveRefresh(async context => {
|
||||||
if (!loading.value) {
|
if (loading.value) return
|
||||||
fetchData()
|
|
||||||
fetchUserData()
|
await Promise.all([fetchData(context), fetchUserData()])
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { useUserStore } from '@/stores'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import { useConfirm } from '@/composables/useConfirm'
|
import { useConfirm } from '@/composables/useConfirm'
|
||||||
import { useKeepAliveRefresh } from '@/composables/useKeepAliveRefresh'
|
import { useKeepAliveRefresh, type KeepAliveRefreshContext } from '@/composables/useKeepAliveRefresh'
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -205,15 +205,21 @@ async function saveSubscribeOrder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取订阅列表数据
|
// 获取订阅列表数据
|
||||||
async function fetchData() {
|
async function fetchData(context: KeepAliveRefreshContext = {}) {
|
||||||
|
const showLoading = !context.silent || !isRefreshed.value
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
if (showLoading) {
|
||||||
|
loading.value = true
|
||||||
|
}
|
||||||
dataList.value = await api.get('subscribe/')
|
dataList.value = await api.get('subscribe/')
|
||||||
isRefreshed.value = true
|
isRefreshed.value = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
if (showLoading) {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user