Files
novalon-website/src/app/(marketing)/_archive/home-content-v13.tsx
T
zhangxiang 2fdee7e2ae chore: release v1.0.0-phase1 — UI design restructure & CMS-ification
UI design restructure ~85%: design system complete, four-layer narrative
model implemented, Consulting Professional aesthetic established.
CMS-ification ~75%: all pages integrated with CMS data layer, seed script
covers all content types, ISR + dynamic rendering enabled.
Archive 12 legacy component versions to _archive/.
Quality gates: type-check, 992 tests, coverage all passing.
2026-07-31 21:36:43 +08:00

629 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useEffect, useRef, useState } from 'react';
import { motion, useMotionValue, useTransform } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { Badge } from '@/components/ui/badge';
import {
ArrowRight,
Quote, CheckCircle2,
Zap, Award,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
function MagneticButton({ children, href, variant = 'primary', className }: { children: React.ReactNode; href: string; variant?: 'primary' | 'secondary'; className?: string }) {
const ref = useRef<HTMLAnchorElement>(null);
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (e: React.MouseEvent) => {
if (!ref.current) return;
const rect = ref.current.getBoundingClientRect();
const x = (e.clientX - rect.left - rect.width / 2) * 0.15;
const y = (e.clientY - rect.top - rect.height / 2) * 0.15;
setPosition({ x, y });
};
const handleMouseLeave = () => {
setPosition({ x: 0, y: 0 });
};
return (
<motion.a
ref={ref}
href={href}
animate={{ x: position.x, y: position.y }}
transition={{ duration: 0.3, ease: EASE_OUT }}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
className={cn(
'group relative inline-flex items-center gap-3 px-12 py-6 font-semibold text-base overflow-hidden transition-transform duration-200',
variant === 'primary'
? 'bg-brand text-white'
: 'text-ink border border-border-primary hover:border-border-secondary hover:bg-bg-secondary',
className
)}
>
{variant === 'primary' && (
<div className="absolute inset-0 bg-black/20 translate-y-full group-hover:translate-y-0 transition-transform duration-500 ease-out" />
)}
<span className="relative z-10">{children}</span>
<ArrowRight className="w-5 h-5 relative z-10 transition-transform duration-500 group-hover:translate-x-2" />
</motion.a>
);
}
function HeroSection({ heroData, stats }: { heroData?: Record<string, any> | null; stats?: Record<string, any>[] }) {
const containerRef = useRef<HTMLDivElement>(null);
const scrollProgress = useMotionValue(0);
useEffect(() => {
const element = containerRef.current;
if (!element) return;
const updateProgress = () => {
const rect = element.getBoundingClientRect();
const raw = rect.height > 0 ? -rect.top / rect.height : 0;
scrollProgress.set(Math.max(0, Math.min(1, raw)));
};
updateProgress();
window.addEventListener('scroll', updateProgress, { passive: true });
window.addEventListener('resize', updateProgress);
return () => {
window.removeEventListener('scroll', updateProgress);
window.removeEventListener('resize', updateProgress);
};
}, [scrollProgress]);
const y = useTransform(scrollProgress, [0, 1], [0, 160]);
const opacity = useTransform(scrollProgress, [0, 0.7], [1, 0.15]);
// 兼容新旧数据字段
const eyebrow = heroData?.eyebrow || '';
const heading = heroData?.heading || heroData?.headingBottom || '';
const subheading = heroData?.subheading || heroData?.description || '';
const ctaLabel = heroData?.ctaLabel || '预约咨询';
const ctaHref = heroData?.ctaHref || '/contact';
const secondaryCtaLabel = heroData?.secondaryCtaLabel || '';
const secondaryCtaHref = heroData?.secondaryCtaHref || '#';
const hasValidStats = stats?.length && stats[0]?.value;
const displayStats = hasValidStats ? stats!.slice(0, 4) : [];
if (!heroData) {
return (
<section className="relative min-h-screen flex items-center overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 text-center">
<p className="text-text-muted text-lg">首页 Hero 内容未配置</p>
</div>
</section>
);
}
return (
<section
ref={containerRef}
className="relative min-h-screen flex items-center overflow-hidden bg-white"
>
<motion.div
style={{ y, opacity }}
className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-32 sm:py-40 md:py-48 lg:py-56"
>
<div className="max-w-4xl">
{/* 眉标 Badge */}
{eyebrow && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1, ease: EASE_OUT }}
className="mb-8 sm:mb-10"
>
<span className="inline-flex items-center gap-2 px-3 py-1.5 bg-brand/[0.08] border border-brand/20 rounded-full">
<span className="w-1.5 h-1.5 rounded-full bg-brand" />
<span className="text-[13px] font-semibold text-brand tracking-[0.08em] uppercase">
{eyebrow}
</span>
</span>
</motion.div>
)}
{/* 主标题 — Bain 风格:大号加粗 + 品牌红关键词 */}
<motion.h1
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2, ease: EASE_OUT }}
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-black text-ink leading-[0.92] tracking-tightest mb-10 sm:mb-12"
>
<span className="relative">
{heading}
</span>
</motion.h1>
{/* 数据指标条 — Bain 风格:答案优先,数据先行 */}
{displayStats.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.4, ease: EASE_OUT }}
className="flex flex-wrap gap-6 sm:gap-10 md:gap-14 mb-10 sm:mb-12"
>
{displayStats.map((stat, i) => (
<div key={i} className="flex items-baseline gap-2">
<span className="text-3xl sm:text-4xl md:text-5xl font-black text-brand tabular-nums leading-none">
{stat.value}
</span>
<span className="text-sm text-text-muted whitespace-nowrap">
{stat.label}
</span>
</div>
))}
</motion.div>
)}
{/* 副标题 */}
{subheading && (
<motion.p
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.5, ease: EASE_OUT }}
className="text-lg sm:text-xl text-text-secondary/70 max-w-2xl leading-relaxed mb-12 sm:mb-14"
>
{subheading}
</motion.p>
)}
{/* CTA 按钮组 */}
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.6, ease: EASE_OUT }}
className="flex flex-col sm:flex-row sm:items-center gap-4 sm:gap-6"
>
<MagneticButton href={ctaHref}>{ctaLabel}</MagneticButton>
{secondaryCtaLabel && (
<a
href={secondaryCtaHref}
className="group inline-flex items-center gap-2 text-sm font-medium text-text-secondary hover:text-ink transition-colors duration-200 py-4"
>
{secondaryCtaLabel}
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
</a>
)}
</motion.div>
</div>
</motion.div>
</section>
);
}
function ServicesSection({ services }: { services?: Record<string, any>[] }) {
const hasValidData = services?.length && services[0]?.highlights;
const SERVICES = hasValidData ? services : [];
if (SERVICES.length === 0) {
return (
<section className="relative py-28 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 text-center">
<p className="text-text-muted text-lg">暂无服务数据</p>
</div>
</section>
);
}
return (
<section className="relative py-28 sm:py-36 md:py-44 lg:py-56 overflow-hidden bg-white">
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-border-primary to-transparent" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<div className="max-w-3xl mb-20 sm:mb-28 md:mb-36">
<ScrollReveal>
<SectionLabel>核心服务</SectionLabel>
<h2 className="font-sans text-2xl sm:text-3xl md:text-4xl lg:text-6xl font-black text-ink tracking-tight leading-[0.95] mt-6">
从战略到落地,
<br />
可量化的一站式服务
</h2>
</ScrollReveal>
</div>
<div className="grid md:grid-cols-3 gap-px bg-border-primary">
{/* 首张卡片:战略咨询 — 全宽突出 */}
{(() => {
const first = SERVICES[0];
if (!first) return null;
return (
<StaggerReveal className="md:col-span-3" staggerDelay={0.05}>
<a
href={first.href}
className="group relative block p-8 sm:p-10 md:p-12 lg:p-14 transition-all duration-500 hover:bg-bg-secondary overflow-hidden bg-white"
>
<div className="relative z-10 flex flex-col md:flex-row md:items-start gap-8 md:gap-16">
<div className="w-14 h-14 flex items-center justify-center bg-bg-secondary text-text-secondary group-hover:text-ink transition-colors duration-300 shrink-0">
{first.icon}
</div>
<div className="flex-1">
<div className="mb-6">
<h3 className="text-sm text-text-muted mb-3 font-medium">
{first.subtitle}
</h3>
<h3 className="text-xl lg:text-2xl font-bold text-ink transition-colors duration-300">
{first.title}
</h3>
</div>
<p className="text-text-secondary leading-relaxed mb-8 text-sm max-w-2xl">
{first.desc}
</p>
<div className="flex flex-wrap gap-2 mb-10">
{first.highlights.map((h: string, j: number) => (
<span
key={j}
className="px-3 py-1.5 text-xs text-text-muted border border-border-primary group-hover:border-border-secondary group-hover:text-text-secondary transition-all duration-300 font-medium"
>
{h}
</span>
))}
</div>
{(first as any).metrics && (
<div className="flex flex-wrap gap-10 mb-10 pb-10 border-b border-border-primary">
{(first as any).metrics.map((m: any, j: number) => (
<div key={j}>
<div className="text-3xl sm:text-4xl font-black text-brand tracking-tightest mb-1">
{m.value}
</div>
<div className="text-xs text-text-muted font-medium">
{m.label}
</div>
</div>
))}
</div>
)}
<div className="inline-flex items-center gap-2 text-sm font-semibold text-text-secondary group-hover:text-brand transition-colors duration-300">
<span>了解详情</span>
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
</div>
</div>
</div>
</a>
</StaggerReveal>
);
})()}
{/* 其余三张卡片:并排 */}
{SERVICES.slice(1).map((service, i) => (
<StaggerReveal key={i} staggerDelay={0.05}>
<a
href={service.href}
className="group relative block p-8 sm:p-10 md:p-12 lg:p-14 transition-all duration-500 hover:bg-bg-secondary overflow-hidden bg-white h-full flex flex-col"
>
<div className="relative z-10 flex flex-col flex-1">
<div className="flex items-start justify-between mb-10">
<div className="w-12 h-12 flex items-center justify-center bg-bg-secondary text-text-secondary group-hover:text-ink transition-colors duration-300">
{service.icon}
</div>
</div>
<div className="mb-6">
<h3 className="text-sm text-text-muted mb-3 font-medium">
{service.subtitle}
</h3>
<h3 className="text-xl lg:text-2xl font-bold text-ink transition-colors duration-300">
{service.title}
</h3>
</div>
<p className="text-text-secondary leading-relaxed mb-8 text-sm">
{service.desc}
</p>
<div className="flex flex-wrap gap-2 mb-8">
{service.highlights.map((h: string, j: number) => (
<span
key={j}
className="px-3 py-1.5 text-xs text-text-muted border border-border-primary group-hover:border-border-secondary group-hover:text-text-secondary transition-all duration-300 font-medium"
>
{h}
</span>
))}
</div>
{(service as any).metrics && (
<div className="flex gap-6 mb-8 pb-8 border-b border-border-primary">
{(service as any).metrics.map((m: any, j: number) => (
<div key={j}>
<div className="text-2xl sm:text-3xl font-black text-brand tracking-tightest mb-1">
{m.value}
</div>
<div className="text-[11px] text-text-muted font-medium">
{m.label}
</div>
</div>
))}
</div>
)}
<div className="mt-auto pt-2">
<div className="inline-flex items-center gap-2 text-sm font-semibold text-text-secondary group-hover:text-brand transition-colors duration-300">
<span>了解详情</span>
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
</div>
</div>
</div>
</a>
</StaggerReveal>
))}
</div>
</div>
</section>
);
}
function TrustSection() {
const items = [
{ title: '私有化部署', desc: '核心数据不出境,满足金融、医疗、政务合规要求' },
{ title: '资深团队', desc: '平均 10 年以上企业级系统交付经验' },
{ title: '全栈自研', desc: '从底层架构到前端交互,核心技术自主可控' },
{ title: '长期陪跑', desc: '上线不是终点,持续迭代陪伴业务成长' },
];
return (
<section className="relative py-20 sm:py-24 md:py-28 lg:py-32 overflow-hidden bg-bg-secondary">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="mb-12 sm:mb-16 md:mb-20">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-px bg-brand" />
<span className="text-[13px] tracking-[0.15em] font-medium text-brand">
值得信赖
</span>
</div>
<h2 className="font-sans text-xl sm:text-2xl md:text-3xl lg:text-4xl font-black text-ink tracking-tight leading-tight">
12年深耕,500+项目验证的交付能力
</h2>
</ScrollReveal>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-px bg-border-primary">
{items.map((item, i) => (
<StaggerReveal key={item.title} staggerDelay={0.05}>
<div className="bg-bg-secondary p-8 sm:p-10 h-full transition-all duration-300 hover:bg-white group">
<div className="flex items-start gap-4">
<div className="w-8 h-8 flex items-center justify-center bg-brand/10 text-brand font-bold text-sm shrink-0 transition-colors duration-300 group-hover:bg-brand group-hover:text-white">
{String(i + 1).padStart(2, '0')}
</div>
<div>
<h3 className="text-base font-bold text-ink mb-2">{item.title}</h3>
<p className="text-sm text-text-secondary leading-relaxed">{item.desc}</p>
</div>
</div>
</div>
</StaggerReveal>
))}
</div>
</div>
</section>
);
}
function CasesSection({ cases }: { cases?: Record<string, any>[] }) {
const hasValidData = cases?.length && cases[0]?.metrics;
const CASES = hasValidData ? cases : [];
if (CASES.length === 0) {
return (
<section className="relative py-28 overflow-hidden bg-dark-bg">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 text-center">
<p className="text-dark-text-muted text-lg">暂无案例数据</p>
</div>
</section>
);
}
return (
<section className="relative py-28 sm:py-36 md:py-44 lg:py-56 overflow-hidden bg-dark-bg">
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-dark-border to-transparent" />
<div className="absolute inset-0 pointer-events-none">
<div className="absolute top-1/4 left-1/4 w-[400px] h-[400px] bg-brand/[0.03] rounded-full blur-[120px]" />
<div className="absolute bottom-1/3 right-1/4 w-[500px] h-[500px] bg-accent-blue/[0.02] rounded-full blur-[140px]" />
</div>
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<div className="flex flex-col md:flex-row md:items-end md:justify-between gap-8 sm:gap-10 mb-20 sm:mb-28 md:mb-36">
<ScrollReveal className="md:max-w-3xl">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-px bg-brand" />
<span className="text-[13px] tracking-[0.15em] font-medium text-brand">
客户案例
</span>
</div>
<h2 className="font-sans text-2xl sm:text-3xl md:text-4xl lg:text-6xl font-black text-dark-text-primary tracking-tight leading-[0.95] mt-6">
每一个项目,
<br />
都以业务改善为衡量标准
</h2>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<a
href="/cases"
className="group inline-flex items-center gap-3 text-dark-text-primary font-semibold text-sm"
>
查看全部案例
<ArrowRight className="w-4 h-5 transition-transform duration-300 group-hover:translate-x-2 text-dark-text-muted group-hover:text-brand" />
</a>
</ScrollReveal>
</div>
<div className="space-y-16">
{CASES.map((caseItem, i) => (
<ScrollReveal key={i} delay={i * 0.15}>
<article className="group relative overflow-hidden border border-dark-border hover:border-dark-border/80 transition-all duration-500 bg-dark-bg-secondary">
<div className="relative z-10">
<div className="p-10 sm:p-12 lg:p-16 pb-0">
<div className="flex items-center gap-4 mb-10">
<Badge className="bg-dark-bg-tertiary text-dark-text-primary border-transparent">
{caseItem.industry}
</Badge>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-8 sm:gap-10 pb-12 border-b border-dark-border">
{caseItem.metrics.map((metric: Record<string, any>, j: number) => (
<div key={j}>
<motion.div
className={cn(
'text-5xl sm:text-6xl lg:text-7xl font-black mb-4 leading-none tracking-tightest',
j === 0 ? 'text-brand' : 'text-dark-text-primary'
)}
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: 0.1 * j }}
>
{metric.value}
</motion.div>
<div className="mb-4">
<div className="h-1 bg-dark-border rounded-full overflow-hidden">
<motion.div
className={cn(
'h-full rounded-full',
j === 0 ? 'bg-brand' : 'bg-dark-text-primary/60'
)}
initial={{ width: 0 }}
whileInView={{ width: `${(metric as any).progress || 50}%` }}
viewport={{ once: true }}
transition={{ duration: 1, delay: 0.3 + 0.1 * j, ease: 'easeOut' }}
/>
</div>
</div>
<div className="flex items-center gap-3">
<div className={cn('w-8 h-px', j === 0 ? 'bg-brand' : 'bg-dark-border')} />
<div className="text-dark-text-secondary font-medium text-sm">{metric.label}</div>
</div>
</div>
))}
</div>
</div>
<div className="px-10 sm:px-12 lg:px-16 pt-12">
<h3 className="text-xl lg:text-2xl font-bold mb-10 leading-tight text-dark-text-primary">
{caseItem.title}
</h3>
</div>
<div className="px-10 sm:px-12 lg:px-16 pb-12">
<div className="space-y-8">
<div>
<div className="flex items-center gap-3 mb-5">
<Award className="w-4 h-4 text-dark-text-muted" />
<span className="text-xs font-semibold text-dark-text-muted tracking-wide">
挑战
</span>
</div>
<p className="text-dark-text-secondary leading-relaxed text-sm">
{caseItem.challenge}
</p>
</div>
{(caseItem as any).insight && (
<div className="border-t-2 border-brand bg-brand/[0.04] p-5 pr-6">
<div className="flex items-center gap-3 mb-3">
<span className="text-xs font-bold text-brand tracking-wide">
关键洞察
</span>
</div>
<p className="text-dark-text-primary leading-relaxed text-sm font-medium">
{(caseItem as any).insight}
</p>
</div>
)}
<div>
<div className="flex items-center gap-3 mb-5">
<Zap className="w-4 h-4 text-dark-text-muted" />
<span className="text-xs font-semibold text-dark-text-muted tracking-wide">
解决方案
</span>
</div>
<p className="text-dark-text-secondary leading-relaxed text-sm">
{caseItem.solution}
</p>
</div>
</div>
</div>
<div className="px-10 sm:px-12 lg:px-16 pb-12">
<div className="p-6 sm:p-8 bg-dark-bg-tertiary/50 border border-dark-border">
<div className="flex items-center gap-3 mb-4">
<CheckCircle2 className="w-4 h-4 text-brand" />
<span className="text-xs font-bold text-brand tracking-wide">
业务成果
</span>
</div>
<p className="text-dark-text-primary leading-relaxed text-sm font-medium">
{caseItem.result}
</p>
</div>
</div>
{caseItem.testimonial?.quote && (
<div className="px-10 sm:px-12 lg:px-16 pb-12 lg:pb-16 pt-10 border-t border-dark-border">
<div className="flex items-start gap-4">
<Quote className="w-8 h-8 shrink-0 mt-0.5 text-dark-text-muted" />
<div>
<p className="text-dark-text-secondary italic leading-relaxed mb-4 text-sm">
&ldquo;{caseItem.testimonial.quote}&rdquo;
</p>
<p className="text-xs text-dark-text-muted">
— {caseItem.testimonial.author}
{caseItem.testimonial.role && `,${caseItem.testimonial.role}`}
{caseItem.testimonial.company && ` · ${caseItem.testimonial.company}`}
</p>
</div>
</div>
</div>
)}
</div>
</article>
</ScrollReveal>
))}
</div>
</div>
</section>
);
}
function CTASection({ heroData }: { heroData?: Record<string, any> | null }) {
const ctaTitleLine1 = heroData?.ctaTitleLine1 || '';
const ctaTitleLine2 = heroData?.ctaTitleLine2 || '';
const ctaLabel = heroData?.ctaLabel || '';
const ctaHref = heroData?.ctaHref || '#';
return (
<section
className="relative py-32 sm:py-40 md:py-52 lg:py-60 overflow-hidden bg-dark-bg"
>
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-dark-border to-transparent" />
<div className="absolute inset-0 pointer-events-none">
<div className="absolute top-1/3 right-1/4 w-[500px] h-[500px] bg-brand/[0.04] rounded-full blur-[140px]" />
</div>
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl">
<h2 className="font-sans text-2xl sm:text-3xl md:text-4xl lg:text-6xl xl:text-7xl font-black text-dark-text-primary tracking-tightest leading-[0.9] mb-12 sm:mb-16 md:mb-20">
{ctaTitleLine1 && <>{ctaTitleLine1}<br /></>}
<span className="text-brand">{ctaTitleLine2 || '真正转化为业务增长'}</span>
</h2>
<MagneticButton href={ctaHref}>{ctaLabel || '预约咨询'}</MagneticButton>
</ScrollReveal>
</div>
</section>
);
}
export default function HomeContentV13({ services, cases, stats, heroData }: {
services?: Record<string, any>[];
cases?: Record<string, any>[];
stats?: Record<string, any>[];
heroData?: Record<string, any> | null;
}) {
return (
<main>
<HeroSection heroData={heroData} stats={stats} />
<ServicesSection services={services} />
<TrustSection />
<CasesSection cases={cases} />
<CTASection heroData={heroData} />
</main>
);
}