diff --git a/.superpowers/brainstorm/71919-1777542709/.server-stopped b/.superpowers/brainstorm/71919-1777542709/.server-stopped new file mode 100644 index 0000000..050e132 --- /dev/null +++ b/.superpowers/brainstorm/71919-1777542709/.server-stopped @@ -0,0 +1 @@ +{"reason":"idle timeout","timestamp":1777547630053} diff --git a/.superpowers/brainstorm/71919-1777542709/atlassian-comparison.html b/.superpowers/brainstorm/71919-1777542709/atlassian-comparison.html new file mode 100644 index 0000000..1054037 --- /dev/null +++ b/.superpowers/brainstorm/71919-1777542709/atlassian-comparison.html @@ -0,0 +1,116 @@ +

Novalon × Atlassian:设计思路对比

+

分析两者在信息架构、视觉层次、交互模式上的核心差异,探索可借鉴的方向

+ +
+

📐 信息架构对比

+
+
+
当前 Novalon — 单页长滚动
+
+
Logo | 服务 | 解决方案 | 产品 | 案例 | 关于 | 团队 | 新闻
+
Hero 全屏(水墨粒子 + 数据流)
+
核心业务 Section
+
解决方案 Section
+
产品 Section
+
案例 Section
+
关于 + 团队 Section
+
新闻 Section
+
8个Section 纵向堆叠,通过 URL hash 导航
+
+
+
+
Atlassian — 多页面产品矩阵
+
+
Logo | Products ▾ | Solutions ▾ | Resources ▾ | Pricing | Contact
+
Hero(产品主张 + 视频背景)
+
社会证明(300K+ 公司 / 80% Fortune 500)
+
产品矩阵卡片(Jira / Confluence / Loom / Rovo)
+
模板/场景入口(Scrum / Bug / DevOps)
+
客户证言轮播
+
CTA + Footer
+
独立产品页 + 下拉导航 + 场景化入口
+
+
+
+
+ +
+

🎨 视觉设计语言对比

+
+
+
Novalon — 水墨科技风
+
+
+
+
+
+
+
+
字体:Geist Sans + 青柳隶书(品牌标题)
+
动效:水墨粒子 / 数据流 / 印章动画 / 视差
+
氛围:东方韵味 × 科技感
+
节奏:深→白→深→白 明暗交替
+
+
+
+
Atlassian — 清晰现代风
+
+
+
+
+
+
+
+
字体:Atlassian Sans + Atlassian Mono
+
动效:微交互 / 悬浮反馈 / 渐进展示
+
氛围:专业、清晰、可信赖
+
节奏:白底为主 + 品牌色点缀
+
+
+
+
+ +
+

🔑 Atlassian 可借鉴的 5 个核心设计模式

+
+
+
1
+
+

产品矩阵导航

+

顶部导航下拉展示所有产品,每个产品有独立落地页。用户可按"产品"或"场景"两条路径发现内容。Novalon 当前将产品、服务、解决方案平铺在单页中,缺乏独立深度。

+
+
+
+
2
+
+

社会证明前置

+

Hero 之后紧跟"300K+ 公司 / 80% Fortune 500"数据条。Novalon 的数据统计在 Hero 底部,且缺乏大客户 Logo 墙和具体量化成果展示。

+
+
+
+
3
+
+

场景化入口

+

"Get started with a template" — Scrum / Bug Tracking / DevOps 等场景模板,让用户按自身需求快速找到切入点。Novalon 的解决方案页面目前是静态描述,缺乏场景引导。

+
+
+
+
4
+
+

客户证言轮播

+

大段真实客户引言 + 姓名/职位/公司,按企业规模分类(Large Enterprise / SMB)。Novalon 的案例页目前是项目描述,缺乏客户视角的成果证言。

+
+
+
+
5
+
+

设计系统驱动一致性

+

Atlassian Design System 统一了 Token、组件、模式三层。从字体(Atlassian Sans)、图标(1.5px stroke)、颜色(中性偏暖)到组件(按钮/标签/卡片),全产品线一致。Novalon 目前有设计变量但缺乏系统化组件库。

+
+
+
+
+ +
+

👆 点击上方你感兴趣的设计模式(可多选),然后在终端告诉我你最想深入讨论哪些方向

+
diff --git a/.superpowers/brainstorm/71919-1777542709/brand-direction-comparison.html b/.superpowers/brainstorm/71919-1777542709/brand-direction-comparison.html new file mode 100644 index 0000000..38a411c --- /dev/null +++ b/.superpowers/brainstorm/71919-1777542709/brand-direction-comparison.html @@ -0,0 +1,201 @@ +

品牌基因方向对比:A vs B vs C

+

同样的信息架构(借鉴 Atlassian),三种不同的视觉表达

+ +
+

首页 Hero 区域效果模拟

+
+ +
+
+
+
+
+
+
+
+
数字化转型
+
睿新致遠
+
智连未来,成长伙伴
+
预约演示
+
+
+
+
+
+

A — 保留并强化

+

深墨色背景 + 水墨粒子持续运动 + 书法标题 + 朱砂红品牌色。视觉冲击力强,东方辨识度高。

+
+
+ +
+
+
+
+
+
+
数字化转型
+
睿新致遠
+
智连未来,成长伙伴
+
预约演示
+
+
+
+
+

B — 收敛但保留印记

+

白底为主 + 极简装饰线条 + 书法标题保留 + 朱砂红品牌色。克制专业,品牌锚点仍在。

+
+
+ +
+
+
+
+
数字化转型
+
Novalon
+
智连未来,成长伙伴
+
预约演示
+
+
+
+
+

C — 全面现代清晰风

+

纯白底 + 蓝色品牌色 + 西文无衬线 + 圆角标签。极致清晰,但失去东方辨识度。

+
+
+ +
+
+ +
+

产品卡片效果模拟

+
+ +
+
+
+ ERP +
+
睿新ERP管理系统
+
集成财务、采购、销售、库存、生产
+
+
+
了解更多 →
+
+
+ +
+
+
+ ERP +
+
睿新ERP管理系统
+
集成财务、采购、销售、库存、生产
+
+
+
了解更多 →
+
+
+ +
+
+
+ ERP +
+
睿新ERP管理系统
+
集成财务、采购、销售、库存、生产
+
+
+
了解更多 →
+
+
+ +
+
+
A — 深色沉浸卡片
+
B — 暖白克制卡片
+
C — 标准现代卡片
+
+
+ +
+

关键权衡分析

+
+ +
+
A — 保留并强化
+
+
+

✅ 优势

+
    +
  • 独特的东方美学辨识度
  • +
  • 深色背景对比度极高
  • +
  • 品牌差异化明显
  • +
  • 现有动效资产可复用
  • +
+
+
+

⚠️ 风险

+
    +
  • 动效多影响性能/加载
  • +
  • 深色风格可能"太酷"不够"稳"
  • +
  • 企业客户可能偏好传统专业感
  • +
  • 动效维护成本高
  • +
+
+
+
+ +
+
B — 收敛但保留印记 ⭐推荐
+
+
+

✅ 优势

+
    +
  • 专业感与品牌特色兼顾
  • +
  • 白底更适合信息密集展示
  • +
  • 性能友好(减少粒子动效)
  • +
  • 书法+朱砂红仍保持辨识度
  • +
  • 更接近 Atlassian 的清晰架构
  • +
+
+
+

⚠️ 风险

+
    +
  • 需要重新平衡"克制"与"特色"
  • +
  • 部分现有动效组件需弃用
  • +
  • 设计系统需从头建立
  • +
+
+
+
+ +
+
C — 全面现代清晰风
+
+
+

✅ 优势

+
    +
  • 极致清晰,信息传达效率最高
  • +
  • 可直接参考 Atlassian Design System
  • +
  • 开发维护成本最低
  • +
  • 国际化的专业感
  • +
+
+
+

⚠️ 风险

+
    +
  • 完全丧失东方品牌辨识度
  • +
  • 与国内竞品无差异
  • +
  • 品牌重塑成本极高
  • +
  • 现有视觉资产全部废弃
  • +
+
+
+
+ +
+
+ +
+

👆 点击顶部三张 Hero 卡片中你倾向的方向,然后在终端告诉我你的选择

+
diff --git a/.superpowers/brainstorm/71919-1777542709/delivery-pace-comparison.html b/.superpowers/brainstorm/71919-1777542709/delivery-pace-comparison.html new file mode 100644 index 0000000..72c7c49 --- /dev/null +++ b/.superpowers/brainstorm/71919-1777542709/delivery-pace-comparison.html @@ -0,0 +1,118 @@ +

交付节奏方案对比

+

三种纯方案 vs 推荐的混合方案

+ +
+

三种纯方案的问题

+
+ +
+
A — 首页先行
+
首页 → 产品页 → 服务页 → ...
+
+
✅ 快速见效
+
✅ 可验证方向
+
+
+
❌ 首页新风格 → 跳转子页旧风格
+
❌ 体验割裂
+
❌ 每个页面重复造组件
+
+
+ +
+
B — 架构先行
+
全站导航 → 全站框架 → 填充内容
+
+
✅ 全站一致
+
✅ 无割裂感
+
+
+
❌ 所有页面同时动,风险大
+
❌ 长时间看不到完整效果
+
❌ 回滚困难
+
+
+ +
+
C — 设计系统先行
+
Token → 组件库 → 页面迁移
+
+
✅ 质量有保障
+
✅ 组件复用
+
+
+
❌ 前期投入大,零可见产出
+
❌ 容易过度设计(组件脱离实际)
+
❌ "系统建完了但页面还没动"
+
+
+ +
+
+ +
+

⭐ 推荐:A + C 混合 — 首页驱动的设计系统

+

用首页的实际需求驱动组件设计,首页完成时设计系统基础也同步就绪

+ +
+ +
+ +
+
Phase 1:首页重构 + 设计系统基础
+
+
首页改造:
+
• 新 Hero(白底 + 书法标题 + 朱砂红CTA)
+
• 社会证明数据条
+
• 产品矩阵卡片
+
• 场景化入口
+
• 客户证言区
+
同步沉淀:
+
• Design Token 体系(颜色/间距/字体)
+
• 导航组件(下拉产品矩阵)
+
• 产品卡片组件
+
• 排版系统(标题/正文/标签)
+
+
+ +
+
Phase 2:产品页/服务页迁移
+
+
• 基于 Phase 1 沉淀的组件直接组装
+
• 产品详情页(已有独立 Section 组件)
+
• 服务详情页(已有独立 Section 组件)
+
+
Phase 3:其余页面 + 设计系统完善
+
+
• 解决方案页(场景化入口落地)
+
• 案例页(客户证言组件复用)
+
• 关于/团队/新闻页
+
+
+ +
+ +
+
💡 核心原则:组件从实际页面需求中生长
+
+
+
🏗️
+
做首页时发现
需要产品卡片
+
+
+
🔧
+
抽取为通用
ProductCard 组件
+
+
+
♻️
+
产品页/服务页
直接复用组件
+
+
+
+ +
+
+ +
+

确认这个节奏方向?或者你有其他想法?在终端告诉我

+
diff --git a/.superpowers/brainstorm/71919-1777542709/design-spec-full.html b/.superpowers/brainstorm/71919-1777542709/design-spec-full.html new file mode 100644 index 0000000..9d5eee4 --- /dev/null +++ b/.superpowers/brainstorm/71919-1777542709/design-spec-full.html @@ -0,0 +1,234 @@ +

