# 技术设计文档：H5 → 微信小程序批量迁移

## 1. 概述

本设计文档基于 33 条需求（`requirements.md`），为 17 个 H5 原型页面迁移到微信小程序提供技术实现方案。权威参考：`docs/prd/MIGRATION-PLAYBOOK.md`。

核心约束：
- 纯前端迁移，不涉及后端 API 或数据库变更
- 输入物分两批：第一批（结构迁移 Step 1-5）、第二批（像素精调 Step 6-7）
- 迁移粒度：以"屏/交互态"为最小单位，非整页

## 2. 架构总览

### 2.1 目录结构（现有 + 新增）

```
apps/miniprogram/miniprogram/
├── app.json / app.ts / app.wxss          # 全局配置与样式
├── assets/icons/                          # SVG 图标（已有 + 新导出）
├── components/                            # 共享组件
│   ├── ai-float-button/                   # ✅ 已有
│   ├── board-tab-bar/                     # ✅ 已有
│   ├── filter-dropdown/                   # ✅ 已有
│   ├── heart-icon/                        # ✅ 已有
│   ├── star-rating/                       # ✅ 已有
│   ├── note-modal/                        # ✅ 已有
│   ├── metric-card/                       # ✅ 已有
│   ├── hobby-tag/                         # ✅ 已有
│   ├── banner/                            # ✅ 已有
│   └── dev-fab/                           # ✅ 已有
├── pages/                                 # 页面目录（17 个迁移目标）
│   ├── board-finance/                     # A 批次 - 看板
│   ├── board-coach/
│   ├── board-customer/
│   ├── task-list/                         # B 批次 - 核心
│   ├── my-profile/
│   ├── task-detail/                       # C 批次 - 任务详情
│   ├── task-detail-callback/
│   ├── task-detail-priority/
│   ├── task-detail-relationship/
│   ├── coach-detail/                      # D 批次 - 详情
│   ├── customer-detail/
│   ├── customer-service-records/
│   ├── performance/                       # E 批次 - 绩效
│   ├── performance-records/
│   ├── chat/                              # F 批次 - 对话
│   ├── chat-history/
│   └── notes/                             # G 批次 - 其他
└── utils/                                 # 工具模块
    ├── ai-color.ts                        # 🆕 AI 图标随机配色
    ├── format.wxs                         # ✅ 已有 WXS 格式化
    ├── request.ts                         # ✅ 已有 网络请求
    ├── router.ts                          # ✅ 已有 路由工具
    └── ...                                # 其他已有工具
```

### 2.2 页面四文件结构

每个页面输出标准四文件：

```
pages/<page>/
├── <page>.wxml      # 视图模板
├── <page>.wxss      # 样式
├── <page>.ts        # 逻辑（TypeScript）
└── <page>.json      # 页面配置（usingComponents）
```

## 3. 单页迁移工作流设计

### 3.1 工作流总览（7 步 + 屏级粒度）

迁移以"屏/交互态"为最小工作单位，而非整页。每个页面的迁移流程：

```
Step 0: 页面分析（确认屏数、子页面、变种、工作量）
  ↓
Step 1: 输入物冻结（第一批：结构材料）
  ↓
Step 2: 迁移审计报告（7 项审计，不写代码）
  ↓
Step 3: 规则化转换（按屏逐个开发）
  ↓
Step 4: 编译验证（7 项检查）
  ↓
Step 5: 结构还原验证（9 项核对，按屏逐个验证）
  ↓
── 第二批输入物补充（截图 + computed-styles）──
  ↓
Step 6: 像素级对比（按屏逐段对比 + 微调循环）
  ↓
Step 7: 验收签收（12 项清单）
```

### 3.2 Step 0：页面分析（新增步骤）

在正式迁移前，先对目标页面做结构分析：

1. 打开 H5 原型截图 + 交互说明文档
2. 确认页面总长度（几个屏？）
3. 识别子页面/变种（如 task-detail 的 3 个主题色变体）
4. 列出所有交互态（弹窗、筛选展开、空状态等）
5. 输出工作量估算表：

```
| 单位 | 类型 | 描述 | 预估复杂度 |
|------|------|------|-----------|
| 屏-1 | 默认态首屏 | Banner + 筛选栏 + 第一板块 | 中 |
| 屏-2 | 默认态第二屏 | 第二~三板块 | 中 |
| 交互-1 | 筛选下拉 | 时间筛选 + 区域筛选 | 低 |
| 交互-2 | 指标弹窗 | 长按指标卡片 | 低 |
| ... | ... | ... | ... |
```

