fe6e4b1c54
- Remove TestimonialSection from homepage (no customers yet) - Footer: dark theme, NAVIGATION_V2 + MEGA_DROPDOWN_DATA links - Mobile Menu: NAVIGATION_V2 with collapsible dropdown support
672 lines
20 KiB
Markdown
672 lines
20 KiB
Markdown
# 前沿技术升级 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(
|
||
<ViewTransitionWrapper name="hero-title">
|
||
<h1>Test Content</h1>
|
||
</ViewTransitionWrapper>
|
||
);
|
||
|
||
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(
|
||
<ViewTransitionWrapper name="hero-title">
|
||
<h1>Test Content</h1>
|
||
</ViewTransitionWrapper>
|
||
);
|
||
|
||
const element = screen.getByText('Test Content').parentElement;
|
||
expect(element?.style.viewTransitionName).toBe('hero-title');
|
||
});
|
||
|
||
it('renders without name prop (no view-transition-name)', () => {
|
||
render(
|
||
<ViewTransitionWrapper>
|
||
<h1>Test Content</h1>
|
||
</ViewTransitionWrapper>
|
||
);
|
||
|
||
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 (
|
||
<div style={style} className={className}>
|
||
{children}
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **步骤 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:
|
||
<ViewTransitionWrapper name="page-content">
|
||
{children}
|
||
</ViewTransitionWrapper>
|
||
|
||
// 对产品详情页布局的 children 添加:
|
||
<ViewTransitionWrapper name="product-content">
|
||
{children}
|
||
</ViewTransitionWrapper>
|
||
```
|
||
|
||
- [ ] **步骤 3:在 HeroSection 中标记共享元素**
|
||
|
||
修改 `src/components/sections/hero-section.tsx`,对品牌标题添加 view-transition-name:
|
||
|
||
找到 HeroTitle 的渲染位置,在其外层包裹:
|
||
|
||
```tsx
|
||
<ViewTransitionWrapper name="hero-title">
|
||
<HeroTitle isVisible={isVisible} />
|
||
</ViewTransitionWrapper>
|
||
```
|
||
|
||
- [ ] **步骤 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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 200" fill="none">
|
||
<!-- 睿 -->
|
||
<path class="stroke-1" d="M80,40 L80,160 M50,60 L110,60 M50,100 L110,100 M40,140 L120,140 M50,40 L50,160 M110,40 L110,160" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||
<!-- 新 -->
|
||
<path class="stroke-2" d="M200,40 L200,160 M170,60 L230,60 M170,100 L230,100 M160,140 L240,140 M170,40 L170,160 M230,40 L230,160 M200,100 L230,140" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||
<!-- 致 -->
|
||
<path class="stroke-3" d="M320,40 L320,160 M290,60 L350,60 M290,100 L350,100 M280,140 L360,140 M290,40 L290,160 M350,40 L350,160" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||
<!-- 遠 -->
|
||
<path class="stroke-4" d="M440,40 L440,160 M410,60 L470,60 M410,100 L470,100 M400,140 L480,140 M410,40 L410,160 M470,40 L470,160 M440,100 L470,140 M400,80 L440,120" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||
</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(<CalligraphyTitle text="睿新致遠" />);
|
||
expect(screen.getByText('睿新致遠')).toBeInTheDocument();
|
||
});
|
||
|
||
it('applies brand font family', () => {
|
||
render(<CalligraphyTitle text="睿新致遠" />);
|
||
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(<CalligraphyTitle text="睿新致遠" />);
|
||
const element = screen.getByText('睿新致遠');
|
||
expect(element).toHaveStyle({ opacity: '1' });
|
||
|
||
matchMediaSpy.mockRestore();
|
||
});
|
||
|
||
it('applies custom className', () => {
|
||
render(<CalligraphyTitle text="睿新致遠" className="custom-class" />);
|
||
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<SVGSVGElement>(null);
|
||
const containerRef = useRef<HTMLDivElement>(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 (
|
||
<div ref={containerRef} className={`calligraphy-title ${className}`}>
|
||
<svg
|
||
ref={svgRef}
|
||
viewBox="0 0 800 200"
|
||
className="w-full h-auto"
|
||
aria-hidden="true"
|
||
style={{ display: prefersReducedMotion ? 'none' : 'block' }}
|
||
>
|
||
<use href="/brand/ruixin-zhiyuan.svg#strokes" />
|
||
</svg>
|
||
<span
|
||
className={prefersReducedMotion ? '' : 'sr-only'}
|
||
style={{
|
||
fontFamily: "var(--font-aoyagi-reisho), 'Aoyagi Reisho', 'Ma Shan Zheng', serif",
|
||
fontWeight: 'normal',
|
||
}}
|
||
>
|
||
{text}
|
||
</span>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **步骤 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 (
|
||
<h1
|
||
id="hero-heading"
|
||
className="text-5xl sm:text-6xl lg:text-7xl tracking-tight mb-6"
|
||
style={{
|
||
fontFamily: "var(--font-aoyagi-reisho), 'Ma Shan Zheng', 'ZCOOL XiaoWei', 'STKaiti', 'KaiTi', serif",
|
||
fontWeight: 'normal',
|
||
WebkitFontSmoothing: 'antialiased',
|
||
MozOsxFontSmoothing: 'grayscale',
|
||
textRendering: 'optimizeLegibility',
|
||
}}
|
||
>
|
||
{COMPANY_INFO.shortName}
|
||
</h1>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<CalligraphyTitle
|
||
text={COMPANY_INFO.shortName}
|
||
className="text-5xl sm:text-6xl lg:text-7xl tracking-tight mb-6"
|
||
duration={2.5}
|
||
stagger={0.4}
|
||
delay={0.3}
|
||
/>
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **步骤 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
|
||
// 修改前
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 items-stretch">
|
||
|
||
// 修改后
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 items-stretch cq-container-section">
|
||
```
|
||
|
||
- [ ] **步骤 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"
|
||
```
|