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) {
-
-