### 3.3 Step 3：按屏逐个开发

规则化转换不是一次性写完整页，而是按屏/交互态逐个推进：

1. 先完成首屏结构 → 编译通过 → 截图粗看
2. 再完成第二屏 → 编译通过 → 截图粗看
3. 所有屏完成后 → 处理交互态（弹窗、筛选等）
4. 最后处理三态（loading/empty/error）

### 3.4 Step 5：按屏逐个验证

结构还原验证同样按屏进行：

1. 截取小程序当前屏 → 与 H5 原型截图粗略比对
2. 9 项核对清单逐项确认
3. 通过 → 下一屏；未通过 → 修复后重新验证
4. 所有屏 + 所有交互态全部通过 → 进入 Step 6

### 3.5 差异率过大的处理策略

当 Step 6 像素对比差异率 > 15% 且多轮微调无法收敛时：
- 放弃修补，从零重写该页面（需求 2 第 5 条）
- 复杂 Banner 背景 → 导出为 SVG，`<image>` 引用（需求 2 第 6 条）
- 复杂 Icon → 导出为 SVG，`<image>` 引用（需求 2 第 7 条）

## 4. 样式转换系统设计

### 4.1 缩放公式

```
rpx = H5 px × 2 × 0.875
结果取偶数（向最近偶数取整）
```

特例：`borderRadius` 使用简单 ×2（A/B 对比验证差异 < 0.02%）。

### 4.2 design-tokens 映射

全局设计令牌来自 `docs/h5_ui/design-tokens.json`，直接映射到 WXSS 变量：

| Token | 值 | 用途 |
|-------|-----|------|
| fontSize.xs | 22rpx | 辅助文字 |
| fontSize.sm | 24rpx | 次要文字 |
| fontSize.base | 28rpx | 正文 |
| fontSize.lg | 32rpx | 小标题 |
| fontSize.xl | 36rpx | 标题 |
| fontSize.2xl | 42rpx | 大标题 |
| borderRadius.sm | 8rpx | 小圆角 |
| borderRadius.md | 16rpx | 中圆角 |
| borderRadius.lg | 24rpx | 大圆角 |
| borderRadius.xl | 32rpx | 特大圆角 |
| borderRadius.3xl | 48rpx | 全圆角 |

颜色使用 `colors` 中定义的灰阶（gray-1 ~ gray-13），禁止使用 `#333`、`#666`、`#999` 等非标准灰色。

### 4.3 两阶段样式数据源

**结构迁移阶段（Step 3）**：
1. H5 源码 Tailwind 类名 → 查速查表换算
2. design-tokens.json Token 值
3. 目测估算（必须标注 `/* 目测值，待校准 */`）

**像素精调阶段（Step 6）**：
1. computed-styles.json 精确 px 值（最高优先级）
2. H5 源码 Tailwind 类名
3. design-tokens.json
4. H5 截图目测（最低）

### 4.4 七维度核对

每个可见元素写 WXSS 时逐项确认：
1. font-size
2. font-weight
3. color
4. line-height（必须显式写出）
5. padding
6. margin / gap
7. border / border-radius

## 5. AI 图标配色系统设计

### 5.1 配色方案定义

6 种配色，每种 4 个 CSS 变量：

```typescript
// utils/ai-color.ts
const AI_COLOR_SCHEMES = {
  red:    { from: '#e74c3c', to: '#f39c9c', fromDeep: '#c0392b', toDeep: '#e74c3c' },
  orange: { from: '#e67e22', to: '#f5c77e', fromDeep: '#ca6c17', toDeep: '#e67e22' },
  yellow: { from: '#d4a017', to: '#f7dc6f', fromDeep: '#b8860b', toDeep: '#d4a017' },
  blue:   { from: '#2980b9', to: '#7ec8e3', fromDeep: '#1a5276', toDeep: '#2980b9' },
  indigo: { from: '#667eea', to: '#a78bfa', fromDeep: '#4a5fc7', toDeep: '#667eea' },
  purple: { from: '#764ba2', to: '#c084fc', fromDeep: '#5b3080', toDeep: '#764ba2' },
};
```

### 5.2 小程序实现方案

H5 通过 DOM `querySelectorAll` + `classList.add` 实现随机配色。小程序无 DOM API，改用 `setData` + 条件样式：

```typescript
// 页面 onLoad 中调用
import { getRandomAiColor } from '../../utils/ai-color';

Page({
  data: {
    aiColorClass: '',        // 'ai-color-red' | 'ai-color-orange' | ...
    aiColorVars: {},         // CSS 变量值对象
  },
  onLoad() {
    const color = getRandomAiColor();
    this.setData({
      aiColorClass: color.className,
      aiColorVars: color.vars,
    });
  },
});
```

