init: 初始化 AssetX 项目仓库
包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、 antdesign(管理后台)、landingpage(营销落地页)、 数据库 SQL 和配置文件。
This commit is contained in:
83
landingpage/hooks/useCountUp.ts
Normal file
83
landingpage/hooks/useCountUp.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
/**
|
||||
* 数字增长动画 Hook - 进入视口触发,离开视口重置
|
||||
* 使用 requestAnimationFrame 保证与浏览器刷新同步
|
||||
*/
|
||||
export function useCountUp(end: number, duration: number = 1500, startRangePercent: number = 0.75) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [count, setCount] = useState(end); // 初始值设为目标值,避免 hydration 错误
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const startValueRef = useRef<number>(end);
|
||||
const rafRef = useRef<number | null>(null);
|
||||
|
||||
// 客户端挂载后设置随机起始值
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
startValueRef.current = Math.floor(end * (startRangePercent + Math.random() * 0.15));
|
||||
setCount(startValueRef.current);
|
||||
}, [end, startRangePercent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mounted) return;
|
||||
|
||||
const cancelAnimation = () => {
|
||||
if (rafRef.current !== null) {
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
rafRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const startAnimation = () => {
|
||||
cancelAnimation();
|
||||
|
||||
const start = startValueRef.current;
|
||||
const startTime = performance.now();
|
||||
|
||||
const tick = (currentTime: number) => {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
// easeOutCubic 缓动
|
||||
const easeOutCubic = 1 - Math.pow(1 - progress, 3);
|
||||
const currentCount = Math.floor(start + (end - start) * easeOutCubic);
|
||||
|
||||
setCount(currentCount);
|
||||
|
||||
if (progress < 1) {
|
||||
rafRef.current = requestAnimationFrame(tick);
|
||||
} else {
|
||||
setCount(end); // 确保最终值准确
|
||||
rafRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
rafRef.current = requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
startAnimation();
|
||||
} else {
|
||||
cancelAnimation();
|
||||
setCount(startValueRef.current);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
if (elementRef.current) {
|
||||
observer.observe(elementRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
cancelAnimation();
|
||||
};
|
||||
}, [end, duration, mounted]);
|
||||
|
||||
return { count, elementRef };
|
||||
}
|
||||
Reference in New Issue
Block a user