c9cf644ac5
- 添加 build-and-push.sh 脚本用于自动化构建和推送 Docker 镜像 - 更新 .dockerignore 排除 pnpm-workspace.yaml - 更新项目依赖和配置文件
98 lines
3.4 KiB
Vue
Executable File
98 lines
3.4 KiB
Vue
Executable File
<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> |