feat: implement launch loading state management and footer navigation visibility control

This commit is contained in:
jxxghp
2026-06-12 13:59:59 +08:00
parent 4f328add1b
commit 0594d1d5b2
4 changed files with 54 additions and 13 deletions

View File

@@ -121,6 +121,12 @@
overscroll-behavior: contain;
}
html[data-launch-loading="true"] .footer-nav-container {
opacity: 0 !important;
pointer-events: none !important;
visibility: hidden !important;
}
#loading-bg {
position: fixed;
inset: 0;

View File

@@ -12,6 +12,8 @@ import { addBackgroundTimer, removeBackgroundTimer } from '@/utils/backgroundMan
import PWAInstallPrompt from '@/components/PWAInstallPrompt.vue'
import SharedDialogHost from '@/components/dialog/SharedDialogHost.vue'
import { applyStoredThemeCustomizerAppearance } from '@/composables/useThemeCustomizer'
import { completeLaunchLoading } from '@/composables/useLaunchLoading'
import { usePWA } from '@/composables/usePWA'
import { themeManager } from '@/utils/themeManager'
import { applyDocumentThemeChrome, resolveThemeName } from '@/utils/themePalette'
import { configureApexChartsTheme } from '@/utils/apexCharts'
@@ -45,6 +47,7 @@ setI18nLanguage(localeValue as SupportedLocale)
const authStore = useAuthStore()
const isLogin = computed(() => authStore.token)
const route = useRoute()
const { initializePWA } = usePWA()
// 全局设置store
const globalSettingsStore = useGlobalSettingsStore()
@@ -245,19 +248,25 @@ function scheduleAuthenticatedStateInitialization() {
}
// 添加logo动画效果并延迟移除加载界面
function animateAndRemoveLoader() {
async function animateAndRemoveLoader() {
const loadingBg = document.querySelector('#loading-bg') as HTMLElement
if (loadingBg) {
// 只收掉启动内容,背景层保持实色直到节点被移除,避免底部 safe area 先透出页面内容。
loadingBg.classList.add('loading-complete')
window.setTimeout(() => {
removeEl('#loading-bg')
await new Promise<void>(resolve => {
window.setTimeout(() => {
removeEl('#loading-bg')
// 启动阶段的根节点锁定只在 loader 存在时生效,移除后恢复正常页面与弹窗布局。
document.documentElement.removeAttribute('data-launch-loading')
document.documentElement.style.removeProperty('overflow')
document.body.style.removeProperty('overflow')
}, 120)
// 启动阶段的根节点锁定只在 loader 存在时生效,移除后恢复正常页面与弹窗布局。
document.documentElement.removeAttribute('data-launch-loading')
document.documentElement.style.removeProperty('overflow')
document.body.style.removeProperty('overflow')
completeLaunchLoading()
resolve()
}, 120)
})
} else {
completeLaunchLoading()
}
}
@@ -274,13 +283,15 @@ async function removeLoadingWithStateCheck() {
}
globalLoadingStateManager.setLoadingState('pwa-state', false)
// PWA/App 模式会影响布局和底部导航,必须在启动屏退场前稳定下来。
await initializePWA()
await initializeAuthenticatedState()
// 等待所有加载完成
await globalLoadingStateManager.waitForAllComplete()
// 移除加载界面
animateAndRemoveLoader()
await animateAndRemoveLoader()
// 检查未读消息
if (isLogin.value) {
@@ -289,7 +300,7 @@ async function removeLoadingWithStateCheck() {
} catch (error) {
// 即使出错也要移除加载界面
globalLoadingStateManager.reset()
animateAndRemoveLoader()
await animateAndRemoveLoader()
}
}

View File

@@ -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),
}
}

View File

@@ -5,11 +5,12 @@ import { NavMenu } from '@/@layouts/types'
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/stores'
import { buildUserPermissionContext, filterItemsByPermission, filterMenusByPermission, hasItemPermission } from '@/utils/permission'
import { useLaunchLoading } from '@/composables/useLaunchLoading'
import { usePWA } from '@/composables/usePWA'
import type { DynamicButtonMenuItem } from '@/composables/useDynamicButton'
// 是否显示的输入参数
defineProps({
const props = defineProps({
showNav: {
type: Boolean,
default: true,
@@ -19,6 +20,7 @@ defineProps({
const display = useDisplay()
// PWA模式检测
const { appMode } = usePWA()
const { isLaunchLoading } = useLaunchLoading()
const { t, locale } = useI18n()
// 判断当前是否为英文环境
@@ -175,6 +177,8 @@ const visibleDynamicButtonMenuItems = computed(() => {
})
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> = {
'components.subscribeHistory.title': 'dialog.subscribeHistory.title',
@@ -212,8 +216,8 @@ function handleDynamicMenuItemClick(item: DynamicButtonMenuItem) {
</script>
<template>
<Teleport v-if="appMode && showNav" to="body">
<div class="footer-nav-container">
<Teleport v-if="shouldRenderFooterNav" to="body">
<div v-show="shouldRevealFooterNav" class="footer-nav-container">
<TransitionGroup name="footer-nav" tag="div" class="footer-nav-group">
<VCard key="main-nav" elevation="3" class="footer-nav-card border" rounded="pill">
<VCardText class="footer-card-content">