Files

553 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 技术设计文档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` 使用简单 ×2A/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 × 28rpxH5 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 图