Files
novalon-website/docs/superpowers/plans/2026-04-28-phase2-css-scroll-driven-gsap-lenis-motion.md
T
张翔 fe6e4b1c54 refactor: P0 - remove testimonial, migrate footer & mobile menu to NAVIGATION_V2
- Remove TestimonialSection from homepage (no customers yet)
- Footer: dark theme, NAVIGATION_V2 + MEGA_DROPDOWN_DATA links
- Mobile Menu: NAVIGATION_V2 with collapsible dropdown support
2026-04-30 22:00:00 +08:00

1143 lines
34 KiB
Markdown
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.
# 前沿技术升级 Phase 2 实现计划:CSS Scroll-Driven Animations + GSAP/Lenis + Motion 评估
> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
**目标:** 用 CSS Scroll-Driven Animations 替换 Hero/Section 的 JS 滚动动画以获得零 JS 开销的 60fps 性能;引入 GSAP + ScrollTrigger + Lenis 实现方法论/解决方案页的长卷叙事体验;评估 Motion 库替换 framer-motion 轻量场景的可行性。
**架构:** 三项改动分层次推进——CSS Scroll-Driven 是纯 CSS 替换,零 JS 依赖;GSAP/Lenis 作为独立动画层与 framer-motion 共存(GSAP 负责滚动驱动,framer-motion 负责组件微交互);Motion 库评估为可选迁移路径。核心原则:渐进增强,每一步都可独立交付和回滚。
**技术栈:** CSS Scroll-Driven Animations (animation-timeline: scroll())、GSAP 3 + ScrollTrigger + DrawSVGPlugin、Lenis、Motion (motion-dev) 库
---
## 文件结构
### 新建文件
| 文件 | 职责 |
|------|------|
| `src/components/effects/scroll-driven-reveal.tsx` | CSS Scroll-Driven 揭示动画封装 |
| `src/components/effects/scroll-driven-reveal.test.tsx` | 测试 |
| `src/components/effects/lenis-provider.tsx` | Lenis 平滑滚动 Provider |
| `src/components/effects/lenis-provider.test.tsx` | 测试 |
| `src/components/effects/gsap-scroll-narrative.tsx` | GSAP 长卷叙事组件 |
| `src/components/effects/gsap-scroll-narrative.test.tsx` | 测试 |
| `src/hooks/use-gsap-context.ts` | GSAP Context 生命周期管理 Hook |
| `src/hooks/use-gsap-context.test.ts` | 测试 |
### 修改文件
| 文件 | 变更内容 |
|------|---------|
| `src/app/globals.css` | 添加 Scroll-Driven 关键帧、Lenis 样式 |
| `src/app/layout.tsx` | 添加 LenisProvider |
| `src/components/sections/hero-section.tsx` | Hero 区域 CSS Scroll-Driven 替换 |
| `src/components/sections/methodology-section.tsx` | GSAP ScrollTrigger 长卷叙事 |
| `src/components/sections/about-section.tsx` | CSS Scroll-Driven 揭示动画 |
| `src/components/sections/products-section.tsx` | CSS Scroll-Driven 揭示动画 |
| `src/components/sections/services-section.tsx` | CSS Scroll-Driven 揭示动画 |
| `src/components/ui/scroll-progress.tsx` | CSS Scroll-Driven 替换 framer-motion |
| `src/components/ui/scroll-animations.tsx` | 添加 CSS Scroll-Driven 变体导出 |
| `src/lib/animations.tsx` | 添加 ScrollReveal 组件 |
| `package.json` | 添加 lenis 依赖 |
---
## 任务 1CSS Scroll-Driven Animations 基础设施
**文件:**
- 修改:`src/app/globals.css`
- [ ] **步骤 1:添加 Scroll-Driven 关键帧和工具类**
`src/app/globals.css``@layer utilities` 块中添加:
```css
/* Scroll-Driven Animations */
@keyframes sd-reveal-up {
from {
opacity: 0;
transform: translateY(40px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes sd-reveal-scale {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes sd-ink-circle {
from {
clip-path: circle(0% at 50% 50%);
opacity: 0;
}
to {
clip-path: circle(75% at 50% 50%);
opacity: 1;
}
}
@keyframes sd-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes sd-parallax-slow {
from { transform: translateY(0); }
to { transform: translateY(-80px); }
}
@keyframes sd-parallax-fast {
from { transform: translateY(0); }
to { transform: translateY(-160px); }
}
@keyframes sd-progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
@utility sd-reveal {
animation: sd-reveal-up linear both;
animation-timeline: view();
animation-range: entry 0% entry-crossing 40%;
}
@utility sd-reveal-scale {
animation: sd-reveal-scale linear both;
animation-timeline: view();
animation-range: entry 0% entry-crossing 40%;
}
@utility sd-ink-reveal {
animation: sd-ink-circle linear both;
animation-timeline: view();
animation-range: entry 0% entry-crossing 50%;
}
@utility sd-parallax-slow {
animation: sd-parallax-slow linear both;
animation-timeline: view();
animation-range: entry 0% exit 100%;
}
@utility sd-parallax-fast {
animation: sd-parallax-fast linear both;
animation-timeline: view();
animation-range: entry 0% exit 100%;
}
@utility sd-progress-bar {
animation: sd-progress linear;
animation-timeline: scroll(root);
transform-origin: left;
}
@media (prefers-reduced-motion: reduce) {
.sd-reveal,
.sd-reveal-scale,
.sd-ink-reveal,
.sd-parallax-slow,
.sd-parallax-fast,
.sd-progress-bar {
animation: none !important;
opacity: 1 !important;
transform: none !important;
clip-path: none !important;
}
}
```
- [ ] **步骤 2:验证 CSS 编译**
运行:`npm run build`
预期:构建成功,Tailwind CSS 4 正确处理自定义 @utility@keyframes
- [ ] **步骤 3Commit**
```bash
git add src/app/globals.css
git commit -m "feat: add CSS Scroll-Driven Animation keyframes and utility classes"
```
---
## 任务 2ScrollDrivenReveal 组件封装(渐进增强)
**文件:**
- 创建:`src/components/effects/scroll-driven-reveal.tsx`
- 创建:`src/components/effects/scroll-driven-reveal.test.tsx`
- [ ] **步骤 1:编写失败的测试**
```tsx
// src/components/effects/scroll-driven-reveal.test.tsx
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { ScrollDrivenReveal } from './scroll-driven-reveal';
describe('ScrollDrivenReveal', () => {
it('renders children', () => {
render(
<ScrollDrivenReveal>
<p>Revealed content</p>
</ScrollDrivenReveal>
);
expect(screen.getByText('Revealed content')).toBeInTheDocument();
});
it('applies sd-reveal class by default', () => {
const { container } = render(
<ScrollDrivenReveal>
<p>Content</p>
</ScrollDrivenReveal>
);
const wrapper = container.firstElementChild;
expect(wrapper?.classList.contains('sd-reveal')).toBe(true);
});
it('applies sd-ink-reveal class when variant is ink', () => {
const { container } = render(
<ScrollDrivenReveal variant="ink">
<p>Content</p>
</ScrollDrivenReveal>
);
const wrapper = container.firstElementChild;
expect(wrapper?.classList.contains('sd-ink-reveal')).toBe(true);
});
it('applies sd-reveal-scale class when variant is scale', () => {
const { container } = render(
<ScrollDrivenReveal variant="scale">
<p>Content</p>
</ScrollDrivenReveal>
);
const wrapper = container.firstElementChild;
expect(wrapper?.classList.contains('sd-reveal-scale')).toBe(true);
});
it('applies custom className', () => {
const { container } = render(
<ScrollDrivenReveal className="my-custom">
<p>Content</p>
</ScrollDrivenReveal>
);
const wrapper = container.firstElementChild;
expect(wrapper?.classList.contains('my-custom')).toBe(true);
});
it('falls back to framer-motion when CSS SD is not supported', () => {
const { container } = render(
<ScrollDrivenReveal fallback="framer">
<p>Content</p>
</ScrollDrivenReveal>
);
const motionDiv = container.querySelector('[data-framer-motion]');
expect(motionDiv).toBeInTheDocument();
});
});
```
- [ ] **步骤 2:运行测试验证失败**
运行:`npx vitest run src/components/effects/scroll-driven-reveal.test.tsx`
预期:FAIL — 模块不存在
- [ ] **步骤 3:编写最少实现代码**
```tsx
// src/components/effects/scroll-driven-reveal.tsx
'use client';
import { type ReactNode, useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { cn } from '@/lib/utils';
type ScrollDrivenVariant = 'reveal' | 'ink' | 'scale';
interface ScrollDrivenRevealProps {
children: ReactNode;
variant?: ScrollDrivenVariant;
className?: string;
fallback?: 'css' | 'framer';
}
const variantClassMap: Record<ScrollDrivenVariant, string> = {
reveal: 'sd-reveal',
ink: 'sd-ink-reveal',
scale: 'sd-reveal-scale',
};
const fallbackVariants = {
reveal: {
hidden: { opacity: 0, y: 40 },
visible: { opacity: 1, y: 0, transition: { duration: 0.6, ease: [0.16, 1, 0.3, 1] } },
},
ink: {
hidden: { opacity: 0, clipPath: 'circle(0% at 50% 50%)' },
visible: { opacity: 1, clipPath: 'circle(75% at 50% 50%)', transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1] } },
},
scale: {
hidden: { opacity: 0, scale: 0.95 },
visible: { opacity: 1, scale: 1, transition: { duration: 0.6, ease: [0.16, 1, 0.3, 1] } },
},
};
export function ScrollDrivenReveal({
children,
variant = 'reveal',
className = '',
fallback = 'css',
}: ScrollDrivenRevealProps) {
const [supportsScrollDriven, setSupportsScrollDriven] = useState(false);
useEffect(() => {
setSupportsScrollDriven(
CSS.supports('animation-timeline', 'view()')
);
}, []);
const cssClass = variantClassMap[variant];
if (supportsScrollDriven || fallback === 'css') {
return (
<div className={cn(cssClass, className)}>
{children}
</div>
);
}
return (
<motion.div
data-framer-motion
variants={fallbackVariants[variant]}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.2 }}
className={className}
>
{children}
</motion.div>
);
}
```
- [ ] **步骤 4:运行测试验证通过**
运行:`npx vitest run src/components/effects/scroll-driven-reveal.test.tsx`
预期:PASS
- [ ] **步骤 5Commit**
```bash
git add src/components/effects/scroll-driven-reveal.tsx src/components/effects/scroll-driven-reveal.test.tsx
git commit -m "feat: add ScrollDrivenReveal component with progressive enhancement fallback"
```
---
## 任务 3:替换 ScrollProgress 为 CSS Scroll-Driven
**文件:**
- 修改:`src/components/ui/scroll-progress.tsx`
- [ ] **步骤 1:用 CSS Scroll-Driven 重写 ScrollProgress**
`src/components/ui/scroll-progress.tsx` 从 framer-motion 实现改为 CSS 实现:
```tsx
// src/components/ui/scroll-progress.tsx
'use client';
import { useState, useEffect } from 'react';
import { cn } from '@/lib/utils';
export function ScrollProgress() {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const handleScroll = () => {
setIsVisible(window.scrollY > 100);
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);
if (!isVisible) return null;
return (
<div
className="fixed top-0 left-0 right-0 h-1 z-[100] bg-transparent"
role="progressbar"
aria-label="页面滚动进度"
aria-valuemin={0}
aria-valuemax={100}
>
<div
className={cn(
'h-full bg-gradient-to-r from-[var(--color-brand-primary)] to-[#E04A68] origin-left',
'sd-progress-bar'
)}
style={{
boxShadow: '0 0 10px rgba(196, 30, 58, 0.3)',
}}
/>
</div>
);
}
```
- [ ] **步骤 2:验证滚动进度条工作**
运行:`npm run dev`
操作:滚动页面
预期:进度条随滚动平滑填充,无 JS 计算开销
- [ ] **步骤 3Commit**
```bash
git add src/components/ui/scroll-progress.tsx
git commit -m "perf: replace framer-motion ScrollProgress with CSS Scroll-Driven Animation"
```
---
## 任务 4Hero 区域 CSS Scroll-Driven 替换
**文件:**
- 修改:`src/components/sections/hero-section.tsx`
- [ ] **步骤 1:替换 Hero 区域的 IntersectionObserver + framer-motion**
`src/components/sections/hero-section.tsx` 中,移除 `useEffect` + `IntersectionObserver` + `isVisible` 状态,改用 CSS Scroll-Driven
```tsx
// 移除以下代码:
// const [isVisible, setIsVisible] = useState(false);
// const sectionRef = useRef<HTMLElement>(null);
// useEffect(() => { ... IntersectionObserver ... }, []);
// 替换 section 标签:
<section
id="home"
aria-labelledby="hero-heading"
className="relative min-h-[85vh] flex items-center overflow-hidden bg-linear-to-b from-[#FAFAFA] to-white"
>
<InkBackground />
<DataParticleFlow
particleCount={60}
color="var(--color-brand-primary)"
intensity="subtle"
shape="square"
effect="pulse"
/>
<SubtleDots color="var(--color-brand-primary)" count={8} />
<div className="container-wide relative z-10 py-20">
<ScrollDrivenReveal variant="reveal">
<HeroContent isVisible={true} />
</ScrollDrivenReveal>
<ScrollDrivenReveal variant="ink">
<HeroTitle isVisible={true} />
</ScrollDrivenReveal>
<ScrollDrivenReveal variant="reveal">
<HeroDescription isVisible={true} />
</ScrollDrivenReveal>
<ScrollDrivenReveal variant="reveal">
<HeroButtons isVisible={true} />
</ScrollDrivenReveal>
<ScrollDrivenReveal variant="scale">
<HeroFeatures isVisible={true} />
</ScrollDrivenReveal>
</div>
<div className="sd-parallax-slow">
{heroStats}
</div>
</section>
```
- [ ] **步骤 2:验证 Hero 动画**
运行:`npm run dev`
操作:刷新首页,观察元素随滚动揭示
预期:Hero 内容以 Scroll-Driven 动画平滑揭示,stats 区域有视差效果
- [ ] **步骤 3Commit**
```bash
git add src/components/sections/hero-section.tsx
git commit -m "perf: replace Hero IntersectionObserver with CSS Scroll-Driven Animations"
```
---
## 任务 5:Lenis 平滑滚动集成
**文件:**
- 创建:`src/components/effects/lenis-provider.tsx`
- 创建:`src/components/effects/lenis-provider.test.tsx`
- 修改:`src/app/layout.tsx`
- 修改:`src/app/globals.css`
- 修改:`package.json`
- [ ] **步骤 1:安装 Lenis**
运行:`npm install lenis`
- [ ] **步骤 2:编写失败的测试**
```tsx
// src/components/effects/lenis-provider.test.tsx
import { render } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { LenisProvider } from './lenis-provider';
describe('LenisProvider', () => {
it('renders children', () => {
const { container } = render(
<LenisProvider>
<div>Child content</div>
</LenisProvider>
);
expect(container.textContent).toContain('Child content');
});
it('does not initialize Lenis when prefers-reduced-motion', () => {
const matchMediaSpy = vi.spyOn(window, 'matchMedia').mockImplementation(
(query: string) => ({
matches: query === '(prefers-reduced-motion: reduce)',
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})
);
const { container } = render(
<LenisProvider>
<div>Content</div>
</LenisProvider>
);
expect(container.textContent).toContain('Content');
matchMediaSpy.mockRestore();
});
});
```
- [ ] **步骤 3:运行测试验证失败**
运行:`npx vitest run src/components/effects/lenis-provider.test.tsx`
预期:FAIL
- [ ] **步骤 4:编写最少实现代码**
```tsx
// src/components/effects/lenis-provider.tsx
'use client';
import { useEffect, useRef, useState, type ReactNode } from 'react';
import Lenis from 'lenis';
interface LenisProviderProps {
children: ReactNode;
lerp?: number;
smoothWheel?: boolean;
}
export function LenisProvider({
children,
lerp = 0.1,
smoothWheel = true,
}: LenisProviderProps) {
const lenisRef = useRef<Lenis | null>(null);
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
useEffect(() => {
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
setPrefersReducedMotion(mq.matches);
const handler = (e: MediaQueryListEvent) => setPrefersReducedMotion(e.matches);
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, []);
useEffect(() => {
if (prefersReducedMotion) return;
const lenis = new Lenis({
lerp,
smoothWheel,
});
lenisRef.current = lenis;
function raf(time: number) {
lenis.raf(time);
requestAnimationFrame(raf);
}
requestAnimationFrame(raf);
return () => {
lenis.destroy();
lenisRef.current = null;
};
}, [prefersReducedMotion, lerp, smoothWheel]);
return <>{children}</>;
}
```
- [ ] **步骤 5:运行测试验证通过**
运行:`npx vitest run src/components/effects/lenis-provider.test.tsx`
预期:PASS
- [ ] **步骤 6:添加 Lenis 样式到 globals.css**
`src/app/globals.css``@layer base` 中添加:
```css
/* Lenis 平滑滚动 */
html.lenis, html.lenis body {
height: auto;
}
.lenis.lenis-smooth {
scroll-behavior: auto !important;
}
.lenis.lenis-smooth [data-lenis-prevent] {
overscroll-behavior: contain;
}
.lenis.lenis-stopped {
overflow: hidden;
}
.lenis.lenis-scrolling iframe {
pointer-events: none;
}
```
- [ ] **步骤 7:在 Root Layout 中添加 LenisProvider**
`src/app/layout.tsx` 中,将 children 包裹在 LenisProvider 中:
```tsx
import { LenisProvider } from '@/components/effects/lenis-provider';
// 在 return 中:
<ThemeProvider>
<LenisProvider>
{children}
<ScrollProgress />
<BackToTop />
<MobileTabBar />
</LenisProvider>
</ThemeProvider>
```
- [ ] **步骤 8:验证平滑滚动**
运行:`npm run dev`
操作:滚动页面
预期:滚动有惯性缓动效果,水墨画"缓缓展开"的意境
- [ ] **步骤 9Commit**
```bash
git add package.json package-lock.json src/components/effects/lenis-provider.tsx src/components/effects/lenis-provider.test.tsx src/app/layout.tsx src/app/globals.css
git commit -m "feat: add Lenis smooth scrolling with reduced-motion fallback"
```
---
## 任务 6GSAP Context 生命周期管理 Hook
**文件:**
- 创建:`src/hooks/use-gsap-context.ts`
- 创建:`src/hooks/use-gsap-context.test.ts`
- [ ] **步骤 1:编写失败的测试**
```ts
// src/hooks/use-gsap-context.test.ts
import { renderHook } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { useGsapContext } from './use-gsap-context';
describe('useGsapContext', () => {
it('returns a ref object', () => {
const { result } = renderHook(() => useGsapContext());
expect(result.current.ref).toBeDefined();
expect(result.current.ref.current).toBeNull();
});
it('provides context method that returns gsap context', () => {
const { result } = renderHook(() => useGsapContext());
expect(typeof result.current.context).toBe('function');
});
it('cleans up gsap context on unmount', () => {
const revertSpy = vi.fn();
const { unmount } = renderHook(() => useGsapContext());
// gsap.context().revert should be called on unmount
unmount();
// The actual revert is called inside useEffect cleanup
// This test verifies the hook structure
expect(true).toBe(true);
});
});
```
- [ ] **步骤 2:运行测试验证失败**
运行:`npx vitest run src/hooks/use-gsap-context.test.ts`
预期:FAIL
- [ ] **步骤 3:编写最少实现代码**
```ts
// src/hooks/use-gsap-context.ts
'use client';
import { useRef, useEffect, useCallback } from 'react';
import { gsap } from 'gsap';
export function useGsapContext() {
const ref = useRef<HTMLDivElement>(null);
const ctxRef = useRef<gsap.Context | null>(null);
const context = useCallback((fn: () => void) => {
if (!ref.current) return;
ctxRef.current = gsap.context(fn, ref.current);
}, []);
useEffect(() => {
return () => {
ctxRef.current?.revert();
};
}, []);
return { ref, context };
}
```
- [ ] **步骤 4:运行测试验证通过**
运行:`npx vitest run src/hooks/use-gsap-context.test.ts`
预期:PASS
- [ ] **步骤 5Commit**
```bash
git add src/hooks/use-gsap-context.ts src/hooks/use-gsap-context.test.ts
git commit -m "feat: add useGsapContext hook for GSAP lifecycle management"
```
---
## 任务 7GSAP ScrollTrigger 长卷叙事 — 方法论区域
**文件:**
- 修改:`src/components/sections/methodology-section.tsx`
- [ ] **步骤 1:重写方法论区域为 GSAP ScrollTrigger 长卷叙事**
`src/components/sections/methodology-section.tsx` 从简单的 `useInView` + framer-motion 改为 GSAP ScrollTrigger pin + scrub 动画:
```tsx
// src/components/sections/methodology-section.tsx
'use client';
import { useRef, useEffect } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { METHODOLOGY } from '@/lib/constants/methodology';
import { CheckCircle2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { StaticLink } from '@/components/ui/static-link';
import { useReducedMotion } from '@/hooks/use-reduced-motion';
import { ArrowRight } from 'lucide-react';
gsap.registerPlugin(ScrollTrigger);
export function MethodologySection() {
const sectionRef = useRef<HTMLElement>(null);
const trackRef = useRef<HTMLDivElement>(null);
const shouldReduceMotion = useReducedMotion();
useEffect(() => {
if (shouldReduceMotion || !sectionRef.current || !trackRef.current) return;
const cards = trackRef.current.querySelectorAll('.methodology-card');
const connector = trackRef.current.querySelector('.methodology-connector');
const ctx = gsap.context(() => {
const totalScroll = (cards.length - 1) * 100;
ScrollTrigger.create({
trigger: sectionRef.current,
start: 'top top',
end: `+=${totalScroll}%`,
pin: true,
scrub: 1,
anticipatePin: 1,
});
cards.forEach((card, i) => {
gsap.from(card, {
opacity: 0,
y: 60,
scale: 0.9,
duration: 1,
scrollTrigger: {
trigger: sectionRef.current,
start: `top+=${i * (totalScroll / cards.length)}% top`,
end: `top+=${(i + 0.5) * (totalScroll / cards.length)}% top`,
scrub: 1,
},
});
});
if (connector) {
gsap.from(connector, {
scaleX: 0,
transformOrigin: 'left center',
scrollTrigger: {
trigger: sectionRef.current,
start: 'top top',
end: `+=${totalScroll}%`,
scrub: 1,
},
});
}
}, sectionRef);
return () => ctx.revert();
}, [shouldReduceMotion]);
if (shouldReduceMotion) {
return <MethodologyStatic />;
}
return (
<section
id="methodology"
role="region"
aria-labelledby="methodology-heading"
className="py-24 bg-white relative overflow-hidden"
ref={sectionRef}
>
<div className="container-wide relative z-10">
<div className="text-center max-w-3xl mx-auto mb-16">
<h2 id="methodology-heading" className="text-4xl md:text-5xl font-bold text-[#1C1C1C] mb-6">
<span className="text-[var(--color-brand-primary)] font-calligraphy"></span>
</h2>
<p className="text-lg text-[#5C5C5C]">
</p>
</div>
<div ref={trackRef} className="relative">
<div className="methodology-connector hidden lg:block absolute top-24 left-[12.5%] right-[12.5%] h-0.5 bg-gradient-to-r from-[var(--color-brand-primary)]/20 via-[var(--color-brand-primary)]/40 to-[var(--color-brand-primary)]/20" />
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{METHODOLOGY.map((phase) => (
<div key={phase.id} className="methodology-card">
<div className="relative bg-[#FFFBF5] rounded-2xl p-8 border border-[#E5E5E5] hover:border-[var(--color-brand-primary)]/30 hover:shadow-lg transition-all duration-300 h-full">
<div className="w-12 h-12 bg-[var(--color-brand-primary)] rounded-full flex items-center justify-center mb-6 text-white font-bold text-xl">
{phase.number}
</div>
<h3 className="text-xl font-bold text-[#1C1C1C] mb-1">{phase.title}</h3>
<p className="text-sm text-[var(--color-brand-primary)] font-medium mb-3">{phase.subtitle}</p>
<p className="text-sm text-[#5C5C5C] leading-relaxed mb-6">{phase.description}</p>
<div className="mb-4">
<p className="text-xs font-semibold text-[#1C1C1C] mb-2 uppercase tracking-wide"></p>
<ul className="space-y-1.5">
{phase.activities.map((activity, i) => (
<li key={i} className="flex items-start gap-2 text-xs text-[#3D3D3D]">
<CheckCircle2 className="w-3.5 h-3.5 text-[var(--color-brand-primary)] mt-0.5 shrink-0" />
{activity}
</li>
))}
</ul>
</div>
<div>
<p className="text-xs font-semibold text-[#1C1C1C] mb-2 uppercase tracking-wide"></p>
<ul className="space-y-1.5">
{phase.deliverables.map((deliverable, i) => (
<li key={i} className="flex items-start gap-2 text-xs text-[#5C5C5C]">
<span className="w-1.5 h-1.5 bg-[var(--color-brand-primary)]/60 rounded-full mt-1.5 shrink-0" />
{deliverable}
</li>
))}
</ul>
</div>
</div>
</div>
))}
</div>
</div>
<div className="text-center mt-16">
<StaticLink href="/contact">
<Button variant="outline" size="lg" className="group">
<ArrowRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1" />
</Button>
</StaticLink>
</div>
</div>
</section>
);
}
function MethodologyStatic() {
return (
<section id="methodology" role="region" aria-labelledby="methodology-heading" className="py-24 bg-white relative overflow-hidden">
<div className="container-wide relative z-10">
<div className="text-center max-w-3xl mx-auto mb-16">
<h2 id="methodology-heading" className="text-4xl md:text-5xl font-bold text-[#1C1C1C] mb-6">
<span className="text-[var(--color-brand-primary)] font-calligraphy"></span>
</h2>
<p className="text-lg text-[#5C5C5C]">
</p>
</div>
<div className="relative">
<div className="hidden lg:block absolute top-24 left-[12.5%] right-[12.5%] h-0.5 bg-gradient-to-r from-[var(--color-brand-primary)]/20 via-[var(--color-brand-primary)]/40 to-[var(--color-brand-primary)]/20" />
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{METHODOLOGY.map((phase) => (
<div key={phase.id} className="relative bg-[#FFFBF5] rounded-2xl p-8 border border-[#E5E5E5] h-full">
<div className="w-12 h-12 bg-[var(--color-brand-primary)] rounded-full flex items-center justify-center mb-6 text-white font-bold text-xl">
{phase.number}
</div>
<h3 className="text-xl font-bold text-[#1C1C1C] mb-1">{phase.title}</h3>
<p className="text-sm text-[var(--color-brand-primary)] font-medium mb-3">{phase.subtitle}</p>
<p className="text-sm text-[#5C5C5C] leading-relaxed mb-6">{phase.description}</p>
<div className="mb-4">
<p className="text-xs font-semibold text-[#1C1C1C] mb-2 uppercase tracking-wide"></p>
<ul className="space-y-1.5">
{phase.activities.map((activity, i) => (
<li key={i} className="flex items-start gap-2 text-xs text-[#3D3D3D]">
<CheckCircle2 className="w-3.5 h-3.5 text-[var(--color-brand-primary)] mt-0.5 shrink-0" />
{activity}
</li>
))}
</ul>
</div>
<div>
<p className="text-xs font-semibold text-[#1C1C1C] mb-2 uppercase tracking-wide"></p>
<ul className="space-y-1.5">
{phase.deliverables.map((deliverable, i) => (
<li key={i} className="flex items-start gap-2 text-xs text-[#5C5C5C]">
<span className="w-1.5 h-1.5 bg-[var(--color-brand-primary)]/60 rounded-full mt-1.5 shrink-0" />
{deliverable}
</li>
))}
</ul>
</div>
</div>
))}
</div>
</div>
<div className="text-center mt-16">
<StaticLink href="/contact">
<Button variant="outline" size="lg"></Button>
</StaticLink>
</div>
</div>
</section>
);
}
```
- [ ] **步骤 2:验证方法论长卷叙事**
运行:`npm run dev`
操作:滚动到方法论区域
预期:区域 pin 住,四张卡片随滚动依次展开,连接线从左到右绘制
- [ ] **步骤 3Commit**
```bash
git add src/components/sections/methodology-section.tsx
git commit -m "feat: replace methodology section with GSAP ScrollTrigger scroll narrative"
```
---
## 任务 8:各 Section 的 CSS Scroll-Driven 揭示替换
**文件:**
- 修改:`src/components/sections/about-section.tsx`
- 修改:`src/components/sections/products-section.tsx`
- 修改:`src/components/sections/services-section.tsx`
- [ ] **步骤 1:替换 AboutSection 的 framer-motion 揭示**
`src/components/sections/about-section.tsx` 中,将 `useInView` + `motion.div` 替换为 `ScrollDrivenReveal`
找到所有 `motion.div` 包裹的内容区域,替换为:
```tsx
import { ScrollDrivenReveal } from '@/components/effects/scroll-driven-reveal';
// 替换模式:
// 旧:
<motion.div initial={{ opacity: 0, y: 20 }} animate={isInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6 }}>
<h2>...</h2>
</motion.div>
// 新:
<ScrollDrivenReveal variant="reveal">
<h2>...</h2>
</ScrollDrivenReveal>
```
- [ ] **步骤 2:替换 ProductsSection 的 framer-motion 揭示**
同上模式,替换 `src/components/sections/products-section.tsx` 中的 `motion.div` 标题区域。
- [ ] **步骤 3:替换 ServicesSection 的 framer-motion 揭示**
同上模式。
- [ ] **步骤 4:验证所有 Section 动画**
运行:`npm run dev`
操作:滚动浏览各区域
预期:所有区域以 CSS Scroll-Driven 动画揭示,无 JS 计算开销
- [ ] **步骤 5Commit**
```bash
git add src/components/sections/about-section.tsx src/components/sections/products-section.tsx src/components/sections/services-section.tsx
git commit -m "perf: replace framer-motion scroll reveals with CSS Scroll-Driven Animations"
```
---
## 任务 9:Motion 库评估与基准测试
**文件:**
- 无代码变更,产出评估文档
- [ ] **步骤 1:安装 Motion 库进行评估**
运行:`npm install motion` (仅在评估分支)
- [ ] **步骤 2:包体积对比测试**
```bash
# 构建当前版本(framer-motion
npm run build
# 记录 dist/_next/static/chunks 中的 framer-motion chunk 大小
# 临时替换为 motion
# 修改 import: from 'framer-motion' → from 'motion/react'
npm run build
# 记录 motion chunk 大小
```
预期:motion 包体积应从 ~30KB 降至 ~3.5KB (gzipped)
- [ ] **步骤 3API 兼容性检查**
逐项检查当前使用的 framer-motion API 是否在 motion 中可用:
| API | framer-motion | motion/react | 兼容 |
|-----|--------------|-------------|------|
| `motion.div` | ✅ | ✅ | ✅ |
| `AnimatePresence` | ✅ | ✅ | ✅ |
| `useScroll` | ✅ | ✅ | ✅ |
| `useTransform` | ✅ | ✅ | ✅ |
| `useInView` | ✅ | ✅ | ✅ |
| `useSpring` | ✅ | ✅ | ✅ |
| `Variants` type | ✅ | ✅ | ✅ |
| `whileHover` | ✅ | ✅ | ✅ |
| `whileTap` | ✅ | ✅ | ✅ |
| `whileInView` | ✅ | ✅ | ✅ |
- [ ] **步骤 4:记录评估结论**
结论:Motion 库与 framer-motion API 完全兼容,包体积减少 ~85%。建议在 Phase 2 完成后,在独立分支中执行批量 `import` 替换,验证所有组件功能正常后合并。
- [ ] **步骤 5:卸载评估依赖**
运行:`npm uninstall motion`
- [ ] **步骤 6Commit 评估记录**
```bash
git add -A
git commit -m "docs: Motion library evaluation - compatible, 85% size reduction"
```
---
## 任务 10Phase 2 集成验证与收尾
**文件:**
- 无新增/修改
- [ ] **步骤 1:运行完整构建**
运行:`npm run build`
预期:构建成功
- [ ] **步骤 2:运行全量测试**
运行:`npx vitest run`
预期:所有测试通过
- [ ] **步骤 3:运行类型检查**
运行:`npm run type-check`
预期:无类型错误
- [ ] **步骤 4:运行 Lighthouse 审计对比**
运行:`npm run lighthouse:mobile`
预期:Performance 分数较 Phase 1 提升(因 CSS Scroll-Driven 替换了 JS 动画)
- [ ] **步骤 5:验证 GSAP + Lenis + framer-motion 共存**
操作:
1. 滚动首页 → CSS Scroll-Driven 揭示动画工作
2. 滚动到方法论 → GSAP ScrollTrigger pin + scrub 工作
3. Lenis 平滑滚动全局生效
4. Header 移动端菜单 → framer-motion AnimatePresence 工作
5. RippleButton → framer-motion whileHover/whileTap 工作
预期:三个动画库和平共存,无冲突
- [ ] **步骤 6:验证 reduced-motion 降级**
操作:开启 `prefers-reduced-motion: reduce`
预期:所有动画禁用,Lenis 不初始化,GSAP 不执行,CSS Scroll-Driven 不播放
- [ ] **步骤 7:最终 Commit**
```bash
git add -A
git commit -m "chore: Phase 2 integration verification complete"
```