Files
novalon-website/src/app/(marketing)/services/service-detail-content-v4.tsx
T
张翔 10404dbb36 chore: sync marketing pages, CMS extensions, tests and project docs
同步工作区剩余变更,主要包括:
- 营销页面组件与布局持续优化(about/news/services/solutions/team 等)
- 详情页四层叙事组件、布局组件、UI 组件调整
- CMS 数据模型、API 路由、权限、工作流、站内通知、媒体管理扩展
- 新增/补充单元测试与 E2E 测试(cms-workflow.spec.ts 等)
- ESLint 9 迁移、jest/tsconfig 配置更新、依赖调整
- 新增 ADR、CMS 评估文档、Release Review / Acceptance 报告
- 移除水墨装饰组件与大体积未使用字体文件
2026-07-25 08:04:01 +08:00

370 lines
16 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 { motion } from 'framer-motion';
import { ArrowUpRight, CheckCircle2 } from 'lucide-react';
import type { Service, CaseStudy } from '@/lib/constants/services';
const EASE_OUT = [0.22, 1, 0.36, 1] as const;
const FEATURE_ICONS = ['⚡', '⏱', '👥', '🏆', '📈', '🛡'];
function DetailHero({ service }: { service: Service }) {
const firstBenefit = service.benefits?.[0] || '效率提升 40%';
return (
<section className="relative min-h-[85vh] flex items-center overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-28 sm:py-36 md:py-44 lg:py-56">
<div className="max-w-4xl">
<motion.h1
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, 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 md:mb-16"
>
<div className="block">{service.title}</div>
<div className="block mt-4 sm:mt-6">
<span className="relative inline-block">
<span className="relative text-brand font-black">
{firstBenefit}
<motion.span
className="absolute -bottom-2 left-0 w-full h-[3px] bg-brand"
style={{ transformOrigin: 'left' }}
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ duration: 0.8, delay: 1.2, ease: EASE_OUT }}
/>
</span>
</span>
</div>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.5, ease: EASE_OUT }}
className="text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed mb-12 sm:mb-16"
>
{service.description}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.7, ease: EASE_OUT }}
className="grid grid-cols-3 gap-8 mb-12 sm:mb-16 max-w-xl"
>
<div>
<div className="text-3xl sm:text-4xl font-black text-brand tracking-tight leading-none mb-2">
12
</div>
<div className="text-sm text-text-muted"></div>
</div>
<div>
<div className="text-3xl sm:text-4xl font-black text-ink tracking-tight leading-none mb-2">
95%+
</div>
<div className="text-sm text-text-muted"></div>
</div>
<div>
<div className="text-3xl sm:text-4xl font-black text-ink tracking-tight leading-none mb-2">
98%
</div>
<div className="text-sm text-text-muted"></div>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.9, ease: EASE_OUT }}
className="flex flex-col sm:flex-row gap-4 sm:gap-6"
>
<a
href="/contact"
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
>
<ArrowUpRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
</a>
<a
href="#cases"
className="inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-ink border border-border-primary hover:border-ink/30 transition-all duration-300 min-h-[52px] touch-manipulation"
>
</a>
</motion.div>
</div>
</div>
</section>
);
}
function OverviewSection({ service }: { service: Service }) {
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-border-primary" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<div className="grid lg:grid-cols-12 gap-12 sm:gap-16 md:gap-20 items-start">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="lg:col-span-5"
>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95]">
</h2>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, delay: 0.15, ease: EASE_OUT }}
className="lg:col-span-7"
>
<p className="text-text-secondary leading-relaxed text-lg mb-8">
{service.overview}
</p>
<div className="flex flex-wrap gap-3">
{service.benefits?.slice(0, 4).map((benefit, i) => (
<div
key={i}
className="inline-flex items-center gap-2 px-4 py-2 text-sm border border-border-primary text-ink"
>
<CheckCircle2 className="w-4 h-4 shrink-0 text-brand" />
<span className="font-medium">{benefit}</span>
</div>
))}
</div>
</motion.div>
</div>
</div>
</section>
);
}
function FeaturesSection({ service }: { service: Service }) {
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="max-w-3xl mb-16 sm:mb-20 md:mb-24"
>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95]">
</h2>
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
500+
</p>
</motion.div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-px bg-border-primary">
{service.features.map((feature, index) => {
const [title, desc] = feature.split('');
const icon = FEATURE_ICONS[index % FEATURE_ICONS.length];
return (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-60px' }}
transition={{ duration: 0.6, delay: index * 0.06, ease: EASE_OUT }}
className="group relative p-8 sm:p-10 transition-all duration-500 hover:bg-bg-secondary hover:shadow-lg hover:-translate-y-1 bg-white"
>
<div className="relative z-10">
<div className="text-3xl mb-6">{icon}</div>
<h3 className="text-lg sm:text-xl font-bold mb-3 text-ink group-hover:text-brand transition-colors duration-300">
{desc ? title : feature.slice(0, 10)}
</h3>
<p className="text-sm leading-relaxed text-text-secondary">
{desc || feature}
</p>
</div>
</motion.div>
);
})}
</div>
</div>
</section>
);
}
function ProcessSection({ service }: { service: Service }) {
if (!service.process || service.process.length === 0) return null;
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-border-primary" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="max-w-3xl mb-16 sm:mb-20 md:mb-24"
>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95]">
</h2>
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
</p>
</motion.div>
<div className="grid md:grid-cols-2 lg:grid-cols-5 gap-px bg-border-primary">
{service.process.slice(0, 5).map((step, index) => {
const [title, desc] = step.split('');
return (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-60px' }}
transition={{ duration: 0.6, delay: index * 0.08, ease: EASE_OUT }}
className="relative group p-6 sm:p-8 bg-white hover:bg-bg-secondary hover:shadow-lg hover:-translate-y-1 transition-all duration-500"
>
<div
className="w-2 h-2 rounded-full mb-6"
style={{ backgroundColor: index === 0 ? '#C41E3A' : 'var(--color-border-primary)' }}
/>
<h3 className="text-lg font-bold mb-2 text-ink group-hover:text-brand transition-colors duration-300">
{desc ? title : step.slice(0, 6)}
</h3>
<p className="text-sm leading-relaxed text-text-secondary">
{desc || step}
</p>
</motion.div>
);
})}
</div>
</div>
</section>
);
}
function CaseStudiesSection({ caseStudies = [] }: { caseStudies?: CaseStudy[] }) {
if (caseStudies.length === 0) return null;
return (
<section id="cases" className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="max-w-3xl mb-16 sm:mb-20 md:mb-24"
>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95]">
<br className="sm:hidden" />
</h2>
</motion.div>
<div className="grid md:grid-cols-2 gap-px bg-border-primary">
{caseStudies.slice(0, 4).map((cs, i) => (
<motion.a
key={i}
href={`/cases/${cs.client.toLowerCase().replace(/\s+/g, '-')}`}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-60px' }}
transition={{ duration: 0.6, delay: i * 0.08, ease: EASE_OUT }}
className="group relative block p-8 sm:p-10 transition-all duration-500 hover:bg-bg-secondary hover:shadow-lg hover:-translate-y-1 bg-white border border-transparent hover:border-border-primary"
>
<div className="relative z-10">
<div className="flex items-center gap-3 mb-5">
<span className="text-xs font-medium text-brand">
{cs.industry}
</span>
<span className="text-border-primary"></span>
<span className="text-sm text-text-secondary">{cs.client}</span>
</div>
<h3 className="text-xl sm:text-2xl font-bold mb-6 text-ink leading-tight group-hover:text-brand transition-colors duration-300">
{cs.challenge.slice(0, 30)}
</h3>
<p className="text-sm leading-relaxed mb-6 text-text-secondary">
{cs.solution}
</p>
<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>
<ArrowUpRight className="w-4 h-4 group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-transform duration-300" />
</div>
</div>
</motion.a>
))}
</div>
</div>
</section>
);
}
function CTASection() {
return (
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="max-w-3xl"
>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95] mb-8">
</h2>
<p className="text-lg text-text-secondary leading-relaxed mb-10">
<br className="hidden sm:block" />
</p>
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
<a
href="/contact"
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
>
<ArrowUpRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
</a>
<a
href="/services"
className="inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-ink border border-border-primary hover:border-ink/30 transition-all duration-300 min-h-[52px] touch-manipulation"
>
</a>
</div>
</motion.div>
</div>
</section>
);
}
export default function ServiceDetailContentV4({ service }: { service: Service }) {
return (
<main className="min-h-screen bg-white text-ink">
<DetailHero service={service} />
<OverviewSection service={service} />
<FeaturesSection service={service} />
<ProcessSection service={service} />
<CaseStudiesSection caseStudies={service.caseStudies} />
<CTASection />
</main>
);
}