- Add automatic route change tracking for SPA navigation - Implement Cookie consent banner for GDPR compliance - Add performance tracking (LCP, FID, CLS Web Vitals) - Add outbound link click tracking - Integrate contact form submission tracking with conversion events - Add CTA button click tracking in hero section - Integrate error tracking in ErrorBoundary component - Extend analytics utility library with 15+ tracking functions - Configure IP anonymization and privacy settings - Remove unused test files and deployment scripts - Update case studies to include only specified cases - Fix mobile navigation active state issues - Fix lint errors in test files and components BREAKING CHANGE: Google Analytics now requires user consent before tracking
244 lines
8.0 KiB
TypeScript
244 lines
8.0 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { motion } from 'framer-motion';
|
|
import { StaticLink } from '@/components/ui/static-link';
|
|
import { RippleButton, SealButton } from '@/components/ui/ripple-button';
|
|
import { MagneticButton, BlurReveal, CounterWithEffect } from '@/lib/animations';
|
|
import { COMPANY_INFO, STATS } from '@/lib/constants';
|
|
import { ArrowRight, Shield, Zap, Award } from 'lucide-react';
|
|
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
|
import { trackButtonClick, trackServiceInterest } from '@/lib/analytics';
|
|
|
|
interface HeroContentProps {
|
|
isVisible: boolean;
|
|
}
|
|
|
|
const features = [
|
|
{ icon: Shield, text: '安全可靠' },
|
|
{ icon: Zap, text: '高效便捷' },
|
|
{ icon: Award, text: '专业服务' },
|
|
];
|
|
|
|
function scrollTo(id: string) {
|
|
const element = document.getElementById(id);
|
|
if (element) {
|
|
element.scrollIntoView({ behavior: 'smooth' });
|
|
}
|
|
}
|
|
|
|
function handleKeyDown(event: React.KeyboardEvent<HTMLButtonElement>, id: string) {
|
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
event.preventDefault();
|
|
scrollTo(id);
|
|
}
|
|
}
|
|
|
|
export function HeroContent({ isVisible }: HeroContentProps) {
|
|
const shouldReduceMotion = useReducedMotion();
|
|
|
|
return (
|
|
<motion.div
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20, scale: 0.95 }}
|
|
animate={isVisible ? { opacity: 1, y: 0, scale: 1 } : {}}
|
|
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
|
className="mb-8"
|
|
>
|
|
<span className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full border border-[#1C1C1C]/20 bg-[#F5F5F5] text-[#1C1C1C] text-sm font-medium">
|
|
智连未来,成长伙伴
|
|
</span>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
export function HeroTitle({ isVisible }: HeroContentProps) {
|
|
const shouldReduceMotion = useReducedMotion();
|
|
|
|
return (
|
|
<motion.h1
|
|
id="hero-heading"
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
|
animate={isVisible ? { opacity: 1, y: 0 } : {}}
|
|
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, delay: 0.1 }}
|
|
className="text-5xl sm:text-6xl lg:text-7xl tracking-tight mb-6 font-calligraphy"
|
|
style={{
|
|
fontWeight: 'normal',
|
|
WebkitFontSmoothing: 'antialiased',
|
|
MozOsxFontSmoothing: 'grayscale',
|
|
textRendering: 'optimizeLegibility'
|
|
}}
|
|
>
|
|
{COMPANY_INFO.shortName}
|
|
</motion.h1>
|
|
);
|
|
}
|
|
|
|
export function HeroDescription(_props: HeroContentProps) {
|
|
return (
|
|
<div className="mb-10">
|
|
<BlurReveal delay={0.3}>
|
|
<p className="text-xl sm:text-2xl text-[#4A5568] mb-4">
|
|
<span className="font-semibold bg-gradient-to-r from-[#C41E3A] via-[#E04A68] to-[#C41E3A] bg-clip-text text-transparent">
|
|
企业数字化转型服务商
|
|
</span>
|
|
</p>
|
|
</BlurReveal>
|
|
<BlurReveal delay={0.4}>
|
|
<p className="text-lg text-[#718096] max-w-2xl mx-auto leading-relaxed">
|
|
以智慧连接数字趋势,以伙伴身份陪您成长——您的数字化转型同行者
|
|
</p>
|
|
</BlurReveal>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function HeroButtons({ isVisible }: HeroContentProps) {
|
|
const shouldReduceMotion = useReducedMotion();
|
|
|
|
const handleConsultClick = () => {
|
|
trackButtonClick('consult_now', 'hero_section');
|
|
trackServiceInterest('consultation');
|
|
};
|
|
|
|
const handleLearnMoreClick = () => {
|
|
trackButtonClick('learn_more', 'hero_section');
|
|
scrollTo('about');
|
|
};
|
|
|
|
return (
|
|
<motion.div
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
|
animate={isVisible ? { opacity: 1, y: 0 } : {}}
|
|
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, delay: 0.3 }}
|
|
className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-8"
|
|
>
|
|
<MagneticButton strength={0.4}>
|
|
<StaticLink href="/contact" onClick={handleConsultClick}>
|
|
<SealButton size="lg" className="min-w-45">
|
|
立即咨询
|
|
<ArrowRight className="w-4 h-4 ml-2" />
|
|
</SealButton>
|
|
</StaticLink>
|
|
</MagneticButton>
|
|
<MagneticButton strength={0.4}>
|
|
<RippleButton
|
|
size="lg"
|
|
variant="outline"
|
|
onClick={handleLearnMoreClick}
|
|
onKeyDown={(e) => handleKeyDown(e, 'about')}
|
|
className="min-w-45"
|
|
>
|
|
了解更多
|
|
</RippleButton>
|
|
</MagneticButton>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
export function HeroFeatures({ isVisible }: HeroContentProps) {
|
|
const shouldReduceMotion = useReducedMotion();
|
|
|
|
return (
|
|
<motion.div
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
|
animate={isVisible ? { opacity: 1, y: 0 } : {}}
|
|
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, delay: 0.35 }}
|
|
className="flex flex-wrap gap-4 justify-center mb-16"
|
|
>
|
|
{features.map((feature, index) => (
|
|
<motion.div
|
|
key={index}
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, scale: 0.9 }}
|
|
animate={isVisible ? { opacity: 1, scale: 1 } : {}}
|
|
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.4, delay: 0.4 + index * 0.1 }}
|
|
whileHover={shouldReduceMotion ? {} : { scale: 1.05, y: -2 }}
|
|
className="flex items-center gap-2 px-4 py-2 rounded-full bg-[#FAFAFA] border border-[#E5E5E5] transition-all duration-300 hover:border-[#1C1C1C] hover:shadow-md cursor-default"
|
|
>
|
|
<feature.icon className="w-4 h-4 text-[#C41E3A]" />
|
|
<span className="text-sm text-[#3D3D3D]">{feature.text}</span>
|
|
</motion.div>
|
|
))}
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
export function HeroStats() {
|
|
const [statsVisible, setStatsVisible] = useState(true);
|
|
const shouldReduceMotion = useReducedMotion();
|
|
|
|
useEffect(() => {
|
|
const statsEl = document.getElementById('stats-section');
|
|
if (!statsEl) {return;}
|
|
|
|
const observer = new IntersectionObserver(
|
|
([entry]) => {
|
|
if (entry?.isIntersecting) {
|
|
setStatsVisible(true);
|
|
}
|
|
},
|
|
{ threshold: 0.5 }
|
|
);
|
|
|
|
observer.observe(statsEl);
|
|
return () => observer.disconnect();
|
|
}, []);
|
|
|
|
return (
|
|
<motion.div
|
|
id="stats-section"
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
|
animate={statsVisible ? { opacity: 1, y: 0 } : {}}
|
|
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, delay: 0.4 }}
|
|
className="pt-16 border-t border-[#E2E8F0]"
|
|
>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-12">
|
|
{STATS.map((stat, index) => (
|
|
<HeroStatItem
|
|
key={stat.label}
|
|
stat={stat}
|
|
index={index}
|
|
shouldAnimate={statsVisible}
|
|
shouldReduceMotion={shouldReduceMotion}
|
|
/>
|
|
))}
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
function HeroStatItem({ stat, index, shouldAnimate, shouldReduceMotion }: {
|
|
stat: { value: string; label: string };
|
|
index: number;
|
|
shouldAnimate: boolean;
|
|
shouldReduceMotion: boolean;
|
|
}) {
|
|
const numericValue = parseInt(stat.value.replace(/\D/g, ''));
|
|
const suffix = stat.value.replace(/[\d]/g, '');
|
|
|
|
return (
|
|
<motion.div
|
|
className="group cursor-default text-center"
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20, scale: 0.9 }}
|
|
animate={shouldAnimate ? { opacity: 1, y: 0, scale: 1 } : {}}
|
|
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.5, delay: index * 0.1, type: 'spring', stiffness: 100 }}
|
|
whileHover={shouldReduceMotion ? {} : { scale: 1.05, y: -5 }}
|
|
>
|
|
<div className="text-4xl sm:text-5xl font-bold text-[#C41E3A] mb-3">
|
|
{shouldAnimate ? (
|
|
<CounterWithEffect
|
|
end={numericValue}
|
|
suffix={suffix}
|
|
effect="bounce"
|
|
duration={2000}
|
|
/>
|
|
) : (
|
|
<span className="text-[#CBD5E0]">0{suffix}</span>
|
|
)}
|
|
</div>
|
|
<div className="text-sm text-[#718096] group-hover:text-[#4A5568] transition-colors">
|
|
{stat.label}
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
}
|