方案二:品牌融合重构 — 完整设计规格

+

Atlassian 信息架构 × Novalon 品牌叙事 × 收敛但保留印记

+ +
+

📐 整体页面结构

+
+
+
+
+
Navigation
+
Logo + 产品▾ + 解决方案▾ + 服务 + 案例 + 关于 + 联系我们
+
+
+
+
Section 1: Hero
+
品牌主张 + 双CTA + 极简装饰
+
+
+
+
Section 2: 社会证明
+
三列关键数据 + 客户Logo墙(可选)
+
+
+
+
Section 3: 核心产品矩阵
+
2×2 卡片网格 + 朱砂红左边框
+
+
+
+
Section 4: 挑战与场景入口
+
"您面临什么挑战?" 三列痛点卡片
+
+
+
+
Section 5: 客户成果
+
深色背景 + 客户引言 + 量化数据
+
+
+
+
Section 6: CTA
+
朱砂红渐变 + 行动号召
+
+
+
+
Footer
+
品牌信息 + 导航 + 联系方式
+
+
+
+ 视觉节奏:暖白 → 浅灰 → 白 → 暖白 → 深墨 → 朱砂红 → 浅灰 +
+
+
+ +
+

🧭 导航重设计:产品矩阵下拉

+
+
+
当前导航
+
+
Logo | 服务 | 解决方案 | 产品 | 案例 | 关于 | 团队 | 新闻
+
问题:
+
• 8个平铺项,认知负担重
+
• 产品/服务/解决方案关系不清
+
• 无下拉,无法快速预览
+
• "团队""新闻"占位但非核心
+
+
+
+
新导航
+
+
Logo 睿新致遠 | 产品 ▾ | 解决方案 ▾ | 服务 | 案例 | 关于我们
+
+
点击"产品 ▾"展开:
+
+
+
ERP 管理系统
+
财务·采购·销售·库存
+
+
+
CRM 客户管理
+
线索·商机·合同·服务
+
+
+
BI 数据平台
+
报表·仪表盘·预测
+
+
+
CMS 内容平台
+
建站·运营·分发
+
+
+
+
改进:
+
• 6项→核心4项+2个下拉
+
• 下拉预览产品,减少跳转
+
• "团队/新闻"移入"关于我们"
+
+
+
+
+ +
+

🎨 Design Token 体系(收敛版)

+
+
+
+
色彩(保留,微调)
+
+
+
+
墨黑
+
+
+
+
朱砂红
+
+
+
+
宣纸白
+
+
+
+
浅灰
+
+
+
+
红底
+
+
+
✅ 保留现有色彩体系不变
+
➕ 新增场景色:#FFFBEB(黄底) #F0FDF4(绿底)
+
+
+
动效(大幅收敛)
+
❌ 移除:水墨粒子、数据流、粒子星河、鼠标交互粒子
+
❌ 移除:流体波浪、几何抽象、网格线
+
✅ 保留:渐变流动(极简版)、微妙圆点
+
✅ 保留:滚动揭示(FadeUp)、数字动画
+
➕ 新增:悬浮微交互、卡片 hover 提升效果
+
➕ 新增:导航下拉过渡、场景卡片 hover 色变
+
+
+
+
+ +
+

🧩 Phase 1 沉淀的核心组件

+
+
+
MegaDropdown
+
产品矩阵下拉导航
+
复用:产品页、解决方案页
+
+
+
ProductCard
+
朱砂红左边框产品卡片
+
复用:产品列表页、首页
+
+
+
ChallengeCard
+
痛点/场景入口卡片
+
复用:解决方案页、服务页
+
+
+
StatsBar
+
三列数据统计条
+
复用:产品页、关于页
+
+
+
TestimonialBlock
+
深色背景客户证言
+
复用:案例页、产品页
+
+
+
CTASection
+
朱砂红渐变行动号召
+
复用:全站通用
+
+
+
+ +
+

📋 现有组件迁移策略

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
组件策略说明
InkBackground❌ 废弃水墨背景不再使用,用极简装饰替代
DataParticleFlow❌ 废弃粒子动效过于花哨,不符合收敛方向
SubtleDots✅ 保留微妙圆点符合收敛方向
GradientFlow🔧 简化保留但降低强度,作为Hero极淡背景
ScrollReveal/FadeUp✅ 保留滚动揭示动画是核心交互
AnimatedNumber✅ 保留数据统计动画
SealAnimation🔄 重构印章动画保留但仅用于品牌标题区
+
+
+ +
+

以上是方案二的完整设计规格

+

确认无误后,我将编写正式设计文档并进入实现规划阶段

+
diff --git a/.superpowers/brainstorm/71919-1777542709/homepage-layout-options.html b/.superpowers/brainstorm/71919-1777542709/homepage-layout-options.html new file mode 100644 index 0000000..1cde7fd --- /dev/null +++ b/.superpowers/brainstorm/71919-1777542709/homepage-layout-options.html @@ -0,0 +1,224 @@ +

首页重构:3 种布局方案

+

同样的品牌方向(收敛+保留印记),不同的信息组织逻辑

+ +
+

方案一:Atlassian 直接映射

+

将 Atlassian 首页结构直接映射到 Novalon 内容

+
+
首页线框图
+
+
Logo 睿新致遠 | 产品 ▾ | 解决方案 ▾ | 案例 | 关于 | 联系我们
+
+
数字化转型伙伴
+
睿新致遠
+
企业数字化转型的可靠成长伙伴
+
预约演示
+
+
+ 📊 200+ 企业客户 · 覆盖 15+ 行业 · 99.9% 系统可用性 +
+
+
+
ERP
+
企业管理系统
+
+
+
CRM
+
客户关系管理
+
+
+
BI
+
商业智能分析
+
+
+
CMS
+
内容管理平台
+
+
+
+
按场景探索
+
+
制造业
+
金融
+
零售
+
+
+
+
"与睿新合作后,我们的运营效率提升了40%"
+
— 某制造业CTO
+
+
+
开始您的数字化转型
+
预约免费咨询
+
+
+
+
+

✅ 优势

  • 结构清晰,Atlassian 验证过的模式
  • 产品矩阵一目了然
  • 开发参照明确
+

⚠️ 不足

  • 可能过于"复制"Atlassian
  • 缺乏 Novalon 独特叙事
  • 场景入口较浅
+
+
+ +
+

方案二:品牌融合重构 ⭐推荐

+

Atlassian 的信息架构 + Novalon 的品牌叙事逻辑

+
+
首页线框图
+
+
Logo 睿新致遠 | 产品 ▾ | 解决方案 ▾ | 服务 | 案例 | 关于我们
+
+
+
+
NOVALON · 睿新致遠
+
智连未来,成长伙伴
+
从战略规划到系统落地,陪伴企业完成数字化转型的每一步
+
+
预约演示
+
了解方案
+
+
+
+
+
200+
+
企业客户
+
+
+
15+
+
行业覆盖
+
+
+
99.9%
+
系统可用性
+
+
+
+
核心产品
+
+
+
ERP 管理系统
+
财务·采购·销售·库存·生产
+
+
+
CRM 客户管理
+
线索·商机·合同·服务
+
+
+
BI 数据平台
+
报表·仪表盘·预测·决策
+
+
+
CMS 内容平台
+
建站·运营·分发·分析
+
+
+
+
+
您面临什么挑战?
+
+
+
🏭
+
系统孤岛
+
数据不通
+
+
+
📈
+
增长瓶颈
+
效率低下
+
+
+
🔒
+
合规风险
+
安全隐忧
+
+
+
+
+
客户成果
+
"睿新团队不是交付完就走的供应商,而是真正陪伴我们成长的伙伴"
+
+
— 某上市制造企业 CTO
+
+
40%
效率提升
+
6月
交付周期
+
+
+
+
+
开启数字化转型之旅
+
免费咨询 · 专属方案 · 30天试用
+
+
+
+
+

✅ 优势

  • 品牌叙事 + 产品矩阵完美融合
  • "您面临什么挑战"场景入口有温度
  • 深色客户成果区形成视觉节奏
  • 朱砂红左边框 = 品牌印记
+

⚠️ 不足

  • 设计复杂度中等
  • 需要更多内容支撑(客户证言/数据)
+
+
+ +
+

方案三:场景驱动叙事

+

以用户痛点/场景为主线,产品作为解决方案嵌入

+
+
首页线框图
+
+
Logo 睿新致遠 | 行业方案 ▾ | 产品 | 服务 | 案例
+
+
您的挑战,我们的使命
+
选择您最关心的业务挑战
+
+
+
+
🏭
+
制造业数字化转型
+
ERP + MES + BI 一体化
+
查看方案 →
+
+
+
🏦
+
金融合规与风控
+
CRM + BI + 安全审计
+
查看方案 →
+
+
+
🛒
+
零售全渠道运营
+
CRM + CMS + 数据中台
+
查看方案 →
+
+
+
🏗️
+
工程项目管理
+
ERP + 进度追踪 + 成本管控
+
查看方案 →
+
+
+
+ 每个场景背后,都有对应的产品组合支撑 +
+
+
我们的产品
+
+
ERP
+
·
+
CRM
+
·
+
BI
+
·
+
CMS
+
+
+
+
找到您的解决方案
+
+
+
+
+

✅ 优势

  • 用户视角,按痛点找方案
  • 差异化明显(竞品少这么做)
  • 产品自然嵌入场景
+

⚠️ 不足

  • 需要丰富的行业场景内容
  • 产品曝光度相对弱
  • SEO 不利(产品关键词靠后)
  • 内容准备量大
+
+
+ +
+

👆 点击你倾向的方案卡片(方案一/二/三的线框图区域),然后在终端确认

+

我推荐方案二——品牌融合重构,它在 Atlassian 架构优势和 Novalon 品牌特色之间取得了最佳平衡

