Files
asset-homepage/components/StatsSection.tsx

268 lines
9.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState, useEffect, useRef } from 'react';
import { Card, CardBody, Avatar, AvatarGroup } from '@heroui/react';
import { useLanguage } from '@/contexts/LanguageContext';
import { useTheme } from '@/contexts/ThemeContext';
// 数字增长动画Hook - 可重复触发
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 timerRef = useRef<NodeJS.Timeout | 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 observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
// 进入视口 - 检查是否需要开始动画
if (!timerRef.current) {
const start = startValueRef.current;
const startTime = Date.now();
timerRef.current = setInterval(() => {
const elapsed = Date.now() - 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) {
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);
}
},
{ threshold: 0.1 }
);
if (elementRef.current) {
observer.observe(elementRef.current);
}
return () => {
observer.disconnect();
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
};
}, [end, duration, mounted]);
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() {
const { t } = useLanguage();
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);
const [mounted, setMounted] = useState(false);
const [imagesVisible, setImagesVisible] = useState(false);
useEffect(() => {
setMounted(true);
const timer = setTimeout(() => {
setImagesVisible(true);
}, 500);
return () => clearTimeout(timer);
}, []);
const isDark = theme === 'dark';
return (
<section
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]'
}`}
>
<div className="flex flex-row items-center justify-center w-[1440px] h-[183px] relative">
<div
className="flex-shrink-0 grid gap-0 relative w-full"
style={{
gridTemplateColumns: 'repeat(4, minmax(0, 1fr))',
gridTemplateRows: 'repeat(1, minmax(0, 1fr))'
}}
>
<Card
className={`h-[182px] rounded-none border-r transition-colors ${
isDark ? 'bg-[#0a0a0a] border-[#27272a]' : 'bg-white border-[#e5e7eb]'
}`}
shadow="none"
style={{
gridColumn: '1 / span 1',
gridRow: '1 / span 1'
}}
>
<CardBody className="flex flex-col gap-2 items-start justify-center">
<p className={`text-base font-domine transition-colors ${isDark ? 'text-[#a1a1aa]' : 'text-[#4b5563]'}`}>
{t('stats.tvl')}
</p>
<div
ref={tvl.elementRef}
className={`text-5xl font-extrabold font-domine transition-colors ${isDark ? 'text-[#fafafa]' : 'text-[#111827]'}`}
style={{
lineHeight: '110%',
letterSpacing: '-0.01em'
}}
>
{formatNumber(tvl.count, '$', 'M+', 'M')}
</div>
</CardBody>
</Card>
<Card
className={`h-[182px] rounded-none border-r px-8 transition-colors ${
isDark ? 'bg-[#0a0a0a] border-[#27272a]' : 'bg-white border-[#e5e7eb]'
}`}
shadow="none"
style={{
gridColumn: '2 / span 1',
gridRow: '1 / span 1'
}}
>
<CardBody className="flex flex-col gap-2 items-start justify-center">
<p className={`text-base font-domine transition-colors ${isDark ? 'text-[#a1a1aa]' : 'text-[#4b5563]'}`}>
{t('stats.apy')}
</p>
<div
ref={apy.elementRef}
className={`text-5xl font-extrabold font-domine transition-colors ${isDark ? 'text-[#fafafa]' : 'text-[#111827]'}`}
style={{
lineHeight: '110%',
letterSpacing: '-0.01em'
}}
>
{(apy.count / 100).toFixed(2)}%
</div>
</CardBody>
</Card>
<Card
className={`h-[182px] rounded-none border-r px-8 transition-colors ${
isDark ? 'bg-[#0a0a0a] border-[#27272a]' : 'bg-white border-[#e5e7eb]'
}`}
shadow="none"
style={{
gridColumn: '3 / span 1',
gridRow: '1 / span 1'
}}
>
<CardBody className="flex flex-col gap-2 items-start justify-center">
<p className={`text-base font-domine transition-colors ${isDark ? 'text-[#a1a1aa]' : 'text-[#4b5563]'}`}>
{t('stats.yield')}
</p>
<div
ref={yield_.elementRef}
className={`text-5xl font-extrabold font-domine transition-colors ${isDark ? 'text-[#fafafa]' : 'text-[#111827]'}`}
style={{
lineHeight: '110%',
letterSpacing: '-0.01em'
}}
>
{formatNumber(yield_.count, '$', 'M', 'M')}
</div>
</CardBody>
</Card>
<Card
className={`h-[182px] rounded-none pl-8 transition-colors ${
isDark ? 'bg-[#0a0a0a]' : 'bg-white'
}`}
shadow="none"
style={{
gridColumn: '4 / span 1',
gridRow: '1 / span 1'
}}
>
<CardBody className="flex flex-col gap-2 items-start justify-center">
<p className={`text-base font-domine transition-colors ${isDark ? 'text-[#a1a1aa]' : 'text-[#4b5563]'}`}>
{t('stats.users')}
</p>
<div className="flex flex-row items-center justify-between self-stretch">
<div
ref={users.elementRef}
className={`text-5xl font-extrabold font-domine transition-colors ${isDark ? 'text-[#fafafa]' : 'text-[#111827]'}`}
style={{
lineHeight: '110%',
letterSpacing: '-0.01em'
}}
>
{users.count.toLocaleString()}+
</div>
<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>
</div>
</CardBody>
</Card>
</div>
</div>
</section>
);
}