feat(header): 添加logo悬停动画效果并优化代码格式
添加logo元素的悬停动画效果,当鼠标悬停时显示品牌名称和副标题的动画过渡 优化代码格式,包括调整缩进和注释掉未使用的Observer插件 重构logo部分的DOM结构,使用绝对定位确保布局稳定性
This commit is contained in:
@@ -7,13 +7,14 @@ import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
|||||||
import { useAppStore } from '~/stores/app'
|
import { useAppStore } from '~/stores/app'
|
||||||
|
|
||||||
gsap.registerPlugin(ScrollTrigger)
|
gsap.registerPlugin(ScrollTrigger)
|
||||||
|
// gsap.registerPlugin(Observer)
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
|
||||||
// 导航菜单数据配置
|
// 导航菜单数据配置
|
||||||
const navItems = computed<Array<{name: string, path: string}>>(() => [
|
const navItems = computed<Array<{ name: string, path: string }>>(() => [
|
||||||
{ name: t('nav.home'), path: '/' },
|
{ name: t('nav.home'), path: '/' },
|
||||||
{ name: t('nav.studio'), path: '/studio' },
|
{ name: t('nav.studio'), path: '/studio' },
|
||||||
{ name: t('nav.work'), path: '/work' },
|
{ name: t('nav.work'), path: '/work' },
|
||||||
@@ -23,8 +24,10 @@ const navItems = computed<Array<{name: string, path: string}>>(() => [
|
|||||||
|
|
||||||
const topHeaderRef = ref<HTMLElement | null>(null)
|
const topHeaderRef = ref<HTMLElement | null>(null)
|
||||||
const bottomHeaderRef = ref<HTMLElement | null>(null)
|
const bottomHeaderRef = ref<HTMLElement | null>(null)
|
||||||
|
const logoRef = ref<HTMLElement | null>(null)
|
||||||
let topHeaderTween: gsap.core.Tween | null = null
|
let topHeaderTween: gsap.core.Tween | null = null
|
||||||
let bottomHeaderScrollTrigger: ReturnType<typeof ScrollTrigger.create> | null = null
|
let bottomHeaderScrollTrigger: ReturnType<typeof ScrollTrigger.create> | null = null
|
||||||
|
let logoObserver: ReturnType<typeof ScrollTrigger.observe> | null = null
|
||||||
let isBottomHeaderVisible = false
|
let isBottomHeaderVisible = false
|
||||||
let fallbackTimer: ReturnType<typeof setTimeout> | null = null
|
let fallbackTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
@@ -90,6 +93,45 @@ const handleHeroReady = () => {
|
|||||||
playHeaderIntro()
|
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(() => {
|
onMounted(() => {
|
||||||
// 顶部导航在页面刚开始滚动的前 200px 内逐渐上移并隐藏。
|
// 顶部导航在页面刚开始滚动的前 200px 内逐渐上移并隐藏。
|
||||||
topHeaderTween = gsap.to(topHeaderRef.value, {
|
topHeaderTween = gsap.to(topHeaderRef.value, {
|
||||||
@@ -125,6 +167,9 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ScrollTrigger.refresh()
|
ScrollTrigger.refresh()
|
||||||
|
|
||||||
|
// Logo 文字Hover 动画
|
||||||
|
logoHover()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听全局加载状态,当加载完成后再决定 Header 的进场时机
|
// 监听全局加载状态,当加载完成后再决定 Header 的进场时机
|
||||||
@@ -158,21 +203,28 @@ onBeforeUnmount(() => {
|
|||||||
topHeaderTween?.scrollTrigger?.kill()
|
topHeaderTween?.scrollTrigger?.kill()
|
||||||
topHeaderTween?.kill()
|
topHeaderTween?.kill()
|
||||||
bottomHeaderScrollTrigger?.kill()
|
bottomHeaderScrollTrigger?.kill()
|
||||||
|
logoObserver?.kill()
|
||||||
topHeaderTween = null
|
topHeaderTween = null
|
||||||
bottomHeaderScrollTrigger = null
|
bottomHeaderScrollTrigger = null
|
||||||
|
logoObserver = null
|
||||||
isBottomHeaderVisible = false
|
isBottomHeaderVisible = false
|
||||||
if (topHeaderRef.value && bottomHeaderRef.value) {
|
if (topHeaderRef.value && bottomHeaderRef.value) {
|
||||||
gsap.killTweensOf([topHeaderRef.value, bottomHeaderRef.value])
|
gsap.killTweensOf([topHeaderRef.value, bottomHeaderRef.value])
|
||||||
}
|
}
|
||||||
|
if (logoRef.value) {
|
||||||
|
const brand = logoRef.value.querySelector<HTMLElement>('#logo-brand')
|
||||||
|
const name = logoRef.value.querySelector<HTMLElement>('#logo-name')
|
||||||
|
if (brand && name) {
|
||||||
|
gsap.killTweensOf([brand, name])
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header
|
<header ref="topHeaderRef"
|
||||||
ref="topHeaderRef"
|
|
||||||
class="site-header-top fixed left-0 right-0 top-0 z-50 mx-auto flex h-[100px] w-page-container max-w-[calc(100%-2rem)] items-center justify-between px-4 py-5"
|
class="site-header-top fixed left-0 right-0 top-0 z-50 mx-auto flex h-[100px] w-page-container max-w-[calc(100%-2rem)] items-center justify-between px-4 py-5"
|
||||||
style="opacity: 0; transform: translateY(100vh);"
|
style="opacity: 0; transform: translateY(100vh);">
|
||||||
>
|
|
||||||
<!-- 左侧:Logo 和 标题 -->
|
<!-- 左侧:Logo 和 标题 -->
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<div class="logo-icon flex h-10 w-10 items-center justify-center rounded-full bg-black text-white">
|
<div class="logo-icon flex h-10 w-10 items-center justify-center rounded-full bg-black text-white">
|
||||||
@@ -183,22 +235,24 @@ onBeforeUnmount(() => {
|
|||||||
<path d="M2 12h20" />
|
<path d="M2 12h20" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col justify-center">
|
<!-- 改为 relative 并且把子元素设置 absolute 以保证布局不受干扰 -->
|
||||||
<span class="text-base font-bold leading-none tracking-wider text-black">{{ t('app.brand') }}</span>
|
<div ref="logoRef" class="relative flex h-10 items-center overflow-hidden cursor-pointer" id="logo"
|
||||||
<span class="mt-1 text-xs leading-none text-gray-500">{{ t('app.name') }}</span>
|
style="min-width: 100px;">
|
||||||
|
<span class="absolute left-0 text-base font-bold leading-none tracking-wider text-black" id="logo-brand">{{
|
||||||
|
t('app.brand') }}</span>
|
||||||
|
<!-- 将 name 默认隐藏 -->
|
||||||
|
<span class="absolute left-0 text-xs leading-none text-gray-500 invisible" id="logo-name">{{
|
||||||
|
t('app.name') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 中间:导航链接 -->
|
<!-- 中间:导航链接 -->
|
||||||
<nav class="hidden md:flex items-center gap-10 lg:gap-16">
|
<nav class="hidden md:flex items-center gap-10 lg:gap-16">
|
||||||
<NuxtLink
|
<NuxtLink v-for="item in navItems" :key="item.name" :to="item.path"
|
||||||
v-for="item in navItems"
|
|
||||||
:key="item.name"
|
|
||||||
:to="item.path"
|
|
||||||
class="nav-link-item group flex items-center overflow-hidden font-semibold tracking-[-0.7px] text-black"
|
class="nav-link-item group flex items-center overflow-hidden font-semibold tracking-[-0.7px] text-black"
|
||||||
style="font-size: 1rem; justify-content: flex-start; position: relative;"
|
style="font-size: 1rem; justify-content: flex-start; position: relative;">
|
||||||
>
|
<div
|
||||||
<div class="flex items-center transition-transform duration-300 group-hover:translate-x-0 -translate-x-[18px] pr-[18px]">
|
class="flex items-center transition-transform duration-300 group-hover:translate-x-0 -translate-x-[18px] pr-[18px]">
|
||||||
<span class="mr-2 h-2.5 w-2.5 rounded-full bg-black shrink-0"></span>
|
<span class="mr-2 h-2.5 w-2.5 rounded-full bg-black shrink-0"></span>
|
||||||
<span>{{ item.name }}</span>
|
<span>{{ item.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -212,11 +266,9 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<header
|
<header ref="bottomHeaderRef"
|
||||||
ref="bottomHeaderRef"
|
|
||||||
class="fixed bottom-0 left-0 right-0 z-50 mx-auto h-[100px] w-page-container max-w-[calc(100%-2rem)] bg-blue-200 px-4 py-5"
|
class="fixed bottom-0 left-0 right-0 z-50 mx-auto h-[100px] w-page-container max-w-[calc(100%-2rem)] bg-blue-200 px-4 py-5"
|
||||||
style="visibility: hidden; opacity: 0;"
|
style="visibility: hidden; opacity: 0;">
|
||||||
>
|
|
||||||
<nav>Bottom Navigation</nav>
|
<nav>Bottom Navigation</nav>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user