+
diff --git a/GA数据采集系统性排查手册.docx b/GA数据采集系统性排查手册.docx new file mode 100644 index 0000000..9f4c832 Binary files /dev/null and b/GA数据采集系统性排查手册.docx differ diff --git a/docs/StockPilot供应链DSS MVP产品设计文档.docx b/docs/StockPilot供应链DSS MVP产品设计文档.docx new file mode 100644 index 0000000..34f7f29 Binary files /dev/null and b/docs/StockPilot供应链DSS MVP产品设计文档.docx differ diff --git a/docs/superpowers/plans/2026-04-24-erp-product-landing-page.md b/docs/superpowers/plans/2026-04-24-erp-product-landing-page.md new file mode 100644 index 0000000..7afab88 --- /dev/null +++ b/docs/superpowers/plans/2026-04-24-erp-product-landing-page.md @@ -0,0 +1,1261 @@ +# ERP 产品落地页实现计划 + +> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 + +**目标:** 将 ERP 产品详情页改造为 Terminal 风格的全屏沉浸式落地页 + +**架构:** 组件化重构,每个 section 独立组件,复用现有 Framer Motion 动画库,通过客户端组件组合所有 section + +**技术栈:** Next.js 16 + React 19 + Tailwind CSS v4 + Framer Motion + +--- + +## 文件结构 + +``` +新建文件: +src/components/products/ +├── product-hero-section.tsx # Section 1: 全屏沉浸 Hero +├── product-overview-section.tsx # Section 2: 产品概述 +├── product-features-section.tsx # Section 3: 核心功能(滚动叙事) +├── product-benefits-section.tsx # Section 4: 产品优势 +├── product-process-section.tsx # Section 5: 实施流程 +├── product-specs-section.tsx # Section 6: 技术规格 +├── product-pricing-section.tsx # Section 7: 定价方案 +├── product-cta-section.tsx # Section 8: CTA 横幅 +└── index.ts # 统一导出 + +src/app/(marketing)/products/[id]/ +└── product-detail-client.tsx # 客户端组件入口 + +修改文件: +src/app/(marketing)/products/[id]/page.tsx +src/components/layout/header.tsx +``` + +--- + +## 任务 1:创建产品 Hero Section 组件 + +**文件:** +- 创建:`src/components/products/product-hero-section.tsx` + +- [ ] **步骤 1:创建 product-hero-section.tsx** + +```tsx +'use client'; + +import { useEffect, useRef, useState } from 'react'; +import { motion, useScroll, useTransform } from 'framer-motion'; +import dynamic from 'next/dynamic'; +import { MagneticButton } from '@/lib/animations'; +import { ChevronDown } from 'lucide-react'; +import type { Product } from '@/lib/constants/products'; + +const InkBackground = dynamic( + () => import('@/components/ui/ink-decoration').then(mod => ({ default: mod.InkBackground })), + { ssr: false } +); + +const DataParticleFlow = dynamic( + () => import('@/components/effects/data-particle-flow').then(mod => ({ default: mod.DataParticleFlow })), + { ssr: false } +); + +interface ProductHeroSectionProps { + product: Product; +} + +export function ProductHeroSection({ product }: ProductHeroSectionProps) { + const [isVisible, setIsVisible] = useState(false); + const sectionRef = useRef(null); + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry?.isIntersecting) { + setIsVisible(true); + } + }, + { threshold: 0.1 } + ); + + if (sectionRef.current) { + observer.observe(sectionRef.current); + } + + return () => observer.disconnect(); + }, []); + + return ( +
+ {/* 背景特效 */} + + + + {/* 内容 */} +
+
+ {/* 分类标签 */} + + {product.category} + + + {/* 产品名称 */} + + {product.title} + + + {/* 价值主张 */} + + {product.description} + + + {/* CTA 按钮 */} + + + 预约演示 + + +
+
+ + {/* 滚动指示器 */} + + + + + +
+ ); +} +``` + +- [ ] **步骤 2:Commit** + +```bash +git add src/components/products/product-hero-section.tsx +git commit -m "feat(products): add product hero section component" +``` + +--- + +## 任务 2:创建产品概述 Section 组件 + +**文件:** +- 创建:`src/components/products/product-overview-section.tsx` + +- [ ] **步骤 1:创建 product-overview-section.tsx** + +```tsx +'use client'; + +import { motion, useInView } from 'framer-motion'; +import { useRef } from 'react'; +import { TextReveal } from '@/components/ui/scroll-animations'; +import type { Product } from '@/lib/constants/products'; + +interface ProductOverviewSectionProps { + product: Product; +} + +export function ProductOverviewSection({ product }: ProductOverviewSectionProps) { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, margin: '-100px' }); + + return ( +
+
+
+ {/* 标题 */} + + 产品概述 + + + {/* 概述文字 - 逐词揭示 */} + +
+
+
+ ); +} +``` + +- [ ] **步骤 2:Commit** + +```bash +git add src/components/products/product-overview-section.tsx +git commit -m "feat(products): add product overview section component" +``` + +--- + +## 任务 3:创建核心功能 Section 组件(滚动叙事) + +**文件:** +- 创建:`src/components/products/product-features-section.tsx` + +- [ ] **步骤 1:创建 product-features-section.tsx** + +```tsx +'use client'; + +import { motion, useInView, useScroll, useTransform } from 'framer-motion'; +import { useRef } from 'react'; +import type { Product } from '@/lib/constants/products'; + +interface ProductFeaturesSectionProps { + product: Product; +} + +function FeatureItem({ + feature, + index, +}: { + feature: string; + index: number; +}) { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, margin: '-50px' }); + + // 解析功能标题和描述 + const [title, ...descParts] = feature.split(':'); + const description = descParts.join(':'); + + // 编号格式化 + const number = String(index + 1).padStart(2, '0'); + + return ( +
+
+
+ {/* 左侧:编号和文字 */} +
+ {/* 编号 */} + + {number} + + + {/* 功能标题 */} + + {title} + + + {/* 功能描述 */} + {description && ( + + {description} + + )} +
+ + {/* 右侧:动画占位区域 */} + +
+
+
+ +
+
+
+ ); +} + +export function ProductFeaturesSection({ product }: ProductFeaturesSectionProps) { + const sectionRef = useRef(null); + + return ( +
+ {/* 标题 */} +
+
+ + 核心功能 + + + 全方位覆盖企业核心业务场景 + +
+
+ + {/* 功能列表 */} + {product.features.map((feature, index) => ( + + ))} +
+ ); +} +``` + +- [ ] **步骤 2:Commit** + +```bash +git add src/components/products/product-features-section.tsx +git commit -m "feat(products): add product features section with scroll storytelling" +``` + +--- + +## 任务 4:创建产品优势 Section 组件 + +**文件:** +- 创建:`src/components/products/product-benefits-section.tsx` + +- [ ] **步骤 1:创建 product-benefits-section.tsx** + +```tsx +'use client'; + +import { motion, useInView } from 'framer-motion'; +import { useRef } from 'react'; +import { StaggerReveal, StaggerItem, CounterWithEffect } from '@/lib/animations'; +import type { Product } from '@/lib/constants/products'; + +interface ProductBenefitsSectionProps { + product: Product; +} + +// 解析优势中的数字 +function extractNumber(text: string): { number: number; suffix: string } | null { + const match = text.match(/(\d+)%/); + if (match) { + return { number: parseInt(match[1], 10), suffix: '%' }; + } + return null; +} + +function BenefitCard({ + benefit, + index, +}: { + benefit: string; + index: number; +}) { + const numberInfo = extractNumber(benefit); + + return ( + + {numberInfo && ( +
+ + + +
+ )} +

+ {benefit} +

+
+ ); +} + +export function ProductBenefitsSection({ product }: ProductBenefitsSectionProps) { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, margin: '-100px' }); + + return ( +
+
+ {/* 标题 */} + + 产品优势 + + + + 经过验证的业务价值提升 + + + {/* 优势卡片网格 */} + + {product.benefits.map((benefit, index) => ( + + ))} + +
+
+ ); +} +``` + +- [ ] **步骤 2:Commit** + +```bash +git add src/components/products/product-benefits-section.tsx +git commit -m "feat(products): add product benefits section with counter animation" +``` + +--- + +## 任务 5:创建实施流程 Section 组件 + +**文件:** +- 创建:`src/components/products/product-process-section.tsx` + +- [ ] **步骤 1:创建 product-process-section.tsx** + +```tsx +'use client'; + +import { motion, useInView } from 'framer-motion'; +import { useRef } from 'react'; +import type { Product } from '@/lib/constants/products'; + +interface ProductProcessSectionProps { + product: Product; +} + +function ProcessStep({ + step, + index, + total, +}: { + step: string; + index: number; + total: number; +}) { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, margin: '-50px' }); + + // 解析步骤标题和描述 + const [title, ...descParts] = step.split(':'); + const description = descParts.join(':'); + + return ( + + {/* 编号圆圈 */} +
+
+ {index + 1} +
+ {index < total - 1 && ( +
+ )} +
+ + {/* 内容 */} +
+

+ {title} +

+ {description && ( +

+ {description} +

+ )} +
+ + ); +} + +export function ProductProcessSection({ product }: ProductProcessSectionProps) { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, margin: '-100px' }); + + return ( +
+
+
+ {/* 标题 */} + + 实施流程 + + + + 专业团队全程护航,确保项目成功落地 + + + {/* 步骤列表 */} +
+ {product.process.map((step, index) => ( + + ))} +
+
+
+
+ ); +} +``` + +- [ ] **步骤 2:Commit** + +```bash +git add src/components/products/product-process-section.tsx +git commit -m "feat(products): add product process section with timeline" +``` + +--- + +## 任务 6:创建技术规格 Section 组件 + +**文件:** +- 创建:`src/components/products/product-specs-section.tsx` + +- [ ] **步骤 1:创建 product-specs-section.tsx** + +```tsx +'use client'; + +import { motion, useInView } from 'framer-motion'; +import { useRef } from 'react'; +import { StaggerReveal, StaggerItem } from '@/components/ui/scroll-animations'; +import type { Product } from '@/lib/constants/products'; + +interface ProductSpecsSectionProps { + product: Product; +} + +export function ProductSpecsSection({ product }: ProductSpecsSectionProps) { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, margin: '-100px' }); + + return ( +
+
+ {/* 标题 */} + + 技术规格 + + + + 灵活配置,满足多样化业务需求 + + + {/* 规格网格 */} + + {product.specs.map((spec, index) => ( + +
+ {spec} + + ))} + +
+
+ ); +} +``` + +- [ ] **步骤 2:Commit** + +```bash +git add src/components/products/product-specs-section.tsx +git commit -m "feat(products): add product specs section" +``` + +--- + +## 任务 7:创建定价方案 Section 组件 + +**文件:** +- 创建:`src/components/products/product-pricing-section.tsx` + +- [ ] **步骤 1:创建 product-pricing-section.tsx** + +```tsx +'use client'; + +import { motion, useInView } from 'framer-motion'; +import { useRef } from 'react'; +import { StaggerReveal, StaggerItem } from '@/components/ui/scroll-animations'; +import { MagneticButton } from '@/lib/animations'; +import { Check } from 'lucide-react'; +import type { Product } from '@/lib/constants/products'; + +interface ProductPricingSectionProps { + product: Product; +} + +const pricingFeatures = { + base: ['基础功能模块', '邮件支持', '标准报表'], + standard: ['全部功能模块', '电话支持', '自定义报表', '优先响应'], + enterprise: ['全部功能模块', '专属客服', '定制开发', 'SLA保障'], +}; + +function PricingCard({ + name, + price, + features, + isRecommended = false, +}: { + name: string; + price: string; + features: string[]; + isRecommended?: boolean; +}) { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, margin: '-50px' }); + + return ( + + {/* 推荐标签 */} + {isRecommended && ( +
+ 推荐 +
+ )} + + {/* 名称 */} +

+ {name} +

+ + {/* 价格 */} +

+ {price} +

+ + {/* 功能列表 */} +
    + {features.map((feature, index) => ( +
  • + + + {feature} + +
  • + ))} +
+ + {/* 按钮 */} + + 立即咨询 + +
+ ); +} + +export function ProductPricingSection({ product }: ProductPricingSectionProps) { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, margin: '-100px' }); + + return ( +
+
+ {/* 标题 */} + + 价格方案 + + + + 灵活定价,按需选择 + + + {/* 定价卡片 */} +
+ + + +
+
+
+ ); +} +``` + +- [ ] **步骤 2:Commit** + +```bash +git add src/components/products/product-pricing-section.tsx +git commit -m "feat(products): add product pricing section with dark theme" +``` + +--- + +## 任务 8:创建 CTA Section 组件 + +**文件:** +- 创建:`src/components/products/product-cta-section.tsx` + +- [ ] **步骤 1:创建 product-cta-section.tsx** + +```tsx +'use client'; + +import { motion, useInView } from 'framer-motion'; +import { useRef } from 'react'; +import { MagneticButton } from '@/lib/animations'; + +export function ProductCTASection() { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, margin: '-100px' }); + + return ( +
+
+
+ {/* 标题 */} + + 准备好提升企业运营效率了吗? + + + {/* 描述 */} + + 立即联系我们,获取专属解决方案 + + + {/* 按钮 */} + + + 立即咨询 + + + 电话咨询 + + +
+
+
+ ); +} +``` + +- [ ] **步骤 2:Commit** + +```bash +git add src/components/products/product-cta-section.tsx +git commit -m "feat(products): add product CTA section" +``` + +--- + +## 任务 9:创建统一导出文件 + +**文件:** +- 创建:`src/components/products/index.ts` + +- [ ] **步骤 1:创建 index.ts** + +```tsx +export { ProductHeroSection } from './product-hero-section'; +export { ProductOverviewSection } from './product-overview-section'; +export { ProductFeaturesSection } from './product-features-section'; +export { ProductBenefitsSection } from './product-benefits-section'; +export { ProductProcessSection } from './product-process-section'; +export { ProductSpecsSection } from './product-specs-section'; +export { ProductPricingSection } from './product-pricing-section'; +export { ProductCTASection } from './product-cta-section'; +``` + +- [ ] **步骤 2:Commit** + +```bash +git add src/components/products/index.ts +git commit -m "feat(products): add products components index" +``` + +--- + +## 任务 10:创建产品详情客户端组件 + +**文件:** +- 创建:`src/app/(marketing)/products/[id]/product-detail-client.tsx` + +- [ ] **步骤 1:创建 product-detail-client.tsx** + +```tsx +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { PRODUCTS, type Product } from '@/lib/constants/products'; + +// 动态导入所有 section 组件 +const ProductHeroSection = dynamic( + () => import('@/components/products/product-hero-section').then(mod => ({ default: mod.ProductHeroSection })), + { ssr: false } +); + +const ProductOverviewSection = dynamic( + () => import('@/components/products/product-overview-section').then(mod => ({ default: mod.ProductOverviewSection })), + { ssr: false } +); + +const ProductFeaturesSection = dynamic( + () => import('@/components/products/product-features-section').then(mod => ({ default: mod.ProductFeaturesSection })), + { ssr: false } +); + +const ProductBenefitsSection = dynamic( + () => import('@/components/products/product-benefits-section').then(mod => ({ default: mod.ProductBenefitsSection })), + { ssr: false } +); + +const ProductProcessSection = dynamic( + () => import('@/components/products/product-process-section').then(mod => ({ default: mod.ProductProcessSection })), + { ssr: false } +); + +const ProductSpecsSection = dynamic( + () => import('@/components/products/product-specs-section').then(mod => ({ default: mod.ProductSpecsSection })), + { ssr: false } +); + +const ProductPricingSection = dynamic( + () => import('@/components/products/product-pricing-section').then(mod => ({ default: mod.ProductPricingSection })), + { ssr: false } +); + +const ProductCTASection = dynamic( + () => import('@/components/products/product-cta-section').then(mod => ({ default: mod.ProductCTASection })), + { ssr: false } +); + +interface ProductDetailClientProps { + productId: string; +} + +export function ProductDetailClient({ productId }: ProductDetailClientProps) { + const [product, setProduct] = useState(null); + const [isDarkHeader, setIsDarkHeader] = useState(true); + + useEffect(() => { + const found = PRODUCTS.find(p => p.id === productId); + setProduct(found || null); + }, [productId]); + + // 监听滚动,控制导航栏颜色 + useEffect(() => { + const handleScroll = () => { + const heroHeight = window.innerHeight; + setIsDarkHeader(window.scrollY < heroHeight * 0.8); + }; + + window.addEventListener('scroll', handleScroll, { passive: true }); + return () => window.removeEventListener('scroll', handleScroll); + }, []); + + // 设置导航栏颜色(通过 CSS 变量或全局状态) + useEffect(() => { + document.documentElement.setAttribute('data-header-theme', isDarkHeader ? 'dark' : 'light'); + }, [isDarkHeader]); + + if (!product) { + return ( +
+

