# 前沿技术升级 Phase 1 实现计划:View Transitions + SVG drawSVG + Container Queries > **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 **目标:** 在不改变现有静态导出架构的前提下,引入 View Transitions API 实现跨页面流畅过渡、SVG 路径动画实现品牌标题毛笔书写效果、Container Queries 实现组件级响应式布局。 **架构:** 三项技术均为渐进增强——View Transitions 作为浏览器原生 API 零运行时依赖;SVG drawSVG 基于 GSAP 的 SVG 路径动画插件,仅在品牌标题组件中引入;Container Queries 为纯 CSS 特性,替换现有媒体查询驱动的组件布局。三项改动互不耦合,可独立交付。 **技术栈:** React 19.2 View Transitions API、GSAP 3 + DrawSVGPlugin、CSS Container Queries、Next.js 16 App Router --- ## 文件结构 ### 新建文件 | 文件 | 职责 | |------|------| | `src/components/ui/view-transition.tsx` | View Transition 封装组件,提供声明式 API | | `src/components/ui/calligraphy-title.tsx` | 品牌标题 SVG 书写动画组件 | | `src/components/ui/calligraphy-title.test.tsx` | 品牌标题组件测试 | | `src/components/ui/view-transition.test.tsx` | View Transition 组件测试 | | `public/brand/ruixin-zhiyuan.svg` | "睿新致遠" SVG 路径数据 | ### 修改文件 | 文件 | 变更内容 | |------|---------| | `src/app/(marketing)/layout.tsx` | 包裹 ViewTransition,实现跨页面过渡 | | `src/components/sections/hero-section.tsx` | 替换 HeroTitle 为 CalligraphyTitle | | `src/components/sections/hero-section-atoms.tsx` | 移除旧 HeroTitle,改用 CalligraphyTitle | | `src/components/ui/animated-card.tsx` | 添加 Container Query 支持 | | `src/components/sections/products-section.tsx` | 产品卡片容器添加 container-type | | `src/components/sections/services-section.tsx` | 服务卡片容器添加 container-type | | `src/app/globals.css` | 添加 View Transition 关键帧、Container Query 样式 | | `package.json` | 添加 gsap 依赖 | --- ## 任务 1:View Transitions API 基础封装 **文件:** - 创建:`src/components/ui/view-transition.tsx` - 创建:`src/components/ui/view-transition.test.tsx` - [ ] **步骤 1:编写失败的测试** ```tsx // src/components/ui/view-transition.test.tsx import { render, screen } from '@testing-library/react'; import { describe, it, expect, vi } from 'vitest'; import { ViewTransitionWrapper } from './view-transition'; describe('ViewTransitionWrapper', () => { it('renders children when View Transitions API is not supported', () => { const originalStartViewTransition = document.startViewTransition; // @ts-expect-error - testing fallback document.startViewTransition = undefined; render(

Test Content

); expect(screen.getByText('Test Content')).toBeInTheDocument(); // @ts-expect-error - restore document.startViewTransition = originalStartViewTransition; }); it('applies view-transition-name style when name prop is provided', () => { render(

