包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、 antdesign(管理后台)、landingpage(营销落地页)、 数据库 SQL 和配置文件。
176 lines
5.9 KiB
TypeScript
176 lines
5.9 KiB
TypeScript
'use client';
|
|
|
|
import { useRef, useEffect, useState } from 'react';
|
|
import Image from 'next/image';
|
|
import { motion, Variants } from 'framer-motion';
|
|
import { useLanguage } from '@/contexts/LanguageContext';
|
|
import { useTheme } from '@/contexts/ThemeContext';
|
|
import ConsenSysLogo from './ConsenSysLogo';
|
|
|
|
const itemVariants: Variants = {
|
|
hidden: { y: '3rem', opacity: 0 },
|
|
visible: (i: number) => ({
|
|
y: 0,
|
|
opacity: 1,
|
|
transition: { duration: 0.7, ease: 'easeOut' as const, delay: i * 0.1 }
|
|
})
|
|
};
|
|
|
|
export default function TrustedBySection() {
|
|
const { t } = useLanguage();
|
|
const { theme } = useTheme();
|
|
const isDark = theme === 'dark';
|
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
const rafRef = useRef<number>(0);
|
|
const isPaused = useRef(false);
|
|
const [isMounted, setIsMounted] = useState(false);
|
|
|
|
// 确保组件挂载后才启动滚动
|
|
useEffect(() => {
|
|
setIsMounted(true);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!isMounted) return;
|
|
|
|
const el = scrollRef.current;
|
|
if (!el) return;
|
|
|
|
const speed = 1.2;
|
|
let lastTime = performance.now();
|
|
|
|
const tick = (currentTime: number) => {
|
|
const deltaTime = currentTime - lastTime;
|
|
lastTime = currentTime;
|
|
|
|
if (!isPaused.current && el) {
|
|
el.scrollLeft += speed * (deltaTime / 16); // 标准化到60fps
|
|
if (el.scrollLeft >= el.scrollWidth / 2) {
|
|
el.scrollLeft = 0;
|
|
}
|
|
}
|
|
rafRef.current = requestAnimationFrame(tick);
|
|
};
|
|
|
|
// 延迟启动,确保 DOM 完全渲染
|
|
const startTimer = setTimeout(() => {
|
|
rafRef.current = requestAnimationFrame(tick);
|
|
}, 100);
|
|
|
|
return () => {
|
|
clearTimeout(startTimer);
|
|
if (rafRef.current) {
|
|
cancelAnimationFrame(rafRef.current);
|
|
}
|
|
};
|
|
}, [isMounted]);
|
|
|
|
const logoClass = isDark
|
|
? 'brightness-0 invert opacity-50 group-hover:opacity-100'
|
|
: 'brightness-0 opacity-50 group-hover:opacity-100';
|
|
|
|
const partners = [
|
|
{ name: 'BlackRock', src: '/nav-ireland0.svg', width: 200, height: 40 },
|
|
{ name: 'Coinbase', src: '/coinbase-10.svg', width: 180, height: 40 },
|
|
{ name: 'Wintermute', src: '/wintermute0.svg', width: 247, height: 40 },
|
|
{ name: 'Circle', src: '/group0.svg', width: 156, height: 40 },
|
|
{ name: 'ConsenSys', src: '', width: 220, height: 50 },
|
|
];
|
|
|
|
const renderLogo = (partner: typeof partners[number], key: string) => (
|
|
<div
|
|
key={key}
|
|
className="relative flex items-center justify-center transition-all duration-300 group"
|
|
style={{ width: `${partner.width}px`, height: `${partner.height}px` }}
|
|
>
|
|
{partner.name === 'ConsenSys' ? (
|
|
<div className={`w-full h-full flex items-center justify-center transition-all duration-300 ${logoClass}`}>
|
|
<ConsenSysLogo width={partner.width} height={partner.height} />
|
|
</div>
|
|
) : (
|
|
<Image
|
|
src={partner.src}
|
|
alt={partner.name}
|
|
width={partner.width}
|
|
height={partner.height}
|
|
className={`w-full h-full object-contain transition-all duration-300 ${logoClass}`}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<section
|
|
className="flex flex-col items-center justify-center w-full overflow-hidden border-b bg-bg-subtle border-border-normal py-10 gap-8"
|
|
style={{ paddingLeft: 'clamp(16px, 4vw, 80px)', paddingRight: 'clamp(16px, 4vw, 80px)' }}
|
|
>
|
|
<div className="text-lg font-medium text-center font-domine text-text-primary px-4">
|
|
{t('trusted.title')}
|
|
</div>
|
|
|
|
{/* ── 移动端:自动滚动 + 手动滑动 ── */}
|
|
<div
|
|
ref={scrollRef}
|
|
className="md:hidden w-full overflow-x-auto cursor-grab active:cursor-grabbing"
|
|
style={{
|
|
WebkitOverflowScrolling: 'touch',
|
|
scrollbarWidth: 'none',
|
|
msOverflowStyle: 'none',
|
|
}}
|
|
onTouchStart={() => { isPaused.current = true; }}
|
|
onTouchEnd={() => { isPaused.current = false; }}
|
|
onTouchCancel={() => { isPaused.current = false; }}
|
|
>
|
|
<div className="flex items-center gap-10" style={{ width: 'max-content', padding: '0 24px' }}>
|
|
{[...partners, ...partners].map((partner, i) => (
|
|
<div
|
|
key={`${partner.name}-${i}`}
|
|
className="flex-shrink-0 flex items-center justify-center h-[60px]"
|
|
style={{ width: `${partner.width}px` }}
|
|
>
|
|
{renderLogo(partner, `m-${partner.name}-${i}`)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* ── 桌面端:单行不换行 ── */}
|
|
<div className="hidden md:flex w-full max-w-[1440px] 2xl:max-w-[1760px] 3xl:max-w-[2200px] flex-row flex-nowrap items-center justify-between gap-x-6">
|
|
{partners.map((partner, index) => (
|
|
<motion.div
|
|
key={partner.name}
|
|
custom={index}
|
|
variants={itemVariants}
|
|
initial="hidden"
|
|
whileInView="visible"
|
|
viewport={{ once: true, amount: 0.3 }}
|
|
className="relative cursor-pointer group flex items-center justify-center"
|
|
style={{
|
|
maxWidth: `${partner.width}px`,
|
|
width: '100%',
|
|
height: `${partner.height}px`,
|
|
flexShrink: 1,
|
|
flexBasis: `${partner.width}px`,
|
|
minWidth: 60,
|
|
}}
|
|
>
|
|
{partner.name === 'ConsenSys' ? (
|
|
<div className={`w-full h-full flex items-center justify-center transition-all duration-300 ${logoClass}`}>
|
|
<ConsenSysLogo width={partner.width} height={partner.height} />
|
|
</div>
|
|
) : (
|
|
<Image
|
|
src={partner.src}
|
|
alt={partner.name}
|
|
width={partner.width}
|
|
height={partner.height}
|
|
className={`w-full h-full object-contain transition-all duration-300 ${logoClass}`}
|
|
/>
|
|
)}
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|