Files
novalon-website/src/app/(marketing)/services/service-detail-content-v4.tsx
T
zhangxiang f8917d4fda feat(ui): 营销站 UI/UE/UX 治理 P0→P3 收官
复评(impeccable critique)全部 Priority Issues 与 Minor Observations 落地:
- P0 数字口径:MetricsBasisNote 单一真源 + metrics-basis.test fs 防线,接入
  products/solutions/services/home/about;纯渲染端加角注,不动 DB/seed 值
- P1 转化断头路:敬请期待态 + 404 语境出口 + services 空态双出口
- P1 首页并卡:TrustSection 早鸟横幅并入 CasesSection 单张深色共创卡
- P2 CTA 治理:CONSULT_CTA_ALLOWLIST 白名单 + 逐字符扫描器,/contact CTA 全归一
- P3 polish:footer 补 服务/公司列、contact 死三元/空槽/toast、GlobalErrorTracker 幂等日志
- 文档同源:PRODUCT/CONTEXT 动效带统一 180–280ms;DESIGN.md 立 The 10px Floor;
  font-floor.test 守卫禁止再引入 <10px(保留 10/11px 有意 Bain 微标签)

Gates: type-check 0 · jest 1581/132 · lint 0e/126w · contrast 7/7 · headings 10/10
2026-09-20 11:50:58 +08:00

279 lines
12 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 { CTAButton } from '@/components/ui/cta-button';
import { CTA_LABELS } from '@/lib/constants/cta-labels';
import { CheckCircle2, Zap, Clock, Users, Award, TrendingUp, Shield } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import type { Service } from '@/lib/constants/services';
import { L3EmptyFallback, CaseStudiesSection } from '@/components/detail';
const EASE_OUT = [0.22, 1, 0.36, 1] as const;
const FEATURE_ICONS: LucideIcon[] = [Zap, Clock, Users, Award, TrendingUp, Shield];
function DetailHero({ service }: { service: Service }) {
const firstBenefit = service.benefits?.[0] || '效率提升 40%';
return (
<section className="relative overflow-hidden bg-bg-primary">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-16 sm:py-20 lg:py-28">
<div className="max-w-4xl">
<motion.h1
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.2, ease: EASE_OUT }}
className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-black text-ink leading-[0.95] tracking-tightest mb-6 sm:mb-8"
>
<div className="block">{service.title}</div>
<div className="block mt-4 sm:mt-6">
<span className="relative inline-block">
<span className="relative text-brand-ink 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.3, 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.3, delay: 0.5, ease: EASE_OUT }}
className="text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed mb-10 sm:mb-12"
>
{service.description}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.7, ease: EASE_OUT }}
className="flex flex-col sm:flex-row gap-4 sm:gap-6"
>
<CTAButton href="/contact" dataTestId="service-detail-hero-primary" className="min-h-[52px]">
{CTA_LABELS.consult}
</CTAButton>
<CTAButton href="#cases" variant="secondary" className="min-h-[52px] touch-manipulation">
查看案例
</CTAButton>
</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.3, 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.3, 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-ink" />
<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-bg-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.3, 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-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.3, delay: index * 0.06, ease: EASE_OUT }}
className="group relative p-8 sm:p-10 transition-all duration-150 hover:bg-bg-secondary hover:shadow-lg hover:-translate-y-1 bg-bg-primary"
>
<div className="relative z-10">
<Icon className="w-8 h-8 mb-6 text-brand-ink" />
<h3 className="text-lg sm:text-xl font-bold mb-3 text-ink group-hover:text-brand-ink 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.3, 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.3, delay: index * 0.06, ease: EASE_OUT }}
className="relative group p-6 sm:p-8 bg-bg-primary hover:bg-bg-secondary hover:shadow-lg hover:-translate-y-1 transition-all duration-150"
>
<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-ink 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>
);
}
// L3 信任层「客户案例」已合并至共享组件(@/components/detail)。
// 原本地实现为异类:卡片 motion.a 链接 /cases/<slug>(依赖尚不存在的案例数据,链向 404
// 违反零编造)+ 动效 0.8s 违规。统一后卡片不带链接,id="cases" 保留供 hero CTA 锚点。
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.3, 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">
<CTAButton href="/contact" dataTestId="service-detail-bottom-primary" className="min-h-[52px]">
{CTA_LABELS.consult}
</CTAButton>
<CTAButton href="/services" variant="secondary" dataTestId="service-detail-bottom-secondary" className="min-h-[52px]">
返回服务列表
</CTAButton>
</div>
</motion.div>
</div>
</section>
);
}
export default function ServiceDetailContentV4({ service }: { service: Service }) {
return (
<div className="min-h-screen bg-bg-primary text-ink">
<DetailHero service={service} />
<OverviewSection service={service} />
<FeaturesSection service={service} />
<ProcessSection service={service} />
<L3EmptyFallback caseStudies={service.caseStudies} />
<CaseStudiesSection caseStudies={service.caseStudies} id="cases" heading="真实案例,可衡量的成果" />
<CTASection />
</div>
);
}