mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 23:56:42 +08:00
feat: implement launch loading state management and footer navigation visibility control
This commit is contained in:
@@ -121,6 +121,12 @@
|
|||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html[data-launch-loading="true"] .footer-nav-container {
|
||||||
|
opacity: 0 !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
#loading-bg {
|
#loading-bg {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|||||||
+21
-10
@@ -12,6 +12,8 @@ import { addBackgroundTimer, removeBackgroundTimer } from '@/utils/backgroundMan
|
|||||||
import PWAInstallPrompt from '@/components/PWAInstallPrompt.vue'
|
import PWAInstallPrompt from '@/components/PWAInstallPrompt.vue'
|
||||||
import SharedDialogHost from '@/components/dialog/SharedDialogHost.vue'
|
import SharedDialogHost from '@/components/dialog/SharedDialogHost.vue'
|
||||||
import { applyStoredThemeCustomizerAppearance } from '@/composables/useThemeCustomizer'
|
import { applyStoredThemeCustomizerAppearance } from '@/composables/useThemeCustomizer'
|
||||||
|
import { completeLaunchLoading } from '@/composables/useLaunchLoading'
|
||||||
|
import { usePWA } from '@/composables/usePWA'
|
||||||
import { themeManager } from '@/utils/themeManager'
|
import { themeManager } from '@/utils/themeManager'
|
||||||
import { applyDocumentThemeChrome, resolveThemeName } from '@/utils/themePalette'
|
import { applyDocumentThemeChrome, resolveThemeName } from '@/utils/themePalette'
|
||||||
import { configureApexChartsTheme } from '@/utils/apexCharts'
|
import { configureApexChartsTheme } from '@/utils/apexCharts'
|
||||||
@@ -45,6 +47,7 @@ setI18nLanguage(localeValue as SupportedLocale)
|
|||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const isLogin = computed(() => authStore.token)
|
const isLogin = computed(() => authStore.token)
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const { initializePWA } = usePWA()
|
||||||
|
|
||||||
// 全局设置store
|
// 全局设置store
|
||||||
const globalSettingsStore = useGlobalSettingsStore()
|
const globalSettingsStore = useGlobalSettingsStore()
|
||||||
@@ -245,19 +248,25 @@ function scheduleAuthenticatedStateInitialization() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 添加logo动画效果并延迟移除加载界面
|
// 添加logo动画效果并延迟移除加载界面
|
||||||
function animateAndRemoveLoader() {
|
async function animateAndRemoveLoader() {
|
||||||
const loadingBg = document.querySelector('#loading-bg') as HTMLElement
|
const loadingBg = document.querySelector('#loading-bg') as HTMLElement
|
||||||
if (loadingBg) {
|
if (loadingBg) {
|
||||||
// 只收掉启动内容,背景层保持实色直到节点被移除,避免底部 safe area 先透出页面内容。
|
// 只收掉启动内容,背景层保持实色直到节点被移除,避免底部 safe area 先透出页面内容。
|
||||||
loadingBg.classList.add('loading-complete')
|
loadingBg.classList.add('loading-complete')
|
||||||
window.setTimeout(() => {
|
await new Promise<void>(resolve => {
|
||||||
removeEl('#loading-bg')
|
window.setTimeout(() => {
|
||||||
|
removeEl('#loading-bg')
|
||||||
|
|
||||||
// 启动阶段的根节点锁定只在 loader 存在时生效,移除后恢复正常页面与弹窗布局。
|
// 启动阶段的根节点锁定只在 loader 存在时生效,移除后恢复正常页面与弹窗布局。
|
||||||
document.documentElement.removeAttribute('data-launch-loading')
|
document.documentElement.removeAttribute('data-launch-loading')
|
||||||
document.documentElement.style.removeProperty('overflow')
|
document.documentElement.style.removeProperty('overflow')
|
||||||
document.body.style.removeProperty('overflow')
|
document.body.style.removeProperty('overflow')
|
||||||
}, 120)
|
completeLaunchLoading()
|
||||||
|
resolve()
|
||||||
|
}, 120)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
completeLaunchLoading()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,13 +283,15 @@ async function removeLoadingWithStateCheck() {
|
|||||||
}
|
}
|
||||||
globalLoadingStateManager.setLoadingState('pwa-state', false)
|
globalLoadingStateManager.setLoadingState('pwa-state', false)
|
||||||
|
|
||||||
|
// PWA/App 模式会影响布局和底部导航,必须在启动屏退场前稳定下来。
|
||||||
|
await initializePWA()
|
||||||
await initializeAuthenticatedState()
|
await initializeAuthenticatedState()
|
||||||
|
|
||||||
// 等待所有加载完成
|
// 等待所有加载完成
|
||||||
await globalLoadingStateManager.waitForAllComplete()
|
await globalLoadingStateManager.waitForAllComplete()
|
||||||
|
|
||||||
// 移除加载界面
|
// 移除加载界面
|
||||||
animateAndRemoveLoader()
|
await animateAndRemoveLoader()
|
||||||
|
|
||||||
// 检查未读消息
|
// 检查未读消息
|
||||||
if (isLogin.value) {
|
if (isLogin.value) {
|
||||||
@@ -289,7 +300,7 @@ async function removeLoadingWithStateCheck() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 即使出错也要移除加载界面
|
// 即使出错也要移除加载界面
|
||||||
globalLoadingStateManager.reset()
|
globalLoadingStateManager.reset()
|
||||||
animateAndRemoveLoader()
|
await animateAndRemoveLoader()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { readonly, ref } from 'vue'
|
||||||
|
|
||||||
|
function detectInitialLaunchLoading() {
|
||||||
|
if (typeof document === 'undefined') return true
|
||||||
|
|
||||||
|
return document.documentElement.getAttribute('data-launch-loading') === 'true' || Boolean(document.getElementById('loading-bg'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动屏的全局状态,供 Teleport 到 body 的组件避开 iOS PWA 启动阶段的固定层闪烁。
|
||||||
|
const isLaunchLoading = ref(detectInitialLaunchLoading())
|
||||||
|
|
||||||
|
export function completeLaunchLoading() {
|
||||||
|
isLaunchLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLaunchLoading() {
|
||||||
|
return {
|
||||||
|
isLaunchLoading: readonly(isLaunchLoading),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,11 +5,12 @@ import { NavMenu } from '@/@layouts/types'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useUserStore } from '@/stores'
|
import { useUserStore } from '@/stores'
|
||||||
import { buildUserPermissionContext, filterItemsByPermission, filterMenusByPermission, hasItemPermission } from '@/utils/permission'
|
import { buildUserPermissionContext, filterItemsByPermission, filterMenusByPermission, hasItemPermission } from '@/utils/permission'
|
||||||
|
import { useLaunchLoading } from '@/composables/useLaunchLoading'
|
||||||
import { usePWA } from '@/composables/usePWA'
|
import { usePWA } from '@/composables/usePWA'
|
||||||
import type { DynamicButtonMenuItem } from '@/composables/useDynamicButton'
|
import type { DynamicButtonMenuItem } from '@/composables/useDynamicButton'
|
||||||
|
|
||||||
// 是否显示的输入参数
|
// 是否显示的输入参数
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
showNav: {
|
showNav: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
@@ -19,6 +20,7 @@ defineProps({
|
|||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
// PWA模式检测
|
// PWA模式检测
|
||||||
const { appMode } = usePWA()
|
const { appMode } = usePWA()
|
||||||
|
const { isLaunchLoading } = useLaunchLoading()
|
||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
|
|
||||||
// 判断当前是否为英文环境
|
// 判断当前是否为英文环境
|
||||||
@@ -175,6 +177,8 @@ const visibleDynamicButtonMenuItems = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const hasDynamicButtonMenu = computed(() => visibleDynamicButtonMenuItems.value.length > 0)
|
const hasDynamicButtonMenu = computed(() => visibleDynamicButtonMenuItems.value.length > 0)
|
||||||
|
const shouldRenderFooterNav = computed(() => appMode.value && props.showNav)
|
||||||
|
const shouldRevealFooterNav = computed(() => shouldRenderFooterNav.value && !isLaunchLoading.value)
|
||||||
|
|
||||||
const legacyDynamicMenuTitleKeyMap: Record<string, string> = {
|
const legacyDynamicMenuTitleKeyMap: Record<string, string> = {
|
||||||
'components.subscribeHistory.title': 'dialog.subscribeHistory.title',
|
'components.subscribeHistory.title': 'dialog.subscribeHistory.title',
|
||||||
@@ -212,8 +216,8 @@ function handleDynamicMenuItemClick(item: DynamicButtonMenuItem) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport v-if="appMode && showNav" to="body">
|
<Teleport v-if="shouldRenderFooterNav" to="body">
|
||||||
<div class="footer-nav-container">
|
<div v-show="shouldRevealFooterNav" class="footer-nav-container">
|
||||||
<TransitionGroup name="footer-nav" tag="div" class="footer-nav-group">
|
<TransitionGroup name="footer-nav" tag="div" class="footer-nav-group">
|
||||||
<VCard key="main-nav" elevation="3" class="footer-nav-card border" rounded="pill">
|
<VCard key="main-nav" elevation="3" class="footer-nav-card border" rounded="pill">
|
||||||
<VCardText class="footer-card-content">
|
<VCardText class="footer-card-content">
|
||||||
|
|||||||
Reference in New Issue
Block a user