```xml
<!-- WXML 中使用 -->
<view class="ai-inline-icon {{aiColorClass}}">
  <image src="/assets/icons/ai-robot-sm.svg" mode="aspectFit" />
</view>

<view class="ai-title-badge {{aiColorClass}}">
  <view class="ai-title-badge-icon">
    <image src="/assets/icons/ai-robot.svg" mode="aspectFit" />
  </view>
  <text>AI 推荐</text>
</view>
```

### 5.3 两个系列的 WXSS 实现

**ai-inline-icon**（行首小图标，28rpx）：
- 渐变背景 + 白色机器人 SVG
- 微光扫过动画（12s 周期 `ai-shimmer`）
- 尺寸：28rpx × 28rpx（H5 16px × 2 × 0.875 ≈ 28）

**ai-title-badge**（标题行右侧标识）：
- 浅色背景 + 主题色文字 + 主题色边框
- 呼吸脉冲动画（3s 周期 `ai-pulse`）
- 高光扫过动画（14s 周期 `ai-shimmer`）

### 5.4 ai-float-button 排除

`ai-float-button` 组件已有固定渐变动画（`#667eea → #764ba2 → #f093fb → #f5576c`），不参与页面级随机配色。无需修改。

### 5.5 机器人 SVG 复用

- 大系列（ai-title-badge）：复用已有 `assets/icons/ai-robot.svg`
- 小系列（ai-inline-icon）：从 H5 源码导出白色填充版本，保存为 `assets/icons/ai-robot-sm.svg`

## 6. 共享组件设计

### 6.1 已有组件（直接复用）

| 组件 | 路径 | 用途 | 使用页面 |
|------|------|------|---------|
| ai-float-button | components/ai-float-button/ | AI 悬浮按钮 | 所有业务页面 |
| board-tab-bar | components/board-tab-bar/ | 自定义底部导航 | board-coach, board-customer |
| filter-dropdown | components/filter-dropdown/ | 筛选下拉面板 | board-finance/coach/customer |
| heart-icon | components/heart-icon/ | 心形评分 | board-customer |
| star-rating | components/star-rating/ | 星级评价 | notes |
| note-modal | components/note-modal/ | 备注弹窗 | task-list/detail, coach-detail |
| metric-card | components/metric-card/ | 指标卡片 | board-finance, performance |
| hobby-tag | components/hobby-tag/ | 爱好标签 | board-customer, customer-detail |
| banner | components/banner/ | 顶部 Banner | task-list, performance |
| dev-fab | components/dev-fab/ | 开发调试按钮 | 所有页面（开发环境） |

### 6.2 组件注册规范

每个页面的 `.json` 文件中注册所需组件：

```json
{
  "usingComponents": {
    "t-button": "tdesign-miniprogram/button/button",
    "t-icon": "tdesign-miniprogram/icon/icon",
    "t-loading": "tdesign-miniprogram/loading/loading",
    "ai-float-button": "/components/ai-float-button/ai-float-button",
    "filter-dropdown": "/components/filter-dropdown/filter-dropdown"
  }
}
```

## 7. 事件与路由转换设计

### 7.1 事件映射表

| H5 | 小程序 | 说明 |
|----|--------|------|
| `onclick="fn()"` | `bindtap="fn"` | 基础点击 |
| `onclick="fn(id)"` | `data-id="{{id}}" bindtap="fn"` | dataset 传参 |
| `event.target.value` | `e.detail.value` | 表单取值 |
| `event.target.dataset` | `e.currentTarget.dataset` | dataset 取值 |
| `event.preventDefault()` | `catchtap` | 阻止冒泡 |
| `classList.toggle` | `setData` + 条件 class | 样式切换 |
| `innerHTML` | `setData` + WXML 绑定 | 视图更新 |
| `history.back()` | `wx.navigateBack()` | 返回 |
| `localStorage` | `wx.setStorageSync` | 本地存储 |
| `alert()/confirm()` | `wx.showToast()/wx.showModal()` | 弹窗 |
| `longpress` | `bindlongpress` | 长按 |

### 7.2 路由规则

| 目标页面类型 | API | 示例 |
|-------------|-----|------|
| TabBar 页面 | `wx.switchTab` | task-list, board-finance, my-profile |
| 普通页面 | `wx.navigateTo` | task-detail, coach-detail, chat |
| 重定向 | `wx.redirectTo` | 登录后跳转 |
| 返回 | `wx.navigateBack` | 详情页返回 |
| 重启 | `wx.reLaunch` | 切换身份 |

