feat(home): 为首页对话按钮添加悬停动画效果

添加GSAP动画效果,当鼠标悬停在对话按钮上时,文字会向上滑动并显示克隆文字,同时背景色块从底部滑入。离开时恢复原始状态。
This commit is contained in:
2026-04-27 17:51:47 +08:00
parent be0bf01845
commit 5390b70fa9
+66 -2
View File
@@ -18,6 +18,7 @@ const heroRef = ref<HTMLElement | null>(null)
const heroPanelRef = ref<HTMLElement | null>(null) const heroPanelRef = ref<HTMLElement | null>(null)
const brandNameRef = ref<HTMLElement | null>(null) const brandNameRef = ref<HTMLElement | null>(null)
const studioNameRef = ref<HTMLElement | null>(null) const studioNameRef = ref<HTMLElement | null>(null)
const talkBtnRef = ref<HTMLElement | null>(null)
let cleanupHeroScroll: (() => void) | null = null let cleanupHeroScroll: (() => void) | null = null
@@ -112,7 +113,39 @@ const setupHeroScroll = () => {
cleanupHeroScroll = () => gsap.ticker.remove(updateHeroPanelScale) cleanupHeroScroll = () => gsap.ticker.remove(updateHeroPanelScale)
} }
const onTalkHover = () => {
if (!talkBtnRef.value) return
const origs = talkBtnRef.value.querySelectorAll('.btn-char-orig')
const clones = talkBtnRef.value.querySelectorAll('.btn-char-clone')
const bg = talkBtnRef.value.querySelector('.btn-bg-hover')
// 背景从底部滑入 (yPercent: 100 -> 0)
gsap.to(bg, { yPercent: 0, duration: 0.4, ease: 'power3.out', overwrite: true })
gsap.to(origs, { yPercent: -100, duration: 0.4, stagger: { amount: 0.2, from: 'start' }, ease: 'power3.out', overwrite: true })
gsap.to(clones, { yPercent: 0, duration: 0.4, stagger: { amount: 0.2, from: 'start' }, ease: 'power3.out', overwrite: true })
}
const onTalkLeave = () => {
if (!talkBtnRef.value) return
const origs = talkBtnRef.value.querySelectorAll('.btn-char-orig')
const clones = talkBtnRef.value.querySelectorAll('.btn-char-clone')
const bg = talkBtnRef.value.querySelector('.btn-bg-hover')
// 背景向下反向滑出回收 (yPercent: 0 -> 100)
gsap.to(bg, { yPercent: 100, duration: 0.4, ease: 'power3.out', overwrite: true })
gsap.to(origs, { yPercent: 0, duration: 0.4, stagger: { amount: 0.2, from: 'end' }, ease: 'power3.out', overwrite: true })
gsap.to(clones, { yPercent: 100, duration: 0.4, stagger: { amount: 0.2, from: 'end' }, ease: 'power3.out', overwrite: true })
}
onMounted(() => { onMounted(() => {
if (talkBtnRef.value) {
const clones = talkBtnRef.value.querySelectorAll('.btn-char-clone')
const bg = talkBtnRef.value.querySelector('.btn-bg-hover')
gsap.set(clones, { yPercent: 100 })
gsap.set(bg, { yPercent: 100 })
}
// 组件挂载时,先将整个 Hero 强行置于屏幕下方,等待外部调用 `play()` 才滑入 // 组件挂载时,先将整个 Hero 强行置于屏幕下方,等待外部调用 `play()` 才滑入
if (heroRef.value) gsap.set(heroRef.value, { yPercent: 100 }) if (heroRef.value) gsap.set(heroRef.value, { yPercent: 100 })
// 初始化滚动监听逻辑 // 初始化滚动监听逻辑
@@ -178,8 +211,39 @@ defineExpose({ play })
{{ t('hero.description') }} {{ t('hero.description') }}
</p> </p>
<div> <div>
<button class="bg-black text-white px-7 py-3.5 rounded-[2rem] font-semibold text-base transition-transform hover:scale-105 shadow-[0_15px_30px_-5px_rgba(0,0,0,0.3)]"> <button
{{ t('hero.letsTalk') }} ref="talkBtnRef"
@mouseenter="onTalkHover"
@mouseleave="onTalkLeave"
class="relative overflow-hidden bg-black text-white px-7 py-3.5 rounded-[2rem] font-semibold text-[1rem] shadow-[0_15px_30px_-5px_rgba(0,0,0,0.3)] flex items-center justify-center group"
>
<!-- 默认黑色背景层防止文字动画时漏底 -->
<div class="absolute inset-0 bg-black rounded-[2rem]"></div>
<!-- Hover 时的灰色背景色块层 -->
<div
class="btn-bg-hover absolute inset-0 bg-[#333333] rounded-[2rem] will-change-transform"
></div>
<!-- 文本层 -->
<span class="relative inline-flex overflow-hidden z-10">
<span class="inline-flex">
<span
v-for="(char, index) in t('hero.letsTalk').split('')"
:key="`orig-${index}`"
class="btn-char-orig block will-change-transform"
v-html="char === ' ' ? '&nbsp;' : char"
></span>
</span>
<span class="absolute inset-0 inline-flex" aria-hidden="true">
<span
v-for="(char, index) in t('hero.letsTalk').split('')"
:key="`clone-${index}`"
class="btn-char-clone block will-change-transform"
v-html="char === ' ' ? '&nbsp;' : char"
></span>
</span>
</span>
</button> </button>
</div> </div>
</div> </div>