diff --git a/app/components/layouts/ArcticNewOneHeader.vue b/app/components/layouts/ArcticNewOneHeader.vue index 5e85e93..8f10316 100644 --- a/app/components/layouts/ArcticNewOneHeader.vue +++ b/app/components/layouts/ArcticNewOneHeader.vue @@ -68,6 +68,7 @@ const playHeaderIntro = () => { duration: 1.4, ease: 'power4.out', delay: 0, + overwrite: 'auto', clearProps: 'transform', onComplete: () => { // 动画完成时,强制把 style 的 opacity 移除,防止被内联样式锁死 @@ -76,6 +77,7 @@ const playHeaderIntro = () => { } // 进场动画完毕后,再将滚动消失动画挂载到 ScrollTrigger if (topHeaderTween) { + topHeaderTween.scrollTrigger?.kill() ScrollTrigger.create({ animation: topHeaderTween, start: 0, @@ -93,6 +95,39 @@ const handleHeroReady = () => { playHeaderIntro() } +const cleanupHeroReadyHook = () => { + if (fallbackTimer) { + clearTimeout(fallbackTimer) + fallbackTimer = null + } + if (typeof window !== 'undefined') { + window.removeEventListener('hero-intro-done', handleHeroReady) + } +} + +const hideTopHeader = () => { + if (!topHeaderRef.value) return + gsap.set(topHeaderRef.value, { y: '100vh', opacity: 0 }) +} + +const showTopHeaderInstant = () => { + if (!topHeaderRef.value) return + + gsap.set(topHeaderRef.value, { y: 0, opacity: 1 }) + topHeaderRef.value.style.opacity = '' + + if (topHeaderTween) { + topHeaderTween.scrollTrigger?.kill() + ScrollTrigger.create({ + animation: topHeaderTween, + start: 0, + end: 200, + scrub: true, + }) + ScrollTrigger.refresh() + } +} + onMounted(() => { // 顶部导航在页面刚开始滚动的前 200px 内逐渐上移并隐藏。 topHeaderTween = gsap.to(topHeaderRef.value, { @@ -130,36 +165,34 @@ onMounted(() => { }) ScrollTrigger.refresh() + + if (!appStore.isAppLoading && route.path !== '/') { + showTopHeaderInstant() + } }) -// 监听全局加载状态,当加载完成后再决定 Header 的进场时机 -watch(() => appStore.isAppLoading, (isLoading) => { - if (!isLoading && typeof window !== 'undefined') { - if (route.path === '/') { - // 首页:等待自定义事件 - window.addEventListener('hero-intro-done', handleHeroReady, { once: true }) +watch([() => appStore.isAppLoading, () => route.path], ([isLoading, path]) => { + if (isLoading || typeof window === 'undefined') return - // 增加一个保底的 setTimeout,防止首页的动画事件因为某种原因未能触发 - // 从 loading 结束开始算起,首页的 intro 和 hero 动画加起来大约 3~4 秒 - fallbackTimer = setTimeout(() => { - window.removeEventListener('hero-intro-done', handleHeroReady) - playHeaderIntro() - }, 5000) - } else { - // 非首页:加载完成后直接进场 + cleanupHeroReadyHook() + + if (path === '/') { + hideTopHeader() + + window.addEventListener('hero-intro-done', handleHeroReady, { once: true }) + + fallbackTimer = setTimeout(() => { + window.removeEventListener('hero-intro-done', handleHeroReady) playHeaderIntro() - } + }, 5000) + return } + + showTopHeaderInstant() }, { immediate: true }) onBeforeUnmount(() => { - if (fallbackTimer) { - clearTimeout(fallbackTimer) - fallbackTimer = null - } - if (typeof window !== 'undefined') { - window.removeEventListener('hero-intro-done', handleHeroReady) - } + cleanupHeroReadyHook() topHeaderTween?.scrollTrigger?.kill() topHeaderTween?.kill() bottomHeaderScrollTrigger?.kill() diff --git a/app/pages/index.vue b/app/pages/index.vue index 72cc296..ef5158b 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -27,17 +27,24 @@ useSeoMeta({ const homeIntroRef = ref | null>(null) const homeHeroRef = ref | null>(null) +const hasPlayedIntro = ref(false) // 监听全局加载状态,加载完成后依次播放开场和首屏动画 -watch(() => appStore.isAppLoading, async (isLoading) => { - if (!isLoading) { - await homeIntroRef.value?.play() - await homeHeroRef.value?.play() - - // 动画播放完毕后解除页面滚动锁定 +watch( + () => [appStore.isAppLoading, homeIntroRef.value, homeHeroRef.value] as const, + async ([isLoading, intro, hero]) => { + if (hasPlayedIntro.value) return + if (isLoading || !intro || !hero) return + + hasPlayedIntro.value = true + + await intro.play() + await hero.play() + isIntroActive.value = false - } -}, { immediate: true }) + }, + { immediate: true } +)