加载中...

+
+ ); + } + + return ( +
+ + + + + + + + +
+ ); +} +``` + +- [ ] **步骤 2:Commit** + +```bash +git add src/app/\(marketing\)/products/\[id\]/product-detail-client.tsx +git commit -m "feat(products): add product detail client component" +``` + +--- + +## 任务 11:修改产品详情页面入口 + +**文件:** +- 修改:`src/app/(marketing)/products/[id]/page.tsx` + +- [ ] **步骤 1:修改 page.tsx** + +将现有内容替换为: + +```tsx +import { notFound } from 'next/navigation'; +import { PRODUCTS } from '@/lib/constants'; +import { ProductDetailClient } from './product-detail-client'; + +export async function generateStaticParams() { + return PRODUCTS.map((product) => ({ + id: product.id, + })); +} + +export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + const product = PRODUCTS.find((p) => p.id === id); + + if (!product) { + return { + title: '产品未找到', + }; + } + + return { + title: `${product.title} - 睿新致远`, + description: product.description, + }; +} + +export default async function ProductDetailPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + const product = PRODUCTS.find((p) => p.id === id); + + if (!product) { + notFound(); + } + + return ; +} +``` + +- [ ] **步骤 2:Commit** + +```bash +git add src/app/\(marketing\)/products/\[id\]/page.tsx +git commit -m "refactor(products): update product detail page to use client component" +``` + +--- + +## 任务 12:修改 Header 组件支持深色模式 + +**文件:** +- 修改:`src/components/layout/header.tsx` + +- [ ] **步骤 1:修改 Header 组件** + +在 `HeaderContent` 函数中,找到导航栏的 className 定义,修改为支持深色模式: + +```tsx +// 在 HeaderContent 函数开头添加 +const [headerTheme, setHeaderTheme] = useState<'light' | 'dark'>('light'); + +useEffect(() => { + const handleThemeChange = () => { + const theme = document.documentElement.getAttribute('data-header-theme'); + setHeaderTheme(theme === 'dark' ? 'dark' : 'light'); + }; + + // 初始检查 + handleThemeChange(); + + // 使用 MutationObserver 监听属性变化 + const observer = new MutationObserver(handleThemeChange); + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['data-header-theme'], + }); + + return () => observer.disconnect(); +}, []); + +// 修改 header 的 className +
+``` + +同时修改 Logo 和导航链接的颜色: + +```tsx +// Logo 颜色 +{COMPANY_INFO.name} + +// 导航链接颜色 + +``` + +- [ ] **步骤 2:Commit** + +```bash +git add src/components/layout/header.tsx +git commit -m "feat(layout): add dark mode support to header component" +``` + +--- + +## 任务 13:验证构建 + +- [ ] **步骤 1:运行构建** + +```bash +npm run build +``` + +预期:构建成功,无错误 + +- [ ] **步骤 2:本地预览** + +```bash +npm run start +``` + +访问 `http://localhost:3000/products/erp` 验证效果 + +- [ ] **步骤 3:最终 Commit** + +```bash +git add -A +git commit -m "feat(products): complete ERP product landing page with Terminal-style design" +``` + +--- + +## 规格覆盖度检查 + +| 规格章节 | 对应任务 | +|---------|---------| +| Section 1: 全屏沉浸 Hero | 任务 1 | +| Section 2: 产品概述 | 任务 2 | +| Section 3: 核心功能(滚动叙事) | 任务 3 | +| Section 4: 产品优势 | 任务 4 | +| Section 5: 实施流程 | 任务 5 | +| Section 6: 技术规格 | 任务 6 | +| Section 7: 定价方案 | 任务 7 | +| Section 8: CTA 横幅 | 任务 8 | +| 导航栏动态变色 | 任务 12 | +| 文件结构 | 任务 9, 10, 11 | + +✅ 所有规格需求已覆盖 + +--- + +**计划版本**: 1.0 +**最后更新**: 2026-04-24 diff --git a/docs/superpowers/plans/2026-04-24-services-solutions-design-upgrade.md b/docs/superpowers/plans/2026-04-24-services-solutions-design-upgrade.md new file mode 100644 index 0000000..1ec45d7 --- /dev/null +++ b/docs/superpowers/plans/2026-04-24-services-solutions-design-upgrade.md @@ -0,0 +1,299 @@ +# 核心业务与解决方案设计升级 实现计划 + +> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 + +**目标:** 将产品服务页面的成熟设计模式(组件拆分、动画升级、背景特效、独立布局)应用到核心业务和解决方案页面,实现全站设计一致性。 + +**架构:** 参照 `src/components/products/` 的 8 个独立 Section 组件模式,为核心业务创建 6 个独立 Section 组件,为解决方案创建 3 个独立 Section 组件。复用 `@/lib/animations` 动画组件库。核心业务详情页采用独立 Header/Footer 布局。 + +**技术栈:** Next.js 16 + React 19 + Framer Motion 12 + Tailwind CSS 4 + 自定义动画组件库 + +--- + +## 文件结构 + +### 新建文件 + +| 文件 | 职责 | +|------|------| +| `src/components/services/service-hero-section.tsx` | 服务详情 Hero(InkBackground + DataParticleFlow + SealStamp + InkReveal) | +| `src/components/services/service-challenges-section.tsx` | 挑战板块(StaggerContainer + InkCard) | +| `src/components/services/service-features-section.tsx` | 功能/如何帮助板块(ScrollReveal + FadeUp + CheckCircle2) | +| `src/components/services/service-process-section.tsx` | 服务流程(SealStamp + StaggerContainer + FadeUp + 渐变连接线) | +| `src/components/services/service-outcomes-section.tsx` | 成果板块(CountUp + StaggerContainer + InkCard) | +| `src/components/services/service-cases-section.tsx` | 相关案例(StaggerContainer + InkCard) | +| `src/components/services/service-cta-section.tsx` | CTA 区域(FloatingElement + RippleButton + 红色渐变背景) | +| `src/components/solutions/consulting-section.tsx` | 参谋伙伴模块(InkReveal + StaggerContainer + FadeUp) | +| `src/components/solutions/tech-solution-section.tsx` | 技术伙伴模块(InkReveal + StaggerContainer + FadeUp) | +| `src/components/solutions/accompany-section.tsx` | 同行伙伴模块(InkReveal + StaggerContainer + FadeUp) | +| `src/components/layout/service-header.tsx` | 服务详情独立 Header(参照 product-header) | +| `src/components/layout/service-footer.tsx` | 服务详情独立 Footer(参照 product-footer) | + +### 修改文件 + +| 文件 | 变更 | +|------|------| +| `src/app/(marketing)/services/[id]/client.tsx` | 重构:拆分为 7 个独立 Section 组件 + dynamic 懒加载 | +| `src/app/(marketing)/services/[id]/page.tsx` | 添加独立布局支持(isServiceDetailPage 判断) | +| `src/app/(marketing)/solutions/page.tsx` | 重构:拆分为 3 个独立 Section 组件 + dynamic 懒加载 | +| `src/app/(marketing)/layout.tsx` | 添加 ServiceHeader/ServiceFooter 布局切换逻辑 | +| `src/lib/constants/services.ts` | 补充 challenges 和 outcomes 数据结构 | + +--- + +## 任务 1:数据结构补全 + +**文件:** +- 修改:`src/lib/constants/services.ts` + +- [ ] **步骤 1:扩展 Service 类型,补充 challenges 和 outcomes 字段** + +将 `client.tsx` 中硬编码的 `challenges` 和 `outcomes` 对象迁移到 `services.ts`,为每个服务添加: +- `challenges: Array<{ title: string; description: string }>` — 4 项挑战 +- `outcomes: Array<{ value: string; label: string }>` — 3 项成果数据 + +- [ ] **步骤 2:验证类型导出** + +确认 `SERVICES` 从 `@/lib/constants` 正确导出,新字段可被消费。 + +--- + +## 任务 2:核心业务 — Hero Section + +**文件:** +- 创建:`src/components/services/service-hero-section.tsx` + +- [ ] **步骤 1:创建 ServiceHeroSection 组件** + +参照 `product-hero-section.tsx`,实现: +- Props: `{ service: Service }` +- 背景:`InkBackground` + `DataParticleFlow`(particleCount=60, color="#C41E3A", intensity="subtle") +- 布局:`min-h-screen flex items-center justify-center` + `bg-gradient-to-b from-white to-[#F8F8F8]` +- 动画:`SealStamp` 包裹 Badge(delay=0.1),`InkReveal` 包裹 h1(delay=0.2)、描述(delay=0.4)、CTA 按钮(delay=0.6) +- CTA:`RippleButton` 主按钮"了解服务详情" + outline 次按钮"免费咨询" +- 滚动指示器:`FloatingElement` + ChevronDown + +--- + +## 任务 3:核心业务 — 挑战 Section + +**文件:** +- 创建:`src/components/services/service-challenges-section.tsx` + +- [ ] **步骤 1:创建 ServiceChallengesSection 组件** + +- Props: `{ service: Service }` +- 背景:`bg-[#F8F8F8]`(灰色交替背景) +- 标题区:`ScrollReveal` + `slideInLeftVariants` + 朱砂红装饰线 +- 卡片网格:`StaggerContainer` + `StaggerItem` 包裹 2x2 网格 +- 每张卡片:`InkCard`(hoverScale=1.02, hoverShadow 带红色调) +- 卡片内容:挑战标题 + 描述 + +--- + +## 任务 4:核心业务 — 功能 Section + +**文件:** +- 创建:`src/components/services/service-features-section.tsx` + +- [ ] **步骤 1:创建 ServiceFeaturesSection 组件** + +- Props: `{ service: Service }` +- 背景:`bg-white` +- 标题区:`ScrollReveal` + `inkRevealVariants` +- 概述文字:`InkReveal`(delay=0.2) +- 功能列表:`StaggerContainer` + `StaggerItem`,每项用 `CheckCircle2` 图标 + `FadeUp` + +--- + +## 任务 5:核心业务 — 流程 Section + +**文件:** +- 创建:`src/components/services/service-process-section.tsx` + +- [ ] **步骤 1:创建 ServiceProcessSection 组件** + +- Props: `{ service: Service }` +- 背景:`bg-[#F8F8F8]` +- 标题区:`ScrollReveal` + `inkRevealVariants` 居中 +- 步骤列表:`StaggerContainer`(staggerDelay=0.15)+ `StaggerItem` +- 每步:`SealStamp` 编号圆形 + `FadeUp` 标题描述 + 渐变连接线(`w-0.5 h-16 bg-gradient-to-b from-[#C41E3A]/40 to-[#C41E3A]/10`,最后一步无连接线) +- 文本解析:中文冒号 `:` 分隔标题和描述 + +--- + +## 任务 6:核心业务 — 成果 Section + +**文件:** +- 创建:`src/components/services/service-outcomes-section.tsx` + +- [ ] **步骤 1:创建 ServiceOutcomesSection 组件** + +- Props: `{ service: Service }` +- 背景:`bg-white` +- 标题区:`ScrollReveal` + `slideInLeftVariants` +- 数据卡片:`StaggerContainer` + `StaggerItem` 包裹 3 列网格 +- 每张卡片:`InkCard` + `CountUp` 数字动画(从 outcomes.value 提取数字)+ 渐变色文字 +- Benefits 汇总:底部 `InkReveal` 包裹 benefits 文本 + +--- + +## 任务 7:核心业务 — 案例 Section + +**文件:** +- 创建:`src/components/services/service-cases-section.tsx` + +- [ ] **步骤 1:创建 ServiceCasesSection 组件** + +- Props: 无(内部从 `CASES` 常量获取数据) +- 背景:`bg-[#F8F8F8]` +- 标题区:`ScrollReveal` + `slideInLeftVariants` +- 案例网格:`StaggerContainer` + `StaggerItem` 包裹 2 列网格 +- 每张卡片:`InkCard` + Badge 行业标签 + 标题 + 描述 + hover 效果 + +--- + +## 任务 8:核心业务 — CTA Section + +**文件:** +- 创建:`src/components/services/service-cta-section.tsx` + +- [ ] **步骤 1:创建 ServiceCTASection 组件** + +- Props: 无 +- 背景:`bg-gradient-to-r from-[#C41E3A] to-[#E85D75]`(红色渐变) +- 装饰:两个 `FloatingElement` 圆形(右上 280px + 左下 220px) +- 内容:`InkReveal` 标题 + `FadeUp` 描述 + `FadeUp` 按钮组 +- 按钮:`RippleButton` 主按钮"开始您的转型之旅" + outline 次按钮"查看其他服务" + +--- + +## 任务 9:核心业务 — 独立 Header/Footer + +**文件:** +- 创建:`src/components/layout/service-header.tsx` +- 创建:`src/components/layout/service-footer.tsx` + +- [ ] **步骤 1:创建 ServiceHeader** + +参照 `product-header.tsx`: +- Logo + "返回主站"按钮 +- 滚动时 `bg-white/90 backdrop-blur-xl` +- `motion.header` 从顶部滑入动画 +- 区别:CTA 文案改为"免费咨询" + +- [ ] **步骤 2:创建 ServiceFooter** + +参照 `product-footer.tsx`: +- 顶部装饰线 + CTA 区域 + 底部信息 +- 锚点导航改为服务相关(服务概览/面临挑战/服务流程/预期成果/相关案例) +- `RippleButton` + `FloatingElement` + +--- + +## 任务 10:核心业务 — 重构详情页 + +**文件:** +- 修改:`src/app/(marketing)/services/[id]/client.tsx` +- 修改:`src/app/(marketing)/services/[id]/page.tsx` +- 修改:`src/app/(marketing)/layout.tsx` + +- [ ] **步骤 1:重构 client.tsx** + +将当前的单文件 5 板块替换为 7 个 dynamic 懒加载 Section 组件: +```tsx +const ServiceHeroSection = dynamic(() => import('@/components/services/service-hero-section'), { ssr: false }); +const ServiceChallengesSection = dynamic(() => import('@/components/services/service-challenges-section'), { ssr: false }); +const ServiceFeaturesSection = dynamic(() => import('@/components/services/service-features-section'), { ssr: false }); +const ServiceProcessSection = dynamic(() => import('@/components/services/service-process-section'), { ssr: false }); +const ServiceOutcomesSection = dynamic(() => import('@/components/services/service-outcomes-section'), { ssr: false }); +const ServiceCasesSection = dynamic(() => import('@/components/services/service-cases-section'), { ssr: false }); +const ServiceCTASection = dynamic(() => import('@/components/services/service-cta-section'), { ssr: false }); +``` +渲染顺序:Hero → Challenges → Features → Process → Outcomes → Cases → CTA + +- [ ] **步骤 2:修改 layout.tsx 添加服务详情独立布局** + +在 `isProductDetailPage` 逻辑旁添加 `isServiceDetailPage` 判断: +- 路径匹配 `/services/[id]` 时使用 `ServiceHeader` + `ServiceFooter` +- 其他页面保持 `Header` + `Footer` + +- [ ] **步骤 3:修改 page.tsx 确保数据传递正确** + +确认 `generateStaticParams` 和 `generateMetadata` 正常工作,新字段(challenges, outcomes)正确序列化传递。 + +--- + +## 任务 11:解决方案 — 拆分模块组件 + +**文件:** +- 创建:`src/components/solutions/consulting-section.tsx` +- 创建:`src/components/solutions/tech-solution-section.tsx` +- 创建:`src/components/solutions/accompany-section.tsx` + +- [ ] **步骤 1:创建 ConsultingSection(参谋伙伴)** + +- Props: 无(内部硬编码内容,或从常量获取) +- 背景:`bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-12 border border-[#C41E3A]/20` +- 动画升级:`InkReveal` 包裹标题(delay=0),`FadeUp` 包裹描述段落(delay=0.1/0.2/0.3),`StaggerContainer` + `StaggerItem` 包裹核心价值点网格 +- 保持现有内容和布局结构不变 + +- [ ] **步骤 2:创建 TechSolutionSection(技术伙伴)** + +同上模式,delay 递增 0.2。 + +- [ ] **步骤 3:创建 AccompanySection(同行伙伴)** + +同上模式,delay 递增 0.4。 + +--- + +## 任务 12:解决方案 — 重构页面 + +**文件:** +- 修改:`src/app/(marketing)/solutions/page.tsx` + +- [ ] **步骤 1:重构 solutions/page.tsx** + +将 3 个内联 `motion.section` 替换为 dynamic 懒加载的独立组件: +```tsx +const ConsultingSection = dynamic(() => import('@/components/solutions/consulting-section'), { ssr: false }); +const TechSolutionSection = dynamic(() => import('@/components/solutions/tech-solution-section'), { ssr: false }); +const AccompanySection = dynamic(() => import('@/components/solutions/accompany-section'), { ssr: false }); +``` +保持 `MethodologySection` 和底部 CTA 区域不变。 + +--- + +## 任务 13:验证 + +- [ ] **步骤 1:构建验证** + +运行 `npm run build`,确认无 TypeScript 错误、无导入错误。 + +- [ ] **步骤 2:页面验证** + +启动 dev server,逐页检查: +- `/services` 列表页正常渲染 +- `/services/software` 详情页:7 个 Section 全部渲染,动画正常 +- `/services/data` 详情页:同上 +- `/services/consulting` 详情页:同上 +- `/services/solutions` 详情页:同上 +- `/solutions` 页面:3 个模块全部渲染,动画正常 +- 独立 Header/Footer 在服务详情页正确显示 +- 主站 Header/Footer 在其他页面正确显示 + +- [ ] **步骤 3:Commit** + +```bash +git add -A +git commit -m "feat: 升级核心业务和解决方案页面设计,对齐产品服务设计体系 + +- 核心业务详情页拆分为 7 个独立 Section 组件 +- 解决方案页拆分为 3 个独立模块组件 +- 全面接入动画组件库(InkReveal/SealStamp/InkCard/CountUp 等) +- Hero 区域添加 InkBackground + DataParticleFlow 背景特效 +- 服务详情页采用独立 Header/Footer 布局 +- 服务数据结构补全 challenges 和 outcomes 字段 +- 所有组件使用 dynamic 懒加载" +``` diff --git a/docs/superpowers/plans/2026-04-25-remove-phone-and-update-logo-font.md b/docs/superpowers/plans/2026-04-25-remove-phone-and-update-logo-font.md new file mode 100644 index 0000000..dfcc804 --- /dev/null +++ b/docs/superpowers/plans/2026-04-25-remove-phone-and-update-logo-font.md @@ -0,0 +1,428 @@ +# 移除联系电话和更新 Logo 字体 实现计划 + +> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 + +**目标:** 移除网站中"电话咨询"按钮,并更新 Logo SVG 使用青柳隶书字体 + +**架构:** 移除 3 个组件中的"电话咨询"按钮及相关 import,使用青柳隶书字体重新生成 Logo SVG path + +**技术栈:** React, TypeScript, Next.js, SVG + +--- + +## 文件结构 + +| 文件 | 操作 | 职责 | +|------|------|------| +| `src/components/services/service-cta-section.tsx` | 修改 | 移除"电话咨询"按钮和 Phone 图标 import | +| `src/components/products/product-cta-section.tsx` | 修改 | 移除"电话咨询"按钮和 Phone 图标 import | +| `src/app/(marketing)/solutions/[id]/solution-detail-client.tsx` | 修改 | 移除"电话咨询"按钮和 Phone 图标 import | +| `public/logo.svg` | 替换 | 使用青柳隶书字体重新生成 SVG path | +| `public/logo-white.svg` | 替换 | 使用青柳隶书字体重新生成 SVG path | + +--- + +## 任务 1:移除 service-cta-section.tsx 中的"电话咨询"按钮 + +**文件:** +- 修改:`src/components/services/service-cta-section.tsx` + +- [ ] **步骤 1:读取文件内容** + +运行:读取 `src/components/services/service-cta-section.tsx` + +- [ ] **步骤 2:移除 Phone 图标 import** + +修改文件,将: +```tsx +import { Phone } from 'lucide-react'; +``` +改为: +```tsx +``` +(删除整行) + +- [ ] **步骤 3:移除"电话咨询"按钮** + +修改文件,删除以下代码块: +```tsx + + + 电话咨询 + +``` + +- [ ] **步骤 4:验证修改** + +运行:`pnpm build` +预期:构建成功,无错误 + +- [ ] **步骤 5:Commit** + +```bash +git add src/components/services/service-cta-section.tsx +git commit -m "refactor(services): 移除电话咨询按钮 + +- 移除 service-cta-section 中的电话咨询按钮 +- 移除 Phone 图标 import +- 公司暂无对外联系电话" +``` + +--- + +## 任务 2:移除 product-cta-section.tsx 中的"电话咨询"按钮 + +**文件:** +- 修改:`src/components/products/product-cta-section.tsx` + +- [ ] **步骤 1:读取文件内容** + +运行:读取 `src/components/products/product-cta-section.tsx` + +- [ ] **步骤 2:移除 Phone 图标 import** + +修改文件,将: +```tsx +import { Phone } from 'lucide-react'; +``` +改为: +```tsx +``` +(删除整行) + +- [ ] **步骤 3:移除"电话咨询"按钮** + +修改文件,删除以下代码块: +```tsx + + + 电话咨询 + +``` + +- [ ] **步骤 4:验证修改** + +运行:`pnpm build` +预期:构建成功,无错误 + +- [ ] **步骤 5:Commit** + +```bash +git add src/components/products/product-cta-section.tsx +git commit -m "refactor(products): 移除电话咨询按钮 + +- 移除 product-cta-section 中的电话咨询按钮 +- 移除 Phone 图标 import +- 公司暂无对外联系电话" +``` + +--- + +## 任务 3:移除 solution-detail-client.tsx 中的"电话咨询"按钮 + +**文件:** +- 修改:`src/app/(marketing)/solutions/[id]/solution-detail-client.tsx` + +- [ ] **步骤 1:读取文件内容** + +运行:读取 `src/app/(marketing)/solutions/[id]/solution-detail-client.tsx` + +- [ ] **步骤 2:查找并移除"电话咨询"按钮** + +找到包含"电话咨询"的按钮代码并删除: +```tsx + +``` + +- [ ] **步骤 3:检查并移除 Phone 图标 import(如果不再使用)** + +检查文件中是否还有其他地方使用 Phone 图标。如果没有,删除: +```tsx +import { Phone } from 'lucide-react'; +``` + +- [ ] **步骤 4:验证修改** + +运行:`pnpm build` +预期:构建成功,无错误 + +- [ ] **步骤 5:Commit** + +```bash +git add src/app/\(marketing\)/solutions/\[id\]/solution-detail-client.tsx +git commit -m "refactor(solutions): 移除电话咨询按钮 + +- 移除 solution-detail-client 中的电话咨询按钮 +- 移除 Phone 图标 import(如果不再使用) +- 公司暂无对外联系电话" +``` + +--- + +## 任务 4:使用青柳隶书字体重新生成 Logo SVG + +**文件:** +- 替换:`public/logo.svg` +- 替换:`public/logo-white.svg` + +- [ ] **步骤 1:检查青柳隶书字体文件** + +运行:检查 `src/app/fonts/AoyagiReisho.ttf` 是否存在 + +- [ ] **步骤 2:使用 Python 脚本生成新的 Logo SVG** + +创建并运行 Python 脚本,使用青柳隶书字体生成"睿新致遠"的 SVG path: + +```python +#!/usr/bin/env python3 +""" +使用青柳隶书字体生成 Logo SVG path +""" +import os +from fontTools.ttLib import TTFont +from fontTools.pens.svgPathPen import SvgPathPen +from fontTools.pens.t2CharStringPen import T2CharStringPen + +def get_glyph_path(font_path, text, size=48): + """ + 获取文本的 SVG path + """ + font = TTFont(font_path) + glyf_table = font['glyf'] + + paths = [] + x_offset = 0 + + for char in text: + glyph_name = font.getBestCmap().get(ord(char)) + if glyph_name: + glyph = glyf_table[glyph_name] + if glyph.numberOfContours > 0: + pen = SvgPathPen(None) + glyph.draw(pen, glyf_table) + path = pen.getCommands() + paths.append(f'') + x_offset += size * 1.2 + + return '\n'.join(paths) + +def generate_logo_svg(): + """ + 生成 Logo SVG + """ + font_path = 'src/app/fonts/AoyagiReisho.ttf' + text = '睿新致遠' + + if not os.path.exists(font_path): + print(f"错误:字体文件不存在:{font_path}") + return + + # 生成 SVG path + paths = get_glyph_path(font_path, text) + + # 创建 SVG + svg_template = f''' + + + + + + + + + + + + + + + + {paths} + + + + + + {paths} + + NOVALON + +''' + + return svg_template + +if __name__ == '__main__': + svg_content = generate_logo_svg() + if svg_content: + with open('public/logo.svg', 'w', encoding='utf-8') as f: + f.write(svg_content) + print("Logo SVG 已生成:public/logo.svg") +``` + +运行脚本: +```bash +python3 scripts/generate-logo-svg.py +``` + +- [ ] **步骤 3:生成白色版本 Logo** + +复制生成的 Logo SVG 并修改颜色: +```bash +cp public/logo.svg public/logo-white.svg +``` + +修改 `public/logo-white.svg`,将 `fill="#C41E3A"` 改为 `fill="white"` + +- [ ] **步骤 4:验证 Logo 显示** + +运行:`pnpm dev` +访问:http://localhost:3000 +预期:Logo 正确显示,使用青柳隶书字体 + +- [ ] **步骤 5:Commit** + +```bash +git add public/logo.svg public/logo-white.svg +git commit -m "style(logo): 使用青柳隶书字体更新 Logo + +- 使用青柳隶书字体重新生成 Logo SVG path +- 更新 logo.svg 和 logo-white.svg +- 确保品牌字体一致性" +``` + +--- + +## 任务 5:运行完整测试并验证 + +**文件:** +- 无文件修改 + +- [ ] **步骤 1:运行构建** + +运行:`pnpm build` +预期:构建成功,无错误 + +- [ ] **步骤 2:运行 E2E 测试** + +运行:`pnpm test` +预期:所有测试通过 + +- [ ] **步骤 3:手动验证关键页面** + +访问以下页面验证: +- 首页:http://localhost:3000 +- 服务页面:http://localhost:3000/services +- 产品页面:http://localhost:3000/products +- 解决方案详情页:http://localhost:3000/solutions/[id] +- 联系页面:http://localhost:3000/contact + +验证内容: +- ✅ Logo 显示正确 +- ✅ 无"电话咨询"按钮 +- ✅ 联系表单电话字段正常显示 + +- [ ] **步骤 4:最终 Commit(如有遗漏)** + +如果有遗漏的修改,在此步骤提交。 + +--- + +## 任务 6:部署到生产环境 + +**文件:** +- 无文件修改 + +- [ ] **步骤 1:推送代码到远程** + +运行:`git push origin main` +预期:推送成功 + +- [ ] **步骤 2:构建生产版本** + +运行:`pnpm build` +预期:构建成功 + +- [ ] **步骤 3:部署到生产服务器** + +运行:`./deploy-dist.sh` +预期:部署成功 + +- [ ] **步骤 4:验证生产环境** + +访问:https://novalon.cn +验证内容: +- ✅ Logo 显示正确 +- ✅ 无"电话咨询"按钮 +- ✅ 联系表单电话字段正常显示 + +--- + +## 自检清单 + +### 1. 规格覆盖度 + +| 规格需求 | 对应任务 | +|---------|---------| +| 移除 service-cta-section.tsx 中的"电话咨询"按钮 | 任务 1 | +| 移除 product-cta-section.tsx 中的"电话咨询"按钮 | 任务 2 | +| 移除 solution-detail-client.tsx 中的"电话咨询"按钮 | 任务 3 | +| 更新 Logo SVG 使用青柳隶书字体 | 任务 4 | +| 保留联系表单电话字段 | 无需修改(已确认) | +| 测试验证 | 任务 5 | +| 部署生产 | 任务 6 | + +### 2. 占位符扫描 + +- ✅ 无"待定"、"TODO"、"后续实现"等占位符 +- ✅ 所有代码步骤包含完整代码 +- ✅ 所有命令步骤包含完整命令 +- ✅ 无"类似任务 N"的引用 + +### 3. 类型一致性 + +- ✅ 所有文件路径使用精确路径 +- ✅ 所有组件名称一致 +- ✅ 所有 import 语句正确 + +--- + +## 执行选项 + +**计划已完成并保存到 `docs/superpowers/plans/2026-04-25-remove-phone-and-update-logo-font.md`。两种执行方式:** + +**1. 子代理驱动(推荐)** - 每个任务调度一个新的子代理,任务间进行审查,快速迭代 + +**2. 内联执行** - 在当前会话中使用 executing-plans 执行任务,批量执行并设有检查点 + +**选哪种方式?** diff --git a/docs/superpowers/plans/2026-04-28-phase1-view-transitions-svg-container-queries.md b/docs/superpowers/plans/2026-04-28-phase1-view-transitions-svg-container-queries.md new file mode 100644 index 0000000..132c21c --- /dev/null +++ b/docs/superpowers/plans/2026-04-28-phase1-view-transitions-svg-container-queries.md @@ -0,0 +1,671 @@ +# 前沿技术升级 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" +``` diff --git a/docs/superpowers/plans/2026-04-28-phase2-css-scroll-driven-gsap-lenis-motion.md b/docs/superpowers/plans/2026-04-28-phase2-css-scroll-driven-gsap-lenis-motion.md new file mode 100644 index 0000000..0f7cadd --- /dev/null +++ b/docs/superpowers/plans/2026-04-28-phase2-css-scroll-driven-gsap-lenis-motion.md @@ -0,0 +1,1142 @@ +# 前沿技术升级 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 依赖 | + +--- + +## 任务 1:CSS 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 + +- [ ] **步骤 3:Commit** + +```bash +git add src/app/globals.css +git commit -m "feat: add CSS Scroll-Driven Animation keyframes and utility classes" +``` + +--- + +## 任务 2:ScrollDrivenReveal 组件封装(渐进增强) + +**文件:** +- 创建:`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( + +

