18e915bcbb
- 文章管理:计数请求补 page=1 参数,命中分页接口返回正确的 total - 分类管理:编辑模式新增描述输入框,保存时一并提交 description - CSP:img-src 加入 https: 允许加载外部图片 - 关于页:数据存储描述从 JSON 文件更正为 SQLite 数据库 - Footer:添加 ICP 备案号
39 lines
1.1 KiB
TypeScript
39 lines
1.1 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef } from "react";
|
|
import gsap from "gsap";
|
|
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
|
|
|
gsap.registerPlugin(ScrollTrigger);
|
|
|
|
/**
|
|
* GSAP 动画的统一入口。
|
|
*
|
|
* 核心原则:
|
|
* 1. 尊重 prefers-reduced-motion —— 开启时直接跳过动画,内容保持可见。
|
|
* 2. 在 gsap.context 内执行,卸载时自动 revert,避免泄露。
|
|
*
|
|
* 用法:
|
|
* const ref = useGsapAnimation<HTMLElement>((scope, isReduced) => {
|
|
* if (isReduced) return;
|
|
* gsap.from(".item", { y: 40, opacity: 0, stagger: 0.1, scrollTrigger: {...} });
|
|
* });
|
|
*/
|
|
export function useGsapAnimation<T extends HTMLElement = HTMLElement>(
|
|
setup: (scope: T, isReduced: boolean) => void,
|
|
deps: unknown[] = []
|
|
) {
|
|
const ref = useRef<T>(null);
|
|
|
|
useEffect(() => {
|
|
const el = ref.current;
|
|
if (!el) return;
|
|
const isReduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
const ctx = gsap.context(() => setup(el, isReduced), el);
|
|
return () => ctx.revert();
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, deps);
|
|
|
|
return ref;
|
|
}
|