mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-20 15:19:41 +08:00
refactor: implement lazy-loaded tab components with silent background data refresh for settings pages
This commit is contained in:
33
src/composables/useSilentSettingRefresh.ts
Normal file
33
src/composables/useSilentSettingRefresh.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { type MaybeRefOrGetter, toValue } from 'vue'
|
||||
import { useKeepAliveRefresh, type KeepAliveRefreshContext } from '@/composables/useKeepAliveRefresh'
|
||||
|
||||
type RefreshHandler = (context?: KeepAliveRefreshContext) => void | Promise<void>
|
||||
|
||||
interface SilentSettingRefreshOptions {
|
||||
active?: MaybeRefOrGetter<boolean>
|
||||
}
|
||||
|
||||
function isEditingFormField() {
|
||||
if (typeof document === 'undefined') return false
|
||||
|
||||
const element = document.activeElement
|
||||
if (!(element instanceof HTMLElement)) return false
|
||||
|
||||
// 设置页大多是可编辑表单,正在输入时跳过静默刷新,避免覆盖用户未保存内容。
|
||||
return Boolean(element.closest('input, textarea, select, [contenteditable="true"], .ace_text-input'))
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置面板重新可见时静默刷新数据;如果用户正在编辑表单,则本轮刷新让路给输入体验。
|
||||
*/
|
||||
export function useSilentSettingRefresh(refresh: RefreshHandler, options: SilentSettingRefreshOptions = {}) {
|
||||
return useKeepAliveRefresh(
|
||||
async context => {
|
||||
if (context?.silent && isEditingFormField()) return
|
||||
await refresh(context)
|
||||
},
|
||||
{
|
||||
active: options.active === undefined ? undefined : () => Boolean(toValue(options.active)),
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -433,7 +433,7 @@ onMounted(() => {
|
||||
scrollable
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<VCard>
|
||||
<VCard class="system-health-dialog-card">
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
<VIcon icon="mdi-cog" class="me-2" />
|
||||
@@ -442,7 +442,7 @@ onMounted(() => {
|
||||
<VDialogCloseBtn @click="systemTestDialog = false" />
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText class="pa-0">
|
||||
<VCardText class="system-health-dialog-body pa-0">
|
||||
<ModuleTestView />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
@@ -492,3 +492,24 @@ onMounted(() => {
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.system-health-dialog-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.system-health-dialog-body {
|
||||
/* 弹窗正文本身不滚动,滚动只交给健康检查结果列表。 */
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
block-size: min(42rem, calc(100dvh - 8rem - env(safe-area-inset-top) - env(safe-area-inset-bottom)));
|
||||
min-block-size: 0;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
:global(.v-dialog--fullscreen) .system-health-dialog-body {
|
||||
block-size: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,6 +19,26 @@ const AccountSettingSearch = defineAsyncComponent(() => import('@/views/setting/
|
||||
const AccountSettingSubscribe = defineAsyncComponent(() => import('@/views/setting/AccountSettingSubscribe.vue'))
|
||||
const AccountSettingNotification = defineAsyncComponent(() => import('@/views/setting/AccountSettingNotification.vue'))
|
||||
|
||||
const visitedTabs = ref(new Set<string>())
|
||||
|
||||
const settingTabComponents = [
|
||||
{ value: 'system', component: AccountSettingSystem },
|
||||
{ value: 'directory', component: AccountSettingDirectory },
|
||||
{ value: 'site', component: AccountSettingSite },
|
||||
{ value: 'rule', component: AccountSettingRule },
|
||||
{ value: 'search', component: AccountSettingSearch },
|
||||
{ value: 'subscribe', component: AccountSettingSubscribe },
|
||||
{ value: 'notification', component: AccountSettingNotification },
|
||||
]
|
||||
|
||||
function markTabVisited(tab: string) {
|
||||
if (!tab) return
|
||||
|
||||
const nextTabs = new Set(visitedTabs.value)
|
||||
nextTabs.add(tab)
|
||||
visitedTabs.value = nextTabs
|
||||
}
|
||||
|
||||
// 使用动态标签页
|
||||
const { registerHeaderTab } = useDynamicHeaderTab()
|
||||
|
||||
@@ -34,71 +54,23 @@ onMounted(() => {
|
||||
if (!activeTab.value && settingTabs.value.length > 0) {
|
||||
activeTab.value = settingTabs.value[0].tab
|
||||
}
|
||||
markTabVisited(activeTab.value)
|
||||
})
|
||||
|
||||
watch(activeTab, markTabVisited, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VWindow v-model="activeTab" class="disable-tab-transition" :touch="false">
|
||||
<!-- 系统 -->
|
||||
<VWindowItem value="system">
|
||||
<VWindowItem v-for="item in settingTabComponents" :key="item.value" :value="item.value">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingSystem />
|
||||
</div>
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 目录 -->
|
||||
<VWindowItem value="directory">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingDirectory />
|
||||
</div>
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 站点 -->
|
||||
<VWindowItem value="site">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingSite />
|
||||
</div>
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 规则 -->
|
||||
<VWindowItem value="rule">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingRule />
|
||||
</div>
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 搜索 -->
|
||||
<VWindowItem value="search">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingSearch />
|
||||
</div>
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 订阅 -->
|
||||
<VWindowItem value="subscribe">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingSubscribe />
|
||||
</div>
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 通知 -->
|
||||
<VWindowItem value="notification">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingNotification />
|
||||
<component
|
||||
:is="item.component"
|
||||
v-if="visitedTabs.has(item.value)"
|
||||
:active="activeTab === item.value"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
@@ -156,6 +156,7 @@ const router = createRouter({
|
||||
path: '/setting',
|
||||
component: () => import('../pages/setting.vue'),
|
||||
meta: {
|
||||
keepAlive: true,
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -8,10 +8,18 @@ import StorageCard from '@/components/cards/StorageCard.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useTheme } from 'vuetify'
|
||||
import { storageAttributes } from '@/api/constants'
|
||||
import { useSilentSettingRefresh } from '@/composables/useSilentSettingRefresh'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { global: globalTheme } = useTheme()
|
||||
|
||||
const props = defineProps({
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 拖拽排序和分类编辑弹窗按需加载,避免设置框架预加载目录页时带上这些交互依赖。
|
||||
const Draggable = defineAsyncComponent(() => import('vuedraggable').then(module => module.default))
|
||||
const ProgressDialog = defineAsyncComponent(() => import('@/components/dialog/ProgressDialog.vue'))
|
||||
@@ -247,12 +255,17 @@ async function saveSystemSettings(value: any) {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPageData() {
|
||||
await Promise.all([loadDirectories(), loadStorages(), loadMediaCategories(), loadSystemSettings()])
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
onMounted(() => {
|
||||
loadDirectories()
|
||||
loadStorages()
|
||||
loadMediaCategories()
|
||||
loadSystemSettings()
|
||||
loadPageData()
|
||||
})
|
||||
|
||||
useSilentSettingRefresh(loadPageData, {
|
||||
active: computed(() => props.active),
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import NotificationChannelCard from '@/components/cards/NotificationChannelCard.
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { notificationSwitchDict } from '@/api/constants'
|
||||
import { useTheme, useDisplay } from 'vuetify'
|
||||
import { useSilentSettingRefresh } from '@/composables/useSilentSettingRefresh'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
@@ -13,6 +14,13 @@ const display = useDisplay()
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 通知渠道排序和进度弹窗按需加载,避免通知设置 chunk 直接包含拖拽库。
|
||||
const Draggable = defineAsyncComponent(() => import('vuedraggable').then(module => module.default))
|
||||
const ProgressDialog = defineAsyncComponent(() => import('@/components/dialog/ProgressDialog.vue'))
|
||||
@@ -308,12 +316,22 @@ function getNotificationSwitchText(type: string | undefined) {
|
||||
return notificationSwitchDict[type]
|
||||
}
|
||||
|
||||
async function loadPageData() {
|
||||
await Promise.all([
|
||||
loadNotificationSetting(),
|
||||
loadNotificationSwitchs(),
|
||||
loadNotificationTime(),
|
||||
loadTemplateConfigs(),
|
||||
])
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
onMounted(() => {
|
||||
loadNotificationSetting()
|
||||
loadNotificationSwitchs()
|
||||
loadNotificationTime()
|
||||
loadTemplateConfigs()
|
||||
loadPageData()
|
||||
})
|
||||
|
||||
useSilentSettingRefresh(loadPageData, {
|
||||
active: computed(() => props.active && !editorVisible.value),
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -7,10 +7,18 @@ import { CustomRule, FilterRuleGroup } from '@/api/types'
|
||||
import CustomerRuleCard from '@/components/cards/CustomRuleCard.vue'
|
||||
import FilterRuleGroupCard from '@/components/cards/FilterRuleGroupCard.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useSilentSettingRefresh } from '@/composables/useSilentSettingRefresh'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 拖拽库和导入弹窗只在规则编辑交互中需要,拆出设置页入口 chunk。
|
||||
const Draggable = defineAsyncComponent(() => import('vuedraggable').then(module => module.default))
|
||||
const ImportCodeDialog = defineAsyncComponent(() => import('@/components/dialog/ImportCodeDialog.vue'))
|
||||
@@ -365,12 +373,17 @@ async function saveTorrentPriority() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPageData() {
|
||||
await Promise.all([loadMediaCategories(), queryCustomRules(), queryFilterRuleGroups(), queryTorrentPriority()])
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
onMounted(() => {
|
||||
loadMediaCategories()
|
||||
queryCustomRules()
|
||||
queryFilterRuleGroups()
|
||||
queryTorrentPriority()
|
||||
loadPageData()
|
||||
})
|
||||
|
||||
useSilentSettingRefresh(loadPageData, {
|
||||
active: computed(() => props.active),
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,10 +3,18 @@ import { useToast } from 'vue-toastification'
|
||||
import api from '@/api'
|
||||
import type { FilterRuleGroup, Site } from '@/api/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useSilentSettingRefresh } from '@/composables/useSilentSettingRefresh'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
@@ -176,12 +184,16 @@ async function loadSystemSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPageData() {
|
||||
await Promise.all([querySites(), queryFilterRuleGroups(), querySelectedSites(), loadSearchSetting(), loadSystemSettings()])
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
querySites()
|
||||
queryFilterRuleGroups()
|
||||
querySelectedSites()
|
||||
loadSearchSetting()
|
||||
loadSystemSettings()
|
||||
loadPageData()
|
||||
})
|
||||
|
||||
useSilentSettingRefresh(loadPageData, {
|
||||
active: computed(() => props.active),
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,10 +3,18 @@ import { useToast } from 'vue-toastification'
|
||||
import api from '@/api'
|
||||
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useSilentSettingRefresh } from '@/composables/useSilentSettingRefresh'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
@@ -122,6 +130,10 @@ async function saveSiteSetting(value: { [key: string]: any }) {
|
||||
onMounted(() => {
|
||||
loadSiteSettings()
|
||||
})
|
||||
|
||||
useSilentSettingRefresh(loadSiteSettings, {
|
||||
active: computed(() => props.active),
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -4,10 +4,18 @@ import api from '@/api'
|
||||
import type { FilterRuleGroup, Site } from '@/api/types'
|
||||
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useSilentSettingRefresh } from '@/composables/useSilentSettingRefresh'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
@@ -184,12 +192,22 @@ async function saveSubscribeSetting() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPageData() {
|
||||
await Promise.all([
|
||||
querySites(),
|
||||
queryFilterRuleGroups(),
|
||||
querySelectedRssSites(),
|
||||
querySubscribeRules(),
|
||||
loadSystemSettings(),
|
||||
])
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
querySites()
|
||||
queryFilterRuleGroups()
|
||||
querySelectedRssSites()
|
||||
querySubscribeRules()
|
||||
loadSystemSettings()
|
||||
loadPageData()
|
||||
})
|
||||
|
||||
useSilentSettingRefresh(loadPageData, {
|
||||
active: computed(() => props.active),
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { downloaderOptions, mediaServerOptions } from '@/api/constants'
|
||||
import { useDisplay, useTheme } from 'vuetify'
|
||||
import { useLlmProviderDirectory } from '@/composables/useLlmProviderDirectory'
|
||||
import { useSilentSettingRefresh } from '@/composables/useSilentSettingRefresh'
|
||||
|
||||
const display = useDisplay()
|
||||
const theme = useTheme()
|
||||
@@ -19,6 +20,13 @@ const isTransparentTheme = computed(() => theme.name.value === 'transparent')
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 下载器/媒体服务器排序和进度弹窗按需加载,降低系统设置页入口解析量。
|
||||
const Draggable = defineAsyncComponent(() => import('vuedraggable').then(module => module.default))
|
||||
const ProgressDialog = defineAsyncComponent(() => import('@/components/dialog/ProgressDialog.vue'))
|
||||
@@ -847,12 +855,11 @@ async function saveScrapingSwitchs() {
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
onMounted(() => {
|
||||
loadDownloaderSetting()
|
||||
loadMediaServerSetting()
|
||||
loadSystemSettings()
|
||||
loadScrapingSwitchs()
|
||||
})
|
||||
async function loadPageData() {
|
||||
await Promise.all([loadDownloaderSetting(), loadMediaServerSetting(), loadSystemSettings(), loadScrapingSwitchs()])
|
||||
}
|
||||
|
||||
onMounted(loadPageData)
|
||||
|
||||
onActivated(async () => {
|
||||
isRequest.value = true
|
||||
@@ -866,6 +873,16 @@ onBeforeUnmount(() => {
|
||||
invalidateLlmTestState()
|
||||
})
|
||||
|
||||
useSilentSettingRefresh(
|
||||
async () => {
|
||||
if (progressDialog.value || advancedDialog.value || testingLlm.value || savingBasic.value) return
|
||||
await loadPageData()
|
||||
},
|
||||
{
|
||||
active: computed(() => props.active),
|
||||
},
|
||||
)
|
||||
|
||||
watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
|
||||
if (snapshotKey !== previousSnapshotKey) invalidateLlmTestState()
|
||||
})
|
||||
|
||||
@@ -231,6 +231,10 @@ onMounted(getModules)
|
||||
.system-health-check {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
block-size: 100%;
|
||||
min-block-size: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
@@ -316,6 +320,7 @@ onMounted(getModules)
|
||||
flex: 1;
|
||||
min-block-size: 0;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding-block: 0 16px;
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user