Test Content

); const element = screen.getByText('Test Content').parentElement; expect(element?.style.viewTransitionName).toBe('hero-title'); }); it('renders without name prop (no view-transition-name)', () => { render(

Test Content

); const element = screen.getByText('Test Content').parentElement; expect(element?.style.viewTransitionName).toBe(''); }); }); ``` - [ ] **步骤 2:运行测试验证失败** 运行:`npx vitest run src/components/ui/view-transition.test.tsx` 预期:FAIL — 模块 `./view-transition` 不存在 - [ ] **步骤 3:编写最少实现代码** ```tsx // src/components/ui/view-transition.tsx 'use client'; import { type ReactNode, type CSSProperties } from 'react'; interface ViewTransitionWrapperProps { children: ReactNode; name?: string; className?: string; } export function ViewTransitionWrapper({ children, name, className = '', }: ViewTransitionWrapperProps) { const style: CSSProperties = name ? { viewTransitionName: name } : {}; return (
{children}
); } ``` - [ ] **步骤 4:运行测试验证通过** 运行:`npx vitest run src/components/ui/view-transition.test.tsx` 预期:PASS - [ ] **步骤 5:Commit** ```bash git add src/components/ui/view-transition.tsx src/components/ui/view-transition.test.tsx git commit -m "feat: add ViewTransitionWrapper component with progressive enhancement" ``` --- ## 任务 2:View Transitions 跨页面过渡集成 **文件:** - 修改:`src/app/(marketing)/layout.tsx` - 修改:`src/app/globals.css` - [ ] **步骤 1:在 globals.css 添加 View Transition 关键帧** 在 `src/app/globals.css` 的 `@layer base` 块末尾添加: ```css /* View Transitions - 跨页面过渡动画 */ @keyframes vt-fade-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } @keyframes vt-fade-out { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(-8px); } } @keyframes vt-hero-enter { from { opacity: 0; clip-path: circle(0% at 50% 50%); } to { opacity: 1; clip-path: circle(75% at 50% 50%); } } ::view-transition-old(root) { animation: vt-fade-out 0.2s cubic-bezier(0.16, 1, 0.3, 1) both; } ::view-transition-new(root) { animation: vt-fade-in 0.35s cubic-bezier(0.16, 1, 0.3, 1) both; } ::view-transition-old(hero-title) { animation: none; } ::view-transition-new(hero-title) { animation: vt-hero-enter 0.6s cubic-bezier(0.16, 1, 0.3, 1) both; } @media (prefers-reduced-motion: reduce) { ::view-transition-old(root), ::view-transition-new(root) { animation: none !important; } ::view-transition-old(hero-title), ::view-transition-new(hero-title) { animation: none !important; } } ``` - [ ] **步骤 2:修改 MarketingLayout 集成 View Transitions** 在 `src/app/(marketing)/layout.tsx` 中,将主内容区域包裹 ViewTransitionWrapper: 找到 `export default function MarketingLayout` 函数,在 children 外层添加 ViewTransitionWrapper: ```tsx // 在文件顶部添加导入 import { ViewTransitionWrapper } from '@/components/ui/view-transition'; // 在 return 的 children 包裹处,找到所有渲染 children 的位置 // 对主站布局的 children 添加 ViewTransitionWrapper: {children} // 对产品详情页布局的 children 添加: {children} ``` - [ ] **步骤 3:在 HeroSection 中标记共享元素** 修改 `src/components/sections/hero-section.tsx`,对品牌标题添加 view-transition-name: 找到 HeroTitle 的渲染位置,在其外层包裹: ```tsx ``` - [ ] **步骤 4:验证 View Transitions 工作正常** 运行:`npm run dev` 操作:在首页和各子页面之间导航,观察页面过渡动画 预期:页面切换时有淡入淡出效果,品牌标题有圆形展开动画 - [ ] **步骤 5:Commit** ```bash git add src/app/globals.css src/app/\(marketing\)/layout.tsx src/components/sections/hero-section.tsx git commit -m "feat: integrate View Transitions API for cross-page transitions" ``` --- ## 任务 3:GSAP 安装与 SVG 品牌标题路径准备 **文件:** - 创建:`public/brand/ruixin-zhiyuan.svg` - [ ] **步骤 1:安装 GSAP** 运行:`npm install gsap` - [ ] **步骤 2:创建"睿新致遠"SVG 路径文件** 将品牌标题转为 SVG path 数据。由于青柳隷書字体已内嵌,需要用字体渲染后提取路径。 创建 `public/brand/ruixin-zhiyuan.svg`: ```svg ``` > **注意**:上述 SVG 为占位骨架。实际生产需使用字体转 SVG 工具(如 opentype.js 或在线工具 text-to-svg)从青柳隷書字体提取精确路径。此步骤在实现时需用脚本从 `src/app/fonts/AoyagiReisho.ttf` 提取。 - [ ] **步骤 3:验证 SVG 可访问** 运行:`npm run dev` 访问:`http://localhost:3000/brand/ruixin-zhiyuan.svg` 预期:SVG 文件可正常加载 - [ ] **步骤 4:Commit** ```bash git add package.json package-lock.json public/brand/ruixin-zhiyuan.svg git commit -m "feat: add GSAP dependency and brand title SVG path data" ``` --- ## 任务 4:CalligraphyTitle 组件实现 **文件:** - 创建:`src/components/ui/calligraphy-title.tsx` - 创建:`src/components/ui/calligraphy-title.test.tsx` - [ ] **步骤 1:编写失败的测试** ```tsx // src/components/ui/calligraphy-title.test.tsx import { render, screen, act } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { CalligraphyTitle } from './calligraphy-title'; describe('CalligraphyTitle', () => { beforeEach(() => { vi.useFakeTimers(); }); it('renders the title text as fallback', () => { render(); expect(screen.getByText('睿新致遠')).toBeInTheDocument(); }); it('applies brand font family', () => { render(); const element = screen.getByText('睿新致遠'); expect(element.style.fontFamily).toContain('Aoyagi Reisho'); }); it('respects reduced motion preference', () => { 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(), }) ); render(); const element = screen.getByText('睿新致遠'); expect(element).toHaveStyle({ opacity: '1' }); matchMediaSpy.mockRestore(); }); it('applies custom className', () => { render(); const element = screen.getByText('睿新致遠').closest('.custom-class'); expect(element).toBeInTheDocument(); }); }); ``` - [ ] **步骤 2:运行测试验证失败** 运行:`npx vitest run src/components/ui/calligraphy-title.test.tsx` 预期:FAIL — 模块 `./calligraphy-title` 不存在 - [ ] **步骤 3:编写最少实现代码** ```tsx // src/components/ui/calligraphy-title.tsx 'use client'; import { useRef, useEffect, useState } from 'react'; import { gsap } from 'gsap'; import { DrawSVGPlugin } from 'gsap/DrawSVGPlugin'; gsap.registerPlugin(DrawSVGPlugin); interface CalligraphyTitleProps { text: string; className?: string; duration?: number; stagger?: number; delay?: number; } export function CalligraphyTitle({ text, className = '', duration = 2.5, stagger = 0.4, delay = 0.3, }: CalligraphyTitleProps) { const svgRef = useRef(null); const containerRef = useRef(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 || !svgRef.current) return; const paths = svgRef.current.querySelectorAll('path'); if (paths.length === 0) return; gsap.set(paths, { drawSVG: '0%' }); const ctx = gsap.context(() => { gsap.fromTo( paths, { drawSVG: '0%' }, { drawSVG: '100%', duration, stagger, delay, ease: 'power2.inOut', } ); }, svgRef); return () => ctx.revert(); }, [prefersReducedMotion, duration, stagger, delay]); return (
{text}
); } ``` - [ ] **步骤 4:运行测试验证通过** 运行:`npx vitest run src/components/ui/calligraphy-title.test.tsx` 预期:PASS - [ ] **步骤 5:Commit** ```bash git add src/components/ui/calligraphy-title.tsx src/components/ui/calligraphy-title.test.tsx git commit -m "feat: add CalligraphyTitle component with GSAP DrawSVG animation" ``` --- ## 任务 5:集成 CalligraphyTitle 到 Hero 区域 **文件:** - 修改:`src/components/sections/hero-section-atoms.tsx` - 修改:`src/components/sections/hero-section.tsx` - [ ] **步骤 1:替换 HeroTitle 组件** 在 `src/components/sections/hero-section-atoms.tsx` 中,修改 `HeroTitle` 函数: ```tsx // 替换现有 HeroTitle 实现 import { CalligraphyTitle } from '@/components/ui/calligraphy-title'; export function HeroTitle(_props: HeroContentProps) { const shouldReduceMotion = useReducedMotion(); if (shouldReduceMotion) { return (

