'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(null); const startValueRef = useRef(end); const rafRef = useRef(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 }; }