Files
asset-homepage/components/StatsSection.tsx

268 lines
9.2 KiB
TypeScript
Raw Normal View History

2026-01-27 17:26:30 +08:00
'use client';
import { useState, useEffect, useRef } from 'react';
2026-01-29 16:23:10 +08:00
import { Card, CardBody, Avatar, AvatarGroup } from '@heroui/react';
2026-01-28 17:55:01 +08:00
import { useLanguage } from '@/contexts/LanguageContext';
2026-01-29 16:23:10 +08:00
import { useTheme } from '@/contexts/ThemeContext';
2026-01-27 17:26:30 +08:00
2026-01-28 17:55:01 +08:00
// 数字增长动画Hook - 可重复触发
2026-01-27 17:26:30 +08:00
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);
2026-01-28 17:55:01 +08:00
const timerRef = useRef<NodeJS.Timeout | null>(null);
2026-01-27 17:26:30 +08:00
// 客户端挂载后设置随机起始值
useEffect(() => {
setMounted(true);
startValueRef.current = Math.floor(end * (startRangePercent + Math.random() * 0.15));
setCount(startValueRef.current);
}, [end, startRangePercent]);
useEffect(() => {
if (!mounted) return;
const observer = new IntersectionObserver(
(entries) => {
2026-01-28 17:55:01 +08:00
if (entries[0].isIntersecting) {
// 进入视口 - 检查是否需要开始动画
if (!timerRef.current) {
const start = startValueRef.current;
const startTime = Date.now();
2026-01-27 17:26:30 +08:00
2026-01-28 17:55:01 +08:00
timerRef.current = setInterval(() => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
2026-01-27 17:26:30 +08:00
2026-01-28 17:55:01 +08:00
// 使用缓动函数 - easeOutCubic 更自然
const easeOutCubic = 1 - Math.pow(1 - progress, 3);
const currentCount = Math.floor(start + (end - start) * easeOutCubic);
2026-01-27 17:26:30 +08:00
2026-01-28 17:55:01 +08:00
setCount(currentCount);
2026-01-27 17:26:30 +08:00
2026-01-28 17:55:01 +08:00
if (progress === 1) {
setCount(end); // 确保最终值准确
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
}
}, 16);
}
} else {
// 离开视口 - 停止并重置
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
setCount(startValueRef.current);
2026-01-27 17:26:30 +08:00
}
},
{ threshold: 0.1 }
);
if (elementRef.current) {
observer.observe(elementRef.current);
}
2026-01-28 17:55:01 +08:00
return () => {
observer.disconnect();
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
};
}, [end, duration, mounted]);
2026-01-27 17:26:30 +08:00
return { count, elementRef };
}
// 格式化数字 - 确保始终显示正确的单位
function formatNumber(num: number, prefix: string = '', suffix: string = '', forceUnit: 'M' | 'K' | '' = '') {
if (forceUnit === 'M') {
return `${prefix}${Math.floor(num)}${suffix}`;
} else if (forceUnit === 'K') {
return `${prefix}${Math.floor(num)}${suffix}`;
} else if (num >= 1000000) {
return `${prefix}${Math.floor(num / 1000000)}M${suffix}`;
} else if (num >= 1000) {
return `${prefix}${Math.floor(num / 1000)}K${suffix}`;
}
return `${prefix}${num.toLocaleString()}${suffix}`;
}
export default function StatsSection() {
2026-01-28 17:55:01 +08:00
const { t } = useLanguage();
2026-01-29 16:23:10 +08:00
const { theme } = useTheme();
const tvl = useCountUp(485, 1500, 0.80);
const apy = useCountUp(515, 1500, 0.85);
const yield_ = useCountUp(45, 1500, 0.75);
const users = useCountUp(23928, 1800, 0.80);
2026-01-27 17:26:30 +08:00
const [mounted, setMounted] = useState(false);
const [imagesVisible, setImagesVisible] = useState(false);
useEffect(() => {
setMounted(true);
const timer = setTimeout(() => {
setImagesVisible(true);
}, 500);
return () => clearTimeout(timer);
}, []);
2026-01-29 16:23:10 +08:00
const isDark = theme === 'dark';
2026-01-27 17:26:30 +08:00
return (
<section
2026-01-29 16:23:10 +08:00
className={`flex flex-row items-center justify-center flex-shrink-0 w-full relative border-b transition-colors ${
isDark ? 'bg-[#0a0a0a] border-[#27272a]' : 'bg-white border-[#e5e7eb]'
}`}
2026-01-27 17:26:30 +08:00
>
2026-01-29 16:23:10 +08:00
<div className="flex flex-row items-center justify-center w-[1440px] h-[183px] relative">
2026-01-27 17:26:30 +08:00
<div
className="flex-shrink-0 grid gap-0 relative w-full"
style={{
gridTemplateColumns: 'repeat(4, minmax(0, 1fr))',
gridTemplateRows: 'repeat(1, minmax(0, 1fr))'
}}
>
2026-01-29 16:23:10 +08:00
<Card
className={`h-[182px] rounded-none border-r transition-colors ${
isDark ? 'bg-[#0a0a0a] border-[#27272a]' : 'bg-white border-[#e5e7eb]'
}`}
shadow="none"
2026-01-27 17:26:30 +08:00
style={{
gridColumn: '1 / span 1',
gridRow: '1 / span 1'
}}
>
2026-01-29 16:23:10 +08:00
<CardBody className="flex flex-col gap-2 items-start justify-center">
<p className={`text-base font-domine transition-colors ${isDark ? 'text-[#a1a1aa]' : 'text-[#4b5563]'}`}>
2026-01-28 17:55:01 +08:00
{t('stats.tvl')}
2026-01-29 16:23:10 +08:00
</p>
2026-01-27 17:26:30 +08:00
<div
ref={tvl.elementRef}
2026-01-29 16:23:10 +08:00
className={`text-5xl font-extrabold font-domine transition-colors ${isDark ? 'text-[#fafafa]' : 'text-[#111827]'}`}
2026-01-27 17:26:30 +08:00
style={{
lineHeight: '110%',
2026-01-29 16:23:10 +08:00
letterSpacing: '-0.01em'
2026-01-27 17:26:30 +08:00
}}
>
{formatNumber(tvl.count, '$', 'M+', 'M')}
</div>
2026-01-29 16:23:10 +08:00
</CardBody>
</Card>
2026-01-27 17:26:30 +08:00
2026-01-29 16:23:10 +08:00
<Card
className={`h-[182px] rounded-none border-r px-8 transition-colors ${
isDark ? 'bg-[#0a0a0a] border-[#27272a]' : 'bg-white border-[#e5e7eb]'
}`}
shadow="none"
2026-01-27 17:26:30 +08:00
style={{
gridColumn: '2 / span 1',
gridRow: '1 / span 1'
}}
>
2026-01-29 16:23:10 +08:00
<CardBody className="flex flex-col gap-2 items-start justify-center">
<p className={`text-base font-domine transition-colors ${isDark ? 'text-[#a1a1aa]' : 'text-[#4b5563]'}`}>
2026-01-28 17:55:01 +08:00
{t('stats.apy')}
2026-01-29 16:23:10 +08:00
</p>
2026-01-27 17:26:30 +08:00
<div
ref={apy.elementRef}
2026-01-29 16:23:10 +08:00
className={`text-5xl font-extrabold font-domine transition-colors ${isDark ? 'text-[#fafafa]' : 'text-[#111827]'}`}
2026-01-27 17:26:30 +08:00
style={{
lineHeight: '110%',
2026-01-29 16:23:10 +08:00
letterSpacing: '-0.01em'
2026-01-27 17:26:30 +08:00
}}
>
{(apy.count / 100).toFixed(2)}%
</div>
2026-01-29 16:23:10 +08:00
</CardBody>
</Card>
2026-01-27 17:26:30 +08:00
2026-01-29 16:23:10 +08:00
<Card
className={`h-[182px] rounded-none border-r px-8 transition-colors ${
isDark ? 'bg-[#0a0a0a] border-[#27272a]' : 'bg-white border-[#e5e7eb]'
}`}
shadow="none"
2026-01-27 17:26:30 +08:00
style={{
gridColumn: '3 / span 1',
gridRow: '1 / span 1'
}}
>
2026-01-29 16:23:10 +08:00
<CardBody className="flex flex-col gap-2 items-start justify-center">
<p className={`text-base font-domine transition-colors ${isDark ? 'text-[#a1a1aa]' : 'text-[#4b5563]'}`}>
2026-01-28 17:55:01 +08:00
{t('stats.yield')}
2026-01-29 16:23:10 +08:00
</p>
2026-01-27 17:26:30 +08:00
<div
ref={yield_.elementRef}
2026-01-29 16:23:10 +08:00
className={`text-5xl font-extrabold font-domine transition-colors ${isDark ? 'text-[#fafafa]' : 'text-[#111827]'}`}
2026-01-27 17:26:30 +08:00
style={{
lineHeight: '110%',
2026-01-29 16:23:10 +08:00
letterSpacing: '-0.01em'
2026-01-27 17:26:30 +08:00
}}
>
{formatNumber(yield_.count, '$', 'M', 'M')}
</div>
2026-01-29 16:23:10 +08:00
</CardBody>
</Card>
2026-01-27 17:26:30 +08:00
2026-01-29 16:23:10 +08:00
<Card
className={`h-[182px] rounded-none pl-8 transition-colors ${
isDark ? 'bg-[#0a0a0a]' : 'bg-white'
}`}
shadow="none"
2026-01-27 17:26:30 +08:00
style={{
gridColumn: '4 / span 1',
gridRow: '1 / span 1'
}}
>
2026-01-29 16:23:10 +08:00
<CardBody className="flex flex-col gap-2 items-start justify-center">
<p className={`text-base font-domine transition-colors ${isDark ? 'text-[#a1a1aa]' : 'text-[#4b5563]'}`}>
2026-01-28 17:55:01 +08:00
{t('stats.users')}
2026-01-29 16:23:10 +08:00
</p>
2026-01-27 17:26:30 +08:00
2026-01-29 16:23:10 +08:00
<div className="flex flex-row items-center justify-between self-stretch">
2026-01-27 17:26:30 +08:00
<div
ref={users.elementRef}
2026-01-29 16:23:10 +08:00
className={`text-5xl font-extrabold font-domine transition-colors ${isDark ? 'text-[#fafafa]' : 'text-[#111827]'}`}
2026-01-27 17:26:30 +08:00
style={{
lineHeight: '110%',
2026-01-29 16:23:10 +08:00
letterSpacing: '-0.01em'
2026-01-27 17:26:30 +08:00
}}
>
{users.count.toLocaleString()}+
</div>
2026-01-29 16:23:10 +08:00
<AvatarGroup
max={4}
className={`transition-all duration-700 ease-out ${
mounted && imagesVisible ? 'translate-y-0 opacity-100' : 'translate-y-12 opacity-0'
}`}
>
<Avatar
src="/image-230.png"
size="sm"
/>
<Avatar
src="/image-240.png"
size="sm"
/>
<Avatar
src="/image-250.png"
size="sm"
/>
<Avatar
src="/image-251.png"
size="sm"
/>
</AvatarGroup>
2026-01-27 17:26:30 +08:00
</div>
2026-01-29 16:23:10 +08:00
</CardBody>
</Card>
2026-01-27 17:26:30 +08:00
</div>
</div>
</section>
);
}