refactor: standardize keep-alive data refreshing using useKeepAliveRefresh composable across views and dashboards

This commit is contained in:
jxxghp
2026-05-17 10:04:30 +08:00
parent 6900042cf7
commit 5e8489c620
16 changed files with 88 additions and 100 deletions

View File

@@ -6,17 +6,10 @@ import { type PropType } from 'vue'
const elementProps = defineProps({
config: Object as PropType<RenderProps>,
})
// key
const componentKey = ref(0)
onActivated(() => {
componentKey.value++
})
</script>
<template>
<Component
:key="componentKey"
:is="elementProps.config?.component"
v-if="!elementProps.config?.html"
v-bind="elementProps.config?.props"
@@ -34,7 +27,6 @@ onActivated(() => {
/>
</Component>
<Component
:key="componentKey"
:is="elementProps.config?.component"
v-if="elementProps.config?.html"
v-bind="elementProps.config?.props"

View File

@@ -22,6 +22,7 @@ export function useKeepAliveRefresh(refresh: RefreshHandler, options: KeepAliveR
let activatedCount = 0
let refreshing = false
let pendingRefresh = false
let refreshScheduled = false
const isActive = () => options.active === undefined || Boolean(toValue(options.active))
@@ -48,7 +49,14 @@ export function useKeepAliveRefresh(refresh: RefreshHandler, options: KeepAliveR
}
function requestRefresh() {
void nextTick(runRefresh)
// 同一轮激活里可能同时触发路由激活和 tab 激活,合并成一次静默刷新。
if (refreshScheduled) return
refreshScheduled = true
void nextTick(async () => {
refreshScheduled = false
await runRefresh()
})
}
onMounted(() => {

View File

@@ -280,6 +280,40 @@ async function getPluginDashboardMeta() {
}
}
function clearPluginDashboardTimer(pluginDashboardId: string) {
if (!refreshTimers.value[pluginDashboardId]) return
clearTimeout(refreshTimers.value[pluginDashboardId])
delete refreshTimers.value[pluginDashboardId]
}
function schedulePluginDashboardRefresh(item: DashboardItem) {
const pluginDashboardId = buildPluginDashboardId(item.id, item.key)
clearPluginDashboardTimer(pluginDashboardId)
if (
item.attrs?.refresh &&
pluginDashboardRefreshStatus.value[pluginDashboardId] &&
enableConfig.value[pluginDashboardId] &&
isRequest.value
) {
refreshTimers.value[pluginDashboardId] = setTimeout(() => {
getPluginDashboard(item.id, item.key)
}, item.attrs.refresh * 1000)
}
}
function refreshEnabledPluginDashboards() {
if (!superUser || isNullOrEmptyObject(pluginDashboardMeta.value)) return
pluginDashboardMeta.value.forEach((pluginDashboard: { id: string; key: string }) => {
const pluginDashboardId = buildPluginDashboardId(pluginDashboard.id, pluginDashboard.key)
if (enableConfig.value[pluginDashboardId]) {
getPluginDashboard(pluginDashboard.id, pluginDashboard.key)
}
})
}
// 获取一个插件的仪表板配置项
async function getPluginDashboard(id: string, key: string) {
try {
@@ -309,22 +343,7 @@ async function getPluginDashboard(id: string, key: string) {
}
const pluginDashboardId = buildPluginDashboardId(id, key)
// 定时刷新
if (
res.attrs?.refresh &&
pluginDashboardRefreshStatus.value[pluginDashboardId] &&
enableConfig.value[pluginDashboardId] &&
isRequest.value
) {
// 清除之前的定时器
if (refreshTimers.value[pluginDashboardId]) {
clearTimeout(refreshTimers.value[pluginDashboardId])
}
// 设置新的定时器
let timer = setTimeout(() => {
getPluginDashboard(id, key)
}, res.attrs.refresh * 1000)
refreshTimers.value[pluginDashboardId] = timer
}
schedulePluginDashboardRefresh(res)
}
})
} catch (error) {
@@ -346,10 +365,12 @@ onBeforeMount(async () => {
onActivated(() => {
isRequest.value = true
refreshEnabledPluginDashboards()
})
onDeactivated(() => {
isRequest.value = false
Object.keys(refreshTimers.value).forEach(clearPluginDashboardTimer)
})
</script>

View File

@@ -61,10 +61,10 @@ onActivated(async () => {
<template>
<div v-if="downloaders.length > 0">
<VWindow v-model="activeTab" class="disable-tab-transition" :touch="false">
<VWindowItem v-for="item in downloaders" :value="item.name">
<VWindowItem v-for="item in downloaders" :key="item.name" :value="item.name">
<transition name="fade-slide" appear>
<div>
<DownloadingListView :name="item.name" />
<DownloadingListView :name="item.name" :active="activeTab === item.name" />
</div>
</transition>
</VWindowItem>

View File

@@ -296,14 +296,14 @@ onMounted(() => {
<VWindowItem value="popular">
<transition name="fade-slide" appear>
<div>
<SubscribePopularView :type="subType" :active="activeTab === 'popular'" />
<SubscribePopularView :type="subType" />
</div>
</transition>
</VWindowItem>
<VWindowItem value="share">
<transition name="fade-slide" appear>
<div>
<SubscribeShareView :keyword="shareKeyword" :active="activeTab === 'share'" />
<SubscribeShareView :keyword="shareKeyword" />
</div>
</transition>
</VWindowItem>

View File

@@ -15,7 +15,6 @@ const route = useRoute()
const { appMode } = usePWA()
const activeTab = ref((route.query.tab as string) || 'list')
const listViewKey = ref(0)
const workflowListViewRef = ref<InstanceType<typeof WorkflowListView> | null>(null)
// 获取标签页
@@ -37,6 +36,10 @@ function openAddWorkflowDialog() {
workflowListViewRef.value?.openAddDialog()
}
function refreshWorkflowList() {
workflowListViewRef.value?.refresh()
}
const shareKeywordUpdater = debounce((keyword: string) => {
shareKeyword.value = keyword.trim()
}, 300)
@@ -98,14 +101,14 @@ onMounted(() => {
<VWindowItem value="list">
<transition name="fade-slide" appear>
<div>
<WorkflowListView ref="workflowListViewRef" :key="listViewKey" />
<WorkflowListView ref="workflowListViewRef" />
</div>
</transition>
</VWindowItem>
<VWindowItem value="share">
<transition name="fade-slide" appear>
<div>
<WorkflowShareView :keyword="shareKeyword" @update="listViewKey++" />
<WorkflowShareView :keyword="shareKeyword" @update="refreshWorkflowList" />
</div>
</transition>
</VWindowItem>

View File

@@ -4,6 +4,7 @@ import { hexToRgb } from '@layouts/utils'
import api from '@/api'
import { useI18n } from 'vue-i18n'
import { useBackground } from '@/composables/useBackground'
import { useKeepAliveRefresh } from '@/composables/useKeepAliveRefresh'
// 国际化
const { t } = useI18n()
@@ -29,8 +30,6 @@ const variableTheme = controlledComputed(
() => vuetifyTheme.current.value.variables,
)
const chartKey = ref(0)
// 时间序列
const series = ref([
{
@@ -123,18 +122,14 @@ async function loadCpuData() {
}
// 使用数据刷新定时器
const { loading } = useDataRefresh(
const { loading, refresh } = useDataRefresh(
'analytics-cpu',
loadCpuData,
2000, // 2秒间隔
true // 立即执行
)
onActivated(() => {
nextTick(() => {
chartKey.value += 1
})
})
useKeepAliveRefresh(refresh)
</script>
<template>
@@ -148,7 +143,7 @@ onActivated(() => {
<VCardTitle>CPU</VCardTitle>
</VCardItem>
<VCardText>
<VApexChart :key="chartKey" type="line" :options="chartOptions" :series="series" :height="150" />
<VApexChart type="line" :options="chartOptions" :series="series" :height="150" />
<p class="text-center font-weight-medium mb-0">{{ t('dashboard.current') }}{{ current }}%</p>
</VCardText>
</VCard>

View File

@@ -5,6 +5,7 @@ import api from '@/api'
import { formatBytes } from '@/@core/utils/formatters'
import { useI18n } from 'vue-i18n'
import { useBackground } from '@/composables/useBackground'
import { useKeepAliveRefresh } from '@/composables/useKeepAliveRefresh'
// 国际化
const { t } = useI18n()
@@ -30,8 +31,6 @@ const variableTheme = controlledComputed(
() => vuetifyTheme.current.value.variables,
)
const chartKey = ref(0)
// 时间序列
const series = ref([
{
@@ -128,19 +127,14 @@ async function loadMemoryData() {
}
// 使用数据刷新定时器
const { loading } = useDataRefresh(
const { loading, refresh } = useDataRefresh(
'analytics-memory',
loadMemoryData,
3000, // 3秒间隔
true // 立即执行
)
onActivated(() => {
// 使用nextTick确保DOM准备完成后再更新chartKey
nextTick(() => {
chartKey.value += 1
})
})
useKeepAliveRefresh(refresh)
</script>
<template>
@@ -154,7 +148,7 @@ onActivated(() => {
<VCardTitle>{{ t('dashboard.memory') }}</VCardTitle>
</VCardItem>
<VCardText>
<VApexChart :key="chartKey" type="area" :options="chartOptions" :series="series" :height="150" />
<VApexChart type="area" :options="chartOptions" :series="series" :height="150" />
<p class="text-center font-weight-medium mb-0">{{ t('dashboard.current') }}{{ formatBytes(usedMemory) }}</p>
</VCardText>
</VCard>

View File

@@ -4,6 +4,7 @@ import { hexToRgb } from '@layouts/utils'
import api from '@/api'
import { useI18n } from 'vue-i18n'
import { useBackground } from '@/composables/useBackground'
import { useKeepAliveRefresh } from '@/composables/useKeepAliveRefresh'
// 国际化
const { t } = useI18n()
@@ -29,8 +30,6 @@ const variableTheme = controlledComputed(
() => vuetifyTheme.current.value.variables,
)
const chartKey = ref(0)
// 时间序列 - 上行和下行流量
const series = ref([
{
@@ -161,18 +160,14 @@ async function getNetworkUsage() {
}
// 使用数据刷新定时器
useDataRefresh(
const { refresh } = useDataRefresh(
'dashboard-network',
getNetworkUsage,
2000, // 2秒间隔
true // 立即执行
)
onActivated(() => {
nextTick(() => {
chartKey.value += 1
})
})
useKeepAliveRefresh(refresh)
</script>
<template>
@@ -186,7 +181,7 @@ onActivated(() => {
<VCardTitle>{{ t('dashboard.network') }}</VCardTitle>
</VCardItem>
<VCardText>
<VApexChart :key="chartKey" type="line" :options="chartOptions" :series="series" :height="150" />
<VApexChart type="line" :options="chartOptions" :series="series" :height="150" />
<div class="d-flex justify-space-between">
<p class="text-center font-weight-medium mb-0">
<span class="text-warning">{{ t('dashboard.upload') }}</span

View File

@@ -362,9 +362,6 @@ const draggableFolderPlugins = ref<Plugin[]>([])
// 是否正在拖拽排序中
const isDraggingSortMode = ref(false)
// 插件市场分页 key重置后让 VInfiniteScroll 重新触发首屏加载。
const marketInfiniteKey = ref(0)
// 显示的文件夹列表(按排序显示)
const displayedFolders = computed(() => {
if (currentFolder.value) return [] // 在文件夹内不显示其他文件夹
@@ -810,7 +807,6 @@ async function getPluginStatistics() {
async function refreshData() {
await fetchInstalledPlugins()
await fetchUninstalledPlugins()
marketInfiniteKey.value++
getPluginStatistics()
// 重新加载文件夹配置,确保分身插件能正确显示在文件夹中
await loadPluginFolders()
@@ -884,7 +880,6 @@ async function refreshMarket() {
try {
await fetchUninstalledPlugins(true)
getPluginStatistics()
marketInfiniteKey.value++
} catch (error) {
console.error(error)
} finally {
@@ -896,16 +891,10 @@ 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()
@@ -1716,7 +1705,6 @@ function onDragStartPlugin(evt: any) {
mode="intersect"
side="end"
:items="displayUninstalledList"
:key="marketInfiniteKey"
@load="loadMarketMore"
class="overflow-visible"
>

View File

@@ -8,6 +8,7 @@ import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue'
import { useUserStore } from '@/stores'
import { useI18n } from 'vue-i18n'
import { useBackground } from '@/composables/useBackground'
import { useKeepAliveRefresh } from '@/composables/useKeepAliveRefresh'
// 国际化
const { t } = useI18n()
@@ -16,6 +17,7 @@ const { useDataRefresh } = useBackground()
// 定义输入参数
const props = defineProps<{
name: string
active?: boolean
}>()
// 用户 Store
@@ -63,6 +65,10 @@ const { loading: dataLoading } = useDataRefresh(
3000, // 3秒间隔
true // 立即执行
)
useKeepAliveRefresh(fetchData, {
active: computed(() => props.active !== false),
})
</script>
<template>

View File

@@ -306,9 +306,11 @@ watch(
}, 1000),
)
// 获取订阅列表数据
async function fetchData(page = currentPage.value, count = itemsPerPage.value) {
loading.value = true
// 获取历史记录数据keep-alive 重新进入时可静默刷新,避免表格出现重新加载感。
async function fetchData(page = currentPage.value, count = itemsPerPage.value, options: { silent?: boolean } = {}) {
if (!options.silent) {
loading.value = true
}
try {
const result: { [key: string]: any } = await api.get('history/transfer', {
@@ -326,8 +328,11 @@ async function fetchData(page = currentPage.value, count = itemsPerPage.value) {
)
} catch (error) {
console.error(error)
} finally {
if (!options.silent) {
loading.value = false
}
}
loading.value = false
}
// 根据 type 返回不同的图标
@@ -761,7 +766,7 @@ onActivated(() => {
}
if (!loading.value) {
fetchData()
fetchData(currentPage.value, itemsPerPage.value, { silent: true })
}
})

View File

@@ -4,7 +4,6 @@ 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'
// 国际化
@@ -13,10 +12,6 @@ const { t } = useI18n()
// 输入参数
const props = defineProps({
type: String,
active: {
type: Boolean,
default: true,
},
})
// 判断是否有滚动条
@@ -116,10 +111,6 @@ watch(
{ deep: true },
)
useKeepAliveRefresh(resetData, {
active: computed(() => props.active),
})
// 拼装参数
function getParams() {
let params: { [key: string]: any } = {

View File

@@ -4,7 +4,6 @@ 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'
// 国际化
@@ -14,10 +13,6 @@ const { t } = useI18n()
const props = defineProps({
// 过滤关键字
keyword: String,
active: {
type: Boolean,
default: true,
},
})
// 判断是否有滚动条
@@ -129,10 +124,6 @@ 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 } = {

View File

@@ -6,6 +6,7 @@ import WorkflowTaskCard from '@/components/cards/WorkflowTaskCard.vue'
import NoDataFound from '@/components/NoDataFound.vue'
import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue'
import { useI18n } from 'vue-i18n'
import { useKeepAliveRefresh } from '@/composables/useKeepAliveRefresh'
// 国际化
const { t } = useI18n()
@@ -52,9 +53,7 @@ onMounted(() => {
fetchData()
})
onActivated(() => {
fetchData()
})
useKeepAliveRefresh(fetchData)
function openAddDialog() {
addDialog.value = true
@@ -62,6 +61,7 @@ function openAddDialog() {
defineExpose({
openAddDialog,
refresh: fetchData,
})
</script>
<template>

View File

@@ -146,9 +146,8 @@ function removeData(id: string) {
dataList.value = dataList.value.filter(item => item.id !== id)
}
onActivated(() => {
onMounted(() => {
loadEventTypes()
fetchData({ done: () => {} })
})
</script>