refactor: optimize Keep-Alive component rendering and data synchronization by introducing silent refresh states and fallback layout calculations.

This commit is contained in:
jxxghp
2026-05-17 14:06:05 +08:00
parent 348ae6b313
commit 0e005c3c7e
13 changed files with 197 additions and 78 deletions

5
env.d.ts vendored
View File

@@ -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
} }
} }

View File

@@ -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),
}) })

View File

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

View File

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

View File

@@ -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' },
) )

View File

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

View File

@@ -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()
}) })

View File

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

View File

@@ -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: '电视剧',
}, },

View File

@@ -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"

View File

@@ -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),
}) })

View File

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

View File

@@ -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
}
} }
} }