396 lines
14 KiB
TypeScript
396 lines
14 KiB
TypeScript
|
|
'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>
|
|||
|
|
);
|
|||
|
|
}
|