Revealed content

+
+ ); + expect(screen.getByText('Revealed content')).toBeInTheDocument(); + }); + + it('applies sd-reveal class by default', () => { + const { container } = render( + +

Content

+
+ ); + 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( + +

Content

+
+ ); + 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( + +

Content

+
+ ); + const wrapper = container.firstElementChild; + expect(wrapper?.classList.contains('sd-reveal-scale')).toBe(true); + }); + + it('applies custom className', () => { + const { container } = render( + +

Content

+
+ ); + 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( + +

Content

+
+ ); + 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 = { + 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 ( +
+ {children} +
+ ); + } + + return ( + + {children} + + ); +} +``` + +- [ ] **步骤 4:运行测试验证通过** + +运行:`npx vitest run src/components/effects/scroll-driven-reveal.test.tsx` +预期:PASS + +- [ ] **步骤 5:Commit** + +```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 ( +
+
+
+ ); +} +``` + +- [ ] **步骤 2:验证滚动进度条工作** + +运行:`npm run dev` +操作:滚动页面 +预期:进度条随滚动平滑填充,无 JS 计算开销 + +- [ ] **步骤 3:Commit** + +```bash +git add src/components/ui/scroll-progress.tsx +git commit -m "perf: replace framer-motion ScrollProgress with CSS Scroll-Driven Animation" +``` + +--- + +## 任务 4:Hero 区域 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(null); +// useEffect(() => { ... IntersectionObserver ... }, []); + +// 替换 section 标签: +
+ + + + +
+ + + + + + + + + + + + + + + +
+ +
+ {heroStats} +
+
+``` + +- [ ] **步骤 2:验证 Hero 动画** + +运行:`npm run dev` +操作:刷新首页,观察元素随滚动揭示 +预期:Hero 内容以 Scroll-Driven 动画平滑揭示,stats 区域有视差效果 + +- [ ] **步骤 3:Commit** + +```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( + +
Child content
+
+ ); + 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( + +
Content
+
+ ); + + 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(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 中: + + + {children} + + + + + +``` + +- [ ] **步骤 8:验证平滑滚动** + +运行:`npm run dev` +操作:滚动页面 +预期:滚动有惯性缓动效果,水墨画"缓缓展开"的意境 + +- [ ] **步骤 9:Commit** + +```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" +``` + +--- + +## 任务 6:GSAP 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(null); + const ctxRef = useRef(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 + +- [ ] **步骤 5:Commit** + +```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" +``` + +--- + +## 任务 7:GSAP 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(null); + const trackRef = useRef(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 ; + } + + return ( +
+
+
+

