Spaces:
Sleeping
Sleeping
<template> | |
<div ref="containerRef" class="thanks-container"> | |
<div ref="titleRef" class="title">感谢Ta们对项目的赞赏</div> | |
<!-- 添加说明文字 --> | |
<div class="description"> | |
<p>感谢每一位支持者的信任与鼓励</p> | |
<p>正是你们的支持让这个项目能够持续发展</p> | |
</div> | |
<div ref="sponsorsContainer" class="sponsors-container"> | |
<div | |
v-for="(sponsor, index) in randomizedSponsors" | |
:key="sponsor.name" | |
ref="avatarRefs" | |
class="sponsor-avatar" | |
@mouseenter="handleMouseEnter(index)" | |
@mouseleave="handleMouseLeave" | |
> | |
<div | |
ref="avatarWrapperRefs" | |
class="avatar-wrapper" | |
:class="{ | |
active: activeIndex === index, | |
'has-link': sponsor.link, | |
}" | |
@click="handleAvatarClick(sponsor.link)" | |
> | |
<div class="avatar-inner"> | |
<div class="avatar-overlay"></div> | |
<img :src="sponsor.avatar" :alt="sponsor.name" class="avatar-img" /> | |
<div class="name-tag"> | |
{{ sponsor.name }} | |
</div> | |
</div> | |
</div> | |
<div v-if="activeIndex === index && sponsor.message" class="dialog-box"> | |
<div class="dialog-content"> | |
<div :id="`typeIt-${index}`" class="type-it-container"></div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- 添加赞赏按钮 --> | |
<a | |
:href="PROJECT_GITHUB + '?tab=readme-ov-file#支持项目'" | |
target="_blank" | |
class="sponsor-button" | |
@mouseenter="handleButtonHover" | |
@mouseleave="handleButtonLeave" | |
> | |
<div class="button-content"> | |
<svg class="heart-icon" viewBox="0 0 24 24"> | |
<path | |
d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" | |
/> | |
</svg> | |
<span>赞赏支持</span> | |
</div> | |
</a> | |
</div> | |
</template> | |
<script setup> | |
import { ref, onMounted, nextTick, computed, onBeforeUnmount } from "vue"; | |
import TypeIt from "typeit"; | |
import { userApi } from "@/api/user"; | |
import gsap from "gsap"; | |
import { PROJECT_GITHUB } from "@/constants/project"; | |
// 赞助者数据 | |
const sponsors = ref([]); | |
const getSponsors = async () => { | |
const res = await userApi.getSponsors(); | |
sponsors.value = res.data; | |
}; | |
// 随机排序赞助者 | |
const randomizedSponsors = computed(() => { | |
// 有sort的按照sort排序并排在前面,没有的按照随机排序 | |
const sortedSponsors = [...sponsors.value] | |
.filter((item) => item.sort) | |
.sort((a, b) => a.sort - b.sort); | |
const randomSponsors = [...sponsors.value] | |
.filter((item) => !item.sort) | |
.sort(() => Math.random() - 0.5); | |
return [...sortedSponsors, ...randomSponsors]; | |
}); | |
const containerRef = ref(null); | |
const sponsorsContainer = ref(null); | |
const activeIndex = ref(null); | |
const avatarRefs = ref([]); | |
const avatarWrapperRefs = ref([]); | |
let typeItInstance = null; | |
const activeCenter = ref({ x: 0, y: 0 }); | |
const titleRef = ref(null); | |
// 添加头像动画时间轴的引用 | |
const avatarTimelines = ref([]); | |
// 使用 requestAnimationFrame 优化动画更新 | |
let rafId = null; | |
// 添加一个变量来跟踪当前激活的头像 | |
let currentHoverIndex = null; | |
onMounted(async () => { | |
await getSponsors(); | |
// 修改页面入场动画 | |
const tl = gsap.timeline({ | |
defaults: { ease: "power3.out" }, | |
}); | |
// 同时执行所有元素的动画 | |
tl.from([titleRef.value, sponsorsContainer.value, ...avatarWrapperRefs.value], { | |
y: -20, | |
opacity: 0, | |
duration: 0.6, | |
stagger: { | |
amount: 0.3, | |
from: "start", | |
}, | |
ease: "back.out(1.2)", | |
}); | |
// 添加可见性变化监听 | |
document.addEventListener("visibilitychange", handleVisibilityChange); | |
// 添加窗口失焦事件处理 | |
window.addEventListener("blur", handleMouseLeave); | |
}); | |
// 修改 handleVisibilityChange 函数 | |
const handleVisibilityChange = () => { | |
if (document.hidden) { | |
// 页面不可见时清理资源 | |
if (typeItInstance) { | |
typeItInstance.destroy(); | |
typeItInstance = null; | |
} | |
} | |
}; | |
// 修改鼠标移入处理函数 | |
const handleMouseEnter = (() => { | |
let timeout; | |
return async (index) => { | |
if (timeout) { | |
clearTimeout(timeout); | |
} | |
currentHoverIndex = index; | |
activeIndex.value = index; | |
timeout = setTimeout(async () => { | |
// 确保这是最新的hover状态 | |
if (currentHoverIndex !== index) return; | |
const activeAvatar = avatarWrapperRefs.value[index]; | |
if (activeAvatar) { | |
const rect = activeAvatar.getBoundingClientRect(); | |
activeCenter.value = { | |
x: rect.left + rect.width / 2, | |
y: rect.top + rect.height / 2, | |
}; | |
} | |
// 暂停所有浮动动画 | |
avatarTimelines.value.forEach((timeline) => { | |
if (timeline) { | |
timeline.pause(); | |
} | |
}); | |
updateAvatarsEffect(index); | |
await nextTick(); | |
try { | |
// 初始化打字效果 | |
if (typeItInstance) { | |
typeItInstance.destroy(); | |
typeItInstance = null; | |
} | |
const typeItElement = document.getElementById(`typeIt-${index}`); | |
if (typeItElement) { | |
typeItInstance = new TypeIt(typeItElement, { | |
strings: randomizedSponsors.value[index].message, | |
speed: 20, | |
waitUntilVisible: true, | |
}).go(); | |
} | |
} catch (error) { | |
console.error("TypeIt初始化错误:", error); | |
} | |
}, 16); | |
}; | |
})(); | |
// 更新所有头像效果 | |
const updateAvatarsEffect = (activeIndex) => { | |
if (!avatarWrapperRefs.value || activeCenter.value.x === 0) return; | |
if (rafId) { | |
cancelAnimationFrame(rafId); | |
} | |
rafId = requestAnimationFrame(() => { | |
avatarWrapperRefs.value.forEach((wrapper, index) => { | |
const inner = wrapper.querySelector(".avatar-inner"); | |
const avatarContainer = wrapper.closest(".sponsor-avatar"); | |
if (index === activeIndex) { | |
gsap.to(inner, { | |
scale: 1.2, | |
y: -15, | |
zIndex: 10, | |
duration: 0.2, | |
ease: "back.out(1.5)", | |
force3D: true, | |
}); | |
gsap.to(avatarContainer, { | |
filter: "drop-shadow(0 20px 30px rgba(0, 0, 0, 0.25))", | |
duration: 0.2, | |
}); | |
const activeOverlay = wrapper.querySelector(".avatar-overlay"); | |
gsap.to(activeOverlay, { | |
opacity: 0, | |
duration: 0.15, | |
}); | |
return; | |
} | |
const rect = wrapper.getBoundingClientRect(); | |
const centerX = rect.left + rect.width / 2; | |
const centerY = rect.top + rect.height / 2; | |
const deltaX = activeCenter.value.x - centerX; | |
const deltaY = activeCenter.value.y - centerY; | |
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); | |
if (distance < 0.1) return; | |
const maxDistance = 400; | |
const strength = Math.max(0, 1 - distance / maxDistance); | |
// 计算吸引力效果 | |
const attractionStrength = Math.pow(strength, 1.5); | |
const moveX = (deltaX / distance) * 30 * attractionStrength; | |
const moveY = (deltaY / distance) * 30 * attractionStrength; | |
// 计算旋转角度 | |
const rotateX = -Math.atan2(deltaY, distance) * (180 / Math.PI) * strength; | |
const rotateY = Math.atan2(deltaX, distance) * (180 / Math.PI) * strength; | |
// 应用变换效果 | |
gsap.to(inner, { | |
scale: 1 + 0.05 * strength, | |
x: moveX, | |
y: moveY, | |
rotationX: rotateX, | |
rotationY: rotateY, | |
duration: 0.2, | |
ease: "power2.out", | |
force3D: true, | |
}); | |
// 更新阴影效果 | |
const shadowOffsetX = (deltaX / distance) * 15 * strength; | |
const shadowOffsetY = Math.max(6, (deltaY / distance) * 20 * strength + 6); | |
const shadowBlur = 12 + 18 * strength; | |
const shadowOpacity = 0.15 + 0.1 * strength; | |
gsap.to(avatarContainer, { | |
filter: `drop-shadow(${shadowOffsetX}px ${shadowOffsetY}px ${shadowBlur}px rgba(0, 0, 0, ${shadowOpacity}))`, | |
duration: 0.2, | |
}); | |
}); | |
}); | |
}; | |
// 修改鼠标移出处理函数 | |
const handleMouseLeave = () => { | |
currentHoverIndex = null; | |
activeIndex.value = null; | |
activeCenter.value = { x: 0, y: 0 }; | |
if (!avatarWrapperRefs.value) return; | |
avatarWrapperRefs.value.forEach((wrapper) => { | |
const inner = wrapper.querySelector(".avatar-inner"); | |
if (inner) { | |
gsap.killTweensOf(inner); | |
gsap.to(inner, { | |
scale: 1, | |
y: 0, | |
x: 0, | |
rotation: 0, | |
rotationX: 0, | |
rotationY: 0, | |
duration: 0.2, | |
ease: "power2.out", | |
}); | |
} | |
const avatarContainer = wrapper.closest(".sponsor-avatar"); | |
if (avatarContainer) { | |
gsap.killTweensOf(avatarContainer); | |
gsap.to(avatarContainer, { | |
filter: "drop-shadow(0 6px 12px rgba(0, 0, 0, 0.15))", | |
duration: 0.2, | |
}); | |
} | |
const overlayElement = wrapper.querySelector(".avatar-overlay"); | |
if (overlayElement) { | |
gsap.to(overlayElement, { | |
opacity: 1, | |
duration: 0.15, | |
}); | |
} | |
}); | |
if (typeItInstance) { | |
typeItInstance.destroy(); | |
typeItInstance = null; | |
} | |
}; | |
// 添加点击处理函数 | |
const handleAvatarClick = (link) => { | |
if (link) { | |
window.open(link, "_blank"); | |
} | |
}; | |
// 组件卸载时清理 | |
onBeforeUnmount(() => { | |
window.removeEventListener("blur", handleMouseLeave); | |
document.removeEventListener("visibilitychange", handleVisibilityChange); | |
// 清理打字实例 | |
if (typeItInstance) { | |
typeItInstance.destroy(); | |
typeItInstance = null; | |
} | |
}); | |
// 添加按钮悬浮效果 | |
const handleButtonHover = () => { | |
gsap.to(".sponsor-button", { | |
scale: 1.05, | |
duration: 0.3, | |
ease: "power2.out", | |
}); | |
}; | |
const handleButtonLeave = () => { | |
gsap.to(".sponsor-button", { | |
scale: 1, | |
duration: 0.3, | |
ease: "power2.out", | |
}); | |
}; | |
</script> | |
<style scoped> | |
.thanks-container { | |
width: 100%; | |
box-sizing: border-box; | |
height: calc(100vh - 100px); | |
overflow: auto; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
padding: 40px 20px; | |
background: linear-gradient(135deg, #f6f8fd 0%, #f1f4f9 100%); | |
position: relative; | |
z-index: 1; | |
transform: translateZ(0); | |
will-change: transform; | |
backface-visibility: hidden; | |
} | |
.gradient-circle { | |
position: absolute; | |
border-radius: 50%; | |
filter: blur(40px); | |
opacity: 0.5; | |
will-change: transform; | |
backface-visibility: hidden; | |
transform: translateZ(0); | |
} | |
.circle-1 { | |
width: 600px; | |
height: 600px; | |
background: linear-gradient(45deg, rgba(142, 68, 173, 0.2), rgba(91, 177, 235, 0.2)); | |
top: -200px; | |
left: -200px; | |
} | |
.circle-2 { | |
width: 500px; | |
height: 500px; | |
background: linear-gradient(45deg, rgba(91, 177, 235, 0.2), rgba(142, 68, 173, 0.2)); | |
bottom: -150px; | |
right: -150px; | |
} | |
.circle-3 { | |
width: 400px; | |
height: 400px; | |
background: linear-gradient(45deg, rgba(241, 196, 15, 0.1), rgba(142, 68, 173, 0.1)); | |
top: 40%; | |
left: 30%; | |
} | |
/* 装饰层 */ | |
.decoration-layer { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
pointer-events: none; | |
z-index: 0; | |
} | |
.floating-dot { | |
position: absolute; | |
width: 6px; | |
height: 6px; | |
background: rgba(142, 68, 173, 0.2); | |
border-radius: 50%; | |
animation: floatingDot 8s ease-in-out infinite; | |
animation-delay: var(--delay); | |
will-change: transform; | |
backface-visibility: hidden; | |
transform: translateZ(0); | |
} | |
@keyframes floatingDot { | |
0%, | |
100% { | |
transform: translate(0, 0); | |
} | |
25% { | |
transform: translate(100px, 50px); | |
} | |
50% { | |
transform: translate(50px, 100px); | |
} | |
75% { | |
transform: translate(-50px, 50px); | |
} | |
} | |
.title { | |
margin-bottom: 20px; | |
font-size: 50px; | |
color: #2c3e50; | |
text-align: center; | |
font-weight: 700; | |
background: linear-gradient(45deg, #8e44ad, #3498db); | |
-webkit-background-clip: text; | |
-webkit-text-fill-color: transparent; | |
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1); | |
letter-spacing: 1px; | |
will-change: transform, opacity; | |
backface-visibility: hidden; | |
transform: translateZ(0); | |
} | |
.sponsors-container { | |
width: 70%; | |
max-width: 1200px; | |
display: grid; | |
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); | |
grid-gap: 40px; | |
justify-content: center; | |
padding: 20px; | |
will-change: transform, opacity; | |
backface-visibility: hidden; | |
transform: translateZ(0); | |
opacity: 1; /* 确保容器默认可见 */ | |
} | |
.sponsor-avatar { | |
position: relative; | |
transform-style: preserve-3d; | |
transition: transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); | |
z-index: 1; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
padding-bottom: 20px; | |
filter: drop-shadow(0 6px 12px rgba(0, 0, 0, 0.15)); | |
transition: all 0.3s ease; | |
} | |
.avatar-wrapper { | |
width: 80px; | |
height: 80px; | |
position: relative; | |
z-index: 1; | |
} | |
.avatar-inner { | |
width: 100% !important; | |
height: 100% !important; | |
border-radius: 50%; | |
border: 4px solid #ffffff; | |
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); | |
transition: | |
transform 0.2s ease, | |
filter 0.2s ease; | |
cursor: pointer; | |
position: relative; | |
isolation: isolate; | |
transform-style: preserve-3d; | |
box-sizing: border-box; | |
} | |
.avatar-overlay { | |
position: absolute; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
border-radius: 50%; | |
overflow: hidden; | |
background: linear-gradient( | |
135deg, | |
rgba(255, 255, 255, 0.2) 0%, | |
rgba(255, 255, 255, 0) 50%, | |
rgba(0, 0, 0, 0.1) 100% | |
); | |
opacity: 1; | |
transition: all 0.3s ease; | |
pointer-events: none; | |
z-index: 3; | |
mix-blend-mode: overlay; /* 添加混合模式增强效果 */ | |
} | |
.avatar-wrapper.active .avatar-inner { | |
transform: scale(1.2) translateY(-10px); | |
} | |
.avatar-wrapper.has-link { | |
position: relative; | |
cursor: pointer; | |
} | |
.avatar-wrapper.has-link::before { | |
content: ""; | |
position: absolute; | |
inset: -4px; | |
border-radius: 50%; | |
background: linear-gradient(45deg, #ff3366, #ff6b6b, #4ecdc4, #45b7d1, #96e6a1); | |
opacity: 0; | |
transition: opacity 0.3s ease; | |
z-index: -1; | |
filter: blur(8px); | |
} | |
.avatar-wrapper.has-link:hover::before { | |
opacity: 0.8; | |
animation: borderGlow 2s linear infinite; | |
} | |
.glow-effect { | |
position: absolute; | |
inset: 0; | |
border-radius: 50%; | |
background: transparent; | |
border: 2px solid transparent; | |
transition: all 0.3s ease; | |
z-index: 2; | |
} | |
.avatar-wrapper.has-link:hover .glow-effect { | |
border-color: rgba(255, 255, 255, 0.5); | |
box-shadow: | |
0 0 20px rgba(255, 255, 255, 0.3), | |
inset 0 0 20px rgba(255, 255, 255, 0.3); | |
} | |
@keyframes borderGlow { | |
0% { | |
transform: rotate(0deg); | |
} | |
100% { | |
transform: rotate(360deg); | |
} | |
} | |
/* 确保激活状态下的发光效果仍然可见 */ | |
.avatar-wrapper.active.has-link::before { | |
z-index: -1; | |
} | |
.avatar-img { | |
width: 100%; | |
height: 100%; | |
border-radius: 50%; | |
overflow: hidden; | |
object-fit: cover; | |
position: relative; | |
z-index: 2; | |
} | |
.dialog-box { | |
position: absolute; | |
top: -120px; /* 稍微上调对话框位置 */ | |
left: 50%; | |
transform: translateX(-50%); | |
background: rgba(255, 255, 255, 0.95); | |
backdrop-filter: blur(8px); | |
padding: 16px 20px; | |
border-radius: 16px; | |
box-shadow: | |
0 4px 24px -1px rgba(0, 0, 0, 0.1), | |
0 2px 8px -1px rgba(0, 0, 0, 0.06), | |
inset 0 0 0 1px rgba(255, 255, 255, 0.5), | |
0 0 40px rgba(142, 68, 173, 0.05); | |
min-width: 180px; | |
z-index: 111; | |
opacity: 0; | |
animation: dialogFadeIn 0.25s cubic-bezier(0.16, 1, 0.3, 1) forwards; | |
border: 1px solid rgba(0, 0, 0, 0.05); | |
will-change: transform, opacity; | |
backface-visibility: hidden; | |
transform: translateZ(0); | |
} | |
.dialog-content { | |
position: relative; | |
font-size: 15px; | |
line-height: 1.6; | |
color: #2c3e50; | |
margin: 0; | |
text-align: center; | |
text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5); | |
} | |
/* 修改引号装饰的样式 */ | |
.dialog-content::before, | |
.dialog-content::after { | |
content: '"'; | |
position: absolute; | |
font-size: 28px; | |
color: #8e44ad; | |
opacity: 0.15; | |
text-shadow: none; | |
} | |
.dialog-content::before { | |
left: -15px; | |
top: -12px; | |
} | |
.dialog-content::after { | |
right: -15px; | |
bottom: -24px; | |
} | |
/* 优化淡入动画,使其更加流畅 */ | |
@keyframes dialogFadeIn { | |
0% { | |
opacity: 0; | |
transform: translateX(-50%) translateY(10px) scale(0.98); | |
filter: blur(1px); | |
} | |
100% { | |
opacity: 1; | |
transform: translateX(-50%) translateY(0) scale(1); | |
filter: blur(0); | |
} | |
} | |
/* 优化打字效果容器样式 */ | |
.type-it-container { | |
min-height: 24px; | |
padding: 4px 8px; | |
position: relative; | |
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0)); | |
border-radius: 8px; | |
} | |
/* 添加打字光标样式 */ | |
.ti-cursor { | |
color: #8e44ad; | |
font-weight: 300; | |
} | |
.name-tag { | |
position: absolute; | |
bottom: -10px; | |
left: 50%; | |
transform: translateX(-50%); | |
text-align: center; | |
color: #2c3e50; | |
opacity: 1; | |
font-weight: 500; | |
font-size: 12px; | |
z-index: 20; | |
background-color: #fff; | |
border-radius: 15px; | |
padding: 0px 10px; | |
white-space: nowrap; /* 防止文字换行 */ | |
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); | |
} | |
/* 添加新的样式 */ | |
.description { | |
text-align: center; | |
margin-bottom: 40px; | |
color: #666; | |
line-height: 1.6; | |
max-width: 600px; | |
margin-inline: auto; | |
} | |
.description p { | |
margin: 8px 0; | |
font-size: 16px; | |
} | |
.bottom-text { | |
text-align: center; | |
margin-top: 60px; | |
color: #666; | |
line-height: 1.6; | |
} | |
.bottom-text p { | |
margin: 8px 0; | |
font-size: 16px; | |
} | |
.sponsor-button { | |
position: fixed; | |
bottom: 40px; | |
right: 40px; | |
background: linear-gradient(45deg, #ff3366, #ff6b6b); | |
color: white; | |
padding: 12px 24px; | |
border-radius: 30px; | |
text-decoration: none; | |
font-weight: 500; | |
font-size: 16px; | |
box-shadow: | |
0 4px 15px rgba(255, 51, 102, 0.3), | |
0 2px 8px rgba(255, 51, 102, 0.2); | |
transition: all 0.3s ease; | |
z-index: 1000; | |
} | |
.sponsor-button:hover { | |
transform: translateY(-2px); | |
box-shadow: | |
0 6px 20px rgba(255, 51, 102, 0.4), | |
0 3px 10px rgba(255, 51, 102, 0.3); | |
} | |
.button-content { | |
display: flex; | |
align-items: center; | |
gap: 8px; | |
} | |
.heart-icon { | |
width: 20px; | |
height: 20px; | |
fill: currentColor; | |
animation: heartBeat 1.2s ease-in-out infinite; | |
} | |
@keyframes heartBeat { | |
0% { | |
transform: scale(1); | |
} | |
14% { | |
transform: scale(1.3); | |
} | |
28% { | |
transform: scale(1); | |
} | |
42% { | |
transform: scale(1.3); | |
} | |
70% { | |
transform: scale(1); | |
} | |
} | |
/* 添加响应式样式 */ | |
@media (max-width: 768px) { | |
.sponsor-button { | |
bottom: 20px; | |
right: 20px; | |
padding: 10px 20px; | |
font-size: 14px; | |
} | |
.description, | |
.bottom-text { | |
padding: 0 20px; | |
} | |
} | |
/* 添加悬浮状态的阴影效果 */ | |
.sponsor-avatar:hover { | |
filter: drop-shadow(0 8px 12px rgba(0, 0, 0, 0.15)); | |
} | |
/* 修改激活状态的阴影效果 */ | |
.sponsor-avatar:has(.avatar-wrapper.active) { | |
filter: drop-shadow(0 15px 25px rgba(0, 0, 0, 0.2)); | |
} | |
</style> | |