{COMPANY_INFO.shortName}

); } return ( ); } ``` - [ ] **步骤 2:移除旧的 InkReveal 包裹** 在 `src/components/sections/hero-section-atoms.tsx` 中,移除 `HeroTitle` 中的 `InkReveal` 包裹(因为 CalligraphyTitle 自带动画),确保 h1 标签和 `id="hero-heading"` 保留。 - [ ] **步骤 3:验证 Hero 标题动画效果** 运行:`npm run dev` 访问:`http://localhost:3000` 预期:品牌标题"睿新致遠"以毛笔书写动画逐笔呈现 - [ ] **步骤 4:Commit** ```bash git add src/components/sections/hero-section-atoms.tsx src/components/sections/hero-section.tsx git commit -m "feat: replace HeroTitle with CalligraphyTitle for brush writing animation" ``` --- ## 任务 6:Container Queries 实现 **文件:** - 修改:`src/components/ui/animated-card.tsx` - 修改:`src/components/sections/products-section.tsx` - 修改:`src/components/sections/services-section.tsx` - 修改:`src/app/globals.css` - [ ] **步骤 1:在 globals.css 添加 Container Query 工具类** 在 `src/app/globals.css` 的 `@layer utilities` 块中添加: ```css /* Container Queries */ @utility cq-container { container-type: inline-size; container-name: card; } @utility cq-container-section { container-type: inline-size; container-name: section; } ``` 在 `@layer base` 块之后添加: ```css /* Container Query 响应式卡片布局 */ @container card (min-width: 350px) { .cq-card-horizontal { display: grid; grid-template-columns: 120px 1fr; gap: 1rem; } } @container card (min-width: 500px) { .cq-card-horizontal { grid-template-columns: 180px 1fr; gap: 1.5rem; } } @container section (min-width: 768px) { .cq-section-grid { grid-template-columns: repeat(2, 1fr); } } @container section (min-width: 1024px) { .cq-section-grid { grid-template-columns: repeat(3, 1fr); } } ``` - [ ] **步骤 2:修改 AnimatedCard 添加 container-type** 在 `src/components/ui/animated-card.tsx` 中,找到 `motion.div` 的 `className`,添加 `cq-container`: ```tsx // 修改前 className={cn('ink-card', className)} // 修改后 className={cn('ink-card cq-container', className)} ``` - [ ] **步骤 3:修改 ProductsSection 添加容器查询** 在 `src/components/sections/products-section.tsx` 中,找到产品卡片的 grid 容器 div,添加 `cq-container-section`: ```tsx // 修改前
// 修改后
``` - [ ] **步骤 4:修改 ServicesSection 添加容器查询** 在 `src/components/sections/services-section.tsx` 中做同样的修改。 - [ ] **步骤 5:验证 Container Queries 工作** 运行:`npm run dev` 操作:调整浏览器窗口大小,观察卡片在不同容器宽度下的布局变化 预期:卡片在窄容器中单列显示,宽容器中自动切换为多列 - [ ] **步骤 6:运行全量单元测试** 运行:`npx vitest run` 预期:所有测试通过 - [ ] **步骤 7:Commit** ```bash git add src/app/globals.css src/components/ui/animated-card.tsx src/components/sections/products-section.tsx src/components/sections/services-section.tsx git commit -m "feat: add Container Queries for component-level responsive layouts" ``` --- ## 任务 7:Phase 1 集成验证与收尾 **文件:** - 无新增/修改 - [ ] **步骤 1:运行完整构建** 运行:`npm run build` 预期:构建成功,无错误 - [ ] **步骤 2:运行全量测试** 运行:`npx vitest run` 预期:所有测试通过 - [ ] **步骤 3:运行类型检查** 运行:`npm run type-check` 预期:无类型错误 - [ ] **步骤 4:运行 Lighthouse 审计** 运行:`npm run lighthouse:mobile` 预期:Performance ≥ 90, Accessibility ≥ 95 - [ ] **步骤 5:验证渐进增强降级** 操作: 1. 在不支持 View Transitions 的浏览器(如 Firefox < 126)中访问 → 页面正常显示,无过渡动画 2. 在不支持 Container Queries 的浏览器中 → 卡片使用原有媒体查询布局 3. 开启 `prefers-reduced-motion` → 品牌标题直接显示,无书写动画 预期:所有降级场景均正常工作 - [ ] **步骤 6:最终 Commit** ```bash git add -A git commit -m "chore: Phase 1 integration verification complete" ```