mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 23:56:42 +08:00
perf: optimize initial load by implementing lazy loading for modules and fine-tuning authentication/resource initialization logic.
This commit is contained in:
@@ -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
@@ -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()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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: `
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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) || '')
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,6 +82,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)
|
||||||
|
|
||||||
const timerConfig = {
|
const timerConfig = {
|
||||||
@@ -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: 管理器已销毁')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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="() => {}">
|
||||||
|
|||||||
@@ -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="() => {}">
|
||||||
|
|||||||
@@ -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="() => {}">
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user