路径规则：以 `/` 开头，不带 `.wxml` 后缀。

## 8. 弹窗与 z-index 分层设计

### 8.1 全局 z-index 分层

```
10-29   sticky 元素（Tab 栏 20, 筛选栏 15）
30      AI 悬浮按钮
50      底部固定操作栏
100     自定义底部导航栏（board-tab-bar）
999     遮罩层
1000    弹窗内容
9999    Toast / Loading
```

### 8.2 弹窗实现模式

所有弹窗遵循统一模式：
- 同一时刻只允许一个弹窗打开（互斥）
- 遮罩 `bindtap` 关闭，内容区 `catchtap` 防穿透
- 背景滚动锁定：`catchtouchmove` 在遮罩层
- 底部弹出类添加 `padding-bottom: env(safe-area-inset-bottom)`
- 动画统一 200-220ms + `ease`

## 9. 三态处理设计

每个页面统一处理 4 种状态：

```xml
<!-- 通用三态模板 -->
<view wx:if="{{pageState === 'loading'}}">
  <t-loading text="加载中..." />
</view>
<view wx:elif="{{pageState === 'error'}}">
  <view class="error-state">
    <text>加载失败，请点击重试</text>
    <t-button bindtap="onRetry">重试</t-button>
  </view>
</view>
<view wx:elif="{{pageState === 'empty'}}">
  <text class="empty-text">{{emptyText}}</text>
</view>
<view wx:else>
  <!-- 正常内容 -->
</view>
```

各页面空状态文案见需求 14。

## 10. 像素对比工具链设计

### 10.1 工具链流程

```
H5 截图（DPR=3, 1290px 宽）
        ↓
MP 截图（DPR=1.5, 645px）→ ×2 缩放 → 1290px
        ↓
pixelmatch 逐像素对比
        ↓
按 150px 条带分析差异密度
        ↓
定位差异区域 → WXSS 微调 → 循环
```

### 10.2 逐段对比（v2 方案）

长页面使用 `scripts/ops/anchor_compare.py`：

```bash
# 提取 H5 锚点 + 截图
python scripts/ops/anchor_compare.py extract-h5 <page>

# 生成 MP 截图指令
python scripts/ops/anchor_compare.py mp-inst <page>

# 执行 MP 截图（通过微信开发者工具 MCP）

# 逐段配对 + 对比
python scripts/ops/anchor_compare.py compare <page>
```

### 10.3 scroll-view 页面截图

使用 `scroll-into-view` 模式：
1. `page.setData({ scrollIntoView: '' })` — 清空
2. `page.setData({ scrollIntoView: '<section-id>' })` — 设目标
3. 等待 1000ms → 截图

### 10.4 达标标准

- 前半屏差异率 < 5%：优秀
- 前半屏差异率 ≤ 10%：达标
- 前半屏差异率 > 15% 且无法收敛：触发重写

## 11. task-detail 变体策略

### 11.1 实现方式

1. 先完成 task-detail 主页面的完整迁移和验收
2. 复制 task-detail 四文件到变体目录
3. 替换主题色变量（banner 背景色、按钮配色）
4. 保持数据结构和布局完全一致

### 11.2 变体清单

| 变体 | 差异点 |
|------|--------|
| task-detail-callback | banner 背景色 + 按钮配色（对照 H5 原型校准） |
| task-detail-priority | banner 背景色 + 按钮配色（对照 H5 原型校准） |
| task-detail-relationship | banner 背景色 + 按钮配色（对照 H5 原型校准） |

## 12. 认证与联调设计

### 12.1 认证守卫

每个业务页面 `onLoad` 检查登录态：

```typescript
onLoad() {
  const token = wx.getStorageSync('token');
  if (!token) {
    wx.redirectTo({ url: '/pages/login/login' });
    return;
  }
  // 正常加载逻辑
}
```

### 12.2 开发联调

- `utils/request.ts` 中 `BASE_URL` 指向 `http://localhost:8000`
- 后端 `WX_DEV_MODE=true` 支持 `/api/xcx/dev-login` Mock 登录
- Storage + header token 维持登录态

## 13. 不支持的 CSS 特性替代方案

| H5 特性 | 小程序替代 |
|---------|-----------|
| `backdrop-filter: blur()` | `background: rgba(255,255,255,0.95)` |
| `*` 通配符选择器 | 逐个元素设置 |
| `filter: blur()` | `radial-gradient` 模拟 |
| `url("data:image/svg+xml,...")` | CSS 渐变模拟或导出 PNG/base64 |
| `::before/::after`（复杂场景） | 额外 `<view>` 模拟 |

