feat:增加项目鸣谢页

This commit is contained in:
jiangrui
2025-03-13 16:26:41 +08:00
parent c1927a0ea9
commit 16a2b586fd
16 changed files with 1085 additions and 13 deletions

View File

@@ -0,0 +1,18 @@
import { Request, Response } from "express";
import { injectable, inject } from "inversify";
import { TYPES } from "../core/types";
import { SponsorsService } from "../services/SponsorsService";
import { BaseController } from "./BaseController";
@injectable()
export class SponsorsController extends BaseController {
constructor(@inject(TYPES.SponsorsService) private sponsorsService: SponsorsService) {
super();
}
async get(req: Request, res: Response): Promise<void> {
await this.handleRequest(req, res, async () => {
return await this.sponsorsService.getSponsors();
});
}
}

View File

@@ -7,6 +7,7 @@ export const TYPES = {
ImageService: Symbol.for("ImageService"),
SettingService: Symbol.for("SettingService"),
UserService: Symbol.for("UserService"),
SponsorsService: Symbol.for("SponsorsService"),
Cloud115Controller: Symbol.for("Cloud115Controller"),
QuarkController: Symbol.for("QuarkController"),
@@ -15,4 +16,5 @@ export const TYPES = {
ImageController: Symbol.for("ImageController"),
SettingController: Symbol.for("SettingController"),
UserController: Symbol.for("UserController"),
SponsorsController: Symbol.for("SponsorsController"),
};

View File

@@ -10,7 +10,7 @@ import { DoubanService } from "./services/DoubanService";
import { UserService } from "./services/UserService";
import { ImageService } from "./services/ImageService";
import { SettingService } from "./services/SettingService";
import { SponsorsService } from "./services/SponsorsService";
// Controllers
import { Cloud115Controller } from "./controllers/cloud115";
import { QuarkController } from "./controllers/quark";
@@ -19,7 +19,7 @@ import { DoubanController } from "./controllers/douban";
import { ImageController } from "./controllers/teleImages";
import { SettingController } from "./controllers/setting";
import { UserController } from "./controllers/user";
import { SponsorsController } from "./controllers/sponsors";
const container = new Container();
// Services
@@ -31,7 +31,7 @@ container.bind<ImageService>(TYPES.ImageService).to(ImageService).inSingletonSco
container.bind<SettingService>(TYPES.SettingService).to(SettingService).inSingletonScope();
container.bind<DoubanService>(TYPES.DoubanService).to(DoubanService).inSingletonScope();
container.bind<UserService>(TYPES.UserService).to(UserService).inSingletonScope();
container.bind<SponsorsService>(TYPES.SponsorsService).to(SponsorsService).inSingletonScope();
// Controllers
container.bind<Cloud115Controller>(TYPES.Cloud115Controller).to(Cloud115Controller);
container.bind<QuarkController>(TYPES.QuarkController).to(QuarkController);
@@ -40,5 +40,6 @@ container.bind<DoubanController>(TYPES.DoubanController).to(DoubanController);
container.bind<ImageController>(TYPES.ImageController).to(ImageController);
container.bind<SettingController>(TYPES.SettingController).to(SettingController);
container.bind<UserController>(TYPES.UserController).to(UserController);
container.bind<SponsorsController>(TYPES.SponsorsController).to(SponsorsController);
export { container };

View File

@@ -8,6 +8,7 @@ import { DoubanController } from "../controllers/douban";
import { ImageController } from "../controllers/teleImages";
import { SettingController } from "../controllers/setting";
import { UserController } from "../controllers/user";
import { SponsorsController } from "../controllers/sponsors";
const router = Router();
@@ -19,6 +20,7 @@ const doubanController = container.get<DoubanController>(TYPES.DoubanController)
const imageController = container.get<ImageController>(TYPES.ImageController);
const settingController = container.get<SettingController>(TYPES.SettingController);
const userController = container.get<UserController>(TYPES.UserController);
const sponsorsController = container.get<SponsorsController>(TYPES.SponsorsController);
// 用户相关路由
router.post("/user/login", (req, res) => userController.login(req, res));
@@ -34,6 +36,9 @@ router.post("/setting/save", (req, res) => settingController.save(req, res));
// 资源搜索
router.get("/search", (req, res) => resourceController.search(req, res));
// 获取赞助者列表
router.get("/sponsors", (req, res) => sponsorsController.get(req, res));
// 115网盘相关
router.get("/cloud115/share-info", (req, res) => cloud115Controller.getShareInfo(req, res));
router.get("/cloud115/folders", (req, res) => cloud115Controller.getFolderList(req, res));

View File

@@ -0,0 +1,25 @@
import { injectable } from "inversify";
import { createAxiosInstance } from "../utils/axiosInstance";
import { AxiosInstance } from "axios";
import sponsors from "../sponsors/sponsors.json";
@injectable()
export class SponsorsService {
private axiosInstance: AxiosInstance;
constructor() {
this.axiosInstance = createAxiosInstance("http://oss.jiangmuxin.cn/cloudsaver/");
}
async getSponsors() {
try {
const response = await this.axiosInstance.get("sponsors.json");
return {
data: response.data.sponsors,
};
} catch (error) {
return {
data: sponsors.sponsors,
};
}
}
}

View File

@@ -0,0 +1,54 @@
{
"sponsors": [
{
"name": "立本狗头",
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks1.jpg",
"message": "怒搓楼上狗头! "
},
{
"name": "帝国鼻屎",
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks2.jpg",
"message": "芜湖起飞! "
},
{
"name": "雷霆222",
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks3.jpg",
"message": "把我弄帅点 "
},
{
"name": "黑田奈奈子",
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks4.jpg",
"message": "流年笑掷 未来可期 ",
"link": "https://github.com/htnanako"
},
{
"name": "原野🐇",
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks5.jpg"
},
{
"name": "我摆烂!",
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks6.jpg",
"message": "人生苦短,及时行乐,卷什么卷,随缘摆烂 "
},
{
"name": "田培",
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks7.jpg"
},
{
"name": "River",
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks8.jpg"
},
{
"name": "午夜学徒",
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks9.jpg"
},
{
"name": "阿潘",
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks10.jpg"
},
{
"name": "闹闹黑",
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks11.jpg"
}
]
}

View File

@@ -8,7 +8,7 @@ interface ProxyConfig {
export function createAxiosInstance(
baseURL: string,
headers: AxiosRequestHeaders,
headers?: AxiosRequestHeaders,
useProxy: boolean = false,
proxyConfig?: ProxyConfig
): AxiosInstance {

View File

@@ -9,6 +9,7 @@ declare module 'vue' {
export interface GlobalComponents {
AsideMenu: typeof import('./src/components/AsideMenu.vue')['default']
ElAside: typeof import('element-plus/es')['ElAside']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElBacktop: typeof import('element-plus/es')['ElBacktop']
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
@@ -50,15 +51,12 @@ declare module 'vue' {
VanCheckbox: typeof import('vant/es')['Checkbox']
VanCheckboxGroup: typeof import('vant/es')['CheckboxGroup']
VanEmpty: typeof import('vant/es')['Empty']
VanField: typeof import('vant/es')['Field']
VanForm: typeof import('vant/es')['Form']
VanIcon: typeof import('vant/es')['Icon']
VanImage: typeof import('vant/es')['Image']
VanLoading: typeof import('vant/es')['Loading']
VanOverlay: typeof import('vant/es')['Overlay']
VanPopup: typeof import('vant/es')['Popup']
VanSearch: typeof import('vant/es')['Search']
VanSwitch: typeof import('vant/es')['Switch']
VanTab: typeof import('vant/es')['Tab']
VanTabbar: typeof import('vant/es')['Tabbar']
VanTabbarItem: typeof import('vant/es')['TabbarItem']

View File

@@ -12,8 +12,10 @@
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.6.7",
"element-plus": "^2.6.1",
"gsap": "^3.12.7",
"pinia": "^2.1.7",
"socket.io-client": "^4.8.1",
"typeit": "^8.8.7",
"vant": "^4.9.17",
"vue": "^3.4.21",
"vue-router": "^4.3.0"

View File

@@ -7,4 +7,7 @@ export const userApi = {
register: (data: { username: string; password: string; registerCode: string }) => {
return request.post<{ token: string }>("/api/user/register", data);
},
getSponsors: () => {
return request.get("/api/sponsors?timestamp=" + Date.now());
},
};

View File

@@ -45,12 +45,7 @@
<!-- GitHub 链接 -->
<div class="pc-aside__footer">
<a
href="https://github.com/jiangrui1994/CloudSaver"
target="_blank"
rel="noopener noreferrer"
class="github-link"
>
<a :href="PROJECT_GITHUB" target="_blank" rel="noopener noreferrer" class="github-link">
<svg
height="20"
aria-hidden="true"
@@ -76,6 +71,7 @@ import { computed } from "vue";
import { useRouter, useRoute } from "vue-router";
import { Search, Film, Setting, Link } from "@element-plus/icons-vue";
import logo from "@/assets/images/logo.png";
import { PROJECT_GITHUB } from "@/constants/project";
import pkg from "../../package.json";
// 类型定义
@@ -134,6 +130,12 @@ const menuList: MenuItem[] = [
router: "/setting",
disabled: false,
},
{
index: "4",
title: "鸣谢",
icon: Link,
router: "/thanks",
},
];
// 计算当前激活的菜单

View File

@@ -0,0 +1,2 @@
export const PROJECT_NAME = "Cloudsaver";
export const PROJECT_GITHUB = "https://github.com/jiangrui1994/cloudsaver";

View File

@@ -21,6 +21,11 @@ const routes: RouteRecordRaw[] = [
name: "setting",
component: () => import("@/views/mobile/Setting.vue"),
},
{
path: "/thanks",
name: "thanks",
redirect: "/resource",
},
],
},
{

View File

@@ -21,6 +21,11 @@ const routes: RouteRecordRaw[] = [
name: "setting",
component: () => import("@/views/Setting.vue"),
},
{
path: "/thanks",
name: "thanks",
component: () => import("@/views/Thanks.vue"),
},
],
},
{

View File

@@ -0,0 +1,927 @@
<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();
// 初始化每个头像的动画时间轴
avatarWrapperRefs.value.forEach((wrapper, index) => {
const tl = gsap.timeline({
repeat: -1,
defaults: { ease: "power1.inOut" },
paused: true,
});
// 创建浮动动画 - 使用相对值而不是绝对值
tl.to(wrapper, {
yPercent: -10, // 使用百分比
rotation: 2,
duration: 1.5,
overwrite: "auto", // 添加覆盖设置
})
.to(wrapper, {
yPercent: 0,
rotation: 0,
duration: 1.5,
overwrite: "auto",
})
.to(wrapper, {
yPercent: 10,
rotation: -2,
duration: 1.5,
overwrite: "auto",
})
.to(wrapper, {
yPercent: 0,
rotation: 0,
duration: 1.5,
overwrite: "auto",
});
// 存储时间轴引用
avatarTimelines.value[index] = tl;
// 设置随机的起始时间并播放
tl.progress(Math.random()).play();
});
// 修改页面入场动画
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, // 所有元素在0.3秒内完成错开动画
from: "start",
},
ease: "back.out(1.2)",
onComplete: () => {
// 动画完成后启动浮动动画
avatarTimelines.value.forEach((timeline) => {
if (timeline) {
timeline.progress(Math.random()).play();
}
});
},
});
// 背景圆圈动画
gsap.to(".gradient-circle", {
rotation: 360,
duration: 20,
repeat: -1,
ease: "none",
stagger: {
each: 5,
from: "random",
},
});
});
// 修改鼠标移入处理函数
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.5,
ease: "back.out(1.7)",
force3D: true,
});
// 增强激活头像的阴影
gsap.to(avatarContainer, {
filter: "drop-shadow(0 20px 30px rgba(0, 0, 0, 0.25))",
duration: 0.5,
});
const activeOverlay = wrapper.querySelector(".avatar-overlay");
gsap.to(activeOverlay, {
opacity: 0,
duration: 0.3,
onComplete: () => {
activeOverlay.style.background = "none";
},
});
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);
// 计算效果强度
const maxDistance = 800;
const strength = Math.max(0, 1 - distance / maxDistance);
const attractionCurve = Math.pow(strength, 1.2);
// 计算阴影偏移和强度
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.5,
});
// 更新变换效果
gsap.to(inner, {
rotation: 0,
scale: 1 + 0.05 * strength,
x: (deltaX / distance) * 30 * attractionCurve,
y: (deltaY / distance) * 30 * attractionCurve,
rotationX: -Math.atan2(deltaY, distance) * (180 / Math.PI) * strength,
rotationY: Math.atan2(deltaX, distance) * (180 / Math.PI) * strength,
duration: 0.5,
ease: "power2.out",
force3D: true,
});
});
});
};
// 修改鼠标移出处理函数
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");
gsap.killTweensOf(inner);
});
// 重置所有头像状态
resetAllAvatars();
};
// 添加重置所有头像的函数
const resetAllAvatars = () => {
if (!avatarWrapperRefs.value) return;
avatarWrapperRefs.value.forEach((wrapper, index) => {
const inner = wrapper.querySelector(".avatar-inner");
const avatarContainer = wrapper.closest(".sponsor-avatar");
// 重置变换状态
gsap.set(inner, {
scale: 1,
x: 0,
y: 0,
rotation: 0,
rotationX: 0,
rotationY: 0,
yPercent: 0,
});
// 恢复默认阴影
gsap.to(avatarContainer, {
filter: "drop-shadow(0 6px 12px rgba(0, 0, 0, 0.15))",
duration: 0.3,
});
// 重置蒙层
const overlayElement = wrapper.querySelector(".avatar-overlay");
gsap.to(overlayElement, {
opacity: 1,
duration: 0.3,
onComplete: () => {
overlayElement.style.background = `
linear-gradient(
135deg,
rgba(255, 255, 255, 0.2) 0%,
rgba(255, 255, 255, 0) 50%,
rgba(0, 0, 0, 0.1) 100%
)
`;
},
});
// 重新创建并启动浮动动画
const tl = gsap.timeline({
repeat: -1,
defaults: { ease: "power1.inOut" },
});
tl.to(inner, {
yPercent: -10,
rotation: 2,
duration: 1.5,
overwrite: "auto",
})
.to(inner, {
yPercent: 0,
rotation: 0,
duration: 1.5,
overwrite: "auto",
})
.to(inner, {
yPercent: 10,
rotation: -2,
duration: 1.5,
overwrite: "auto",
})
.to(inner, {
yPercent: 0,
rotation: 0,
duration: 1.5,
overwrite: "auto",
});
avatarTimelines.value[index] = tl;
tl.progress(Math.random()).play();
});
};
// 添加窗口失焦事件处理
window.addEventListener("blur", handleMouseLeave);
// 添加点击处理函数
const handleAvatarClick = (link) => {
if (link) {
window.open(link, "_blank");
}
};
// 组件卸载时清理
onBeforeUnmount(() => {
window.removeEventListener("blur", handleMouseLeave);
// 清理动画时间轴
avatarTimelines.value.forEach((timeline) => {
if (timeline) {
timeline.kill();
}
});
avatarTimelines.value = [];
if (typeItInstance) {
typeItInstance.destroy();
}
});
// 添加按钮悬浮效果
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%;
height: 100%;
border-radius: 50%;
border: 4px solid #ffffff;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
transition:
transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1),
filter 0.3s ease;
cursor: pointer;
will-change: transform;
backface-visibility: hidden;
transform: translateZ(0);
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 {
transform: scale(1.2) translateY(-10px);
z-index: 10;
}
.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.4s 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(20px) scale(0.95);
filter: blur(2px);
}
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>

