动效合规专项(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
165 lines
6.1 KiB
TypeScript
165 lines
6.1 KiB
TypeScript
'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>
|
||
);
|
||
} |