perf: optimize initial load by implementing lazy loading for modules and fine-tuning authentication/resource initialization logic.

This commit is contained in:
jxxghp
2026-05-14 13:19:48 +08:00
parent e2d36da299
commit 34124418f8
18 changed files with 345 additions and 178 deletions
+2
View File
@@ -264,6 +264,8 @@ const target = join(__dirname, 'icons-bundle.js');
console.log(`Saved ${target} (${bundle.length} bytes)`) console.log(`Saved ${target} (${bundle.length} bytes)`)
})().catch((err) => { })().catch((err) => {
console.error(err) console.error(err)
// 构建图标失败时必须终止构建,避免继续发布上一次遗留的超大 icons-bundle。
process.exitCode = 1
}) })
async function collectUsedIcons(rootDir: string): Promise<string[]> { async function collectUsedIcons(rootDir: string): Promise<string[]> {
+89 -26
View File
@@ -14,6 +14,8 @@ import PWAInstallPrompt from '@/components/PWAInstallPrompt.vue'
import { themeManager } from '@/utils/themeManager' import { themeManager } from '@/utils/themeManager'
import { configureApexChartsTheme } from '@/utils/apexCharts' import { configureApexChartsTheme } from '@/utils/apexCharts'
const LOGIN_WALLPAPER_ROUTE = '/login'
// 生效主题 // 生效主题
const { global: globalTheme } = useTheme() const { global: globalTheme } = useTheme()
let themeValue = localStorage.getItem('theme') || 'auto' let themeValue = localStorage.getItem('theme') || 'auto'
@@ -37,6 +39,7 @@ setI18nLanguage(localeValue as SupportedLocale)
// 检查是否登录 // 检查是否登录
const authStore = useAuthStore() const authStore = useAuthStore()
const isLogin = computed(() => authStore.token) const isLogin = computed(() => authStore.token)
const route = useRoute()
// 全局设置store // 全局设置store
const globalSettingsStore = useGlobalSettingsStore() const globalSettingsStore = useGlobalSettingsStore()
@@ -48,6 +51,12 @@ const loginStateKey = computed(() => (isLogin.value ? 'logged-in' : 'logged-out'
const backgroundImages = ref<string[]>([]) const backgroundImages = ref<string[]>([])
const activeImageIndex = ref(0) const activeImageIndex = ref(0)
const isTransparentTheme = computed(() => globalTheme.name.value === 'transparent') const isTransparentTheme = computed(() => globalTheme.name.value === 'transparent')
const shouldLoadBackgroundImages = computed(
() => (!isLogin.value && route.path === LOGIN_WALLPAPER_ROUTE) || (Boolean(isLogin.value) && isTransparentTheme.value),
)
let backgroundRetryTimer: number | null = null
let backgroundRequestController: AbortController | null = null
let authenticatedStateTimer: number | null = null
function getStoredNumber(key: string, fallback: number, min: number, max: number) { function getStoredNumber(key: string, fallback: number, min: number, max: number) {
const parsed = Number.parseFloat(localStorage.getItem(key) || '') const parsed = Number.parseFloat(localStorage.getItem(key) || '')
@@ -109,9 +118,10 @@ function updateHtmlThemeAttribute(themeName: string) {
// 获取背景图片 // 获取背景图片
async function fetchBackgroundImages() { async function fetchBackgroundImages() {
try { try {
const controller = new AbortController() backgroundRequestController?.abort()
backgroundRequestController = new AbortController()
backgroundImages.value = await api.get(`/login/wallpapers`, { backgroundImages.value = await api.get(`/login/wallpapers`, {
signal: controller.signal, signal: backgroundRequestController.signal,
}) })
activeImageIndex.value = 0 activeImageIndex.value = 0
} catch (e) { } catch (e) {
@@ -153,6 +163,42 @@ function startBackgroundRotation() {
} }
} }
function stopBackgroundLoading() {
backgroundRequestController?.abort()
backgroundRequestController = null
if (backgroundRetryTimer) {
window.clearTimeout(backgroundRetryTimer)
backgroundRetryTimer = null
}
removeBackgroundTimer('background-rotation')
}
async function initializeAuthenticatedState() {
if (!isLogin.value) return
try {
globalLoadingStateManager.setLoadingState('global-settings', true)
await globalSettingsStore.initialize()
await globalSettingsStore.loadUserSettings()
} finally {
globalLoadingStateManager.setLoadingState('global-settings', false)
}
}
function scheduleAuthenticatedStateInitialization() {
if (authenticatedStateTimer) {
window.clearTimeout(authenticatedStateTimer)
}
// 登录后会立刻发生路由切换,稍后再拉取设置可避开导航中止请求。
authenticatedStateTimer = window.setTimeout(() => {
authenticatedStateTimer = null
initializeAuthenticatedState()
}, 150)
}
// 添加logo动画效果并延迟移除加载界面 // 添加logo动画效果并延迟移除加载界面
function animateAndRemoveLoader() { function animateAndRemoveLoader() {
const loadingBg = document.querySelector('#loading-bg') as HTMLElement const loadingBg = document.querySelector('#loading-bg') as HTMLElement
@@ -175,8 +221,6 @@ async function removeLoadingWithStateCheck() {
try { try {
// 设置各个组件的加载状态 // 设置各个组件的加载状态
globalLoadingStateManager.setLoadingState('pwa-state', true) globalLoadingStateManager.setLoadingState('pwa-state', true)
globalLoadingStateManager.setLoadingState('global-settings', true)
globalLoadingStateManager.setLoadingState('background-images', true)
// 静默检查PWA状态恢复 // 静默检查PWA状态恢复
const pwaController = (window as any).pwaStateController const pwaController = (window as any).pwaStateController
@@ -185,22 +229,7 @@ async function removeLoadingWithStateCheck() {
} }
globalLoadingStateManager.setLoadingState('pwa-state', false) globalLoadingStateManager.setLoadingState('pwa-state', false)
// 并行加载关键资源 await initializeAuthenticatedState()
await Promise.all([
globalSettingsStore.initialize().then(async () => {
// 如果已登录,加载用户相关设置
if (isLogin.value) {
await globalSettingsStore.loadUserSettings()
}
globalLoadingStateManager.setLoadingState('global-settings', false)
}),
new Promise(resolve => {
setTimeout(() => {
globalLoadingStateManager.setLoadingState('background-images', false)
resolve(void 0)
}, 50)
}),
])
// 等待所有加载完成 // 等待所有加载完成
await globalLoadingStateManager.waitForAllComplete() await globalLoadingStateManager.waitForAllComplete()
@@ -209,7 +238,9 @@ async function removeLoadingWithStateCheck() {
animateAndRemoveLoader() animateAndRemoveLoader()
// 检查未读消息 // 检查未读消息
checkAndEmitUnreadMessages() if (isLogin.value) {
checkAndEmitUnreadMessages()
}
} catch (error) { } catch (error) {
// 即使出错也要移除加载界面 // 即使出错也要移除加载界面
globalLoadingStateManager.reset() globalLoadingStateManager.reset()
@@ -228,7 +259,8 @@ async function loadBackgroundImages(retryCount = 0) {
if (retryCount < maxRetries) { if (retryCount < maxRetries) {
const baseDelay = isAbortError ? 1000 : 3000 const baseDelay = isAbortError ? 1000 : 3000
const retryDelay = Math.min(baseDelay * Math.pow(2, retryCount), 10000) const retryDelay = Math.min(baseDelay * Math.pow(2, retryCount), 10000)
setTimeout(() => { backgroundRetryTimer = window.setTimeout(() => {
backgroundRetryTimer = null
loadBackgroundImages(retryCount + 1) loadBackgroundImages(retryCount + 1)
}, retryDelay) }, retryDelay)
} }
@@ -264,20 +296,51 @@ onMounted(async () => {
}, },
) )
// 加载背景图片 // 登录页壁纸仅在未登录登录页需要,避免其他首屏额外发起图片列表请求。
loadBackgroundImages() watch(
shouldLoadBackgroundImages,
shouldLoad => {
stopBackgroundLoading()
if (shouldLoad) {
loadBackgroundImages()
} else if (!isTransparentTheme.value) {
backgroundImages.value = []
}
},
{ immediate: true },
)
// 使用优化后的加载界面移除逻辑 // 使用优化后的加载界面移除逻辑
ensureRenderComplete(() => { ensureRenderComplete(() => {
nextTick(removeLoadingWithStateCheck) nextTick(removeLoadingWithStateCheck)
}) })
// 启动心跳 // 启动心跳
startHeartbeat() if (isLogin.value) {
startHeartbeat()
}
// 登录状态可能在当前单页会话中变化,这里按需补齐登录后初始化和心跳。
watch(isLogin, loggedIn => {
if (loggedIn) {
startHeartbeat()
scheduleAuthenticatedStateInitialization()
} else {
if (authenticatedStateTimer) {
window.clearTimeout(authenticatedStateTimer)
authenticatedStateTimer = null
}
stopHeartbeat()
}
})
}) })
onUnmounted(() => { onUnmounted(() => {
// 清除背景轮换定时器 // 清除背景轮换定时器
removeBackgroundTimer('background-rotation') stopBackgroundLoading()
if (authenticatedStateTimer) {
window.clearTimeout(authenticatedStateTimer)
authenticatedStateTimer = null
}
// 停止心跳 // 停止心跳
stopHeartbeat() stopHeartbeat()
}) })
+6 -4
View File
@@ -1,10 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import draggable from 'vuedraggable'
import { copyToClipboard } from '@/@core/utils/navigator' import { copyToClipboard } from '@/@core/utils/navigator'
import { CustomRule, FilterRuleGroup } from '@/api/types' import { CustomRule, FilterRuleGroup } from '@/api/types'
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue' import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
import filter_group_svg from '@images/svg/filter-group.svg' import filter_group_svg from '@images/svg/filter-group.svg'
import { cloneDeep } from 'lodash-es' import { cloneDeep } from 'lodash-es'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
@@ -16,6 +14,10 @@ const display = useDisplay()
// 获取i18n实例 // 获取i18n实例
const { t } = useI18n() const { t } = useI18n()
// 规则组详情弹窗内才需要拖拽和导入代码,避免规则组卡片列表首屏带入重交互依赖。
const Draggable = defineAsyncComponent(() => import('vuedraggable').then(module => module.default))
const ImportCodeDialog = defineAsyncComponent(() => import('@/components/dialog/ImportCodeDialog.vue'))
// 输入参数 // 输入参数
const props = defineProps({ const props = defineProps({
// 单个规则组 // 单个规则组
@@ -273,7 +275,7 @@ function onClose() {
</VRow> </VRow>
</VCardItem> </VCardItem>
<VCardText> <VCardText>
<draggable <Draggable
v-model="filterRuleCards" v-model="filterRuleCards"
handle=".cursor-move" handle=".cursor-move"
item-key="pri" item-key="pri"
@@ -291,7 +293,7 @@ function onClose() {
@close="filterCardClose(element.pri)" @close="filterCardClose(element.pri)"
/> />
</template> </template>
</draggable> </Draggable>
<div class="text-center" v-if="filterRuleCards.length == 0">{{ t('filterRule.add') }}</div> <div class="text-center" v-if="filterRuleCards.length == 0">{{ t('filterRule.add') }}</div>
</VCardText> </VCardText>
<VCardActions class="pt-3"> <VCardActions class="pt-3">
+3 -1
View File
@@ -11,13 +11,15 @@ import VersionHistory from '@/components/misc/VersionHistory.vue'
import ProgressDialog from '../dialog/ProgressDialog.vue' import ProgressDialog from '../dialog/ProgressDialog.vue'
import PluginConfigDialog from '../dialog/PluginConfigDialog.vue' import PluginConfigDialog from '../dialog/PluginConfigDialog.vue'
import PluginDataDialog from '../dialog/PluginDataDialog.vue' import PluginDataDialog from '../dialog/PluginDataDialog.vue'
import LoggingView from '@/views/system/LoggingView.vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify' import { useDisplay } from 'vuetify'
// 显示器宽度 // 显示器宽度
const display = useDisplay() const display = useDisplay()
// 插件日志面板只有点击“查看日志”时才需要,延后加载可减轻插件列表首屏。
const LoggingView = defineAsyncComponent(() => import('@/views/system/LoggingView.vue'))
// 输入参数 // 输入参数
const props = defineProps({ const props = defineProps({
plugin: Object as PropType<Plugin>, plugin: Object as PropType<Plugin>,
+13 -3
View File
@@ -123,10 +123,10 @@ onMounted(() => {
'transition-transform duration-300 hover:-translate-y-1', 'transition-transform duration-300 hover:-translate-y-1',
!props.user.is_active ? 'opacity-85 bg-surface-lighten-1' : '', !props.user.is_active ? 'opacity-85 bg-surface-lighten-1' : '',
]" ]"
class="flex flex-column" class="user-card flex flex-column h-full"
@click="userEditDialog = true" @click="userEditDialog = true"
> >
<div class="flex-grow"> <div class="user-card__body flex-grow flex-grow-1">
<!-- 用户头像和基本信息 --> <!-- 用户头像和基本信息 -->
<VCardItem :class="[user.is_superuser ? 'admin-header' : '']"> <VCardItem :class="[user.is_superuser ? 'admin-header' : '']">
<template v-slot:prepend> <template v-slot:prepend>
@@ -247,7 +247,7 @@ onMounted(() => {
</div> </div>
<!-- 独立的邮箱显示 --> <!-- 独立的邮箱显示 -->
<VDivider class="mx-4" /> <VDivider class="mx-4" />
<div> <div class="user-card__footer">
<VCardText class="d-flex align-center py-2 px-4 text-medium-emphasis"> <VCardText class="d-flex align-center py-2 px-4 text-medium-emphasis">
<VIcon icon="mdi-email-outline" size="small" color="primary" class="mr-2 opacity-70" /> <VIcon icon="mdi-email-outline" size="small" color="primary" class="mr-2 opacity-70" />
<span class="text-body-2 truncate">{{ user.email || t('user.noEmail') }}</span> <span class="text-body-2 truncate">{{ user.email || t('user.noEmail') }}</span>
@@ -308,6 +308,16 @@ onMounted(() => {
</template> </template>
<style scoped> <style scoped>
.user-card {
block-size: 100%;
}
/* 让邮箱和订阅统计固定在卡片底部,保证同一行用户卡片视觉等高。 */
.user-card__footer {
flex-shrink: 0;
margin-block-start: auto;
}
.admin-decoration { .admin-decoration {
position: absolute; position: absolute;
z-index: 1; z-index: 1;
+61 -14
View File
@@ -1,21 +1,70 @@
<script setup lang="ts"> <script setup lang="ts">
import { h, resolveComponent } from 'vue'
import api from '@/api' import api from '@/api'
import { DashboardItem } from '@/api/types' import { DashboardItem } from '@/api/types'
import AnalyticsMediaStatistic from '@/views/dashboard/AnalyticsMediaStatistic.vue'
import AnalyticsScheduler from '@/views/dashboard/AnalyticsScheduler.vue'
import AnalyticsSpeed from '@/views/dashboard/AnalyticsSpeed.vue'
import AnalyticsStorage from '@/views/dashboard/AnalyticsStorage.vue'
import AnalyticsWeeklyOverview from '@/views/dashboard/AnalyticsWeeklyOverview.vue'
import AnalyticsCpu from '@/views/dashboard/AnalyticsCpu.vue'
import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
import AnalyticsNetwork from '@/views/dashboard/AnalyticsNetwork.vue'
import MediaServerLatest from '@/views/dashboard/MediaServerLatest.vue'
import MediaServerLibrary from '@/views/dashboard/MediaServerLibrary.vue'
import MediaServerPlaying from '@/views/dashboard/MediaServerPlaying.vue'
import DashboardRender from '@/components/render/DashboardRender.vue' import DashboardRender from '@/components/render/DashboardRender.vue'
import { isNullOrEmptyObject } from '@/@core/utils' import { isNullOrEmptyObject } from '@/@core/utils'
import { loadRemoteComponent } from '@/utils/federationLoader' import { loadRemoteComponent } from '@/utils/federationLoader'
const DashboardSkeleton = {
setup() {
const SkeletonLoader = resolveComponent('VSkeletonLoader')
// 用 render 函数避免 runtime-only Vue 为异步 loadingComponent 解析模板。
return () => h(SkeletonLoader, { type: 'card' })
},
}
const asyncDashboardOptions = {
loadingComponent: DashboardSkeleton,
}
// 内置仪表盘按需加载,关闭的卡片不再挤进 dashboard 首屏 chunk。
const AnalyticsStorage = defineAsyncComponent({
loader: () => import('@/views/dashboard/AnalyticsStorage.vue'),
...asyncDashboardOptions,
})
const AnalyticsMediaStatistic = defineAsyncComponent({
loader: () => import('@/views/dashboard/AnalyticsMediaStatistic.vue'),
...asyncDashboardOptions,
})
const AnalyticsWeeklyOverview = defineAsyncComponent({
loader: () => import('@/views/dashboard/AnalyticsWeeklyOverview.vue'),
...asyncDashboardOptions,
})
const AnalyticsSpeed = defineAsyncComponent({
loader: () => import('@/views/dashboard/AnalyticsSpeed.vue'),
...asyncDashboardOptions,
})
const AnalyticsScheduler = defineAsyncComponent({
loader: () => import('@/views/dashboard/AnalyticsScheduler.vue'),
...asyncDashboardOptions,
})
const AnalyticsCpu = defineAsyncComponent({
loader: () => import('@/views/dashboard/AnalyticsCpu.vue'),
...asyncDashboardOptions,
})
const AnalyticsMemory = defineAsyncComponent({
loader: () => import('@/views/dashboard/AnalyticsMemory.vue'),
...asyncDashboardOptions,
})
const AnalyticsNetwork = defineAsyncComponent({
loader: () => import('@/views/dashboard/AnalyticsNetwork.vue'),
...asyncDashboardOptions,
})
const MediaServerLibrary = defineAsyncComponent({
loader: () => import('@/views/dashboard/MediaServerLibrary.vue'),
...asyncDashboardOptions,
})
const MediaServerPlaying = defineAsyncComponent({
loader: () => import('@/views/dashboard/MediaServerPlaying.vue'),
...asyncDashboardOptions,
})
const MediaServerLatest = defineAsyncComponent({
loader: () => import('@/views/dashboard/MediaServerLatest.vue'),
...asyncDashboardOptions,
})
// 输入参数 // 输入参数
const props = defineProps({ const props = defineProps({
// 仪表板配置 // 仪表板配置
@@ -53,9 +102,7 @@ const dynamicPluginComponent = defineAsyncComponent({
} }
}, },
// 加载中显示的组件 // 加载中显示的组件
loadingComponent: { loadingComponent: DashboardSkeleton,
template: '<VSkeletonLoader type="card"></VSkeletonLoader>',
},
// 添加错误处理 // 添加错误处理
errorComponent: { errorComponent: {
template: ` template: `
+11 -9
View File
@@ -1,13 +1,4 @@
<script lang="ts" setup> <script lang="ts" setup>
import NameTestView from '@/views/system/NameTestView.vue'
import NetTestView from '@/views/system/NetTestView.vue'
import LoggingView from '@/views/system/LoggingView.vue'
import RuleTestView from '@/views/system/RuleTestView.vue'
import ModuleTestView from '@/views/system/ModuleTestView.vue'
import MessageView from '@/views/system/MessageView.vue'
import WordsView from '@/views/system/WordsView.vue'
import CacheView from '@/views/system/CacheView.vue'
import AccountSettingService from '@/views/system/ServiceView.vue'
import api from '@/api' import api from '@/api'
import { useDisplay } from 'vuetify' import { useDisplay } from 'vuetify'
import { getQueryValue } from '@/@core/utils' import { getQueryValue } from '@/@core/utils'
@@ -26,6 +17,17 @@ const { t } = useI18n()
// 显示器宽度 // 显示器宽度
const display = useDisplay() const display = useDisplay()
// 快捷工具只在弹窗打开时使用,按需加载避免默认布局首屏带上所有 system 视图。
const NameTestView = defineAsyncComponent(() => import('@/views/system/NameTestView.vue'))
const NetTestView = defineAsyncComponent(() => import('@/views/system/NetTestView.vue'))
const LoggingView = defineAsyncComponent(() => import('@/views/system/LoggingView.vue'))
const RuleTestView = defineAsyncComponent(() => import('@/views/system/RuleTestView.vue'))
const ModuleTestView = defineAsyncComponent(() => import('@/views/system/ModuleTestView.vue'))
const MessageView = defineAsyncComponent(() => import('@/views/system/MessageView.vue'))
const WordsView = defineAsyncComponent(() => import('@/views/system/WordsView.vue'))
const CacheView = defineAsyncComponent(() => import('@/views/system/CacheView.vue'))
const AccountSettingService = defineAsyncComponent(() => import('@/views/system/ServiceView.vue'))
// App捷径 // App捷径
const appsMenu = ref(false) const appsMenu = ref(false)
+42 -23
View File
@@ -13,32 +13,47 @@ import i18n from '@/plugins/i18n'
import App from '@/App.vue' import App from '@/App.vue'
import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar' import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar'
// 4. 工具函数和其他辅助模块 // 4. 其他插件和功能模块
import { loadRemoteComponents } from './utils/federationLoader'
// 5. 其他插件和功能模块
import Toast from 'vue-toastification' import Toast from 'vue-toastification'
import ConfirmDialog from '@/composables/useConfirm' import ConfirmDialog from '@/composables/useConfirm'
import { configureApexChartsTheme } from '@/utils/apexCharts' import { configureApexChartsTheme } from '@/utils/apexCharts'
// 6. 注册自定义组件 // 5. 注册自定义组件
import DialogCloseBtn from '@/@core/components/DialogCloseBtn.vue' import DialogCloseBtn from '@/@core/components/DialogCloseBtn.vue'
import ScrollToTopBtn from '@/@core/components/ScrollToTopBtn.vue' import ScrollToTopBtn from '@/@core/components/ScrollToTopBtn.vue'
import PageContentTitle from './@core/components/PageContentTitle.vue' import PageContentTitle from './@core/components/PageContentTitle.vue'
// 7. 样式文件 - 合并为单一导入 // 6. 样式文件 - 合并为单一导入
import '@/styles/main.scss' import '@/styles/main.scss'
// 8. 状态恢复插件 // 7. 状态恢复插件
import stateRestorePlugin from '@/plugins/stateRestore' import stateRestorePlugin from '@/plugins/stateRestore'
// 9. 后台优化工具 function runWhenBrowserIdle(callback: () => void, timeout = 1500) {
import { backgroundManager } from '@/utils/backgroundManager' const requestIdle = globalThis.requestIdleCallback
import { sseManagerSingleton } from '@/utils/sseManager' if (requestIdle) {
requestIdle(callback, { timeout })
return
}
const iconBundlePromise = import('@/@iconify/icons-bundle').catch(error => { globalThis.setTimeout(callback, 0)
console.error('Failed to load icon bundle', error) }
})
function loadIconBundle() {
import('@/@iconify/icons-bundle').catch(error => {
console.error('Failed to load icon bundle', error)
})
}
function loadRemoteComponentsAfterLogin() {
import('./utils/federationLoader')
.then(({ loadRemoteComponents }) => loadRemoteComponents())
.catch(error => {
console.error('Failed to load remote components', error)
})
}
let remoteComponentsInitialized = false
const AsyncAceEditor = defineAsyncComponent(async () => { const AsyncAceEditor = defineAsyncComponent(async () => {
await import('./ace-config') await import('./ace-config')
@@ -70,11 +85,6 @@ const app = createApp(App)
// 1. 注册pinia // 1. 注册pinia
app.use(pinia) app.use(pinia)
// 异步加载远程组件(不阻塞启动)
loadRemoteComponents().catch(error => {
console.error('Failed to load remote components', error)
})
// 2. 注册 UI 框架 // 2. 注册 UI 框架
app.use(vuetify) app.use(vuetify)
@@ -105,11 +115,20 @@ app
.use(ConfirmDialog) .use(ConfirmDialog)
.use(i18n) .use(i18n)
await iconBundlePromise
app.mount('#app') app.mount('#app')
// 页面卸载时清理后台管理器 // 图标全集很大,延后到首屏挂载后的空闲时间加载,避免阻塞登录页首次渲染。
window.addEventListener('beforeunload', () => { runWhenBrowserIdle(loadIconBundle)
backgroundManager.destroy()
sseManagerSingleton.closeAllManagers() // 插件远程入口只在登录后有用,延后初始化可以减少未登录首屏请求和解析成本。
router.isReady().then(() => {
const loadIfAuthenticated = () => {
if (!remoteComponentsInitialized && pinia.state.value.auth?.token) {
remoteComponentsInitialized = true
runWhenBrowserIdle(loadRemoteComponentsAfterLogin)
}
}
loadIfAuthenticated()
router.afterEach(loadIfAuthenticated)
}) })
+1 -7
View File
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { VForm } from 'vuetify/components/VForm' import { VForm } from 'vuetify/components/VForm'
import { useAuthStore, useUserStore, useGlobalSettingsStore } from '@/stores' import { useAuthStore, useUserStore } from '@/stores'
import { authState, userState } from '@/stores/types' import { authState, userState } from '@/stores/types'
import { requiredValidator } from '@/@validators' import { requiredValidator } from '@/@validators'
import api from '@/api' import api from '@/api'
@@ -20,9 +20,6 @@ const { t } = useI18n()
const authStore = useAuthStore() const authStore = useAuthStore()
//用户 Store //用户 Store
const userStore = useUserStore() const userStore = useUserStore()
// 全局设置 Store
const globalSettingsStore = useGlobalSettingsStore()
// 获取有权限的菜单 // 获取有权限的菜单
const navMenus = computed(() => getNavMenus(t)) const navMenus = computed(() => getNavMenus(t))
@@ -373,9 +370,6 @@ async function handleLoginSuccess(response: any) {
authStore.login(authPayLoad) authStore.login(authPayLoad)
userStore.loginUser(userPayload) userStore.loginUser(userPayload)
// 登录后加载用户相关的全局设置
await globalSettingsStore.loadUserSettings()
await afterLogin(userPayload.superUser, userPayload, filteredMenus) await afterLogin(userPayload.superUser, userPayload, filteredMenus)
} }
+9 -7
View File
@@ -1,13 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import router from '@/router' import router from '@/router'
import AccountSettingNotification from '@/views/setting/AccountSettingNotification.vue'
import AccountSettingSite from '@/views/setting/AccountSettingSite.vue'
import AccountSettingSearch from '@/views/setting/AccountSettingSearch.vue'
import AccountSettingSubscribe from '@/views/setting/AccountSettingSubscribe.vue'
import AccountSettingSystem from '@/views/setting/AccountSettingSystem.vue'
import AccountSettingDirectory from '@/views/setting/AccountSettingDirectory.vue'
import AccountSettingRule from '@/views/setting/AccountSettingRule.vue'
import { getSettingTabs } from '@/router/i18n-menu' import { getSettingTabs } from '@/router/i18n-menu'
import { useDynamicHeaderTab } from '@/composables/useDynamicHeaderTab' import { useDynamicHeaderTab } from '@/composables/useDynamicHeaderTab'
@@ -17,6 +10,15 @@ const route = useRoute()
const activeTab = ref((route.query.tab as string) || '') const activeTab = ref((route.query.tab as string) || '')
const settingTabs = computed(() => getSettingTabs(t)) const settingTabs = computed(() => getSettingTabs(t))
// 设置页的每个大类都很重,按标签页拆包,避免进入设置时一次性下载全部配置面板。
const AccountSettingSystem = defineAsyncComponent(() => import('@/views/setting/AccountSettingSystem.vue'))
const AccountSettingDirectory = defineAsyncComponent(() => import('@/views/setting/AccountSettingDirectory.vue'))
const AccountSettingSite = defineAsyncComponent(() => import('@/views/setting/AccountSettingSite.vue'))
const AccountSettingRule = defineAsyncComponent(() => import('@/views/setting/AccountSettingRule.vue'))
const AccountSettingSearch = defineAsyncComponent(() => import('@/views/setting/AccountSettingSearch.vue'))
const AccountSettingSubscribe = defineAsyncComponent(() => import('@/views/setting/AccountSettingSubscribe.vue'))
const AccountSettingNotification = defineAsyncComponent(() => import('@/views/setting/AccountSettingNotification.vue'))
// 使用动态标签页 // 使用动态标签页
const { registerHeaderTab } = useDynamicHeaderTab() const { registerHeaderTab } = useDynamicHeaderTab()
+8 -4
View File
@@ -1,10 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { debounce } from 'lodash-es' import { debounce } from 'lodash-es'
import SubscribeListView from '@/views/subscribe/SubscribeListView.vue' import SubscribeListView from '@/views/subscribe/SubscribeListView.vue'
import SubscribePopularView from '@/views/subscribe/SubscribePopularView.vue'
import SubscribeShareView from '@/views/subscribe/SubscribeShareView.vue'
import SubscribeEditDialog from '@/components/dialog/SubscribeEditDialog.vue'
import SubscribeShareStatisticsDialog from '@/components/dialog/SubscribeShareStatisticsDialog.vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useDynamicHeaderTab } from '@/composables/useDynamicHeaderTab' import { useDynamicHeaderTab } from '@/composables/useDynamicHeaderTab'
import { useDynamicButton } from '@/composables/useDynamicButton' import { useDynamicButton } from '@/composables/useDynamicButton'
@@ -20,6 +16,14 @@ const route = useRoute()
const userStore = useUserStore() const userStore = useUserStore()
const { appMode } = usePWA() const { appMode } = usePWA()
// 非默认标签页和弹窗按需加载,避免进入订阅列表时同步下载分享/统计相关代码。
const SubscribePopularView = defineAsyncComponent(() => import('@/views/subscribe/SubscribePopularView.vue'))
const SubscribeShareView = defineAsyncComponent(() => import('@/views/subscribe/SubscribeShareView.vue'))
const SubscribeEditDialog = defineAsyncComponent(() => import('@/components/dialog/SubscribeEditDialog.vue'))
const SubscribeShareStatisticsDialog = defineAsyncComponent(
() => import('@/components/dialog/SubscribeShareStatisticsDialog.vue'),
)
const subType = route.meta.subType?.toString() const subType = route.meta.subType?.toString()
const subId = ref(route.query.id as string) const subId = ref(route.query.id as string)
const activeTab = ref((route.query.tab as string) || '') const activeTab = ref((route.query.tab as string) || '')
+52 -42
View File
@@ -11,56 +11,61 @@ export class BackgroundManager {
runInBackground?: boolean runInBackground?: boolean
}> = new Map() }> = new Map()
private readonly activityEvents = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click']
private readonly handleVisibilityChange = () => {
const wasBackground = this.isBackground
this.isBackground = document.hidden
if (this.isBackground && !wasBackground) {
console.log('Background: 进入后台,暂停定时器')
this.pauseAllTimers()
} else if (!this.isBackground && wasBackground) {
console.log('Background: 回到前台,恢复定时器')
this.resumeAllTimers()
}
}
private readonly handleBeforeUnload = () => {
this.destroy()
}
private readonly updateActivity = () => {
this.lastActivityTime = Date.now()
}
private isBackground = false private isBackground = false
private isDestroyed = false private isDestroyed = false
private lastActivityTime = Date.now() private lastActivityTime = Date.now()
private activityTimer: ReturnType<typeof setInterval> | null = null private isInitialized = false
constructor() { private ensureInitialized() {
if (this.isInitialized || this.isDestroyed) return
this.isInitialized = true
this.isBackground = document.hidden
this.setupVisibilityListener() this.setupVisibilityListener()
this.setupActivityTracking() this.setupActivityTracking()
} }
private setupVisibilityListener() { private setupVisibilityListener() {
document.addEventListener('visibilitychange', () => { document.addEventListener('visibilitychange', this.handleVisibilityChange)
const wasBackground = this.isBackground window.addEventListener('beforeunload', this.handleBeforeUnload)
this.isBackground = document.hidden
if (this.isBackground && !wasBackground) {
console.log('Background: 进入后台,暂停定时器')
this.pauseAllTimers()
} else if (!this.isBackground && wasBackground) {
console.log('Background: 回到前台,恢复定时器')
this.resumeAllTimers()
}
})
// 页面卸载时清理
window.addEventListener('beforeunload', () => {
this.destroy()
})
} }
private setupActivityTracking() { private setupActivityTracking() {
// 跟踪用户活动 // 按需跟踪用户活动,避免应用启动时就注册一批全局监听。
const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click'] this.activityEvents.forEach(event => {
document.addEventListener(event, this.updateActivity, { passive: true })
const updateActivity = () => {
this.lastActivityTime = Date.now()
}
events.forEach(event => {
document.addEventListener(event, updateActivity, { passive: true })
}) })
}
// 定期更新活动状态 private removeLifecycleListeners() {
this.activityTimer = setInterval(() => { if (!this.isInitialized) return
// 如果超过5分钟没有活动,可以考虑减少后台活动
const inactiveTime = Date.now() - this.lastActivityTime document.removeEventListener('visibilitychange', this.handleVisibilityChange)
if (inactiveTime > 5 * 60 * 1000) { window.removeEventListener('beforeunload', this.handleBeforeUnload)
console.log('Background: 用户长时间不活跃') this.activityEvents.forEach(event => {
} document.removeEventListener(event, this.updateActivity)
}, 60000) // 每分钟检查一次 })
this.isInitialized = false
} }
/** /**
@@ -76,6 +81,9 @@ export class BackgroundManager {
} = {} } = {}
) { ) {
const { runInBackground = false, skipInitialRun = false } = options const { runInBackground = false, skipInitialRun = false } = options
if (this.isDestroyed) return
this.ensureInitialized()
this.removeTimer(id) this.removeTimer(id)
@@ -122,6 +130,11 @@ export class BackgroundManager {
} }
this.timers.delete(id) this.timers.delete(id)
console.log(`Background: 移除定时器 ${id}`) console.log(`Background: 移除定时器 ${id}`)
// 没有任务时释放监听,首屏只导入模块不会产生常驻开销。
if (this.timers.size === 0) {
this.removeLifecycleListeners()
}
} }
} }
@@ -237,11 +250,8 @@ export class BackgroundManager {
}) })
this.timers.clear() this.timers.clear()
// 清理活动跟踪定时器 // 清理按需注册的生命周期与活动监听
if (this.activityTimer) { this.removeLifecycleListeners()
clearInterval(this.activityTimer)
this.activityTimer = null
}
console.log('Background: 管理器已销毁') console.log('Background: 管理器已销毁')
} }
@@ -273,4 +283,4 @@ export function removeBackgroundTimer(id: string) {
export function getBackgroundTimerStatus(id: string) { export function getBackgroundTimerStatus(id: string) {
return backgroundManager.getTimerStatus(id) return backgroundManager.getTimerStatus(id)
} }
+9 -7
View File
@@ -1,15 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import draggable from 'vuedraggable'
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import api from '@/api' import api from '@/api'
import type { Plugin } from '@/api/types' import type { Plugin } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue' import NoDataFound from '@/components/NoDataFound.vue'
import PluginAppCard from '@/components/cards/PluginAppCard.vue'
import { getLogoUrl } from '@/utils/imageUtils' import { getLogoUrl } from '@/utils/imageUtils'
import { useDisplay } from 'vuetify' import { useDisplay } from 'vuetify'
import { isNullOrEmptyObject } from '@/@core/utils' import { isNullOrEmptyObject } from '@/@core/utils'
import { getPluginTabs } from '@/router/i18n-menu' import { getPluginTabs } from '@/router/i18n-menu'
import PluginMarketSettingDialog from '@/components/dialog/PluginMarketSettingDialog.vue'
import { useDynamicButton } from '@/composables/useDynamicButton' import { useDynamicButton } from '@/composables/useDynamicButton'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import PluginMixedSortCard from '@/components/cards/PluginMixedSortCard.vue' import PluginMixedSortCard from '@/components/cards/PluginMixedSortCard.vue'
@@ -22,6 +19,11 @@ const { t } = useI18n()
const route = useRoute() const route = useRoute()
// 市场卡片、拖拽排序和市场设置只在对应标签/操作中需要,延迟到真正使用时加载。
const Draggable = defineAsyncComponent(() => import('vuedraggable').then(module => module.default))
const PluginAppCard = defineAsyncComponent(() => import('@/components/cards/PluginAppCard.vue'))
const PluginMarketSettingDialog = defineAsyncComponent(() => import('@/components/dialog/PluginMarketSettingDialog.vue'))
// 显示器宽度 // 显示器宽度
const display = useDisplay() const display = useDisplay()
@@ -1536,7 +1538,7 @@ function onDragStartPlugin(evt: any) {
<!-- 混合排序列表文件夹和插件 --> <!-- 混合排序列表文件夹和插件 -->
<template v-if="!currentFolder"> <template v-if="!currentFolder">
<!-- 主列表使用draggable进行混合排序 --> <!-- 主列表使用draggable进行混合排序 -->
<draggable <Draggable
v-if="canDragSort" v-if="canDragSort"
v-model="mixedSortList" v-model="mixedSortList"
@end="saveMixedSortOrder" @end="saveMixedSortOrder"
@@ -1565,7 +1567,7 @@ function onDragStartPlugin(evt: any) {
@drop-to-folder="(event, folderName) => handleDropToFolder(event, folderName)" @drop-to-folder="(event, folderName) => handleDropToFolder(event, folderName)"
/> />
</template> </template>
</draggable> </Draggable>
<ProgressiveCardGrid <ProgressiveCardGrid
v-else-if="shouldVirtualizeInstalledMainList" v-else-if="shouldVirtualizeInstalledMainList"
:items="mixedSortList" :items="mixedSortList"
@@ -1598,7 +1600,7 @@ function onDragStartPlugin(evt: any) {
<template v-else> <template v-else>
<!-- 文件夹内:使用draggable排序 + 移出按钮 --> <!-- 文件夹内:使用draggable排序 + 移出按钮 -->
<draggable <Draggable
v-if="canDragSort" v-if="canDragSort"
v-model="draggableFolderPlugins" v-model="draggableFolderPlugins"
@end="saveFolderPluginOrder" @end="saveFolderPluginOrder"
@@ -1624,7 +1626,7 @@ function onDragStartPlugin(evt: any) {
@remove-from-folder="removeFromFolder" @remove-from-folder="removeFromFolder"
/> />
</template> </template>
</draggable> </Draggable>
<ProgressiveCardGrid <ProgressiveCardGrid
v-else-if="shouldVirtualizeInstalledFolderList" v-else-if="shouldVirtualizeInstalledFolderList"
:items="draggableFolderPlugins" :items="draggableFolderPlugins"
@@ -1,14 +1,10 @@
<!-- eslint-disable sonarjs/no-duplicate-string --> <!-- eslint-disable sonarjs/no-duplicate-string -->
<script lang="ts" setup> <script lang="ts" setup>
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import draggable from 'vuedraggable'
import { VRow } from 'vuetify/lib/components/index.mjs'
import api from '@/api' import api from '@/api'
import { TransferDirectoryConf, StorageConf } from '@/api/types' import { TransferDirectoryConf, StorageConf } from '@/api/types'
import DirectoryCard from '@/components/cards/DirectoryCard.vue' import DirectoryCard from '@/components/cards/DirectoryCard.vue'
import StorageCard from '@/components/cards/StorageCard.vue' import StorageCard from '@/components/cards/StorageCard.vue'
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
import CategoryEditDialog from '@/components/dialog/CategoryEditDialog.vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useTheme } from 'vuetify' import { useTheme } from 'vuetify'
import { storageAttributes } from '@/api/constants' import { storageAttributes } from '@/api/constants'
@@ -16,6 +12,11 @@ import { storageAttributes } from '@/api/constants'
const { t } = useI18n() const { t } = useI18n()
const { global: globalTheme } = useTheme() const { global: globalTheme } = useTheme()
// 拖拽排序和分类编辑弹窗按需加载,避免设置框架预加载目录页时带上这些交互依赖。
const Draggable = defineAsyncComponent(() => import('vuedraggable').then(module => module.default))
const ProgressDialog = defineAsyncComponent(() => import('@/components/dialog/ProgressDialog.vue'))
const CategoryEditDialog = defineAsyncComponent(() => import('@/components/dialog/CategoryEditDialog.vue'))
// 所有下载目录 // 所有下载目录
const directories = ref<TransferDirectoryConf[]>([]) const directories = ref<TransferDirectoryConf[]>([])
@@ -264,7 +265,7 @@ onMounted(() => {
<VCardSubtitle>{{ t('setting.directory.storageDesc') }}</VCardSubtitle> <VCardSubtitle>{{ t('setting.directory.storageDesc') }}</VCardSubtitle>
</VCardItem> </VCardItem>
<VCardText> <VCardText>
<draggable <Draggable
v-model="storages" v-model="storages"
handle=".cursor-move" handle=".cursor-move"
item-key="name" item-key="name"
@@ -274,7 +275,7 @@ onMounted(() => {
<template #item="{ element }"> <template #item="{ element }">
<StorageCard :storage="element" @close="removeStorage(element)" @done="loadStorages" /> <StorageCard :storage="element" @close="removeStorage(element)" @done="loadStorages" />
</template> </template>
</draggable> </Draggable>
</VCardText> </VCardText>
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
@@ -309,7 +310,7 @@ onMounted(() => {
<VCardSubtitle>{{ t('setting.directory.directoryDesc') }}</VCardSubtitle> <VCardSubtitle>{{ t('setting.directory.directoryDesc') }}</VCardSubtitle>
</VCardItem> </VCardItem>
<VCardText> <VCardText>
<draggable <Draggable
v-model="directories" v-model="directories"
handle=".cursor-move" handle=".cursor-move"
item-key="pri" item-key="pri"
@@ -331,7 +332,7 @@ onMounted(() => {
@close="removeDirectory(element)" @close="removeDirectory(element)"
/> />
</template> </template>
</draggable> </Draggable>
</VCardText> </VCardText>
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
@@ -1,10 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import api from '@/api' import api from '@/api'
import draggable from 'vuedraggable'
import type { NotificationConf, NotificationSwitchConf } from '@/api/types' import type { NotificationConf, NotificationSwitchConf } from '@/api/types'
import NotificationChannelCard from '@/components/cards/NotificationChannelCard.vue' import NotificationChannelCard from '@/components/cards/NotificationChannelCard.vue'
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { notificationSwitchDict } from '@/api/constants' import { notificationSwitchDict } from '@/api/constants'
import { useTheme, useDisplay } from 'vuetify' import { useTheme, useDisplay } from 'vuetify'
@@ -15,6 +13,10 @@ const display = useDisplay()
// 国际化 // 国际化
const { t } = useI18n() const { t } = useI18n()
// 通知渠道排序和进度弹窗按需加载,避免通知设置 chunk 直接包含拖拽库。
const Draggable = defineAsyncComponent(() => import('vuedraggable').then(module => module.default))
const ProgressDialog = defineAsyncComponent(() => import('@/components/dialog/ProgressDialog.vue'))
// 初始化模板配置字典 // 初始化模板配置字典
const templateConfigs = ref<Record<string, string>>({ const templateConfigs = ref<Record<string, string>>({
organizeSuccess: '{}', organizeSuccess: '{}',
@@ -324,7 +326,7 @@ onMounted(() => {
<VCardSubtitle>{{ t('setting.notification.channelsDesc') }}</VCardSubtitle> <VCardSubtitle>{{ t('setting.notification.channelsDesc') }}</VCardSubtitle>
</VCardItem> </VCardItem>
<VCardText> <VCardText>
<draggable <Draggable
v-model="notifications" v-model="notifications"
handle=".cursor-move" handle=".cursor-move"
item-key="name" item-key="name"
@@ -339,7 +341,7 @@ onMounted(() => {
@close="removeNotification(element)" @close="removeNotification(element)"
/> />
</template> </template>
</draggable> </Draggable>
</VCardText> </VCardText>
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
+8 -6
View File
@@ -2,17 +2,19 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import { copyToClipboard } from '@/@core/utils/navigator' import { copyToClipboard } from '@/@core/utils/navigator'
import draggable from 'vuedraggable'
import api from '@/api' import api from '@/api'
import { CustomRule, FilterRuleGroup } from '@/api/types' import { CustomRule, FilterRuleGroup } from '@/api/types'
import CustomerRuleCard from '@/components/cards/CustomRuleCard.vue' import CustomerRuleCard from '@/components/cards/CustomRuleCard.vue'
import FilterRuleGroupCard from '@/components/cards/FilterRuleGroupCard.vue' import FilterRuleGroupCard from '@/components/cards/FilterRuleGroupCard.vue'
import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
// 国际化 // 国际化
const { t } = useI18n() const { t } = useI18n()
// 拖拽库和导入弹窗只在规则编辑交互中需要,拆出设置页入口 chunk。
const Draggable = defineAsyncComponent(() => import('vuedraggable').then(module => module.default))
const ImportCodeDialog = defineAsyncComponent(() => import('@/components/dialog/ImportCodeDialog.vue'))
// 自定义规则列表 // 自定义规则列表
const customRules = ref<CustomRule[]>([]) const customRules = ref<CustomRule[]>([])
@@ -381,7 +383,7 @@ onMounted(() => {
<VCardSubtitle>{{ t('setting.rule.customRulesDesc') }}</VCardSubtitle> <VCardSubtitle>{{ t('setting.rule.customRulesDesc') }}</VCardSubtitle>
</VCardItem> </VCardItem>
<VCardText> <VCardText>
<draggable <Draggable
v-model="customRules" v-model="customRules"
handle=".cursor-move" handle=".cursor-move"
item-key="name" item-key="name"
@@ -396,7 +398,7 @@ onMounted(() => {
@change="onRuleChange" @change="onRuleChange"
/> />
</template> </template>
</draggable> </Draggable>
</VCardText> </VCardText>
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
@@ -432,7 +434,7 @@ onMounted(() => {
<VCardSubtitle>{{ t('setting.rule.priorityRuleGroupsDesc') }}</VCardSubtitle> <VCardSubtitle>{{ t('setting.rule.priorityRuleGroupsDesc') }}</VCardSubtitle>
</VCardItem> </VCardItem>
<VCardText> <VCardText>
<draggable <Draggable
v-model="filterRuleGroups" v-model="filterRuleGroups"
handle=".cursor-move" handle=".cursor-move"
item-key="name" item-key="name"
@@ -449,7 +451,7 @@ onMounted(() => {
@change="changeRuleGroup" @change="changeRuleGroup"
/> />
</template> </template>
</draggable> </Draggable>
</VCardText> </VCardText>
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
+8 -7
View File
@@ -1,14 +1,11 @@
<!-- eslint-disable sonarjs/no-duplicate-string --> <!-- eslint-disable sonarjs/no-duplicate-string -->
<script lang="ts" setup> <script lang="ts" setup>
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import { VRow } from 'vuetify/lib/components/index.mjs'
import draggable from 'vuedraggable'
import api from '@/api' import api from '@/api'
import { DownloaderConf, MediaServerConf } from '@/api/types' import { DownloaderConf, MediaServerConf } from '@/api/types'
import DownloaderCard from '@/components/cards/DownloaderCard.vue' import DownloaderCard from '@/components/cards/DownloaderCard.vue'
import MediaServerCard from '@/components/cards/MediaServerCard.vue' import MediaServerCard from '@/components/cards/MediaServerCard.vue'
import { copyToClipboard } from '@/@core/utils/navigator' import { copyToClipboard } from '@/@core/utils/navigator'
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { downloaderOptions, mediaServerOptions } from '@/api/constants' import { downloaderOptions, mediaServerOptions } from '@/api/constants'
import { useDisplay, useTheme } from 'vuetify' import { useDisplay, useTheme } from 'vuetify'
@@ -22,6 +19,10 @@ const isTransparentTheme = computed(() => theme.name.value === 'transparent')
// 国际化 // 国际化
const { t } = useI18n() const { t } = useI18n()
// 下载器/媒体服务器排序和进度弹窗按需加载,降低系统设置页入口解析量。
const Draggable = defineAsyncComponent(() => import('vuedraggable').then(module => module.default))
const ProgressDialog = defineAsyncComponent(() => import('@/components/dialog/ProgressDialog.vue'))
// 系统设置项 // 系统设置项
const SystemSettings = ref<any>({ const SystemSettings = ref<any>({
// 基础设置 // 基础设置
@@ -1405,7 +1406,7 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
<VCardSubtitle>{{ t('setting.system.downloadersDesc') }}</VCardSubtitle> <VCardSubtitle>{{ t('setting.system.downloadersDesc') }}</VCardSubtitle>
</VCardItem> </VCardItem>
<VCardText> <VCardText>
<draggable <Draggable
v-model="downloaders" v-model="downloaders"
handle=".cursor-move" handle=".cursor-move"
item-key="name" item-key="name"
@@ -1421,7 +1422,7 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
:allow-refresh="isRequest" :allow-refresh="isRequest"
/> />
</template> </template>
</draggable> </Draggable>
</VCardText> </VCardText>
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
@@ -1456,7 +1457,7 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
<VCardSubtitle>{{ t('setting.system.mediaServersDesc') }}</VCardSubtitle> <VCardSubtitle>{{ t('setting.system.mediaServersDesc') }}</VCardSubtitle>
</VCardItem> </VCardItem>
<VCardText> <VCardText>
<draggable <Draggable
v-model="mediaServers" v-model="mediaServers"
handle=".cursor-move" handle=".cursor-move"
item-key="name" item-key="name"
@@ -1471,7 +1472,7 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
@change="onMediaServerChange" @change="onMediaServerChange"
/> />
</template> </template>
</draggable> </Draggable>
</VCardText> </VCardText>
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
+8 -6
View File
@@ -1,12 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import draggable from 'vuedraggable'
import api from '@/api' import api from '@/api'
import type { Site, SiteUserData } from '@/api/types' import type { Site, SiteUserData } from '@/api/types'
import SiteCard from '@/components/cards/SiteCard.vue' import SiteCard from '@/components/cards/SiteCard.vue'
import NoDataFound from '@/components/NoDataFound.vue' import NoDataFound from '@/components/NoDataFound.vue'
import SiteAddEditDialog from '@/components/dialog/SiteAddEditDialog.vue'
import SiteStatisticsDialog from '@/components/dialog/SiteStatisticsDialog.vue'
import SiteImportDialog from '@/components/dialog/SiteImportDialog.vue'
import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue' import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue'
import { useDynamicButton } from '@/composables/useDynamicButton' import { useDynamicButton } from '@/composables/useDynamicButton'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
@@ -25,6 +21,12 @@ const route = useRoute()
// APP // APP
const { appMode } = usePWA() const { appMode } = usePWA()
//
const Draggable = defineAsyncComponent(() => import('vuedraggable').then(module => module.default))
const SiteAddEditDialog = defineAsyncComponent(() => import('@/components/dialog/SiteAddEditDialog.vue'))
const SiteStatisticsDialog = defineAsyncComponent(() => import('@/components/dialog/SiteStatisticsDialog.vue'))
const SiteImportDialog = defineAsyncComponent(() => import('@/components/dialog/SiteImportDialog.vue'))
// //
const siteList = ref<Site[]>([]) const siteList = ref<Site[]>([])
@@ -402,7 +404,7 @@ useDynamicButton({
</VAlert> </VAlert>
<LoadingBanner v-if="!isRefreshed" class="mt-12" /> <LoadingBanner v-if="!isRefreshed" class="mt-12" />
<draggable <Draggable
v-if="draggableSiteList.length > 0 && canDragSort" v-if="draggableSiteList.length > 0 && canDragSort"
v-model="draggableSiteList" v-model="draggableSiteList"
@end="savaSitesPriority" @end="savaSitesPriority"
@@ -421,7 +423,7 @@ useDynamicButton({
@refresh-stats="handleRefreshStats" @refresh-stats="handleRefreshStats"
/> />
</template> </template>
</draggable> </Draggable>
<ProgressiveCardGrid <ProgressiveCardGrid
v-else-if="draggableSiteList.length > 0 && shouldVirtualizeList" v-else-if="draggableSiteList.length > 0 && shouldVirtualizeList"
:items="draggableSiteList" :items="draggableSiteList"