Files
novalon-website/src/components/detail/service-value.tsx
T
zhangxiang 8f4a860180 refactor(motion): 收敛末批 P1 入场时长与变量递增 stagger 步进
动效合规专项(Task #16)收尾。

改动:
- page-transition.tsx:入场 duration 0.4/0.5 → 0.3(末处真 P1,由 client-layout 消费)
- 17 文件 21 处子元素 stagger 步进 80/100ms → 60ms
  (CONTEXT.md 原则4「子元素 stagger 30-60ms」;属已批准决策 04e91e3 的漏网,
   因当时审计脚本看不见 delay: i * N 的乘法写法)
- 审计脚本补盲区:新增变量递增 stagger 识别 + 明细表(此前漏掉全站 33 处)
- 审计脚本新增 NON_ENTRANCE 排除:flip-clock 机械翻转、design-system 死代码令牌
- delay 判据自我纠正:硬禁令约束的是 duration(动效时长),
  delay 属调度而非动效本身,一律判 P2(偏离指引),不得判 P0

门禁(全绿):
- tsc 0 error
- eslint 0 error(50 warnings 均为既有)
- jest 130/130 套件,1573 通过 / 2 跳过 / 0 失败
- 运行时冒烟 16/16 PASS(8 路由 × 浅深双版,0 控制台报错)
- 审计:P0=0 · P1=0 · stagger 步进 P2=0
2026-09-04 08:39:00 +08:00

165 lines
6.1 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 {
CheckCircle2,
Zap,
Clock,
Users,
Award,
TrendingUp,
type LucideIcon,
} from 'lucide-react';
import type { Service } from '@/lib/constants/services';
interface ServiceValueSectionProps {
service: Service;
}
export function ServiceValueSection({ service }: ServiceValueSectionProps) {
const icons: LucideIcon[] = [Zap, Clock, Users, Award, TrendingUp];
return (
<section className="relative bg-white py-20 sm:py-28 md:py-36 overflow-hidden">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative">
<motion.div
initial={{ opacity: 0, y: 32 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-120px' }}
transition={{ duration: 0.3, ease: [0.22, 1, 0.36, 1] }}
className="mb-16 sm: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.12em] font-semibold text-brand">
服务能力
</span>
</div>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-black text-ink tracking-tight leading-[0.95] max-w-3xl">
为什么选择我们的{service.title}?
</h2>
<p className="text-lg text-text-secondary max-w-2xl mt-6 leading-relaxed">
{service.overview}
</p>
</motion.div>
{/* 核心能力 */}
<div className="mb-16">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-px bg-brand" />
<span className="text-[13px] tracking-[0.12em] font-semibold text-brand">
核心能力
</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
{service.features.map((feature, index) => {
const Icon = icons[index % icons.length]!;
return (
<motion.div
key={feature}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{
delay: index * 0.06,
duration: 0.3,
ease: [0.22, 1, 0.36, 1],
}}
className="bain-card p-6"
>
<div className="flex items-center gap-4">
<div className={`shrink-0 w-11 h-11 flex items-center justify-center ${
index === 0
? 'bg-brand text-white'
: 'bg-bg-secondary text-text-secondary'
}`}>
<Icon className="w-5 h-5" />
</div>
<p className="text-sm font-medium text-text-secondary leading-relaxed">
{feature.split(':')[1] || feature}
</p>
</div>
</motion.div>
);
})}
</div>
</div>
{/* 服务优势 + 服务流程:不对称布局 */}
<div className="asymmetric-wide-narrow">
{/* 服务优势 */}
<div>
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-px bg-brand" />
<span className="text-[13px] tracking-[0.12em] font-semibold text-brand">
服务优势
</span>
</div>
<div className="space-y-4">
{service.benefits.map((benefit, index) => (
<motion.div
key={index}
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{
delay: index * 0.06,
duration: 0.3,
ease: [0.22, 1, 0.36, 1],
}}
className="bain-card p-5 flex items-start gap-3"
>
<CheckCircle2 className="w-5 h-5 text-brand mt-0.5 shrink-0" />
<p className="text-sm font-medium text-text-secondary leading-relaxed">{benefit}</p>
</motion.div>
))}
</div>
</div>
{/* 服务流程 */}
{service.process && service.process.length > 0 && (
<div>
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-px bg-brand" />
<span className="text-[13px] tracking-[0.12em] font-semibold text-brand">
服务流程
</span>
</div>
<div className="space-y-4">
{service.process.map((step, index) => (
<motion.div
key={step}
initial={{ opacity: 0, x: 20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{
delay: index * 0.06,
duration: 0.3,
ease: [0.22, 1, 0.36, 1],
}}
className="bain-card p-5"
>
<div className="flex items-start gap-3">
<div className="w-6 h-6 rounded-full bg-brand text-white flex items-center justify-center shrink-0 mt-0.5">
<span className="text-xs font-bold">{index + 1}</span>
</div>
<div>
<h4 className="font-bold text-ink text-sm mb-1">
{step.split(':')[0]}
</h4>
<p className="text-xs text-text-secondary leading-relaxed">
{step.split(':')[1] || step}
</p>
</div>
</div>
</motion.div>
))}
</div>
</div>
)}
</div>
</div>
</section>
);
}