init: 初始化 AssetX 项目仓库

包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、
antdesign(管理后台)、landingpage(营销落地页)、
数据库 SQL 和配置文件。
This commit is contained in:
2026-03-27 11:26:43 +00:00
commit 2ee4553b71
634 changed files with 988255 additions and 0 deletions

View 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 };
}