+ 实施方法论 +

+

+ 经过多年实践验证的四阶段模型,确保每个项目都能科学推进、高效落地 +

+
+ +
+
+ +
+ {METHODOLOGY.map((phase) => ( +
+
+
+ {phase.number} +
+

{phase.title}

+

{phase.subtitle}

+

{phase.description}

+
+

核心活动

+
    + {phase.activities.map((activity, i) => ( +
  • + + {activity} +
  • + ))} +
+
+
+

交付物

+
    + {phase.deliverables.map((deliverable, i) => ( +
  • + + {deliverable} +
  • + ))} +
+
+
+
+ ))} +
+
+ +
+ + + +
+
+
+ ); +} + +function MethodologyStatic() { + return ( +
+
+
+

+ 实施方法论 +

+

+ 经过多年实践验证的四阶段模型,确保每个项目都能科学推进、高效落地 +

+
+
+
+
+ {METHODOLOGY.map((phase) => ( +
+
+ {phase.number} +
+

{phase.title}

+

{phase.subtitle}

+

{phase.description}

+
+

核心活动

+
    + {phase.activities.map((activity, i) => ( +
  • + + {activity} +
  • + ))} +
+
+
+

交付物

+
    + {phase.deliverables.map((deliverable, i) => ( +
  • + + {deliverable} +
  • + ))} +
+
+
+ ))} +
+
+
+ + + +
+
+
+ ); +} +``` + +- [ ] **步骤 2:验证方法论长卷叙事** + +运行:`npm run dev` +操作:滚动到方法论区域 +预期:区域 pin 住,四张卡片随滚动依次展开,连接线从左到右绘制 + +- [ ] **步骤 3:Commit** + +```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'; + +// 替换模式: +// 旧: + +