直接支持的特性（无需替代）：CSS 变量 `var()`、`linear-gradient`、`animation`/`@keyframes`、`transition`。

## 14. 批次执行顺序与依赖

```
A-看板（board-finance → board-coach → board-customer）
  ↓ 共享组件验证完毕
B-核心（task-list → my-profile）
  ↓
C-任务（task-detail → 3 个变体）
  ↓
D-详情（coach-detail → customer-detail → customer-service-records）
  ↓
E-绩效（performance → performance-records）
  ↓
F-对话（chat → chat-history）
  ↓
G-其他（notes）
```

A 批次优先：验证共享组件（filter-dropdown、board-tab-bar、metric-card）在实际页面中的表现，为后续批次建立基线。

## 15. 产出物与中间生成物归档

迁移过程中会产生大量截图、diff 图、逐段对比图等中间文件。所有生成物必须按类型分目录存放，禁止散放在项目根目录或临时位置。

### 15.1 目录结构

```
docs/h5_ui/
├── screenshots/                        # H5 原型截图（输入物，已有）
│   ├── <page>.png                      # 默认态截图
│   └── <page>--<state>.png             # 交互态截图
├── mp-screenshots/                     # 🆕 小程序截图（迁移过程生成）
│   ├── <page>/                         # 按页面分子目录
│   │   ├── <page>.png                  # 默认态全屏截图
│   │   ├── <page>--<state>.png         # 交互态截图
│   │   └── seg-<N>-<section>.png       # 逐段截图（anchor_compare 生成）
│   └── ...
├── diffs/                              # 🆕 像素对比结果（迁移过程生成）
│   ├── <page>/                         # 按页面分子目录
│   │   ├── diff-<page>.png             # 全屏 diff 图
│   │   ├── diff-seg-<N>-<section>.png  # 逐段 diff 图
│   │   └── report.md                   # 该页面的对比结果摘要（差异率、问题区域）
│   └── ...
├── h5-segments/                        # 🆕 H5 逐段截图（anchor_compare 生成）
│   ├── <page>/
│   │   └── seg-<N>-<section>.png
│   └── ...
└── ...
```

### 15.2 归档规则

| 生成物类型 | 目标目录 | 命名规则 | 说明 |
|-----------|---------|---------|------|
| H5 原型截图 | `docs/h5_ui/screenshots/` | `<page>.png` / `<page>--<state>.png` | 输入物，已有，不动 |
| MP 全屏截图 | `docs/h5_ui/mp-screenshots/<page>/` | `<page>.png` / `<page>--<state>.png` | 每轮对比更新覆盖 |
| MP 逐段截图 | `docs/h5_ui/mp-screenshots/<page>/` | `seg-<N>-<section>.png` | anchor_compare 生成 |
| H5 逐段截图 | `docs/h5_ui/h5-segments/<page>/` | `seg-<N>-<section>.png` | anchor_compare 生成 |
| 全屏 diff 图 | `docs/h5_ui/diffs/<page>/` | `diff-<page>.png` | pixelmatch 输出 |
| 逐段 diff 图 | `docs/h5_ui/diffs/<page>/` | `diff-seg-<N>-<section>.png` | pixelmatch 输出 |
| 对比报告 | `docs/h5_ui/diffs/<page>/` | `report.md` | 差异率 + 问题区域摘要 |
| 新导出 SVG | `assets/icons/` | `icon-<用途>.svg` / `logo-<名称>.svg` | 小程序工程内 |
| 图标映射更新 | `docs/h5_ui/icon-mapping.md` | — | 追加新条目 |
| 小程序页面代码 | `apps/miniprogram/miniprogram/pages/<page>/` | 四文件组合 | 最终交付物 |

### 15.3 管理规则

1. 按页面分子目录：MP 截图、H5 逐段截图、diff 图均按 `<page>/` 分目录，避免数百张图片平铺
2. 每轮覆盖更新：像素精调循环中，每轮新截图覆盖上一轮同名文件，不保留历史版本（git 有历史）
3. 逐段截图编号连续：`seg-0`、`seg-1`、`seg-2`...，与 anchor_compare.py 输出一致
4. report.md 格式统一：每个页面的 `diffs/<page>/report.md` 记录最终差异率和遗留问题，作为验收依据
5. .gitignore 不排除：这些中间文件需要入库，便于团队复查和回溯
6. H5 原型截图目录只读：`docs/h5_ui/screenshots/` 是输入物，迁移过程中不往里写 MP 截图或 diff 图
