- 重构 About、Contact、Team 等静态营销页面 - 重构 News 新闻列表与详情页 - 重构 Products 产品目录、详情与独立产品页面 - 重构 Services 与 Solutions 服务/解决方案页面 - 重构 Detail 四层叙事组件 (Hero → Value → Trust → CTA) - 重构 Sections 页面区块组件 (Hero, CTA, SocialProof, WhyUs 等) - 新增 SectionHeader、ServiceCard、CaseCard 等可复用组件
167 lines
6.4 KiB
TypeScript
167 lines
6.4 KiB
TypeScript
'use client';
|
|
|
|
import { motion } from 'framer-motion';
|
|
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
|
|
|
interface ListPageHeroProps {
|
|
title: string;
|
|
subtitle: string;
|
|
description?: string;
|
|
stats?: Array<{ value: string; label: string }>;
|
|
badge?: {
|
|
text: string;
|
|
variant?: 'brand' | 'blue' | 'green' | 'neutral';
|
|
};
|
|
}
|
|
|
|
const BADGE_VARIANTS = {
|
|
brand: {
|
|
bg: 'var(--color-brand-bg)',
|
|
text: 'var(--color-brand)',
|
|
border: 'rgba(var(--color-brand-rgb), 0.15)',
|
|
dot: 'var(--color-brand)',
|
|
},
|
|
blue: {
|
|
bg: 'rgba(var(--color-accent-blue-rgb), 0.08)',
|
|
text: 'var(--color-accent-blue)',
|
|
border: 'rgba(var(--color-accent-blue-rgb), 0.15)',
|
|
dot: 'var(--color-accent-blue)',
|
|
},
|
|
green: {
|
|
bg: 'rgba(34, 197, 94, 0.08)',
|
|
text: '#16a34a',
|
|
border: 'rgba(34, 197, 94, 0.15)',
|
|
dot: '#22c55e',
|
|
},
|
|
neutral: {
|
|
bg: 'var(--color-bg-section)',
|
|
text: 'var(--color-text-secondary)',
|
|
border: 'var(--color-border-primary)',
|
|
dot: 'var(--color-text-subtle)',
|
|
},
|
|
};
|
|
|
|
export function ListPageHero({
|
|
title,
|
|
subtitle,
|
|
description,
|
|
stats,
|
|
badge,
|
|
}: ListPageHeroProps) {
|
|
const shouldReduceMotion = useReducedMotion();
|
|
const badgeStyle = BADGE_VARIANTS[badge?.variant || 'brand'];
|
|
|
|
return (
|
|
<section className="relative overflow-hidden">
|
|
{/* Multi-layer background */}
|
|
<div className="absolute inset-0 bg-gradient-to-br from-[var(--color-bg-section)] via-[var(--color-bg-secondary)] to-[var(--color-bg-section)]" />
|
|
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,_rgba(var(--color-brand-rgb),_0.05)_0%,_transparent_50%)]" />
|
|
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,_rgba(var(--color-brand-rgb),_0.03)_0%,_transparent_50%)]" />
|
|
|
|
{/* Subtle grid texture */}
|
|
<div
|
|
className="absolute inset-0 opacity-[0.03]"
|
|
style={{
|
|
backgroundImage:
|
|
'linear-gradient(rgba(var(--color-border-primary-rgb), 0.5) 1px, transparent 1px), linear-gradient(90deg, rgba(var(--color-border-primary-rgb), 0.5) 1px, transparent 1px)',
|
|
backgroundSize: '60px 60px',
|
|
}}
|
|
aria-hidden="true"
|
|
/>
|
|
|
|
{/* Decorative accent line */}
|
|
<div className="absolute top-0 left-0 right-0 h-px" style={{ background: 'linear-gradient(90deg, transparent, var(--color-brand), transparent)' }} aria-hidden="true" />
|
|
|
|
<div className="container-wide relative z-10 py-20 lg:py-28">
|
|
<motion.div
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
|
className="max-w-4xl mx-auto text-center"
|
|
>
|
|
{/* Dynamic Badge */}
|
|
{badge && badge.text && (
|
|
<motion.div
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, scale: 0.95, y: 8 }}
|
|
whileInView={{ opacity: 1, scale: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ delay: 0.1, duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
|
|
className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full text-sm font-medium mb-6 backdrop-blur-sm"
|
|
style={{ backgroundColor: badgeStyle.bg, color: badgeStyle.text, border: `1px solid ${badgeStyle.border}` }}
|
|
>
|
|
<span className="w-2 h-2 rounded-full animate-pulse" style={{ backgroundColor: badgeStyle.dot }} />
|
|
{badge?.text || ''}
|
|
</motion.div>
|
|
)}
|
|
|
|
{/* Title */}
|
|
<motion.h1
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 24 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ delay: 0.15, duration: 0.7, ease: [0.16, 1, 0.3, 1] }}
|
|
className="text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight mb-6 text-[var(--color-text-primary)]"
|
|
>
|
|
{title}
|
|
</motion.h1>
|
|
|
|
{/* Subtitle */}
|
|
<motion.p
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ delay: 0.25, duration: 0.65, ease: [0.16, 1, 0.3, 1] }}
|
|
className="text-lg md:text-xl max-w-2xl mx-auto leading-relaxed mb-4 text-[var(--color-text-secondary)]"
|
|
>
|
|
{subtitle}
|
|
</motion.p>
|
|
|
|
{/* Description */}
|
|
{description && (
|
|
<motion.p
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 18 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ delay: 0.35, duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
|
className="text-base max-w-xl mx-auto mb-10 text-[var(--color-text-muted)]"
|
|
>
|
|
{description}
|
|
</motion.p>
|
|
)}
|
|
|
|
{/* Stats Row */}
|
|
{stats && stats.length > 0 && (
|
|
<motion.div
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 24 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ delay: 0.4, duration: 0.7, ease: [0.16, 1, 0.3, 1] }}
|
|
className="flex flex-wrap justify-center gap-8 md:gap-12 mt-12 pt-12 border-t border-[var(--color-border-primary)]"
|
|
>
|
|
{stats.map((stat, index) => (
|
|
<motion.div
|
|
key={stat.label}
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, scale: 0.95 }}
|
|
whileInView={{ opacity: 1, scale: 1 }}
|
|
viewport={{ once: true }}
|
|
transition={{ delay: 0.5 + index * 0.08, duration: 0.45, ease: [0.16, 1, 0.3, 1] }}
|
|
className="text-center min-w-[100px]"
|
|
>
|
|
<div className="text-3xl md:text-4xl font-bold tracking-tight text-[var(--color-text-primary)]">
|
|
{stat.value}
|
|
</div>
|
|
<div className="text-sm mt-1 text-[var(--color-text-subtle)]">{stat.label}</div>
|
|
</motion.div>
|
|
))}
|
|
</motion.div>
|
|
)}
|
|
</motion.div>
|
|
</div>
|
|
|
|
{/* Bottom fade */}
|
|
<div className="absolute bottom-0 left-0 right-0 h-20 bg-gradient-to-t from-[var(--color-bg-primary)] to-transparent pointer-events-none" aria-hidden="true" />
|
|
</section>
|
|
);
|
|
}
|