...

+
+ +// 新: + +

...

+
+``` + +- [ ] **步骤 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 计算开销 + +- [ ] **步骤 5:Commit** + +```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) + +- [ ] **步骤 3:API 兼容性检查** + +逐项检查当前使用的 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` + +- [ ] **步骤 6:Commit 评估记录** + +```bash +git add -A +git commit -m "docs: Motion library evaluation - compatible, 85% size reduction" +``` + +--- + +## 任务 10:Phase 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" +``` diff --git a/docs/superpowers/plans/2026-04-28-phase3-webgpu-ppr-wasm.md b/docs/superpowers/plans/2026-04-28-phase3-webgpu-ppr-wasm.md new file mode 100644 index 0000000..32a9860 --- /dev/null +++ b/docs/superpowers/plans/2026-04-28-phase3-webgpu-ppr-wasm.md @@ -0,0 +1,1135 @@ +# 前沿技术升级 Phase 3 实现计划:WebGPU Shader Art + Partial Prerendering + WebAssembly + +> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 + +**目标:** 引入 WebGPU Compute Shader 实现高性能水墨流体粒子系统(替代当前 Canvas 2D 粒子);评估 Partial Prerendering 从静态导出迁移到混合渲染的可行性;探索 WebAssembly (Rust → Wasm) 实现前端图像处理能力。 + +**架构:** Phase 3 是差异化创新阶段,每项技术独立评估和实现。WebGPU 粒子系统封装为独立组件,自动降级到 Canvas 2D;PPR 需要架构级变更(从 `output: 'export'` 到 Node.js 服务器),仅做可行性评估和原型验证;Wasm 图像处理模块为可选增强。核心原则:**不破坏现有静态导出能力**,所有改动在特性检测后渐进增强。 + +**技术栈:** WebGPU API + WGSL、Next.js Partial Prerendering (experimental)、Rust + wasm-pack + wasm-bindgen + +--- + +## 文件结构 + +### 新建文件 +| 文件 | 职责 | +|------|------| +| `src/components/effects/webgpu-particle.tsx` | WebGPU 粒子系统组件 | +| `src/components/effects/webgpu-particle.test.tsx` | 测试 | +| `src/components/effects/webgpu-ink-shader.wgsl` | 水墨粒子 WGSL 着色器 | +| `src/lib/webgpu-detect.ts` | WebGPU 能力检测工具 | +| `src/lib/webgpu-detect.test.ts` | 测试 | +| `src/components/effects/particle-effect.tsx` | 统一粒子效果入口(自动选择 WebGPU/Canvas2D) | +| `src/components/effects/particle-effect.test.tsx` | 测试 | +| `docs/superpowers/plans/ppr-migration-assessment.md` | PPR 迁移评估文档 | +| `wasm/ink-filter/` | Rust Wasm 图像处理模块目录 | +| `wasm/ink-filter/src/lib.rs` | Rust 水墨滤镜核心实现 | +| `wasm/ink-filter/Cargo.toml` | Rust 项目配置 | +| `src/lib/wasm-loader.ts` | Wasm 模块动态加载器 | +| `src/lib/wasm-loader.test.ts` | 测试 | + +### 修改文件 +| 文件 | 变更内容 | +|------|---------| +| `src/components/sections/hero-section.tsx` | 替换 DataParticleFlow 为 ParticleEffect | +| `src/components/effects/data-particle-flow.tsx` | 保留作为 Canvas 2D 降级方案 | +| `src/components/effects/index.ts` | 导出 ParticleEffect | +| `next.config.ts` | 添加 WebGPU 相关配置(如需要) | + +--- + +## 任务 1:WebGPU 能力检测工具 + +**文件:** +- 创建:`src/lib/webgpu-detect.ts` +- 创建:`src/lib/webgpu-detect.test.ts` + +- [ ] **步骤 1:编写失败的测试** + +```ts +// src/lib/webgpu-detect.test.ts +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { detectWebGPU, getWebGPUCapabilities } from './webgpu-detect'; + +describe('detectWebGPU', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it('returns false when navigator.gpu is undefined', async () => { + const originalGpu = navigator.gpu; + Object.defineProperty(navigator, 'gpu', { value: undefined, configurable: true }); + + const result = await detectWebGPU(); + expect(result).toBe(false); + + Object.defineProperty(navigator, 'gpu', { value: originalGpu, configurable: true }); + }); + + it('returns false when requestAdapter fails', async () => { + Object.defineProperty(navigator, 'gpu', { + value: { requestAdapter: vi.fn().mockResolvedValue(null) }, + configurable: true, + }); + + const result = await detectWebGPU(); + expect(result).toBe(false); + }); + + it('returns true when WebGPU is available', async () => { + Object.defineProperty(navigator, 'gpu', { + value: { + requestAdapter: vi.fn().mockResolvedValue({ + requestDevice: vi.fn().mockResolvedValue({}), + }), + }, + configurable: true, + }); + + const result = await detectWebGPU(); + expect(result).toBe(true); + }); +}); + +describe('getWebGPUCapabilities', () => { + it('returns capabilities object with expected fields', async () => { + const mockDevice = { + features: new Set(['texture-compression-bc']), + limits: { maxStorageBufferBindingSize: 128 * 1024 * 1024 }, + }; + + Object.defineProperty(navigator, 'gpu', { + value: { + requestAdapter: vi.fn().mockResolvedValue({ + requestDevice: vi.fn().mockResolvedValue(mockDevice), + features: new Set(['texture-compression-bc']), + limits: { maxStorageBufferBindingSize: 128 * 1024 * 1024 }, + }), + getPreferredCanvasFormat: vi.fn().mockReturnValue('bgra8unorm'), + }, + configurable: true, + }); + + const caps = await getWebGPUCapabilities(); + expect(caps.supported).toBe(true); + expect(caps.preferredFormat).toBe('bgra8unorm'); + }); +}); +``` + +- [ ] **步骤 2:运行测试验证失败** + +运行:`npx vitest run src/lib/webgpu-detect.test.ts` +预期:FAIL + +- [ ] **步骤 3:编写最少实现代码** + +```ts +// src/lib/webgpu-detect.ts +export interface WebGPUCapabilities { + supported: boolean; + preferredFormat?: GPUTextureFormat; + maxStorageBufferBindingSize?: number; + features?: string[]; +} + +export async function detectWebGPU(): Promise { + if (!navigator.gpu) return false; + + try { + const adapter = await navigator.gpu.requestAdapter(); + if (!adapter) return false; + + const device = await adapter.requestDevice(); + return !!device; + } catch { + return false; + } +} + +export async function getWebGPUCapabilities(): Promise { + if (!navigator.gpu) { + return { supported: false }; + } + + try { + const adapter = await navigator.gpu.requestAdapter(); + if (!adapter) { + return { supported: false }; + } + + const device = await adapter.requestDevice(); + const preferredFormat = navigator.gpu.getPreferredCanvasFormat(); + + return { + supported: true, + preferredFormat, + maxStorageBufferBindingSize: adapter.limits.maxStorageBufferBindingSize, + features: [...adapter.features], + }; + } catch { + return { supported: false }; + } +} +``` + +- [ ] **步骤 4:运行测试验证通过** + +运行:`npx vitest run src/lib/webgpu-detect.test.ts` +预期:PASS + +- [ ] **步骤 5:Commit** + +```bash +git add src/lib/webgpu-detect.ts src/lib/webgpu-detect.test.ts +git commit -m "feat: add WebGPU capability detection utilities" +``` + +--- + +## 任务 2:WGSL 水墨粒子着色器 + +**文件:** +- 创建:`src/components/effects/webgpu-ink-shader.wgsl` + +- [ ] **步骤 1:编写水墨粒子 Compute Shader** + +创建 `src/components/effects/webgpu-ink-shader.wgsl`: + +```wgsl +// 水墨粒子数据结构 +struct Particle { + position: vec2f, + velocity: vec2f, + color: vec4f, + size: f32, + life: f32, + maxLife: f32, + opacity: f32, +}; + +// Uniform 参数 +struct Params { + deltaTime: f32, + time: f32, + mouseX: f32, + mouseY: f32, + mouseInfluence: f32, + particleCount: u32, + canvasWidth: f32, + canvasHeight: f32, +}; + +@group(0) @binding(0) var particles: array; +@group(0) @binding(1) var params: Params; + +// 伪随机数生成 +fn hash(value: f32) -> f32 { + return fract(sin(value) * 43758.5453123); +} + +// 水墨扩散物理模拟 +@compute @workgroup_size(64) +fn main(@builtin(global_invocation_id) id: vec3u) { + let i = id.x; + if (i >= params.particleCount) { return; } + + var p = particles[i]; + + // 鼠标吸引力 + let mousePos = vec2f(params.mouseX / params.canvasWidth, 1.0 - params.mouseY / params.canvasHeight); + let toMouse = mousePos - p.position; + let mouseDist = length(toMouse); + if (mouseDist > 0.001 && mouseDist < 0.3) { + let mouseForce = normalize(toMouse) * params.mouseInfluence * (0.3 - mouseDist) / 0.3; + p.velocity += mouseForce * params.deltaTime; + } + + // 水墨扩散 - 布朗运动 + let noiseX = hash(p.position.x * 1000.0 + params.time) - 0.5; + let noiseY = hash(p.position.y * 1000.0 + params.time + 42.0) - 0.5; + p.velocity += vec2f(noiseX, noiseY) * 0.02; + + // 阻尼 - 模拟墨水在宣纸上的阻力 + p.velocity *= 0.98; + + // 更新位置 + p.position += p.velocity * params.deltaTime; + + // 边界处理 - 柔和反弹 + if (p.position.x < 0.0) { p.position.x = 0.0; p.velocity.x *= -0.5; } + if (p.position.x > 1.0) { p.position.x = 1.0; p.velocity.x *= -0.5; } + if (p.position.y < 0.0) { p.position.y = 0.0; p.velocity.y *= -0.5; } + if (p.position.y > 1.0) { p.position.y = 1.0; p.velocity.y *= -0.5; } + + // 生命周期 + p.life -= params.deltaTime; + if (p.life <= 0.0 { + // 重生 - 模拟新的墨滴 + p.position = vec2f(hash(params.time + f32(i) * 0.1), hash(params.time + f32(i) * 0.1 + 100.0)); + p.velocity = vec2f(0.0, 0.0); + p.life = p.maxLife; + p.opacity = 0.3 + hash(f32(i)) * 0.5; + } + + // 透明度随生命衰减 - 模拟墨迹干涸 + let lifeRatio = p.life / p.maxLife; + p.opacity = p.opacity * smoothstep(0.0, 0.2, lifeRatio); + + particles[i] = p; +} +``` + +- [ ] **步骤 2:编写水墨粒子渲染 Vertex/Fragment Shader** + +在同一文件中追加: + +```wgsl +// 渲染管线着色器 +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) uv: vec2f, + @location(1) color: vec4f, + @location(2) opacity: f32, +}; + +struct RenderParams { + canvasWidth: f32, + canvasHeight: f32, +}; + +@group(0) @binding(0) var renderParticles: array; +@group(0) @binding(1) var renderParams: RenderParams; + +@vertex +fn vertexMain(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) instanceIndex: u32) -> VertexOutput { + let p = renderParticles[instanceIndex]; + + // 四边形顶点偏移 + let quadPos = array( + vec2f(-1.0, -1.0), vec2f(1.0, -1.0), vec2f(-1.0, 1.0), + vec2f(-1.0, 1.0), vec2f(1.0, -1.0), vec2f(1.0, 1.0) + ); + let quadUv = array( + vec2f(0.0, 0.0), vec2f(1.0, 0.0), vec2f(0.0, 1.0), + vec2f(0.0, 1.0), vec2f(1.0, 0.0), vec2f(1.0, 1.0) + ); + + let offset = quadPos[vertexIndex] * p.size; + let screenPos = vec2f( + p.position.x * renderParams.canvasWidth + offset.x, + (1.0 - p.position.y) * renderParams.canvasHeight + offset.y + ); + + var output: VertexOutput; + output.position = vec4f( + screenPos.x / renderParams.canvasWidth * 2.0 - 1.0, + screenPos.y / renderParams.canvasHeight * 2.0 - 1.0, + 0.0, + 1.0 + ); + output.uv = quadUv[vertexIndex]; + output.color = p.color; + output.opacity = p.opacity; + + return output; +} + +@fragment +fn fragmentMain(input: VertexOutput) -> @location(0) vec4f { + // 圆形粒子 + 柔和边缘 - 模拟墨滴 + let center = input.uv - vec2f(0.5); + let dist = length(center); + if (dist > 0.5) { discard; } + + // 水墨晕染效果 - 中心浓、边缘淡 + let inkDensity = smoothstep(0.5, 0.1, dist); + let alpha = inkDensity * input.opacity; + + return vec4f(input.color.rgb, alpha); +} +``` + +- [ ] **步骤 3:Commit** + +```bash +git add src/components/effects/webgpu-ink-shader.wgsl +git commit -m "feat: add WGSL ink particle compute and render shaders" +``` + +--- + +## 任务 3:WebGPU 粒子系统组件 + +**文件:** +- 创建:`src/components/effects/webgpu-particle.tsx` +- 创建:`src/components/effects/webgpu-particle.test.tsx` + +- [ ] **步骤 1:编写失败的测试** + +```tsx +// src/components/effects/webgpu-particle.test.tsx +import { render, screen } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import { WebGPUParticle } from './webgpu-particle'; + +describe('WebGPUParticle', () => { + it('renders canvas element', () => { + render(); + const canvas = screen.getByRole('img', { hidden: true }); + expect(canvas).toBeInTheDocument(); + }); + + it('renders fallback when WebGPU is not supported', async () => { + Object.defineProperty(navigator, 'gpu', { value: undefined, configurable: true }); + + render(Fallback
} />); + + expect(screen.getByText('Fallback')).toBeInTheDocument(); + }); + + it('applies className to container', () => { + render(); + const container = document.querySelector('.test-class'); + expect(container).toBeInTheDocument(); + }); + + it('respects 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(), + }) + ); + + render(); + matchMediaSpy.mockRestore(); + }); +}); +``` + +- [ ] **步骤 2:运行测试验证失败** + +运行:`npx vitest run src/components/effects/webgpu-particle.test.tsx` +预期:FAIL + +- [ ] **步骤 3:编写最少实现代码** + +```tsx +// src/components/effects/webgpu-particle.tsx +'use client'; + +import { useRef, useEffect, useState, type ReactNode } from 'react'; +import { detectWebGPU } from '@/lib/webgpu-detect'; +import { cn } from '@/lib/utils'; + +interface WebGPUParticleProps { + particleCount?: number; + color?: string; + className?: string; + fallback?: ReactNode; + intensity?: 'subtle' | 'normal' | 'prominent'; +} + +interface ParticleData { + position: [number, number]; + velocity: [number, number]; + color: [number, number, number, number]; + size: number; + life: number; + maxLife: number; + opacity: number; +} + +export function WebGPUParticle({ + particleCount = 5000, + className = '', + fallback, + intensity = 'normal', +}: WebGPUParticleProps) { + const canvasRef = useRef(null); + const containerRef = useRef(null); + const [supported, setSupported] = useState(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(() => { + detectWebGPU().then(setSupported); + }, []); + + useEffect(() => { + if (supported === false || prefersReducedMotion || !canvasRef.current) return; + + const canvas = canvasRef.current; + let animationFrameId: number; + let device: GPUDevice | null = null; + + async function init() { + if (!navigator.gpu) return; + + const adapter = await navigator.gpu.requestAdapter(); + if (!adapter) return; + + device = await adapter.requestDevice(); + if (!device) return; + + const context = canvas.getContext('webgpu'); + if (!context) return; + + const format = navigator.gpu.getPreferredCanvasFormat(); + context.configure({ device, format, alphaMode: 'premultiplied' }); + + const intensityConfig = { + subtle: { sizeMin: 2, sizeMax: 6, opacityMin: 0.1, opacityMax: 0.3 }, + normal: { sizeMin: 4, sizeMax: 12, opacityMin: 0.2, opacityMax: 0.6 }, + prominent: { sizeMin: 8, sizeMax: 20, opacityMin: 0.3, opacityMax: 0.8 }, + }; + const config = intensityConfig[intensity]; + + const particles: ParticleData[] = Array.from({ length: particleCount }, () => ({ + position: [Math.random(), Math.random()], + velocity: [(Math.random() - 0.5) * 0.01, (Math.random() - 0.5) * 0.01], + color: [0.11, 0.11, 0.11, 1.0], + size: Math.random() * (config.sizeMax - config.sizeMin) + config.sizeMin, + life: Math.random() * 10 + 5, + maxLife: Math.random() * 10 + 5, + opacity: Math.random() * (config.opacityMax - config.opacityMin) + config.opacityMin, + })); + + // Canvas 2D fallback rendering (WebGPU pipeline would replace this in production) + const ctx2d = canvas.getContext('2d'); + if (!ctx2d) return; + + function render() { + if (!ctx2d) return; + ctx2d.clearRect(0, 0, canvas.width, canvas.height); + + for (const p of particles) { + p.position[0] += p.velocity[0]; + p.position[1] += p.velocity[1]; + p.velocity[0] += (Math.random() - 0.5) * 0.001; + p.velocity[1] += (Math.random() - 0.5) * 0.001; + p.velocity[0] *= 0.99; + p.velocity[1] *= 0.99; + p.life -= 0.016; + + if (p.life <= 0) { + p.position[0] = Math.random(); + p.position[1] = Math.random(); + p.velocity[0] = (Math.random() - 0.5) * 0.01; + p.velocity[1] = (Math.random() - 0.5) * 0.01; + p.life = p.maxLife; + } + + const lifeRatio = p.life / p.maxLife; + const alpha = p.opacity * Math.min(1, lifeRatio * 5); + + ctx2d.beginPath(); + ctx2d.arc( + p.position[0] * canvas.width, + p.position[1] * canvas.height, + p.size * (0.5 + lifeRatio * 0.5), + 0, + Math.PI * 2 + ); + ctx2d.fillStyle = `rgba(28, 28, 28, ${alpha})`; + ctx2d.fill(); + } + + animationFrameId = requestAnimationFrame(render); + } + + render(); + } + + init(); + + return () => { + cancelAnimationFrame(animationFrameId); + device?.destroy(); + }; + }, [supported, prefersReducedMotion, particleCount, intensity]); + + if (supported === null) { + return