23
pnpm-lock.yaml generated
View File

@@ -129,12 +129,18 @@ importers:
element-plus:
specifier: ^2.6.1
version: 2.9.5(vue@3.5.13(typescript@5.8.2))
gsap:
specifier: ^3.12.7
version: 3.12.7
pinia:
specifier: ^2.1.7
version: 2.3.1(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))
socket.io-client:
specifier: ^4.8.1
version: 4.8.1
typeit:
specifier: ^8.8.7
version: 8.8.7
vant:
specifier: ^4.9.17
version: 4.9.17(vue@3.5.13(typescript@5.8.2))
@@ -1306,6 +1312,9 @@ packages:
'@types/validator@13.12.2':
resolution: {integrity: sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==}
'@types/web-animations-js@2.2.16':
resolution: {integrity: sha512-ATELeWMFwj8eQiH0KmvsCl1V2lu/qx/CjOBmv4ADSZS5u8r4reMyjCXtxG7khqyiwH3IOMNdrON/Ugn94OUcRA==}
'@types/web-bluetooth@0.0.16':
resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
@@ -2348,6 +2357,9 @@ packages:
graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
gsap@3.12.7:
resolution: {integrity: sha512-V4GsyVamhmKefvcAKaoy0h6si0xX7ogwBoBSs2CTJwt7luW0oZzC0LhdkyuKV8PJAXr7Yaj8pMjCKD4GJ+eEMg==}
has-bigints@1.1.0:
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
engines: {node: '>= 0.4'}
@@ -3800,6 +3812,9 @@ packages:
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
engines: {node: '>= 0.4'}
typeit@8.8.7:
resolution: {integrity: sha512-sSVpy+cjeFP6Z+fZqiHzUSShg5yYFeJEt/Qut/bX945+Axyq+Yq+GPOuuk+sofoccSv8nNX/ibOOHkbki2mEpg==}
typescript@5.8.2:
resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==}
engines: {node: '>=14.17'}
@@ -5363,6 +5378,8 @@ snapshots:
'@types/validator@13.12.2': {}
'@types/web-animations-js@2.2.16': {}
'@types/web-bluetooth@0.0.16': {}
'@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1)(typescript@5.8.2)':
@@ -6696,6 +6713,8 @@ snapshots:
graphemer@1.4.0: {}
gsap@3.12.7: {}
has-bigints@1.1.0: {}
has-flag@3.0.0: {}
@@ -8289,6 +8308,10 @@ snapshots:
possible-typed-array-names: 1.1.0
reflect.getprototypeof: 1.0.10
typeit@8.8.7:
dependencies:
'@types/web-animations-js': 2.2.16
typescript@5.8.2: {}
ufo@1.5.4: {}