18e915bcbb
- 文章管理:计数请求补 page=1 参数,命中分页接口返回正确的 total - 分类管理:编辑模式新增描述输入框,保存时一并提交 description - CSP:img-src 加入 https: 允许加载外部图片 - 关于页:数据存储描述从 JSON 文件更正为 SQLite 数据库 - Footer:添加 ICP 备案号
87 lines
2.2 KiB
TypeScript
87 lines
2.2 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef } from "react";
|
|
import gsap from "gsap";
|
|
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
|
|
|
gsap.registerPlugin(ScrollTrigger);
|
|
|
|
interface GsapRevealProps {
|
|
children: React.ReactNode;
|
|
variant?: "fade-up" | "fade-in" | "slide-left" | "slide-right" | "scale";
|
|
delay?: number;
|
|
duration?: number;
|
|
stagger?: number;
|
|
className?: string;
|
|
once?: boolean;
|
|
}
|
|
|
|
const VARIANTS = {
|
|
"fade-up": { y: 40, opacity: 0 },
|
|
"fade-in": { opacity: 0 },
|
|
"slide-left": { x: -40, opacity: 0 },
|
|
"slide-right": { x: 40, opacity: 0 },
|
|
scale: { scale: 0.92, opacity: 0 },
|
|
} as const;
|
|
|
|
export default function GsapReveal({
|
|
children,
|
|
variant = "fade-up",
|
|
delay = 0,
|
|
duration = 0.8,
|
|
stagger = 0,
|
|
className = "",
|
|
once = true,
|
|
}: GsapRevealProps) {
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
const el = ref.current;
|
|
if (!el) return;
|
|
|
|
// 关键降级:尊重用户系统设置,无障碍优先,不做任何位移。
|
|
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
if (reduceMotion) return;
|
|
|
|
const targets = el.children.length > 1 ? Array.from(el.children) : [el];
|
|
|
|
const ctx = gsap.context(() => {
|
|
if (stagger > 0 && targets.length > 1) {
|
|
targets.forEach((child, i) => {
|
|
gsap.from(child, {
|
|
...VARIANTS[variant],
|
|
duration,
|
|
delay: delay + i * stagger,
|
|
ease: "power3.out",
|
|
scrollTrigger: {
|
|
trigger: child,
|
|
start: "top 92%",
|
|
toggleActions: once ? "play none none none" : "play none none reverse",
|
|
},
|
|
});
|
|
});
|
|
} else {
|
|
gsap.from(targets, {
|
|
...VARIANTS[variant],
|
|
duration,
|
|
delay,
|
|
ease: "power3.out",
|
|
scrollTrigger: {
|
|
trigger: el,
|
|
start: "top 88%",
|
|
toggleActions: once ? "play none none none" : "play none none reverse",
|
|
},
|
|
});
|
|
}
|
|
}, el);
|
|
|
|
return () => ctx.revert();
|
|
}, [variant, delay, duration, stagger, once]);
|
|
|
|
return (
|
|
<div ref={ref} className={className}>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|