From 5bb0c344b9823a07ade828a5a0cfc29b122ea5f5 Mon Sep 17 00:00:00 2001 From: chenchun Date: Mon, 27 Apr 2026 15:41:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(header):=20=E6=B7=BB=E5=8A=A0logo=E6=82=AC?= =?UTF-8?q?=E5=81=9C=E5=8A=A8=E7=94=BB=E6=95=88=E6=9E=9C=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E4=BB=A3=E7=A0=81=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加logo元素的悬停动画效果,当鼠标悬停时显示品牌名称和副标题的动画过渡 优化代码格式,包括调整缩进和注释掉未使用的Observer插件 重构logo部分的DOM结构,使用绝对定位确保布局稳定性 --- app/components/layouts/ArcticNewOneHeader.vue | 108 +++++++++++++----- 1 file changed, 80 insertions(+), 28 deletions(-) diff --git a/app/components/layouts/ArcticNewOneHeader.vue b/app/components/layouts/ArcticNewOneHeader.vue index 6a20c99..f563f3c 100644 --- a/app/components/layouts/ArcticNewOneHeader.vue +++ b/app/components/layouts/ArcticNewOneHeader.vue @@ -7,13 +7,14 @@ import { ScrollTrigger } from 'gsap/ScrollTrigger' import { useAppStore } from '~/stores/app' gsap.registerPlugin(ScrollTrigger) +// gsap.registerPlugin(Observer) const { t } = useI18n() const route = useRoute() const appStore = useAppStore() // 导航菜单数据配置 -const navItems = computed>(() => [ +const navItems = computed>(() => [ { name: t('nav.home'), path: '/' }, { name: t('nav.studio'), path: '/studio' }, { name: t('nav.work'), path: '/work' }, @@ -23,8 +24,10 @@ const navItems = computed>(() => [ const topHeaderRef = ref(null) const bottomHeaderRef = ref(null) +const logoRef = ref(null) let topHeaderTween: gsap.core.Tween | null = null let bottomHeaderScrollTrigger: ReturnType | null = null +let logoObserver: ReturnType | null = null let isBottomHeaderVisible = false let fallbackTimer: ReturnType | null = null @@ -54,17 +57,17 @@ const playHeaderIntro = () => { fallbackTimer = null } - gsap.fromTo(topHeaderRef.value, + gsap.fromTo(topHeaderRef.value, { - y: '100vh', + y: '100vh', opacity: 0 // 恢复初始透明度为0,让它在屏幕最底部时是隐形的 }, { - y: 0, + y: 0, opacity: 1, // 伴随飞入的过程逐渐完全显示 - duration: 1.4, - ease: 'power4.out', - delay: 0, + duration: 1.4, + ease: 'power4.out', + delay: 0, clearProps: 'transform', onComplete: () => { // 动画完成时,强制把 style 的 opacity 移除,防止被内联样式锁死 @@ -82,7 +85,7 @@ const playHeaderIntro = () => { ScrollTrigger.refresh() } } - }) + }) } // 监听自定义事件,当首页的 Hero 动画完成或到达特定阶段时触发导航栏入场 @@ -90,6 +93,45 @@ const handleHeroReady = () => { playHeaderIntro() } +// Logo 文字Hover 动画 +const logoHover = () => { + if (!logoRef.value) return + + logoObserver?.kill() + + logoObserver = ScrollTrigger.observe({ + target: logoRef.value, + onHover: (self) => { + gsap.to(self.target.children[0] as HTMLElement, { + y: -10, + duration: 0.3, + ease: 'power2.out', + }) + gsap.to(self.target.children[1] as HTMLElement, { + y: 10, + duration: 0.3, + ease: 'power2.out', + opacity: 1, + visibility: 'visible', + }) + }, + onHoverEnd: (self) => { + gsap.to(self.target.children[0] as HTMLElement, { + y: 0, + duration: 0.3, + ease: 'power2.out', + }) + gsap.to(self.target.children[1] as HTMLElement, { + y: 0, + duration: 0.3, + ease: 'power2.out', + opacity: 0, + visibility: 'hidden', + }) + }, + }) +} + onMounted(() => { // 顶部导航在页面刚开始滚动的前 200px 内逐渐上移并隐藏。 topHeaderTween = gsap.to(topHeaderRef.value, { @@ -125,6 +167,9 @@ onMounted(() => { }) ScrollTrigger.refresh() + + // Logo 文字Hover 动画 + logoHover() }) // 监听全局加载状态,当加载完成后再决定 Header 的进场时机 @@ -133,13 +178,13 @@ watch(() => appStore.isAppLoading, (isLoading) => { if (route.path === '/') { // 首页:等待自定义事件 window.addEventListener('hero-intro-done', handleHeroReady, { once: true }) - + // 增加一个保底的 setTimeout,防止首页的动画事件因为某种原因未能触发 // 从 loading 结束开始算起,首页的 intro 和 hero 动画加起来大约 3~4 秒 fallbackTimer = setTimeout(() => { window.removeEventListener('hero-intro-done', handleHeroReady) playHeaderIntro() - }, 5000) + }, 5000) } else { // 非首页:加载完成后直接进场 playHeaderIntro() @@ -158,21 +203,28 @@ onBeforeUnmount(() => { topHeaderTween?.scrollTrigger?.kill() topHeaderTween?.kill() bottomHeaderScrollTrigger?.kill() + logoObserver?.kill() topHeaderTween = null bottomHeaderScrollTrigger = null + logoObserver = null isBottomHeaderVisible = false if (topHeaderRef.value && bottomHeaderRef.value) { gsap.killTweensOf([topHeaderRef.value, bottomHeaderRef.value]) } + if (logoRef.value) { + const brand = logoRef.value.querySelector('#logo-brand') + const name = logoRef.value.querySelector('#logo-name') + if (brand && name) { + gsap.killTweensOf([brand, name]) + } + } })