Files
asset-homepage/components/StatsSection.tsx
2026-01-27 17:26:30 +08:00

396 lines
14 KiB
TypeScript
Raw 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 Image from 'next/image';
// 数字增长动画Hook
function useCountUp(end: number, duration: number = 1500, startRangePercent: number = 0.75) {
const [mounted, setMounted] = useState(false);
const [count, setCount] = useState(end); // 初始值设为目标值避免hydration错误
const [hasAnimated, setHasAnimated] = useState(false);
const elementRef = useRef<HTMLDivElement>(null);
const startValueRef = useRef<number>(end);
// 客户端挂载后设置随机起始值
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 && !hasAnimated) {
setHasAnimated(true);
const start = startValueRef.current;
const startTime = Date.now();
const timer = 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); // 确保最终值准确
clearInterval(timer);
}
}, 16);
return () => clearInterval(timer);
}
},
{ threshold: 0.1 }
);
if (elementRef.current) {
observer.observe(elementRef.current);
}
return () => observer.disconnect();
}, [end, duration, hasAnimated, 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 tvl = useCountUp(485, 1500, 0.80); // 从80-95%开始
const apy = useCountUp(515, 1500, 0.85); // 5.15 * 100从85-100%开始
const yield_ = useCountUp(45, 1500, 0.75); // 从75-90%开始
const users = useCountUp(23928, 1800, 0.80); // 从80-95%开始
const [mounted, setMounted] = useState(false);
const [imagesVisible, setImagesVisible] = useState(false);
useEffect(() => {
// 客户端挂载
setMounted(true);
// 延迟显示图片动画
const timer = setTimeout(() => {
setImagesVisible(true);
}, 500);
return () => clearTimeout(timer);
}, []);
return (
<section
className="bg-white flex flex-row items-center justify-center flex-shrink-0 w-full relative"
style={{
borderStyle: 'solid',
borderColor: '#e5e7eb',
borderWidth: '0px 0px 1px 0px',
padding: '0px 0px 1px 0px'
}}
>
{/* .section2 */}
<div
className="flex flex-row items-center justify-center w-[1440px] h-[183px] relative"
>
{/* .container4 - Grid container */}
<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 1 - Total Value Locked */}
<div
className="bg-white flex flex-row items-center justify-between h-[182px] relative"
style={{
borderStyle: 'solid',
borderColor: '#e5e7eb',
borderWidth: '0px 1px 0px 0px',
gridColumn: '1 / span 1',
gridRow: '1 / span 1'
}}
>
<div className="flex flex-col gap-2 items-start justify-center flex-1 relative">
<div
className="text-[#4b5563] text-left relative font-inter"
style={{
fontSize: '16px',
lineHeight: '150%',
fontWeight: 400
}}
>
Total Value Locked
</div>
<div
ref={tvl.elementRef}
className="text-[#111827] text-left relative font-jetbrains"
style={{
fontSize: '48px',
lineHeight: '110%',
letterSpacing: '-0.01em',
fontWeight: 800
}}
>
{formatNumber(tvl.count, '$', 'M+', 'M')}
</div>
</div>
</div>
{/* Card 2 - Avg. APY */}
<div
className="bg-white flex flex-row items-center justify-between h-[182px] relative"
style={{
borderStyle: 'solid',
borderColor: '#e5e7eb',
borderWidth: '0px 1px 0px 0px',
padding: '0px 32px',
gridColumn: '2 / span 1',
gridRow: '1 / span 1'
}}
>
<div className="flex flex-col gap-2 items-start justify-center flex-1 relative">
<div
className="text-[#4b5563] text-left relative font-inter"
style={{
fontSize: '16px',
lineHeight: '150%',
fontWeight: 400
}}
>
Avg. APY
</div>
<div
ref={apy.elementRef}
className="text-[#111827] text-left relative font-jetbrains"
style={{
fontSize: '48px',
lineHeight: '110%',
letterSpacing: '-0.01em',
fontWeight: 800
}}
>
{(apy.count / 100).toFixed(2)}%
</div>
</div>
</div>
{/* Card 3 - Yield Captured */}
<div
className="bg-white flex flex-row items-center justify-between h-[182px] relative"
style={{
borderStyle: 'solid',
borderColor: '#e5e7eb',
borderWidth: '0px 1px 0px 0px',
padding: '0px 32px',
gridColumn: '3 / span 1',
gridRow: '1 / span 1'
}}
>
<div className="flex flex-col gap-2 items-start justify-center flex-1 relative">
<div
className="text-[#4b5563] text-left relative font-inter"
style={{
fontSize: '16px',
lineHeight: '150%',
fontWeight: 400
}}
>
Yield Captured
</div>
<div
ref={yield_.elementRef}
className="text-[#111827] text-left relative self-stretch font-jetbrains"
style={{
fontSize: '48px',
lineHeight: '110%',
letterSpacing: '-0.01em',
fontWeight: 800
}}
>
{formatNumber(yield_.count, '$', 'M', 'M')}
</div>
</div>
</div>
{/* Card 4 - Connected Users */}
<div
className="bg-white flex flex-row items-center justify-between h-[182px] relative"
style={{
borderStyle: 'solid',
borderColor: 'transparent',
borderWidth: '0px 1px 0px 0px',
padding: '0px 0px 0px 32px',
gridColumn: '4 / span 1',
gridRow: '1 / span 1'
}}
>
<div className="flex flex-col gap-2 items-start justify-center flex-1 relative">
<div
className="text-[#4b5563] text-left relative font-inter"
style={{
fontSize: '16px',
lineHeight: '150%',
fontWeight: 400
}}
>
Connected Users
</div>
{/* .frame-38 - Number and avatars */}
<div className="flex flex-row items-center justify-between self-stretch flex-shrink-0 relative">
{/* Number */}
<div
ref={users.elementRef}
className="text-[#111827] text-left relative font-jetbrains"
style={{
fontSize: '48px',
lineHeight: '110%',
letterSpacing: '-0.01em',
fontWeight: 800
}}
>
{users.count.toLocaleString()}+
</div>
{/* .container9 - Avatar group */}
<div className="flex flex-row items-center justify-start flex-shrink-0 relative overflow-hidden">
{/* Avatar 1 - image-23 */}
<div
className={`flex-shrink-0 w-9 h-9 relative overflow-hidden border-white transition-all duration-700 ease-out ${
mounted && imagesVisible ? 'translate-y-0 opacity-100' : 'translate-y-12 opacity-0'
}`}
style={{
borderRadius: '15239273px',
borderStyle: 'solid',
borderWidth: '1.82px',
padding: '1.82px',
aspectRatio: 1,
transitionDelay: '100ms'
}}
>
<Image
src="/image-230.png"
alt="User"
width={33}
height={33}
className="absolute object-cover"
style={{
left: '1.82px',
top: '1.82px',
aspectRatio: 1
}}
/>
</div>
{/* Avatar 2 - image-24 */}
<div
className={`flex-shrink-0 w-9 h-9 relative overflow-hidden border-white transition-all duration-700 ease-out ${
mounted && imagesVisible ? 'translate-y-0 opacity-100' : 'translate-y-12 opacity-0'
}`}
style={{
margin: '0 0 0 -8px',
borderRadius: '15239273px',
borderStyle: 'solid',
borderWidth: '1.82px',
padding: '1.82px',
aspectRatio: 1,
transitionDelay: '200ms'
}}
>
<Image
src="/image-240.png"
alt="User"
width={36}
height={36}
className="absolute object-cover"
style={{
left: '0px',
top: '0px',
aspectRatio: 1
}}
/>
</div>
{/* Avatar 3 - image-25 */}
<div
className={`flex-shrink-0 w-9 h-9 relative overflow-hidden border-white transition-all duration-700 ease-out ${
mounted && imagesVisible ? 'translate-y-0 opacity-100' : 'translate-y-12 opacity-0'
}`}
style={{
margin: '0 0 0 -8px',
borderRadius: '15239273px',
borderStyle: 'solid',
borderWidth: '1.82px',
padding: '1.82px',
aspectRatio: 1,
transitionDelay: '300ms'
}}
>
<Image
src="/image-250.png"
alt="User"
width={40}
height={40}
className="absolute object-cover"
style={{
left: '6px',
top: '0.5px',
aspectRatio: 1
}}
/>
</div>
{/* Avatar 4 - image-252 */}
<div
className={`flex-shrink-0 w-9 h-9 relative overflow-hidden border-white transition-all duration-700 ease-out ${
mounted && imagesVisible ? 'translate-y-0 opacity-100' : 'translate-y-12 opacity-0'
}`}
style={{
margin: '0 0 0 -8px',
borderRadius: '15239273px',
borderStyle: 'solid',
borderWidth: '1.82px',
padding: '1.82px',
aspectRatio: 1,
transitionDelay: '400ms'
}}
>
<Image
src="/image-251.png"
alt="User"
width={36}
height={36}
className="absolute object-cover"
style={{
left: '0px',
top: '0px',
aspectRatio: 1
}}
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
}