feat: redesign hero section with dark tech theme and framer-motion

This commit is contained in:
张翔
2026-02-21 22:58:09 +08:00
parent 1b0454998d
commit 554011f0e7
+115 -118
View File
@@ -1,18 +1,21 @@
'use client'; 'use client';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { CTAButton } from '@/components/ui/cta-button'; import { motion } from 'framer-motion';
import { AnimatedNumber } from '@/components/ui/animated-number'; import { Button } from '@/components/ui/button';
import { SealParticle } from '@/components/effects/seal-particle';
import { SealAnimationEnhanced } from '@/components/effects/seal-animation-enhanced';
import { StampAnimation } from '@/components/effects/stamp-animation';
import { COMPANY_INFO, STATS } from '@/lib/constants'; import { COMPANY_INFO, STATS } from '@/lib/constants';
import { ArrowRight, Sparkles } from 'lucide-react'; import { ArrowRight, Sparkles, Code2, Cloud, Shield, BarChart3 } from 'lucide-react';
const iconMap = {
Code: Code2,
Cloud: Cloud,
BarChart3: BarChart3,
Shield: Shield,
};
export function HeroSection() { export function HeroSection() {
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const sectionRef = useRef<HTMLElement>(null); const sectionRef = useRef<HTMLElement>(null);
const statsRef = useRef<HTMLDivElement>(null);
const [statsVisible, setStatsVisible] = useState(false); const [statsVisible, setStatsVisible] = useState(false);
useEffect(() => { useEffect(() => {
@@ -33,7 +36,8 @@ export function HeroSection() {
}, []); }, []);
useEffect(() => { useEffect(() => {
if (!statsRef.current) return; const statsEl = document.getElementById('stats-section');
if (!statsEl) return;
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
([entry]) => { ([entry]) => {
@@ -44,7 +48,7 @@ export function HeroSection() {
{ threshold: 0.5 } { threshold: 0.5 }
); );
observer.observe(statsRef.current); observer.observe(statsEl);
return () => observer.disconnect(); return () => observer.disconnect();
}, []); }, []);
@@ -59,126 +63,91 @@ export function HeroSection() {
<section <section
id="home" id="home"
ref={sectionRef} ref={sectionRef}
className="relative min-h-screen flex items-center pt-16 overflow-hidden" className="relative min-h-screen flex items-center pt-16 overflow-hidden bg-[var(--color-bg-primary)]"
> >
<SealParticle <div className="absolute inset-0 pointer-events-none overflow-hidden">
particleCount={30} <div className="absolute top-1/4 left-1/4 w-96 h-96 bg-[var(--color-tech-blue)]/5 rounded-full blur-3xl animate-pulse-glow" />
colors={['#C41E3A', '#D4A574', '#8B4513']} <div className="absolute bottom-1/4 right-1/4 w-80 h-80 bg-[var(--color-tech-purple)]/5 rounded-full blur-3xl animate-pulse-glow" style={{ animationDelay: '1s' }} />
minSize={1} <div className="absolute top-1/2 right-1/3 w-64 h-64 bg-[var(--color-tech-cyan)]/5 rounded-full blur-3xl animate-pulse-glow" style={{ animationDelay: '2s' }} />
maxSize={4}
speed={0.3}
/>
{/* 增强版印章动画 - 桌面端显示 */}
<div className="absolute right-0 top-1/2 -translate-y-1/2 hidden lg:block z-10">
<SealAnimationEnhanced
width={400}
height={400}
particleCount={150}
colors={['#C41E3A', '#D4A574', '#8B4513']}
sealText="睿新"
animationStages={true}
onStageChange={(stage) => console.log('Animation stage:', stage)}
className="opacity-80"
/>
</div> </div>
<div className="absolute inset-0 pointer-events-none"> <div className="absolute inset-0 pointer-events-none">
<div className="absolute top-20 right-0 w-96 h-96 bg-gradient-radial from-[#C41E3A]/5 to-transparent rounded-full blur-3xl" /> <svg className="absolute w-full h-full opacity-10">
<div className="absolute bottom-20 left-0 w-80 h-80 bg-gradient-radial from-[#171717]/3 to-transparent rounded-full blur-3xl" /> <defs>
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="var(--color-tech-blue)" strokeWidth="0.5" />
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#grid)" />
</svg>
</div> </div>
<div className="container-wide py-24 md:py-32 lg:py-40 relative z-10"> <div className="container-wide py-24 md:py-32 lg:py-40 relative z-10">
<div className="max-w-3xl"> <div className="max-w-4xl mx-auto text-center">
<div <motion.div
className={` initial={{ opacity: 0, y: 20 }}
opacity-0 translate-y-4 animate={isVisible ? { opacity: 1, y: 0 } : {}}
${isVisible ? 'animate-fade-in-up' : ''} transition={{ duration: 0.6 }}
`} className="mb-6"
> >
<div className="flex items-center gap-3 mb-8"> <span className="inline-flex items-center gap-2 px-4 py-2 rounded-full border border-[var(--color-tech-blue)]/30 bg-[var(--color-tech-blue)]/5 text-[var(--color-tech-blue)] text-sm font-medium">
<StampAnimation delay={300}> <Sparkles className="w-4 h-4" />
<div className="relative"> ·
<div className="w-3 h-3 bg-[#C41E3A] rounded-sm rotate-45" /> </span>
<div className="absolute inset-0 w-3 h-3 bg-[#C41E3A] rounded-sm rotate-45 animate-ping opacity-30" /> </motion.div>
</div>
</StampAnimation>
<span className="text-sm text-[#525252] tracking-wide font-medium">
×
</span>
</div>
</div>
<h1 <motion.h1
className={` initial={{ opacity: 0, y: 20 }}
text-4xl sm:text-5xl lg:text-6xl font-semibold animate={isVisible ? { opacity: 1, y: 0 } : {}}
text-[#171717] leading-[1.1] tracking-tight mb-6 transition={{ duration: 0.6, delay: 0.1 }}
opacity-0 translate-y-4 className="text-4xl sm:text-5xl lg:text-6xl font-bold text-white leading-tight tracking-tight mb-6"
${isVisible ? 'animate-fade-in-up stagger-1' : ''}
`}
> >
<StampAnimation delay={500}> <span className="tech-gradient-text">{COMPANY_INFO.shortName}</span>
<span className="inline-block">{COMPANY_INFO.name}</span> <br />
</StampAnimation> <span className="text-gray-300"></span>
</h1> </motion.h1>
<p <motion.p
className={` initial={{ opacity: 0, y: 20 }}
text-xl sm:text-2xl text-[#525252] mb-8 animate={isVisible ? { opacity: 1, y: 0 } : {}}
opacity-0 translate-y-4 transition={{ duration: 0.6, delay: 0.2 }}
${isVisible ? 'animate-fade-in-up stagger-2' : ''} className="text-lg sm:text-xl text-gray-400 mb-8 max-w-2xl mx-auto leading-relaxed"
`}
>
<span className="text-[#C41E3A] font-medium">{COMPANY_INFO.slogan.split('')[0]}</span>
{COMPANY_INFO.slogan.split('')[1]}
</p>
<p
className={`
text-base sm:text-lg text-[#737373] max-w-2xl mb-12
leading-relaxed
opacity-0 translate-y-4
${isVisible ? 'animate-fade-in-up stagger-3' : ''}
`}
> >
{COMPANY_INFO.description} {COMPANY_INFO.description}
</p> </motion.p>
<div <motion.div
className={` initial={{ opacity: 0, y: 20 }}
flex flex-col sm:flex-row items-start sm:items-center gap-4 mb-20 animate={isVisible ? { opacity: 1, y: 0 } : {}}
opacity-0 translate-y-4 transition={{ duration: 0.6, delay: 0.3 }}
${isVisible ? 'animate-fade-in-up stagger-4' : ''} className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-16"
`}
> >
<CTAButton <Button
size="lg" size="lg"
onClick={() => handleScrollTo('about')} onClick={() => handleScrollTo('about')}
icon={<ArrowRight className="w-4 h-4" />} className="min-w-[160px]"
iconPosition="right"
> >
</CTAButton> <ArrowRight className="w-4 h-4 ml-2" />
<CTAButton </Button>
size="lg" <Button
size="lg"
variant="outline" variant="outline"
onClick={() => handleScrollTo('contact')} onClick={() => handleScrollTo('contact')}
icon={<Sparkles className="w-4 h-4" />} className="min-w-[160px]"
iconPosition="left"
> >
</CTAButton> </Button>
</div> </motion.div>
<div <motion.div
ref={statsRef} id="stats-section"
className={` initial={{ opacity: 0, y: 20 }}
pt-12 border-t border-[#E5E5E5]/50 animate={isVisible ? { opacity: 1, y: 0 } : {}}
opacity-0 translate-y-4 transition={{ duration: 0.6, delay: 0.4 }}
${isVisible ? 'animate-fade-in-up stagger-5' : ''} className="pt-12 border-t border-gray-800"
`}
> >
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-12"> <div className="grid grid-cols-2 md:grid-cols-4 gap-8">
{STATS.map((stat, index) => ( {STATS.map((stat, index) => (
<StatItem <StatItem
key={stat.label} key={stat.label}
@@ -188,12 +157,11 @@ export function HeroSection() {
/> />
))} ))}
</div> </div>
</div> </motion.div>
</div> </div>
</div> </div>
<div className="absolute right-0 top-1/2 -translate-y-1/2 w-1/3 h-px bg-gradient-to-l from-[#C41E3A]/10 to-transparent hidden lg:block" /> <div className="absolute bottom-0 left-0 right-0 h-32 bg-gradient-to-t from-[var(--color-bg-primary)] to-transparent pointer-events-none" />
<div className="absolute left-0 bottom-20 w-1/4 h-px bg-gradient-to-r from-[#171717]/5 to-transparent hidden lg:block" />
</section> </section>
); );
} }
@@ -207,18 +175,47 @@ function StatItem({ stat, index, shouldAnimate }: {
const suffix = stat.value.replace(/[\d]/g, ''); const suffix = stat.value.replace(/[\d]/g, '');
return ( return (
<div className="group cursor-default"> <motion.div
<div className="text-2xl sm:text-3xl font-semibold text-[#C41E3A] mb-1 transition-all duration-300 group-hover:translate-x-0.5"> className="group cursor-default text-center"
<AnimatedNumber initial={{ opacity: 0, y: 20 }}
value={numericValue} animate={shouldAnimate ? { opacity: 1, y: 0 } : {}}
suffix={suffix} transition={{ duration: 0.5, delay: index * 0.1 }}
duration={2000} >
trigger={shouldAnimate} <div className="text-3xl sm:text-4xl font-bold tech-gradient-text mb-2">
/> {shouldAnimate ? (
<AnimatedCounter value={numericValue} suffix={suffix} />
) : (
`0${suffix}`
)}
</div> </div>
<div className="text-sm text-[#737373] transition-colors duration-200 group-hover:text-[#525252]"> <div className="text-sm text-gray-500 group-hover:text-gray-400 transition-colors">
{stat.label} {stat.label}
</div> </div>
</div> </motion.div>
); );
} }
function AnimatedCounter({ value, suffix }: { value: number; suffix: string }) {
const [count, setCount] = useState(0);
useEffect(() => {
const duration = 2000;
const steps = 60;
const increment = value / steps;
let current = 0;
const timer = setInterval(() => {
current += increment;
if (current >= value) {
setCount(value);
clearInterval(timer);
} else {
setCount(Math.floor(current));
}
}, duration / steps);
return () => clearInterval(timer);
}, [value]);
return <>{count}{suffix}</>;
}