diff --git a/app/components/layouts/ArcticNewOneHeader.vue b/app/components/layouts/ArcticNewOneHeader.vue index f5bea92..4c29a80 100644 --- a/app/components/layouts/ArcticNewOneHeader.vue +++ b/app/components/layouts/ArcticNewOneHeader.vue @@ -37,22 +37,81 @@ const setBottomHeaderVisible = (visible: boolean) => { } onMounted(() => { - // 底部导航默认藏在视口下方,只有滚动到深处并向上滚时才出现。 - gsap.set(bottomHeaderRef.value, { - yPercent: 140, - autoAlpha: 0, - }) + // 移除之前这里强制设置透明度的代码,直接依靠 css 控制 + // gsap.set(topHeaderRef.value, { y: '100vh', opacity: 0 }) // 顶部导航在页面刚开始滚动的前 200px 内逐渐上移并隐藏。 topHeaderTween = gsap.to(topHeaderRef.value, { yPercent: -140, autoAlpha: 0, ease: 'none', - scrollTrigger: { - start: 0, - end: 200, - scrub: true, - }, + paused: true, // 初始不执行,等进场动画完毕再绑定到 ScrollTrigger + }) + + // 定义一个供外部或延迟调用的入场动画方法 + const playHeaderIntro = () => { + if (!topHeaderRef.value) return + + gsap.fromTo(topHeaderRef.value, + { + y: '100vh', + opacity: 0 // 恢复初始透明度为0,让它在屏幕最底部时是隐形的 + }, + { + y: 0, + opacity: 1, // 伴随飞入的过程逐渐完全显示 + duration: 1.4, + ease: 'power4.out', + delay: 0, + clearProps: 'transform', + onComplete: () => { + // 动画完成时,强制把 style 的 opacity 移除,防止被内联样式锁死 + if (topHeaderRef.value) { + topHeaderRef.value.style.opacity = '' + // 进场动画完毕后,再将滚动消失动画挂载到 ScrollTrigger + if (topHeaderTween) { + ScrollTrigger.create({ + animation: topHeaderTween, + start: 0, + end: 200, + scrub: true, + }) + ScrollTrigger.refresh() + } + } + } + }) + } + + // 监听自定义事件,当首页的 Hero 动画完成或到达特定阶段时触发导航栏入场 + const handleHeroReady = () => { + playHeaderIntro() + } + + // 如果是在客户端执行,判断是否在首页 + if (typeof window !== 'undefined') { + // 使用 setTimeout 宏任务确保所有 GSAP 设置生效,并且跳过初始渲染卡顿 + setTimeout(() => { + if (window.location.pathname === '/') { + // 首页:等待自定义事件 + window.addEventListener('hero-intro-done', handleHeroReady, { once: true }) + + // 增加一个保底的 setTimeout,防止首页的动画事件因为某种原因未能触发(例如用户快速切换路由) + setTimeout(() => { + // 不再判断太复杂的 opacity 计算,只要 3.5s 到了,且 topHeaderRef 还有,直接执行,防止各种 bug 导致锁死 + playHeaderIntro() + }, 3500) // 等待 3.5 秒(足够首页 intro 动画执行完) + } else { + // 非首页的保底进场 + playHeaderIntro() + } + }, 0) + } + + // 底部导航默认藏在视口下方,只有滚动到深处并向上滚时才出现。 + gsap.set(bottomHeaderRef.value, { + yPercent: 140, + autoAlpha: 0, }) bottomHeaderScrollTrigger = ScrollTrigger.create({ @@ -93,6 +152,7 @@ onBeforeUnmount(() => {
@@ -112,18 +172,18 @@ onBeforeUnmount(() => { diff --git a/app/pages/index.vue b/app/pages/index.vue index a09fc0a..a553354 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -252,15 +252,35 @@ onMounted(() => { yPercent: 0, duration: 1.05, ease: 'power4.inOut', + onUpdate: function() { + // 当 Hero (白色背景) 的 yPercent 运动到 40% 的时候(即大部分背景已经上浮到了屏幕内,还没完全贴顶),触发导航栏出场 + const progress = this.progress() + // 使用一个标记防止重复触发 + if (progress > 0.6 && !this._headerTriggered) { + this._headerTriggered = true + window.dispatchEvent(new Event('hero-intro-done')) + } + }, + onStart: function() { + this._headerTriggered = false + } }, 'introOut') - if (topHeader) { - introTimeline.to(topHeader, { - yPercent: 0, - autoAlpha: 1, - duration: 0.55, - ease: 'power3.out', - }, 'introOut+=0.45') - } + // 取消原本通过 index.vue 直接控制导航栏 yPercent 的行为 + // 因为我们要让 Header 自己执行从屏幕底部(y: 100vh)飞入的高级动画 + // if (topHeader) { + // introTimeline.to(topHeader, { + // yPercent: 0, + // autoAlpha: 1, + // duration: 0.55, + // ease: 'power3.out', + // }, 'introOut+=0.45') + // } + + // 在动画即将完成或者 Hero 入场时触发自定义事件,通知导航栏执行自身的从下往上飞入动画 + // 将触发时机再延后,等白色背景开始渲染的瞬间就触发 + // introTimeline.add(() => { + // window.dispatchEvent(new Event('hero-intro-done')) + // }, 'introOut') } const releasePageScroll = () => { @@ -283,9 +303,6 @@ onMounted(() => { gsap.set(introCharElements, { y: 0, yPercent: 110 }) gsap.set(tmElement, { autoAlpha: 0 }) gsap.set(heroElement, { yPercent: 100 }) - if (topHeader) { - gsap.set(topHeader, { yPercent: -140, autoAlpha: 0 }) - } setupHeroScroll() loaderTween = gsap.to(loaderBarElement, {