diff --git a/index.html b/index.html index 40a417ff..0321b870 100644 --- a/index.html +++ b/index.html @@ -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; diff --git a/src/App.vue b/src/App.vue index 4168de46..405b3c46 100644 --- a/src/App.vue +++ b/src/App.vue @@ -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(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() } } diff --git a/src/composables/useLaunchLoading.ts b/src/composables/useLaunchLoading.ts new file mode 100644 index 00000000..38353633 --- /dev/null +++ b/src/composables/useLaunchLoading.ts @@ -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), + } +} diff --git a/src/layouts/components/Footer.vue b/src/layouts/components/Footer.vue index bec59e7d..9a364cef 100644 --- a/src/layouts/components/Footer.vue +++ b/src/layouts/components/Footer.vue @@ -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 = { 'components.subscribeHistory.title': 'dialog.subscribeHistory.title', @@ -212,8 +216,8 @@ function handleDynamicMenuItemClick(item: DynamicButtonMenuItem) {