feat(页脚): 添加多语言支持和动画效果
重构页脚组件,增加中英文翻译支持 创建 FooterLogo 和 AnimatedButton 通用组件 实现滚动动画和按钮悬停效果
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import gsap from 'gsap'
|
||||
|
||||
interface Props {
|
||||
text: string
|
||||
to?: string
|
||||
theme?: 'dark' | 'light'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
theme: 'dark'
|
||||
})
|
||||
|
||||
const btnRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const onHover = () => {
|
||||
if (!btnRef.value) return
|
||||
const origs = btnRef.value.querySelectorAll('.btn-char-orig')
|
||||
const clones = btnRef.value.querySelectorAll('.btn-char-clone')
|
||||
const bg = btnRef.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 onLeave = () => {
|
||||
if (!btnRef.value) return
|
||||
const origs = btnRef.value.querySelectorAll('.btn-char-orig')
|
||||
const clones = btnRef.value.querySelectorAll('.btn-char-clone')
|
||||
const bg = btnRef.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(() => {
|
||||
if (btnRef.value) {
|
||||
const clones = btnRef.value.querySelectorAll('.btn-char-clone')
|
||||
const bg = btnRef.value.querySelector('.btn-bg-hover')
|
||||
gsap.set(clones, { yPercent: 100 })
|
||||
gsap.set(bg, { yPercent: 100 })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink :to="props.to" class="inline-flex items-center shrink-0">
|
||||
<button
|
||||
ref="btnRef"
|
||||
@mouseenter="onHover"
|
||||
@mouseleave="onLeave"
|
||||
class="relative overflow-hidden px-6 py-3 md:px-7 md:py-3.5 rounded-[2rem] font-semibold text-sm md:text-[1rem] flex items-center justify-center group"
|
||||
:class="[
|
||||
props.theme === 'dark' ? 'bg-black text-white shadow-[0_15px_30px_-5px_rgba(0,0,0,0.3)]' : 'bg-[#1f1f1f] text-white',
|
||||
$attrs.class
|
||||
]"
|
||||
>
|
||||
<!-- 默认背景层,防止文字动画时漏底 -->
|
||||
<div
|
||||
class="absolute inset-0 rounded-[2rem]"
|
||||
:class="props.theme === 'dark' ? 'bg-black' : 'bg-[#1f1f1f]'"
|
||||
></div>
|
||||
|
||||
<!-- Hover 时的背景色块层 -->
|
||||
<div
|
||||
class="btn-bg-hover absolute inset-0 rounded-[2rem] will-change-transform"
|
||||
:class="props.theme === 'dark' ? 'bg-[#333333]' : 'bg-[#2f2f2f]'"
|
||||
></div>
|
||||
|
||||
<!-- 文本层 -->
|
||||
<span class="relative inline-flex overflow-hidden z-10">
|
||||
<span class="inline-flex">
|
||||
<span
|
||||
v-for="(char, index) in props.text.split('')"
|
||||
:key="`orig-${index}`"
|
||||
class="btn-char-orig block will-change-transform"
|
||||
v-html="char === ' ' ? ' ' : char"
|
||||
></span>
|
||||
</span>
|
||||
<span class="absolute inset-0 inline-flex" aria-hidden="true">
|
||||
<span
|
||||
v-for="(char, index) in props.text.split('')"
|
||||
:key="`clone-${index}`"
|
||||
class="btn-char-clone block will-change-transform"
|
||||
v-html="char === ' ' ? ' ' : char"
|
||||
></span>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
@@ -0,0 +1,101 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import gsap from 'gsap'
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger)
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
|
||||
const logoRef = ref<HTMLElement | null>(null)
|
||||
let logoObserver: ReturnType<typeof ScrollTrigger.observe> | null = null
|
||||
|
||||
const handleClick = () => {
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
const logoHover = () => {
|
||||
if (!logoRef.value) return
|
||||
|
||||
logoObserver?.kill()
|
||||
|
||||
const firstChild = logoRef.value.children[0] as HTMLElement
|
||||
const secondChild = logoRef.value.children[1] as HTMLElement
|
||||
|
||||
if (!firstChild || !secondChild) return
|
||||
|
||||
gsap.set(secondChild, {
|
||||
opacity: 0,
|
||||
visibility: 'hidden',
|
||||
y: 0,
|
||||
})
|
||||
|
||||
logoObserver = ScrollTrigger.observe({
|
||||
target: logoRef.value,
|
||||
onHover: (self) => {
|
||||
gsap.to(firstChild, {
|
||||
y: -10,
|
||||
duration: 0.3,
|
||||
ease: 'power2.out',
|
||||
})
|
||||
gsap.to(secondChild, {
|
||||
y: 10,
|
||||
duration: 0.3,
|
||||
ease: 'power2.out',
|
||||
opacity: 1,
|
||||
visibility: 'visible',
|
||||
})
|
||||
},
|
||||
onHoverEnd: (self) => {
|
||||
gsap.to(firstChild, {
|
||||
y: 0,
|
||||
duration: 0.3,
|
||||
ease: 'power2.out',
|
||||
})
|
||||
gsap.to(secondChild, {
|
||||
y: 0,
|
||||
duration: 0.3,
|
||||
ease: 'power2.out',
|
||||
opacity: 0,
|
||||
visibility: 'hidden',
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
logoHover()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
logoObserver?.kill()
|
||||
if (logoRef.value) {
|
||||
gsap.killTweensOf([logoRef.value.children[0], logoRef.value.children[1]])
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center cursor-pointer gap-2" @click="handleClick">
|
||||
<div class="logo-icon flex h-[1.8rem] w-[1.8rem] items-center justify-center rounded-full bg-black overflow-hidden">
|
||||
<video
|
||||
src="/assets/logo.mp4"
|
||||
class="h-[220%] w-[220%] object-cover pointer-events-none"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
playsinline
|
||||
disablepictureinpicture
|
||||
disableremoteplayback
|
||||
controlslist="nodownload noplaybackrate noremoteplayback"
|
||||
/>
|
||||
</div>
|
||||
<div ref="logoRef" class="relative flex items-center overflow-hidden min-w-[70px] md:min-w-[100px] h-10">
|
||||
<span class="absolute left-0 text-[20px] font-bold leading-none tracking-wider text-white">{{ t('app.brand') }}</span>
|
||||
<span class="absolute left-0 text-[12px] font-bold leading-none text-white/60 opacity-0 invisible">{{ t('app.name') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user