Files
Neo-ZQYY/docs/prd/MIGRATION-PLAYBOOK.md

2097 lines
87 KiB
Markdown
Raw 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 → 微信小程序批量迁移执行手册
> 本手册是 SPEC 执行的唯一权威文档,整合了原 `apps/miniprogram/doc/` 下全部迁移文档。
> 目标:将 `docs/h5_ui/pages/` 下 17 个 HTML 原型页面迁移为原生微信小程序页面。
> 创建日期2026-03-08
> 原始文档来源migration-guide.md、migration-method-full-path.md、h5-to-miniprogram-pitfalls.md、
> shared-component-specs.md、h5-input-material-guide.md、howtodo.md、auth-integration-guide.md、
> migration-tracker.md、prd.md — 已全部整合至此,原文件不再维护。
---
## 一、迁移范围
### 排除页面4 个,用户指定不迁移)
- login、no-permission、reviewing、apply
### 排除页面补充2 个,材料不完整,本次不处理)
- home-settings无交互说明
- ai-icon-demo无截图、无交互说明
### 全量迁移清单17 个页面,全部需要走完整流程)
> board-coach、board-customer 虽有历史实现board-finance 正在迁移中,
> 但本次 SPEC 统一纳入,全部按标准流程重新迁移/验收。
| 批次 | 页面 | 复杂度 | 交互说明 | 截图 | 历史状态 | 备注 |
|------|------|--------|----------|------|----------|------|
| A-看板 | board-finance | 高 | ✅ | ✅ (4张) | 🔧 迁移中 | 6 板块,含筛选下拉+指标弹窗+目录面板 |
| A-看板 | board-coach | 高 | ✅ | ✅ (8张) | ✅ 有历史实现 | 4 维度卡片 + 3 种筛选下拉 |
| A-看板 | board-customer | 高 | ✅ | ✅ (10张) | ✅ 有历史实现 | 8 维度卡片 + heart-icon + 最专一表格 |
| B-核心 | task-list | 高 | ✅ | ✅ (3张) | 待迁移 | TabBar 主页,含长按菜单+备注弹窗 |
| B-核心 | my-profile | 中 | ✅ | ✅ | 待迁移 | TabBar 主页 |
| C-任务 | task-detail | 高 | ✅ | ✅ (3张) | 待迁移 | 含放弃弹窗+备注弹窗 |
| C-任务 | task-detail-callback | 低 | ✅ | ✅ | 待迁移 | task-detail 主题色变体(仅 banner 背景+按钮配色),数据结构同 task-detail |
| C-任务 | task-detail-priority | 低 | ✅ | ✅ | 待迁移 | task-detail 主题色变体(仅 banner 背景+按钮配色),数据结构同 task-detail |
| C-任务 | task-detail-relationship | 低 | ✅ | ✅ | 待迁移 | task-detail 主题色变体(仅 banner 背景+按钮配色),数据结构同 task-detail |
| D-详情 | coach-detail | 中 | ✅ | ✅ (2张) | 待迁移 | 含备注弹窗 |
| D-详情 | customer-detail | 中 | ✅ | ✅ | 待迁移 | 客户详情 |
| D-详情 | customer-service-records | 中 | ✅ | ✅ | 待迁移 | 客户服务记录 |
| E-绩效 | performance | 中 | ✅ | ✅ | 待迁移 | 业绩总览 |
| E-绩效 | performance-records | 中 | ✅ | ✅ | 待迁移 | 业绩明细 |
| F-对话 | chat | 中 | ✅ | ✅ | 待迁移 | AI 对话 |
| F-对话 | chat-history | 低 | ✅ | ✅ | 待迁移 | 对话历史 |
| G-其他 | notes | 低 | ✅ | ✅ | 待迁移 | 备忘录(有历史实现,需重写) |
### 有历史实现的页面处理方式
- board-coach、board-customer、board-finance、notes已有小程序代码本次按标准流程重新审计 → 对比 H5 原型 → 差异修复 → 像素级验收
- 其余 13 页:从零开始全流程迁移
---
## 二、每页迁移标准流程7 步)
### Step 1输入物冻结加载顺序严格执行
```
1. 规则层:
- 本文档(转换规范 + 避坑指南 + 组件规范,全部在此)
2. 全局资源层:
- docs/h5_ui/design-tokens.json颜色/间距/字号/圆角/阴影)
- docs/h5_ui/icon-mapping.md图标处理方案
3. 页面源码层:
- docs/h5_ui/pages/<page>.htmlH5 源码 — 唯一结构真相)
- docs/h5_ui/css/<page>.css自定义 CSS如有
- docs/h5_ui/computed-styles.json 中 <page> key精确 px 值,如有)
4. 行为层:
- docs/h5_ui/interactions/<page>.md状态变量 + 操作响应 + 状态枚举)
5. 视觉校验层:
- docs/h5_ui/screenshots/<page>.png默认态截图
- docs/h5_ui/screenshots/<page>--*.png交互态截图
```
**缺一不可。** 缺失材料时先标记,不猜测。
### Step 2迁移审计先输出报告不写代码
输出《迁移审计报告》,包含 7 项:
| 审计项 | 内容 |
|--------|------|
| A. 页面结构 | 主要区域划分header/list/card/footer组件化边界建议 |
| B. CSS 风险点 | 不支持的 CSS 特性清单 + 替代方案(含原因/影响/验收方式) |
| C. 关键样式映射 | Tailwind 类 → computed 值 → WXSS按第五章七维度逐项核对取 computed-styles 精确值) |
| D. 图标处理 | 每个 SVG 的处理决策TDesign / 导出 SVG / Emoji |
| E. 交互映射 | H5 DOM 操作 → setData + 事件绑定映射表 |
| F. 外部依赖 | CDN 资源Tailwind/Google Fonts本地化方案 |
| G. 缺失信息 | 需要用户补充的材料清单 |
### Step 3规则化转换详见第三章
按第三章的标签映射、样式转换、事件转换、路由规则执行。
### Step 4编译验证
| 检查项 | 合格标准 |
|--------|---------|
| WXML 编译 | 无编译错误(特别注意 `.toFixed()` 等 JS 方法不能在 WXML 中使用) |
| WXSS 编译 | 无警告(检查不支持的选择器) |
| 控制台 | 无 JS 运行时错误 |
| 图片加载 | 无 404/500 错误(所有 `/assets/` 引用的文件必须存在) |
| 组件注册 | 无 "component not found" 警告 |
| 路由跳转 | 无 "navigateTo:fail" 错误 |
| TS 类型 | `Page<IData>()` 类型定义完整data 中所有字段有初始值 |
### Step 5结构还原验证必须在像素对比之前完成
> ⚠️ 这是最关键的一步。未迁移页面的结构与 H5 原型差异巨大,
> 如果跳过结构验证直接做像素对比,差异率会高达 20%~80%,完全没有参考价值。
> **结构没对齐之前,禁止进入像素级对比。**
逐项对照 H5 原型截图和交互说明,确认以下结构要素:
| # | 检查项 | 对照源 | 合格标准 | 常见问题 |
|---|--------|--------|---------|---------|
| 1 | 区域划分 | H5 截图 | header/content/footer/tabbar 区域与 H5 一致 | 缺少区域、区域顺序错误 |
| 2 | 元素层级 | H5 源码 | 嵌套层级与 H5 DOM 结构对应 | 多余嵌套或缺少容器 |
| 3 | 列表/卡片数量 | H5 截图 | Mock 数据条数与 H5 一致(便于截图对比) | 数据条数不同导致高度差异 |
| 4 | 文本内容 | H5 源码 | 标题、标签、按钮文案与 H5 完全一致 | 文案遗漏或写错 |
| 5 | 图标完整性 | icon-mapping.md | 所有图标位置有对应实现TDesign/SVG/Emoji | 图标缺失显示空白 |
| 6 | 导航结构 | interactions/*.md | 自定义导航栏/返回按钮/TabBar 行为正确 | 导航栏缺失或返回失效 |
| 7 | 弹窗/浮层 | interactions/*.md | 所有弹窗/下拉/浮层能正常触发和关闭 | 弹窗不弹出或无法关闭 |
| 8 | 滚动行为 | H5 截图 | 长页面可滚动,吸顶元素正确固定 | 内容溢出不可滚动 |
| 9 | 三态占位 | PRD 规范 | loading/empty/error 三种状态有对应 UI 结构 | 缺少状态处理 |
**通过标准:** 以上 9 项全部通过后,才可进入 Step 6 像素级对比。
**未通过处理:** 记录问题 → 修复 → 重新验证,循环直到全部通过。
### Step 6像素级视觉对比详见第五章
> **前置条件Step 5 结构还原验证全部通过。**
> 结构未对齐时的像素对比结果无参考价值,禁止跳过 Step 5。
### Step 7验收签收
| 验收项 | 怎么看 | 合格标准 | 常见失败表现 |
|--------|--------|---------|-------------|
| 布局结构 | 对比截图整体布局 | 区域划分、层级关系一致 | 元素错位、层级混乱 |
| 间距系统 | 对比元素间距 | 与 H5 截图一致±4rpx 容差) | 间距过大/过小 |
| 字体系统 | 对比字号、字重、行高 | 与 design-tokens 一致 | 字号偏差、行高不对 |
| 颜色 | 对比背景色、文字色、边框色 | 与 design-tokens 一致 | 颜色偏差 |
| 圆角 | 对比卡片、按钮圆角 | 与 design-tokens 一致 | 圆角过大/过小 |
| 阴影 | 对比卡片阴影 | 有阴影且不突兀 | 无阴影或阴影过重 |
| 图标 | 对比图标位置、大小、颜色 | TDesign 图标正确显示 | 图标缺失或错位 |
| 交互完整性 | 按交互说明逐项操作 | 所有操作有正确响应 | 点击无反应、状态不切换 |
| 三态处理 | 切换 loading/empty/error | 三种状态均有对应 UI | 缺少空状态或加载态 |
| 安全区 | 刘海屏设备检查 | 内容不被刘海遮挡 | 顶部内容被裁切 |
**未通过返工流程:**
验收未通过时,按问题严重度回退到对应步骤修复:
| 问题类型 | 回退到 | 说明 |
|----------|--------|------|
| 结构错误(区域缺失/层级错乱/元素遗漏) | Step 5 结构还原验证 | 结构问题必须重新走结构验证 |
| 样式偏差(间距/字号/颜色/圆角/阴影) | Step 6 像素级对比 | 用 diff 图定位具体偏差点,补丁式修复 |
| 交互缺陷(点击无响应/状态不切换/弹窗异常) | Step 3 规则化转换 | 对照 interactions/*.md 补全事件绑定 |
| 编译报错(修复过程中引入新错误) | Step 4 编译验证 | 修复后必须重新过编译检查 |
| 真机差异(模拟器正常但真机异常) | Step 6 像素级对比 | 真机截图对比,针对性修复 |
> 每次返工修复后,必须从回退步骤开始重新往下走完所有后续步骤,不可跳步。
> 例如:回退到 Step 5 修复结构问题后,需依次重新通过 Step 5 → Step 6 → Step 7。
---
## 三、转换规则大全
### 3.1 核心缩放公式(强制)
```
最终 rpx = H5 px × 2 × 0.875
结果取偶数(向最近偶数取整)
```
来源iPhone 15 Pro Max 430px 宽 → 小程序 750rpx 基准87.5% = 750/430/2。
先后尝试了非统一缩放、80%、87.5% 三种方案87.5% 在 iPhone 15 Pro Max 上与 H5 原型视觉一致度最高。
### 3.2 标签映射(硬性规则)
| HTML | WXML | 说明 |
|------|------|------|
| `<div>` | `<view>` | 容器 |
| `<span>` / `<p>` | `<text>` | 文本必须用 `<text>` 包裹;`<text>` 内只能嵌套 `<text>` |
| `<img>` | `<image mode="">` | 必须指定 mode 和宽高,默认 320×240 会变形 |
| `<svg>` 内联 | `<image src="xx.svg">``<t-icon>` | 不支持内联 SVG |
| `<button>` | `<t-button>` | TDesign 优先 |
| `<input>` | `<t-input>` | TDesign 优先,事件是 `bind:change` 而非 `bindinput` |
| `<textarea>` | `<t-textarea>` | 同上 |
| `<select>` | `<t-picker>` | 完全不同的交互 |
| `<a>` | `bindtap` + `wx.navigateTo` | 无超链接 |
| `<ul>/<li>` | `<view wx:for>` | 无列表语义标签,必须加 `wx:key` |
| `<h1>`~`<h6>` | `<text>` + 样式类 | 无语义标题标签 |
| `<table>` | `<view>` 手动布局 | 无表格标签 |
| `<section>` | `<view>` | 语义标签统一用 view |
| `<label for="id">` | `<label for="id">` | 支持,但 for 只能绑定 checkbox/radio/switch |
| `<iframe>` | `<web-view>` | 需配置业务域名白名单 |
| scroll 容器 | `<scroll-view>` | 必须设固定高度,否则不滚动 |
| `<block>` | `<block>` | 只是逻辑包裹,不产生真实 DOM需要样式时改用 `<view>` |
**严禁在 WXML 中使用 HTML 标签。**
### 3.3 事件转换
| H5 | 小程序 | 说明 |
|----|--------|------|
| `onclick="fn()"` | `bindtap="fn"` | 不能传参 |
| `onclick="fn(id)"` | `data-id="{{id}}" bindtap="fn"` | dataset 传参 |
| `addEventListener` | 不支持 | 只能声明式绑定 |
| `event.target.value` | `e.detail.value` | 取值路径不同 |
| `event.target.dataset` | `e.currentTarget.dataset` | 注意 currentTarget |
| `event.preventDefault()` | `catchtap` | catch 前缀阻止冒泡 |
| `classList.toggle('active')` | `setData({ active: !this.data.active })` + `class="{{active ? 'on' : ''}}"` | |
| `innerHTML = '...'` | `setData({ content: '...' })` + WXML 数据绑定 | |
| `history.back()` | `wx.navigateBack()` | |
| `window.location.href` | `wx.navigateTo({ url: '...' })` | |
| `localStorage.setItem` | `wx.setStorageSync` | 上限 10MB |
| `alert()` / `confirm()` | `wx.showToast()` / `wx.showModal()` | |
**dataset 命名坑**`data-` 属性名自动转换 — 连字符转驼峰(`data-user-id``dataset.userId`),大写转小写(`data-userId``dataset.userid`)。
### 3.4 路由规则
| 场景 | 小程序 API | 说明 |
|------|-----------|------|
| 普通页面跳转 | `wx.navigateTo` | 保留当前页,页面栈 +1最多 10 层) |
| TabBar 页面跳转 | `wx.switchTab` | 必须用 switchTabnavigateTo 会报错 |
| 替换当前页 | `wx.redirectTo` | 关闭当前页 |
| 清空页面栈 | `wx.reLaunch` | 登录/登出场景 |
| 返回上一页 | `wx.navigateBack` | 页面栈 -1 |
**TabBar 页面task-list、board-finance、my-profile**
路径必须以 `/` 开头,且不带 `.wxml` 后缀。
### 3.5 SVG 导出规则
1. 扫描目标页面 H5 源码中的所有 `<svg>` 标签
2. TDesign 有语义等价图标(如返回箭头 → `chevron-left`)→ 用 `<t-icon>`,不导出
3. 品牌/自定义图标 → 导出为 `apps/miniprogram/miniprogram/assets/icons/<name>.svg`
4. 命名规则:`icon-<用途>.svg`(如 `icon-wechat.svg`Logo 类用 `logo-<名称>.svg`
5. 导出时保留原始 `viewBox``fill``path`
6. 小程序引用:`<image src="/assets/icons/<name>.svg" mode="aspectFit" />`,必须指定宽高
7. TDesign `<t-button icon="xxx">``icon` 属性只支持内置图标名,品牌图标传入无效名称会不显示
**已导出清单**
| 文件名 | 来源/用途 | 说明 |
|--------|---------|------|
| `logo-billiard.svg` | login | 台球 Logo |
| `icon-wechat.svg` | login | 微信品牌图标 |
| `icon-clock-circle.svg` | reviewing | 时钟主图标 |
| `icon-forbidden.svg` | no-permission | 禁止符号主图标 |
| `ai-robot.svg` | ai-float-button | AI 助手机器人图标 |
| `arrow-left.svg` | 通用 | 返回箭头 |
| `chart.svg` | board 系列 | 图表/数据图标 |
| `chat.svg` / `chat-gray.svg` | chat | 对话图标(彩色/灰色) |
| `check-bold.svg` / `check-circle.svg` | 通用 | 勾选图标 |
| `clock.svg` | 通用 | 时钟图标 |
| `forbidden.svg` | 通用 | 禁止图标 |
| `help-circle.svg` | board-finance | 帮助/指标说明图标 |
| `info-circle.svg` / `info-error.svg` / `info-warning.svg` | 通用 | 信息/错误/警告图标 |
| `logout.svg` | my-profile | 登出图标 |
| `task.svg` | task 系列 | 任务图标 |
| `wechat.svg` | 通用 | 微信图标 |
| `tab-*-nav.svg` / `tab-*-nav-active.svg` | board-tab-bar | 底部导航栏 SVG 图标6 个) |
| `tab-*.png` / `tab-*-active.png` | TabBar | 系统 TabBar PNG 图标6 个) |
| `icon-ai-float.png` / `icon-ai-inline.png` | AI 组件 | AI 悬浮按钮/内联图标 |
> 完整文件列表见 `apps/miniprogram/miniprogram/assets/icons/` 目录。
> 每次迁移新页面时,将新导出的图标追加到此清单。
### 3.6 转换执行顺序
```
1. 创建 4 文件骨架(.wxml / .wxss / .ts / .json
2. .json 注册 usingComponentsTDesign + 自定义组件)
3. 转换 WXML标签映射保持层级一致
4. 转换 WXSSTailwind → 手写 WXSS87.5% 缩放)
5. 转换 TSDOM → setData事件 → bindtap
6. Mock 数据(贴近真实 API 格式,标记 TODO
7. 三态处理loading / empty / normal / error
```
### 3.7 状态栏适配(所有 custom 导航页面强制)
```typescript
// TS onLoad
const sysInfo = wx.getSystemInfoSync()
this.setData({ statusBarHeight: sysInfo.statusBarHeight || 20 })
```
```xml
<!-- WXML -->
<view class="page" style="padding-top: {{statusBarHeight}}px;">
```
```css
/* WXSS */
.page { box-sizing: border-box; }
```
禁止使用 `env(safe-area-inset-top)`(部分机型不生效)。
### 3.8 一屏页面布局(不滚动的页面)
```css
.page {
height: 100vh; /* 固定一屏高度,不用 min-height */
display: flex;
flex-direction: column;
box-sizing: border-box; /* padding-top 从 100vh 中扣除 */
overflow: hidden; /* 防止内容溢出产生滚动 */
}
.hero { flex: 1; } /* 主内容区域占满剩余空间 */
.bottom-area { /* 固定高度,不参与 flex 伸缩 */ }
```
如果页面内容可能超过一屏(如表单页),改用 `min-height: 100vh` + 允许滚动。
### 3.9 长页面滚动与吸顶处理
> 详细规范滚动方案选择、sticky 层级、筛选栏隐显、板块吸顶、下拉刷新、结构验证补充检查项)
> 见第十四章「长页面迁移专题」。本节仅列出核心要点。
核心要点:
- 页面级滚动优先,不要用 `scroll-view` 包裹整个页面
- `sticky``scroll-view` 内部不生效(最常见的坑)
- sticky 元素必须设 `background-color`,否则内容穿透
- 多层 sticky 叠加时 `z-index` 从上到下递减20 → 15 → 10
- 底部固定栏用 `position: fixed`,内容区末尾加占位 `<view>` 防遮挡
- 下拉刷新适用页面task-list、board-finance、board-coach、board-customer、notes、chat-history
### 3.10 交互子状态处理规范
> 详细规范弹窗类型与实现方案、z-index 分层策略、遮罩模式、长按菜单、星星评分、
> AI 悬浮按钮、底部固定操作栏、三态处理、表单处理、交互态验证清单)
> 见第十五章「交互弹窗与状态管理专题」。本节仅列出核心要点。
核心要点:
- 遮罩 `bindtap` 关闭弹窗,弹窗内容 `catchtap=""` 阻止穿透
- 底部弹窗必须加 `padding-bottom: env(safe-area-inset-bottom)`
- 弹窗打开时用 `catchtouchmove` 阻止背景滚动
- 同一时刻只允许一个弹窗打开(互斥)
- z-index 分层sticky 10-29 → 悬浮按钮 30 → 底部栏 50 → 导航栏 100 → 遮罩 999 → 弹窗 1000
- 长按菜单用 `bindlongpress`,注意与 `bindtap` 的冲突处理
- 每个页面必须处理 loading / empty / error / normal 四种状态
---
## 四、样式转换详解
### 4.1 Tailwind → WXSS 速查表87.5% 缩放)
| Tailwind | H5 px | ×2 | ×0.875 | 最终 rpx |
|----------|-------|----|--------|----------|
| text-xs (12px) | 12 | 24 | 21 | 22 |
| text-sm (14px) | 14 | 28 | 24.5 | 24 |
| text-base (16px) | 16 | 32 | 28 | 28 |
| text-lg (18px) | 18 | 36 | 31.5 | 32 |
| text-xl (20px) | 20 | 40 | 35 | 36 |
| text-2xl (24px) | 24 | 48 | 42 | 42 |
| gap-2 / p-2 (8px) | 8 | 16 | 14 | 14 |
| gap-3 / p-3 (12px) | 12 | 24 | 21 | 22 |
| gap-4 / p-4 (16px) | 16 | 32 | 28 | 28 |
| gap-5 / p-5 (20px) | 20 | 40 | 35 | 36 |
| gap-6 / p-6 (24px) | 24 | 48 | 42 | 42 |
| gap-8 / p-8 (32px) | 32 | 64 | 56 | 56 |
| rounded-xl (12px) | 12 | 24 | 21 | 24 |
| rounded-2xl (16px) | 16 | 32 | 28 | 32 |
| rounded-3xl (24px) | 24 | 48 | 42 | 48 |
> **圆角例外**A/B 视觉对比验证 ×2 与 ×0.875 差异 <0.02%,圆角统一采用简单 ×2不乘 0.875),数值更整洁且与 `app.wxss` CSS 变量一致。
| w-10 (40px) | 40 | 80 | 70 | 70 |
| w-14 (56px) | 56 | 112 | 98 | 98 |
| w-24 (96px) | 96 | 192 | 168 | 168 |
| w-28 (112px) | 112 | 224 | 196 | 196 |
### 4.2 Tailwind 类 → WXSS 属性映射
| Tailwind | WXSS |
|----------|------|
| `p-4` | `padding: 28rpx;` |
| `px-4` | `padding-left: 28rpx; padding-right: 28rpx;` |
| `m-3` | `margin: 22rpx;` |
| `gap-3` | `gap: 22rpx;` |
| `space-y-3` | 子元素 `margin-top: 22rpx;`(首个除外,用 `.child + .child` 选择器) |
| `flex` | `display: flex;` |
| `flex-col` | `flex-direction: column;` |
| `items-center` | `align-items: center;` |
| `justify-between` | `justify-content: space-between;` |
| `flex-1` | `flex: 1;` |
| `min-h-screen` | `min-height: 100vh;` |
| `sticky top-0` | `position: sticky; top: 0;` |
| `z-10` | `z-index: 10;` |
| `font-medium` | `font-weight: 500;` |
| `font-semibold` | `font-weight: 600;` |
| `leading-relaxed` | `line-height: 1.625;` |
| `shadow-sm` | `box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);` |
| `shadow-lg` | `box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.06);` |
| `shadow-xl` | `box-shadow: 0 16rpx 48rpx rgba(0,0,0,0.08);` |
| `bg-white` | `background-color: #ffffff;` |
| `bg-gray-1` | `background-color: #f3f3f3;` |
| `bg-white/60` | `background: rgba(255,255,255,0.6);` |
| `text-gray-13` | `color: #242424;` |
| `text-gray-6` | `color: #a6a6a6;` |
| `border-b border-gray-2` | `border-bottom: 2rpx solid #eeeeee;` |
| `backdrop-blur-sm` | ❌ 不支持,改为 `rgba()` 半透明 |
### 4.3 颜色速查design-tokens.json
| Token | Hex | 用途 |
|-------|-----|------|
| primary | #0052d9 | 主色、active 态 |
| primary-light | #ecf2fe | 主色浅底 |
| success | #00a870 | 成功/助教标签 |
| warning | #ed7b2f | 警告 |
| error | #e34d59 | 错误/跟 badge |
| gray-1 | #f3f3f3 | 页面背景 |
| gray-2 | #eeeeee | 分隔线 |
| gray-3 | #e7e7e7 | |
| gray-4 | #dcdcdc | 禁用态 |
| gray-5 | #c5c5c5 | |
| gray-6 | #a6a6a6 | 次要文字 |
| gray-7 | #8b8b8b | TabBar 默认色 |
| gray-8 | #777777 | |
| gray-9 | #5e5e5e | |
| gray-10 | #4b4b4b | |
| gray-11 | #393939 | |
| gray-12 | #2c2c2c | |
| gray-13 | #242424 | 主文字 |
### 4.4 不支持的 CSS 替代方案
| CSS 特性 | 小程序支持 | 替代方案 |
|----------|-----------|---------|
| `backdrop-filter: blur()` | ❌ | `background: rgba(255,255,255,0.95);` |
| `*` 通配符选择器 | ❌ | 逐个元素设置Tailwind 的 `*` reset 全部失效) |
| `filter: blur()` | ❌ | 用 `radial-gradient` 模拟扩散效果 |
| `::before` / `::after` | ⚠️ 部分支持 | 复杂场景用额外 `<view>` 元素模拟 |
| 远程 `@font-face` | ⚠️ | `wx.loadFontFace()` 或系统字体 |
| `clip-path` | ❌ | 改为图片或 CSS 渐变近似 |
| CSS 变量 `var()` | ✅ | TDesign 大量使用,可直接迁移 |
| `linear-gradient` | ✅ | 正常使用 |
| `animation` / `@keyframes` | ✅ | 正常使用 |
| `transition` | ✅ | 正常使用 |
| `env(safe-area-inset-*)` | ✅ | 刘海屏适配可用(但 top 建议用 JS |
| `url("data:image/svg+xml,...")` | ❌ | 用 CSS 渐变模拟或导出为 PNG/base64 |
| `position: fixed` | ⚠️ 部分场景异常 | 用 `position: sticky` 或组件自带吸顶 |
| `gap` (flexbox) | ⚠️ 基础库 2.30+ | 低版本用 margin |
### 4.5 样式作用域
- `app.wxss` = 全局样式
- 页面 `.wxss` = 仅当前页面生效(自动隔离)
- 组件 `.wxss` = 默认隔离(`styleIsolation: 'isolated'`
- H5 的全局 CSS reset`* { box-sizing: border-box; }`)在小程序中无效,需逐个元素设置
---
## 五、样式精调执行规范(像素级对齐核心)
> 第四章提供了速查表和映射工具,本章定义的是"怎么用这些工具写出精确的 WXSS"。
> AI 在写每个 WXML 元素的 WXSS 时,必须按本章清单逐项确认,不可凭感觉估值。
### 5.1 数据源优先级(强制)
取值时按以下优先级,高优先级覆盖低优先级:
```
1. computed-styles.json 中该元素的精确 px 值(最高优先级)
→ 直接用公式换算rpx = px × 2 × 0.875,取偶数
2. H5 源码中的 Tailwind 类名
→ 查 4.1 速查表换算
3. design-tokens.json 中的 Token 值
→ 颜色、圆角、阴影直接引用
4. H5 截图目测估算(最低优先级,仅在以上三者均缺失时使用)
→ 必须标注 /* 目测值,待校准 */
```
**禁止**:不查任何数据源,直接写一个"看起来差不多"的值。
#### 5.2 七维度逐项核对清单
每个可见元素(文字、卡片、按钮、图标、分隔线等)写 WXSS 时,必须逐一确认:
| # | 维度 | 必须确认的属性 | 常见错误 | 正确做法 |
|---|------|---------------|---------|---------|
| 1 | **字号** | `font-size` | 凭感觉写 28rpx | 查 computed-styles 或 Tailwind 类,用公式换算 |
| 2 | **字重** | `font-weight` | 遗漏 semibold/medium | `font-medium` → 500`font-semibold` → 600`font-bold` → 700 |
| 3 | **文字颜色** | `color` | 用近似灰色 | 严格使用 design-tokens 色号(见下方色阶表) |
| 4 | **行高** | `line-height` | 遗漏或用错单位 | Tailwind 比例值 → 换算为具体值(见下方行高表) |
| 5 | **内间距** | `padding`(上右下左) | 四个方向不分别确认 | 逐方向查值,上下和左右通常不同 |
| 6 | **外间距** | `margin``gap` | 兄弟元素间距遗漏 | 优先用父容器 `gap`,其次用 `margin-top`(首个除外) |
| 7 | **边框与圆角** | `border``border-radius` | 边框粗细或颜色错误 | 边框最小 2rpx1rpx 部分设备不显示),颜色查 tokens |
#### 5.3 文字颜色层级体系(强制绑定)
| 用途 | Token | Hex | 典型场景 |
|------|-------|-----|---------|
| 主文字 | gray-13 | `#242424` | 标题、姓名、金额数值 |
| 次要文字 | gray-9 | `#5e5e5e` | 副标题、说明文字 |
| 辅助文字 | gray-6 | `#a6a6a6` | 时间戳、占位符、标签 |
| 禁用文字 | gray-4 | `#dcdcdc` | 不可点击的按钮文字 |
| 强调文字 | primary | `#0052d9` | 链接、active 态、关键操作 |
| 成功文字 | success | `#00a870` | 正向指标、完成状态 |
| 警告文字 | warning | `#ed7b2f` | 待处理、注意事项 |
| 错误文字 | error | `#e34d59` | 错误提示、紧急标签 |
**禁止**:使用不在此表中的灰色值(如 `#333``#666``#999`)。所有灰色必须从 gray-1 到 gray-13 中选取。
#### 5.4 行高换算规则
Tailwind 的 `leading-*` 是比例值,不是 px。换算方式`line-height = font-size × 比例`
| Tailwind 类 | 比例值 | 搭配 text-sm (24rpx) | 搭配 text-base (28rpx) | 搭配 text-lg (32rpx) |
|-------------|--------|---------------------|----------------------|---------------------|
| `leading-none` | 1.0 | 24rpx | 28rpx | 32rpx |
| `leading-tight` | 1.25 | 30rpx | 36rpx | 40rpx |
| `leading-snug` | 1.375 | 34rpx | 38rpx | 44rpx |
| `leading-normal` | 1.5 | 36rpx | 42rpx | 48rpx |
| `leading-relaxed` | 1.625 | 40rpx | 46rpx | 52rpx |
| `leading-loose` | 2.0 | 48rpx | 56rpx | 64rpx |
**默认行高**:如果 H5 源码没有显式指定 `leading-*`Tailwind 默认 `leading-normal`1.5)。小程序中必须显式写出 `line-height`,不能依赖浏览器默认值(小程序默认行高与浏览器不同)。
#### 5.5 高频元素标准间距参考
以下是项目中高频出现的元素间距标准值(从已完成页面提取的实际值):
| 元素类型 | 属性 | 标准值 | 说明 |
|----------|------|--------|------|
| 页面容器 | `padding` | `0 28rpx` | 左右 28rpx上下由内容决定 |
| 卡片容器 | `padding` | `28rpx` | 四方向统一 |
| 卡片容器 | `border-radius` | `24rpx` | rounded-xl12px × 2 |
| 卡片容器 | `margin-bottom` | `24rpx` | 卡片间距12px × 2 |
| 卡片容器 | `box-shadow` | `0 2rpx 8rpx rgba(0,0,0,0.05)` | shadow-sm |
| 列表项 | `padding` | `24rpx 28rpx` | 上下 24rpx左右 28rpx |
| 列表项分隔线 | `border-bottom` | `2rpx solid #eeeeee` | gray-2 |
| 标题文字 | `font-size` + `font-weight` | `32rpx` + `600` | text-base + semibold |
| 正文文字 | `font-size` + `font-weight` | `28rpx` + `400` | text-sm + normal |
| 辅助文字 | `font-size` + `color` | `24rpx` + `#a6a6a6` | text-xs + gray-6 |
| 金额数值 | `font-size` + `font-weight` | `36rpx` + `600` | text-lg + semibold |
| 标签tag | `padding` | `4rpx 14rpx` | 紧凑内间距 |
| 标签tag | `font-size` + `border-radius` | `22rpx` + `8rpx` | 小字 + 小圆角 |
| 按钮 | `height` + `border-radius` | `80rpx` + `48rpx` | 大按钮rounded-3xl: 24px × 2 |
| 图标 | `width` + `height` | `36rpx` × `36rpx` | 行内小图标 |
| 元素间纵向间距 | `gap``margin-top` | `14rpx`(紧凑)/ `22rpx`(标准)/ `28rpx`(宽松) | 三档间距 |
#### 5.6 典型错误案例(踩坑记录)
| # | 错误描述 | 视觉表现 | 根因 | 正确做法 |
|---|---------|---------|------|---------|
| 1 | 卡片 padding 写 `24rpx` 而非 `28rpx` | 卡片内容偏挤,与 H5 对比左右差 4rpx | 没查 computed-styles凭感觉写了 p-3 的值 | 查 H5 源码确认是 `p-4`16px → 28rpx |
| 2 | 文字颜色用 `#666666` | 灰度偏深,与 H5 不一致 | 没用 design-tokens 色号 | 查色阶表,次要文字用 `#5e5e5e`gray-9 |
| 3 | 行高遗漏 | 多行文字间距偏窄,整体显得拥挤 | 没写 `line-height`,小程序默认值比浏览器小 | 显式写 `line-height: 42rpx`text-sm + leading-normal |
| 4 | 字重写 `bold` 而非 `600` | 文字过粗 | `bold` = 700H5 用的是 `font-semibold` = 600 | 查 Tailwind 类确认字重等级 |
| 5 | 兄弟元素间距用 `margin-bottom` | 最后一个元素多出底部空白 | 应该用 `gap``.item + .item { margin-top }` | 父容器 `gap: 22rpx``.item + .item` 选择器 |
| 6 | 边框写 `1rpx` | 部分安卓设备边框不显示 | 1rpx 在低 DPR 设备上被四舍五入为 0 | 边框最小用 `2rpx` |
| 7 | 圆角写 `16rpx` 而非 `24rpx` | 卡片圆角偏小 | 混淆了 `rounded-lg`8px→16rpx`rounded-xl`12px→24rpx | 查 4.1 速查表确认 Tailwind 类对应值 |
#### 5.7 每个元素的写码流程(强制)
```
写一个元素的 WXSS 时,按以下顺序执行:
1. 确认元素在 H5 中的 Tailwind 类名(或 computed-styles 值)
2. 逐一换算 7 个维度:
□ font-size → 查速查表或公式换算
□ font-weight → 查 Tailwind 类medium/semibold/bold
□ color → 查色阶表,禁止用非标准灰色
□ line-height → 查行高表,必须显式写出
□ padding → 四方向分别确认
□ margin/gap → 与兄弟元素的间距
□ border/radius → 边框最小 2rpx圆角查速查表
3. 写入 WXSS
4. 自检:与 H5 源码逐属性对比,确认无遗漏
```
**不适用此流程的元素**:纯布局容器(只有 `display: flex` 等布局属性、无视觉表现的 view
---
## 六、像素级视觉对比工具链
> **核心思想**`computed-styles.json` 中提取的精确 px 值,经 `rpx = px × 2 × 0.875` 换算后,
> 在浏览器和小程序中理论上应产生相同的视觉效果。但由于两个平台的渲染引擎差异
> (字体 hinting、亚像素抗锯齿、flex 布局舍入策略、行高默认值等),
> 相同的数值会产生 1-3rpx 级别的渲染偏差。
>
> 当页面结构已完成迁移并通过 Step 5 验证后,像素级对比的唯一目的就是:
> **系统性地发现并消除这些"理论一致但实际不一致"的渲染偏差**
> 将 gap 收敛到人眼不可察觉的程度(前半屏差异率 < 5%)。
>
> 换言之,结构对齐是前提,像素对比只解决最后一公里的精度问题。
### 6.1 前置准备
#### H5 截图(一次性)
1. 用户启动 Go LiveVS Code 插件),端口 5500
2. Playwright MCP 导航到 `http://127.0.0.1:5500/docs/h5_ui/pages/<page>.html`
3. 设置视口 `430×752`(与 MP windowHeight 统一DPR=3
4. 逐段截图 → `docs/h5_ui/screenshots/h5-<page>--seg-<i>.png`(每张 1290×2256
5. 交互态截图(下拉、弹窗、面板等)→ `<page>--<state>.png`
#### 微信开发者工具连接
```bash
# 用户手动启动自动化端口
& "C:\dev\WechatDevtools\cli.bat" auto --project "C:\NeoZQYY\apps\miniprogram" --auto-port 9420
```
连接规范:
- 只能用 `wsEndpoint` 策略,`ws://127.0.0.1:9420`
- 禁止 auto/launch/connect/discover 策略
- 导航到 tabbar 页面必须用 `relaunch`,路径前加 `/`
### 6.2 DPR 换算关系
| 平台 | 视口(逻辑) | DPR | 截图尺寸(物理) |
|------|-------------|-----|-----------------|
| H5 截图 | 430×752 | 3 | 1290×2256 |
| MP 截图 | 430×752 | 1.5 | 645×1128 |
统一对比尺寸1290×2256MP ×2 缩放)。
两端视口高度统一为 752pxMP windowHeight截图自然 1:1 对齐,无需裁剪。
### 6.3 对比流程
长页面和短页面使用不同的对比策略:
#### 6.3.1 短页面(一屏内)— 全页面对比
```
Step 1: 截图
mcp_weixin_devtools_mcp_relaunch → /pages/<page>/<page>
mcp_weixin_devtools_mcp_waitFor → 2000ms
mcp_weixin_devtools_mcp_screenshot → mp-<page>.png
Step 2: 尺寸统一
python scripts/ops/resize_and_compare_v2.py
→ H5 保持 1290px 宽
→ MP 截图 ×2 缩放到 1290px
→ H5 裁剪到 MP 逻辑高度对应的物理高度
→ 输出 cmp-h5.png + cmp-mp.png同尺寸
Step 3: 像素对比
mcp_image_compare_compare_images
→ image1: cmp-h5.png, image2: cmp-mp.png
→ threshold: 0.1
→ 输出 diff 图 + 差异百分比
Step 4: wxss 微调 → 回到 Step 1 循环
```
#### 6.3.2 长页面 — v2 逐段截图对比anchor_compare.py
长页面(超过一屏)使用 `scripts/ops/anchor_compare.py` 进行逐段截图对比。
核心思路:两端都按 section 锚点逐段滚动 + 单屏截图。
两端视口高度统一为 430×752MP windowHeight截图自然 1:1 对齐。
DPR 换算:
- H5: viewport 430×752, DPR=3 → 每张截图 1290×2256
- MP: viewport 430×752, DPR=1.5 → 每张截图 645×1128 → ×2 缩放到 1290×2256
两端截图从顶部自然对齐:
- H5: safe-area-top(~46px) + filterBar(70px) ≈ 116px sticky 区域
- MP: board-tabs(45px) + filter-bar(70px) = 115px sticky 区域
- 差异 ~1px 可忽略,无需裁剪顶部
```
Step 1: 提取 H5 锚点坐标 + 逐段截图
python scripts/ops/anchor_compare.py extract-h5 <page>
→ 需要 Go Live 运行在 5500 端口
→ Playwright 视口 430×752, DPR=3, headless=True
→ 逐个锚点滚动scrollTo = anchor_top - stickyHeight+ 单屏截图
→ 输出 docs/h5_ui/anchors/<page>.json锚点坐标 + segments 数组)
→ 输出 docs/h5_ui/screenshots/h5-<page>--seg-<i>.png每张 1290×2256
Step 2: 生成 MP 截图指令
python scripts/ops/anchor_compare.py mp-inst <page>
→ 输出 docs/h5_ui/anchors/<page>-mp-instructions.json
→ 包含每段的 scroll_into_view_id、等待时间、截图文件名
Step 3: AI 按指令执行 MP 截图(通过 MCP 工具手动执行)
→ scroll-view 页面使用 scroll_into_view 模式:
1. evaluate_script: page.setData({ scrollIntoView: '' }) // 先清空
2. evaluate_script: page.setData({ scrollIntoView: '<id>' }) // 设目标
3. waitFor: delay 1000ms
4. screenshot: mp-<page>--seg-<i>.png645×1128
→ page_scroll 页面使用 wx.pageScrollTo({scrollTop})
Step 4: 逐段对比
python scripts/ops/anchor_compare.py compare <page>
→ H5 截图直接使用(已是 1290×2256
→ MP 截图 ×2 缩放到 1290×2256
→ 输出 seg-h5-<page>-<i>.png + seg-mp-<page>-<i>.png
Step 5: 像素对比
mcp_image_compare_compare_images
→ 逐段对比 seg-h5 vs seg-mp
→ 输出 diff-<page>-<i>.png + 差异百分比
Step 6: wxss 微调 → 回到 Step 3 循环
```
已知限制:
- `sys.stdout = io.TextIOWrapper(...)` 与 Playwright asyncio 冲突,已修复为延迟到 `main()` 入口调用
- MP 截图需通过 MCP 工具手动执行,无法全自动化
### 6.4 差异阈值
| 差异% | 评价 | 行动 |
|-------|------|------|
| < 5% | 优秀 | 字体渲染级差异,可接受 |
| 5-10% | 良好 | 检查是否有结构性差异,字号字体边距行距等 |
| 10-15% | 需调整 | 定位差异区域,微调间距,字号字体边距行距等 |
| > 15% | 较大 | 可能有布局错误,需逐元素排查,字号字体边距行距等 |
注意底部区域MP 只截一屏)的差异是结构性的,不算样式问题。评估时应关注前半屏的差异。
### 6.5 交互态验证
| 交互态 | 触发方式 | 截图命名 |
|--------|---------|---------|
| 筛选下拉 | 点击 filter-dropdown | `<page>--filter-dropdown.png` |
| 指标弹窗 | 点击 help-icon | `<page>--tip-modal.png` |
| 目录导航 | 点击 toc-btn | `<page>--toc-panel.png` |
交互态对比更多是视觉检查(弹窗位置、遮罩透明度),不需要像默认态那样精确到像素。
### 6.6 每轮修复原则
- 只输出需要改动的文件和改动段落diff 风格),不整文件重贴
- 优先使用 flex/盒模型的确定性方案,不用"碰运气"的魔法数
- rpx 换算统一,严禁同一类间距混用 rpx 和 px
- 每轮控制 2-5 处修改,避免一次改太多难以定位效果
- 每次修复后重新编译验证,防止修 A 坏 B
### 6.7 差异收敛规律
- 第一轮调整通常能降 3-5 个百分点(修复明显的间距错误)
- 第二轮再降 2-3 个百分点(精细间距对齐)
- 低于 10% 后继续调整收益递减(剩余差异多为字体渲染和平台差异)
- 前半屏 < 5% 即可视为达标
### 6.8 工具脚本
| 脚本 | 路径 | 功能 |
|------|------|------|
| anchor_compare.py | `scripts/ops/` | 锚点分区对齐的长页面像素级对比(推荐) |
| resize_and_compare_v2.py | `scripts/ops/` | 短页面全页面统一尺寸 + 裁剪 |
| analyze_diff.py | `scripts/ops/` | 按条带分析差异分布 |
| screenshot_h5_pages.py | `scripts/ops/` | H5 页面全页面截图Playwright |
anchor_compare.py 子命令:
| 子命令 | 说明 |
|--------|------|
| `extract-h5 <page>` | 提取 H5 锚点坐标 + 逐段截图(视口 430×752, DPR=3输出 `docs/h5_ui/anchors/<page>.json` |
| `mp-inst <page>` | 生成 MP 截图指令,输出 `docs/h5_ui/anchors/<page>-mp-instructions.json` |
| `compare <page>` | MP ×2 缩放 + 逐段配对,输出 `seg-h5-<page>-<i>.png` + `seg-mp-<page>-<i>.png` |
| `full <page>` | 一键执行 extract-h5 + mp-inst + compareMP 截图仍需手动) |
| `list` | 列出所有已配置锚点的页面 |
---
## 七、高频踩坑完整清单
### 7.1 结构层
| # | 坑 | 说明 | 解决方案 |
|---|-----|------|---------|
| 1 | 内联 SVG 不支持 | WXML 不能写 `<svg>` 标签 | 提取为 `.svg` 文件,用 `<image>``<t-icon>` |
| 2 | 没有 DOM API | `document.getElementById` 等全部不可用 | 用 `this.setData()` 驱动视图更新 |
| 3 | `<text>` 内只能嵌套 `<text>` | 不能在 `<text>` 内放 `<view>` | 需要块级布局时外层用 `<view>` |
| 4 | `checked="false"` 是 true | 字符串 `"false"` 是 truthy | 必须写 `checked="{{false}}"` |
| 5 | `wx:key` 必须提供 | 列表渲染不加 key 会警告且性能差 | `wx:key="id"``wx:key="*this"` |
| 6 | `<block>` 不渲染 DOM | 只是逻辑包裹,不产生真实节点 | 需要样式时改用 `<view>` |
### 7.2 样式层
| # | 坑 | 说明 | 解决方案 |
|---|-----|------|---------|
| 7 | `*` 选择器无效 | 全局 reset 失效 | 逐个元素设置 `box-sizing` |
| 8 | `backdrop-filter` 不支持 | 毛玻璃效果无法实现 | 用半透明背景色 `rgba()` 近似 |
| 9 | `image` 默认 320×240 | 不设宽高会变形 | 始终指定 `width`/`height` + `mode` |
| 10 | rpx 小数精度 | 1rpx 在某些设备上不显示 | 边框最小用 2rpx |
| 11 | 组件样式隔离 | 页面样式穿不进自定义组件 | 用外部样式类或 `styleIsolation: 'shared'` |
| 12 | `!important` 滥用 | TDesign 组件内部样式优先级高 | 优先用 CSS 变量覆盖 |
### 7.3 逻辑层
| # | 坑 | 说明 | 解决方案 |
|---|-----|------|---------|
| 13 | `setData` 性能 | 大数据量传输卡顿 | 只传变化字段:`'list[2].name': 'new'` |
| 14 | 没有 Cookie | 登录态不能靠 Cookie | Storage + header token |
| 15 | `eval()` 不可用 | 动态代码执行被禁止 | 预编译逻辑 |
| 16 | 页面栈 10 层限制 | `navigateTo` 超过 10 层静默失败 | 合理使用 `redirectTo` / `reLaunch` |
| 17 | `alert()` 不存在 | 没有浏览器弹窗 API | `wx.showToast()` / `wx.showModal()` |
| 18 | `window` / `document` 不存在 | 所有 Web API 不可用 | 用 `wx.*` API 替代 |
### 7.4 TDesign 相关
| # | 坑 | 说明 | 解决方案 |
|---|-----|------|---------|
| 19 | `style: v2` 冲突 | app.json 中的 `"style": "v2"` 导致样式错乱 | 删除该配置 |
| 20 | npm 构建遗忘 | 安装新包后忘记构建 npm | 每次 `npm install` 后在开发者工具中"构建 npm" |
| 21 | 事件名差异 | TDesign 用 `bind:change`,原生用 `bindinput` | 查阅组件文档确认事件名 |
| 22 | 外部样式类命名 | `t-class` / `t-class-input` 等各组件不同 | 查阅组件文档的 External Classes |
### 7.5 实战踩坑记录(按发现时间倒序)
| # | 坑 | 触发页面 | 解决方案 |
|---|-----|---------|---------|
| P1 | WXML 中不能调用 JS 方法(`.toFixed()` 等) | 所有页面 | 创建 WXS 模块 `utils/format.wxs` |
| P2 | TabBar 页面不能用 navigateTo | task-list 等 | 跳转前判断目标是否 TabBar 页面 |
| P3 | 图片 500 错误(资源文件不存在) | 所有引用图片的页面 | CSS 渐变/emoji/t-icon 替代不存在的图片 |
| P4 | `env(safe-area-inset-top)` 部分机型不生效 | 所有 custom 导航页面 | JS 获取 statusBarHeight |
| P5 | statusBarHeight padding 导致一屏页面底部溢出 | login 等一屏页面 | `height: 100vh` + `box-sizing: border-box` |
| P6 | TDesign Button icon 不支持自定义图标 | login | 原生 view + image 组合按钮 |
| P7 | TDesign Button 默认样式覆盖自定义 WXSS | login | 高度定制时用原生 view |
| P8 | WXSS 不支持 `::before`/`::after` 伪元素 | reviewing | 用实际 `<view>` 替代 |
| P9 | WXSS 不支持 `url("data:image/svg+xml,...")` | reviewing | 用 `repeating-linear-gradient` 模拟 |
| P10 | 不支持 `filter: blur()` | reviewing | 用更大尺寸 `radial-gradient` 模拟 |
| P11 | `max-w-sm` 机械换算后过宽 | reviewing | 实测调整,不机械换算 |
| P12 | Tailwind 颜色变体需逐一核对 | reviewing | 查 Tailwind 官方色板取精确 hex |
---
## 八、共享组件规范
### 8.1 AI 悬浮按钮ai-float-button
- 组件路径:`components/ai-float-button/`
- 机器人 SVG icon + 渐变流动动画背景
- 默认 bottom 220rpx在自定义底部导航栏上方
### 8.2 自定义底部导航栏board-tab-bar
- 组件路径:`components/board-tab-bar/`
- 用于非 TabBar 的看板子页面board-coach、board-customer
- SVG icon 从 H5 原型提取,路径 `/assets/icons/tab-*-nav*.svg`
| 属性 | 值 |
|------|-----|
| 高度 | 100rpx |
| 背景 | #ffffff |
| 边框 | 1rpx solid #eeeeee |
| icon 尺寸 | 44rpx × 44rpx |
| label 字号 | 20rpx |
| label 颜色 | #8b8b8b(默认)/ #0052d9active |
| active 字重 | 500 |
| gapicon↔label | 4rpx |
| safe-area | padding-bottom: env(safe-area-inset-bottom) |
### 8.3 筛选下拉组件filter-dropdown
- 组件路径:`components/filter-dropdown/`
- 全屏宽度面板 + 半透明遮罩 + 动态 top 计算
触发按钮样式:
| 属性 | 值 |
|------|-----|
| padding | 16rpx 20rpx |
| 背景 | #ffffff |
| 边框 | 2rpx solid var(--color-gray-1) |
| 圆角 | var(--radius-md) |
| label 字号 | 24rpx |
| label 字重 | 600 |
| active 边框色 | var(--color-primary) |
| active 背景 | var(--color-primary-light) |
下拉面板样式:
| 属性 | 值 |
|------|-----|
| 定位 | fixed, left:0, right:0 |
| 最大高度 | 60vh |
| 圆角 | 0 0 28rpx 28rpx |
| 阴影 | 0 16rpx 48rpx rgba(0,0,0,0.15) |
| 选项 padding | 34rpx 32rpx |
| 选项字号 | 28rpx |
| 分隔线 | 1rpx solid rgba(0,0,0,0.03) |
| active 颜色 | var(--color-primary) |
| active 字重 | 500 |
| 遮罩背景 | rgba(0,0,0,0.5) |
| z-index | 999遮罩/ 1000面板 |
### 8.4 顶部看板 Tab 栏
- 直接写在页面 wxml 中(非独立组件)
- sticky top: 0, z-index: 20
| 属性 | 值 |
|------|-----|
| 背景 | #ffffff |
| 边框 | 2rpx solid #eeeeee |
| tab padding | 24rpx 0 |
| tab 字号 | 26rpx |
| tab 字重 | 500默认/ 600active |
| tab 颜色 | #8b8b8b(默认)/ #0052d9active |
| 下划线宽 | 42rpx |
| 下划线高 | 5rpx |
| 下划线渐变 | linear-gradient(90deg, #0052d9, #5b9cf8) |
### 8.5 筛选栏容器
- sticky top: 70rpx, z-index: 15
- 滚动隐藏/显示220ms ease 过渡)
| 属性 | 值 |
|------|-----|
| 背景 | #f3f3f3 |
| padding | 14rpx 28rpx |
| 内框背景 | #ffffff |
| 内框圆角 | 14rpx |
| 内框 padding | 10rpx |
| 内框 gap | 14rpx |
| 内框边框 | 2rpx solid #eeeeee |
| 第一个筛选项 | flex: 1.8 |
| 其他筛选项 | flex: 1 |
### 8.6 heart-icon 组件
- 组件路径:`components/heart-icon/`
- 用 TS `observers` 监听 `score` 属性变化,计算对应 emoji 字符串
- WXS 不支持 emoji surrogate pair必须用 TS 计算
- 样式:`font-size: 22rpx; line-height: 1; position: relative; top: -4rpx`
### 8.7 其他组件
| 组件 | 路径 | 说明 |
|------|------|------|
| hobby-tag | `components/hobby-tag/` | 客户爱好标签board-customer 使用 |
| metric-card | `components/metric-card/` | 指标卡片board 系列页面使用 |
| note-modal | `components/note-modal/` | 备注弹窗task-detail/coach-detail 等使用 |
| star-rating | `components/star-rating/` | 星星评分board-coach 使用 |
| dev-fab | `components/dev-fab/` | 开发调试悬浮按钮,所有页面(全局注册于 app.json |
---
## 九、TDesign 组件使用规范
### 9.1 常见替代关系
| H5 原型元素 | TDesign 组件 | 注意事项 |
|-------------|-------------|---------|
| `<button>` | `<t-button>` | 用 CSS 变量定制样式 |
| `<input>` | `<t-input>` | 事件是 `bind:change` 而非 `bindinput` |
| `<textarea>` | `<t-textarea>` | 同上 |
| `<select>` | `<t-picker>` | 完全不同的交互 |
| `<checkbox>` | `<t-checkbox>` | 或手动实现 |
| `<radio>` | `<t-radio>` / `<t-radio-group>` | `bind:change` 取值 |
| SVG 图标 | `<t-icon name="xxx">` | TDesign 内置图标库 |
| 加载动画 | `<t-loading>` | 替代 CSS spinner |
| 弹窗 | `<t-dialog>` / `<t-toast>` | 替代 `alert()` / `confirm()` |
### 9.2 样式覆盖 4 种方式(优先级从高到低)
1. CSS 变量(推荐):`--td-button-large-height: 96rpx`
2. 外部样式类:`t-class="my-class"` + `.my-class { ... !important }`
3. 解除隔离TDesign 已开启 `addGlobalClass`,页面样式可直接覆盖
4. style 属性:`style="background: #f5f5f5;"`
原则TDesign 组件适合"接近默认样式"的场景;与原型差异大时,原生 view 实现更可控。
---
## 十、认证联调指南
### 10.1 环境准备
后端:
```env
# .env.local
WX_DEV_MODE=true # 开启开发模式,跳过真实微信 code2Session
JWT_SECRET_KEY=dev-secret
```
```bash
cd apps/backend && uvicorn app.main:app --reload # 监听 http://localhost:8000
```
小程序开发者工具设置:
- 勾选「不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书」
- `utils/request.ts``BASE_URL` 指向 `http://localhost:8000`
### 10.2 Mock 登录
```bash
# 默认创建 pending 状态用户
curl -X POST http://localhost:8000/api/xcx/dev-login \
-H "Content-Type: application/json" \
-d '{"openid": "test_user_001"}'
# 指定用户状态
curl -X POST http://localhost:8000/api/xcx/dev-login \
-H "Content-Type: application/json" \
-d '{"openid": "test_user_001", "status": "approved"}'
```
### 10.3 API 端点汇总
小程序端:
| 端点 | 方法 | 说明 |
|------|------|------|
| `/api/xcx/login` | POST | 微信登录 |
| `/api/xcx/dev-login` | POST | Mock 登录(仅 WX_DEV_MODE=true |
| `/api/xcx/apply` | POST | 提交申请 |
| `/api/xcx/me` | GET | 查询用户状态 |
| `/api/xcx/me/sites` | GET | 查询关联店铺 |
| `/api/xcx/switch-site` | POST | 切换店铺 |
| `/api/xcx/refresh` | POST | 刷新令牌 |
管理端:
| 端点 | 方法 | 说明 |
|------|------|------|
| `/api/admin/applications` | GET | 查询申请列表 |
| `/api/admin/applications/{id}` | GET | 申请详情 + 候选匹配 |
| `/api/admin/applications/{id}/approve` | POST | 批准申请 |
| `/api/admin/applications/{id}/reject` | POST | 拒绝申请 |
### 10.4 常见问题
| 错误 | 原因 | 排查 |
|------|------|------|
| 401 | token 过期或无效 | 检查 Storage 中 access_token用 dev-login 重新获取 |
| 403 | 用户 disabled 或权限不足 | `GET /api/xcx/me` 检查 status |
| 409 | 重复提交申请 | 查看现有申请状态 |
| 422 | 表单格式错误 | site_code 格式 2字母+3数字phone 11位 |
| 网络失败 | 后端未启动或域名校验 | 访问 `http://localhost:8000/docs` 确认 |
---
## 十一、迁移进度追踪
> 数据来源2026-03-08 像素级对比结果。
> 对比方法H5 截图DPR=3, 1290px 宽vs MP 截图DPR=1.5, 645px → ×2 缩放至 1290px裁剪到相同高度后 pixelmatch 对比。
> 截图目录:`docs/h5_ui/screenshots/`
### 11.1 页面进度总览
#### 已有历史实现(需重新走完整流程验收)
| 页面 | 小程序路径 | 历史状态 | 默认态差异% | 备注 |
|------|-----------|----------|------------|------|
| board-coach | `pages/board-coach/` | ✅ 已完成 | 7.04% | 4 维度卡片 + 3 种筛选下拉 |
| board-customer | `pages/board-customer/` | ✅ 已完成 | 7.51% | 8 维度卡片 + heart-icon + 最专一表格 |
| board-finance | `pages/board-finance/` | 🔧 迁移中 | 9.04% | 6 板块完整重写 |
#### 待迁移14 页)
| 页面 | 小程序路径 | H5 原型 | 交互说明 | 优先级 | 默认态差异% |
|------|-----------|---------|----------|--------|------------|
| task-list | `pages/task-list/` | ✅ | ✅ | 高 | 43.78% |
| my-profile | `pages/my-profile/` | ✅ | ✅ | 高 | 3.61% |
| task-detail | `pages/task-detail/` | ✅ | ✅ | 高 | 20.66% |
| task-detail-callback | `pages/task-detail-callback/` | ✅ | ✅ | 低 | 23.69% |
| task-detail-priority | `pages/task-detail-priority/` | ✅ | ✅ | 低 | 23.05% |
| task-detail-relationship | `pages/task-detail-relationship/` | ✅ | ✅ | 低 | 20.67% |
| coach-detail | `pages/coach-detail/` | ✅ | ✅ | 中 | 34.25% |
| customer-detail | `pages/customer-detail/` | ✅ | ✅ | 中 | 38.48% |
| customer-service-records | `pages/customer-service-records/` | ✅ | ✅ | 中 | 25.33% |
| performance | `pages/performance/` | ✅ | ✅ | 中 | 38.70% |
| performance-records | `pages/performance-records/` | ✅ | ✅ | 中 | 28.42% |
| chat | `pages/chat/` | ✅ | ✅ | 中 | 25.35% |
| chat-history | `pages/chat-history/` | ✅ | ✅ | 中 | 8.98% |
| notes | `pages/notes/` | ✅ | ✅ | 低 | 10.85% |
> 待迁移页面差异率偏高属预期H5 mock 数据 vs MP 真实数据、字体渲染差异等)。迁移后差异率应降至 ≤10%。
### 11.2 已迁移页面交互态对比
| 页面 | 交互态 | 差异% | diff 文件 |
|------|--------|-------|-----------|
| board-coach | perf-high | 7.05% | `diff-board-coach--perf-high.png` |
| board-coach | salary-high | 10.55% | `diff-board-coach--salary-high.png` |
| board-coach | storage-value | 10.28% | `diff-board-coach--storage-value.png` |
| board-coach | task-complete | 6.36% | `diff-board-coach--task-complete.png` |
| board-coach | sort-dropdown | 23.90% | `diff-board-coach--sort-dropdown.png` |
| board-coach | skill-dropdown | 23.30% | `diff-board-coach--skill-dropdown.png` |
| board-coach | time-dropdown | 24.10% | `diff-board-coach--time-dropdown.png` |
| board-customer | recall | 7.46% | `diff-board-customer--recall.png` |
| board-customer | potential | 7.35% | `diff-board-customer--potential.png` |
| board-customer | balance | 7.30% | `diff-board-customer--balance.png` |
| board-customer | recharge | 7.46% | `diff-board-customer--recharge.png` |
| board-customer | recent | 6.89% | `diff-board-customer--recent.png` |
| board-customer | spend60 | 6.85% | `diff-board-customer--spend60.png` |
| board-customer | freq60 | 11.43% | `diff-board-customer--freq60.png` |
| board-customer | loyal | 10.03% | `diff-board-customer--loyal.png` |
| board-customer | type-dropdown | 24.82% | `diff-board-customer--type-dropdown.png` |
| board-customer | project-dropdown | 23.03% | `diff-board-customer--project-dropdown.png` |
| board-finance | filter-dropdown | 14.29% | `diff-board-finance--filter-dropdown.png` |
| board-finance | tip-modal | 24.84% | `diff-board-finance--tip-modal.png` |
| board-finance | toc-panel | 10.90% | `diff-board-finance--toc-panel.png` |
> 下拉菜单类交互态差异率偏高20%+)属预期——弹出层遮罩和动画导致像素差异大,重点关注内容区域还原度。
### 11.3 无需 H5 迁移的页面
| 页面 | 小程序路径 | 说明 |
|------|-----------|------|
| mvp | `pages/mvp/` | 临时入口/路由分发页 |
| index | `pages/index/` | 框架默认页 |
| logs | `pages/logs/` | 框架默认日志页 |
| dev-tools | `pages/dev-tools/` | 开发调试工具页 |
---
## 十二、AI 工作流提示词模板
> 以下 6 阶段提示词模板用于指导 AI 执行迁移。每个阶段有明确的输入/输出要求。
> 实际使用时,将 `<page-name>` 替换为具体页面名,将 `<<<...>>>` 替换为实际内容。
### 阶段 1迁移前审计与准备
```
你是一名资深微信小程序前端架构师+CSS布局专家。我要把一套 Web 页面HTML/CSS/少量JS
迁移为"原生微信小程序"WXML/WXSS/JS/JSON要求结构与样式细节还原度尽可能高。
输入(我将分段提供):
- 页面HTML可含多页面
- 全量CSS含reset/公共样式)
- 资源清单(图片/字体/图标svg等路径
- 若有:目标效果截图/设计稿
请先不要写最终代码,先做《迁移审计与准备报告》,输出必须包含:
A. 页面结构清单:主要区域及建议组件化边界
B. CSS复杂度审计高风险点sticky、复杂选择器、伪元素、滤镜等+ 替代策略
C. HTML→WXML映射规则语义标签映射、事件绑定映射、表单控件映射、wx:for 策略
D. CSS→WXSS映射规则单位策略px→rpx、选择器扁平化、样式隔离
E. 资源处理:图片/字体/图标方案、包体与分包建议
F. 产物目录规划pages/、components/、styles/、assets/ 目录树
G. 最小补充信息清单:高还原必须补充的信息
输出要求:表格+要点并存;每个高风险点给出"原因 + 影响 + 处理方案 + 验收方式"。
```
### 阶段 2生成第一版代码
```
基于《迁移审计与准备报告》,生成可运行的小程序第一版代码。
硬性要求:
A. 输出完整目录树(含 pages、components、styles、assets
B. 按文件逐个输出:.wxml / .wxss / .js / .json
C. 公共样式抽到 styles/common.wxss页面只放差异
D. 尽量避免第三方库;如必须引入,说明理由和替代方案
E. 对"Web有但小程序不完全支持"的效果:写清替代实现与预期差异
我将提供:
- <PAGE_NAME> 的HTML<<<...>>>
- 全量CSS<<<...>>>
- 截图/设计稿要点:<<<...>>>
请输出:
I. 目录树
II. 逐文件代码(从 app.json/app.wxss/app.js 开始)
III. 编译与运行说明
IV. 第一版"已知差异清单"P0/P1/P2
```
### 阶段 3高保真样式对齐
```
进入"高保真样式对齐"阶段。目标:不破坏现有结构,视觉细节贴近 Web/设计稿。
我会提供:
- 当前小程序代码片段wxml/wxss
- Web 参考HTML/CSS片段或截图差异描述
- 具体差异点列表
你的任务:
A. 每个差异点:根因判断 + 最小修改方案 + 修改后风险
B. 输出"补丁式修改"diff 风格),不要整文件重贴
C. 验收步骤:开发者工具与真机分别怎么验证
约束:
- 优先 flex/盒模型确定性方案,减少魔法数
- px→rpx 换算统一,禁止同一类间距混用单位
```
### 阶段 4结构还原与组件化质量提升
```
对当前小程序实现做"结构与可维护性重构",不改变 UI 效果。
输出:
A. 现状问题清单(重复样式、选择器过深、耦合、命名不一致)
B. 组件拆分方案组件名、props、slot、事件、数据流
C. 样式治理方案BEM/命名规范、公共变量、设计token化
D. 重构后目录树
E. 逐文件补丁diff风格说明每个改动为何不影响UI
约束不引第三方UI库不改变页面路由与业务数据接口。
```
### 阶段 5像素级对比与验收清单
```
输出《像素级对比与验收清单》,用于逐项对照 Web 版本与小程序版本。
要求:
- 按页面输出(每页一个小节)
- 每页包含:布局结构、间距系统、字体系统、颜色、边框/圆角、阴影、
图片裁切、交互态、滚动与吸顶、列表与空状态
- 每条验收项:怎么看 + 合格标准 + 常见失败表现
- 额外输出《差异追踪表》:
差异描述|截图/位置严重度P0/P1/P2可能原因建议修复方案修复代价回归点
输出用表格为主,可直接复制当验收用例。
```
### 阶段 6编译报错/真机差异快速修复
```
你是微信小程序调试与兼容性专家。我会贴出:
- 微信开发者工具编译报错/警告日志
- 真机预览与模拟器表现差异描述
- 相关代码片段
请你:
A. 定位根因(按概率排序列出 1~3 个最可能原因)
B. 最小修复补丁diff风格
C. 回归测试点防止修A坏B
D. 如果是基础库兼容问题,说明最低基础库版本或降级方案
```
---
## 十三、输入素材准备指南
> 每次迁移一个页面前,需按本章要求准备输入素材,提高 AI 转换还原度。
### 13.1 HTML 文件规范
当前 H5 原型是 Tailwind CDN + 内联 SVG + 原生 JS 的单文件结构,存在以下转换障碍:
- Tailwind 工具类需逐个展开为 WXSS容易遗漏或换算错误
- 内联 SVG 在小程序中完全不可用
- JS 交互逻辑对小程序没有参考价值
推荐做法:
1. 提供渲染后的最终 DOM浏览器中 Copy outerHTML保存为 `docs/h5_ui/rendered/<page-name>.html`
2. 去掉所有 `<script>` 标签,交互逻辑单独用结构化文本描述
3. 内联关键样式(二选一):
- 方式 A手动在 Chrome DevTools → Computed 面板记录关键属性计算值
- 方式 B推荐在 Chrome Console 执行批量导出脚本
批量导出脚本:
```javascript
function exportStyles() {
const props = [
'width','height','padding','margin','gap',
'fontSize','fontWeight','lineHeight','color',
'background','backgroundColor','borderRadius',
'boxShadow','display','flexDirection','alignItems',
'justifyContent','position','opacity'
];
const elements = document.querySelectorAll('[class]');
const result = [];
elements.forEach((el, i) => {
const cs = getComputedStyle(el);
const styles = {};
props.forEach(p => {
const v = cs[p];
if (v && v !== 'none' && v !== 'normal' && v !== '0px' && v !== 'auto') {
styles[p] = v;
}
});
if (Object.keys(styles).length > 0) {
result.push({
tag: el.tagName.toLowerCase(),
classes: el.className,
text: el.textContent?.trim().slice(0, 30),
styles
});
}
});
console.log(JSON.stringify(result, null, 2));
}
exportStyles();
```
输出追加到 `docs/h5_ui/computed-styles.json`(按页面名分 key 存放)。
### 13.2 设计 Token全局做一次
从 Tailwind 自定义主题提取,保存为 `docs/h5_ui/design-tokens.json`
```json
{
"colors": {
"primary": "#0052d9", "primary-light": "#ecf2fe",
"success": "#00a870", "warning": "#ed7b2f", "error": "#e34d59",
"gray-1": "#f3f3f3", "gray-6": "#a6a6a6", "gray-9": "#5e5e5e", "gray-13": "#242424"
},
"spacing": { "comment": "Tailwind 1 unit = 4px = 8rpx750rpx 基准)", "base": 8, "unit": "rpx" },
"borderRadius": { "sm": "8rpx", "md": "16rpx", "lg": "24rpx", "xl": "32rpx" },
"fontSize": { "xs": "24rpx", "sm": "28rpx", "base": "32rpx", "lg": "36rpx", "xl": "40rpx" },
"shadows": { "lg": "0 8rpx 32rpx rgba(0,0,0,0.06)", "xl": "0 16rpx 48rpx rgba(0,0,0,0.08)" }
}
```
### 13.3 自定义 CSS 特性处理
| CSS 特性 | 小程序支持 | 处理方式 |
|----------|-----------|----------|
| `@keyframes` + `animation` | ✅ | 直接迁移 |
| `transition` | ✅ | 直接迁移 |
| `linear-gradient` | ✅ | 直接迁移 |
| `backdrop-filter: blur()` | ❌ | 改为 `rgba()` 半透明背景 |
| `blur-xl`Tailwind | ❌ | 去掉或改为纯色 |
| CSS 变量 `var()` | ✅ | 直接迁移TDesign 大量使用) |
### 13.4 交互描述规范
每个页面提供一份交互说明,保存为 `docs/h5_ui/interactions/<page-name>.md`,格式:
```markdown
# 页面名:<page-name>
## 状态变量
| 变量名 | 类型 | 初始值 | 说明 |
## 用户操作 → 响应
| 操作 | 触发条件 | 响应行为 | 目标状态 |
## 页面状态枚举
| 状态名 | 视觉表现 | 触发条件 |
```
每个页面必须覆盖的状态:正常态(有数据)、空数据态、加载中态、错误态、登录态差异。
### 13.5 视觉截图规范
- 分辨率iPhone 6/7/8375×667小程序 750rpx 设计基准的 1:1 对应
- 命名:`<page-name>--<state>.png`(如 `board-customer--filter-open.png`
- 存放:`docs/h5_ui/screenshots/`
- 长页面Chrome DevTools → Ctrl+Shift+P → "Capture full size screenshot"
- 每页至少截:默认态、关键交互态、空数据态(如适用)、弹窗/浮层
### 13.6 图标处理
优先级TDesign 内置图标 > 提取独立 SVG > PNG/JPG 图片
图标映射表(全局维护 `docs/h5_ui/icon-mapping.md`
| H5 中的图标描述 | 处理方式 | 小程序引用 |
|----------------|----------|-----------|
| Logo 台球图标 | 自定义SVG | `/assets/icons/icon-billiard.svg` |
| 任务管理图标 | TDesign | `<t-icon name="task" />` |
| 返回箭头 | TDesign | `<t-icon name="chevron-left" />` |
### 13.7 每页准备流程
1. 在 Chrome 中打开 H5 页面,切换 iPhone 6/7/8 设备模式
2. 运行 `exportStyles()` 脚本,输出追加到 `computed-styles.json`
3. 截图各状态,放入 `screenshots/`
4. 写交互说明,放入 `interactions/`
5. 检查图标,补充映射表
### 13.8 最低限度清单
| 优先级 | 材料 | 作用 | 频率 |
|--------|------|------|------|
| P0 | 截图默认态375px 宽) | 校验还原度的唯一视觉参考 | 每页 |
| P0 | 交互说明(状态变量 + 操作响应表) | 避免逻辑错误 | 每页 |
| P0 | 设计 Token JSON | 避免颜色/间距换算错误 | 一次 |
| P1 | 计算样式 JSON | 显著提高还原度 | 每页(可选) |
| P1 | 图标映射表 | 避免图标处理失误 | 一次 + 增量 |
| P2 | 渲染后 DOM | 复杂页面时有帮助 | 按需 |
---
## 十四、长页面迁移专题
> 17 个迁移页面中task-detail 系列、coach-detail、customer-detail、performance、board-finance 等均为长页面(内容超过一屏)。
> 本章统一规范长页面的滚动方案、sticky 元素处理、底部固定栏实现、截图对比流程。
### 14.1 滚动方案选择
| 场景 | 方案 | 说明 |
|------|------|------|
| 整页纵向滚动(绝大多数页面) | 页面自然滚动 | 不需要 `scroll-view`页面本身就是滚动容器。WXSS 用 `min-height: 100vh`,不设 `overflow: hidden` |
| 局部区域独立滚动(如对话消息列表) | `<scroll-view scroll-y>` | 必须设固定高度(`calc(100vh - 顶部高度 - 底部高度)`),否则不滚动 |
| 横向滚动列表(如标签栏溢出) | `<scroll-view scroll-x>` | 设 `white-space: nowrap`,子元素 `display: inline-block` |
| 看板页面board-finance 等) | `<scroll-view scroll-into-view>` | 页面主体用 scroll-view 包裹,配合 `scroll-into-view` 属性实现锚点定位 |
决策规则:优先用页面自然滚动。只有当页面中某个区域需要独立于页面滚动时(如 chat 页的消息区),才用 `scroll-view`
#### scroll-view 页面的识别与处理
部分页面(如 board-finance使用 `<scroll-view scroll-into-view="{{scrollIntoView}}">` 作为主滚动容器,而非页面自然滚动。这类页面有以下特征:
- WXML 中存在 `<scroll-view scroll-y scroll-into-view="{{scrollIntoView}}">`
- `wx.pageScrollTo()` 无效(因为页面本身不滚动,滚动发生在 scroll-view 内部)
- `position: sticky` 在 scroll-view 内部不生效
识别方法:在微信开发者工具中执行 `wx.pageScrollTo({scrollTop: 100})`,如果页面不动,说明是 scroll-view 页面。
对于 scroll-view 页面的像素级对比MP 截图时需要使用 `setData({scrollIntoView: '<section-id>'})` 而非 `wx.pageScrollTo()`。详见第六章 6.3.2 锚点分区对比流程。
### 14.2 sticky 元素处理
长页面中常见的 sticky 元素:顶部 Tab 栏、筛选栏、分组标题。
```css
/* 顶部 Tab 栏 — 紧贴状态栏下方 */
.tab-bar {
position: sticky;
top: 0;
z-index: 20;
background: #ffffff;
}
/* 筛选栏 — 紧贴 Tab 栏下方 */
.filter-bar {
position: sticky;
top: 70rpx; /* = Tab 栏高度 */
z-index: 15;
background: #f3f3f3;
}
```
注意事项:
- `position: sticky` 在小程序中正常工作,但父容器不能有 `overflow: hidden`
- sticky 元素的 `top` 值需要考虑状态栏高度(通过 JS `wx.getSystemInfoSync().statusBarHeight` 获取)
- 多个 sticky 元素叠加时,`z-index` 从上到下递减20 → 15 → 10
- 如果页面使用 `navigationStyle: 'custom'`sticky top 需要加上 `statusBarHeight + 导航栏高度`
### 14.3 底部固定栏实现
task-detail、coach-detail、customer-detail 等页面底部有固定操作栏(问问助手 + 备注)。
```xml
<!-- WXML 结构 -->
<view class="page">
<!-- 可滚动内容区 -->
<view class="content">
<!-- 页面主体内容 -->
...
<!-- 底部占位,防止内容被固定栏遮挡 -->
<view class="bottom-placeholder"></view>
</view>
<!-- 底部固定栏 -->
<view class="bottom-bar">
<view class="btn-ask">问问助手</view>
<view class="btn-note">备注</view>
</view>
</view>
```
```css
/* WXSS */
.page {
min-height: 100vh;
padding-bottom: 0; /* 不在 page 上加 padding */
}
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 50;
background: #ffffff;
padding: 16rpx 28rpx;
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
border-top: 2rpx solid #eeeeee;
display: flex;
gap: 20rpx;
}
.bottom-placeholder {
height: 120rpx; /* ≥ 底部固定栏高度 + safe-area */
}
```
关键点:
-`position: fixed` 而非 `sticky`(底部栏需要始终可见)
- 必须加 `env(safe-area-inset-bottom)` 适配刘海屏底部
- 必须在内容区末尾加占位 `<view>`,高度 ≥ 底部栏高度,防止最后一段内容被遮挡
- 底部栏 `z-index: 50`,高于所有 sticky 元素但低于弹窗遮罩
### 14.4 长页面截图对比流程
> 长页面对比已升级为 v2 逐段截图方案(`anchor_compare.py`),替代旧的长截图裁剪方式。
> 完整流程见第六章 6.3.2。本节补充长页面特有的注意事项和实测经验。
#### v2 逐段截图方案(推荐)
```bash
# 提取 H5 锚点坐标 + 逐段截图(需要 Go Live 运行在 5500 端口)
python scripts/ops/anchor_compare.py extract-h5 board-finance
# 生成 MP 截图指令
python scripts/ops/anchor_compare.py mp-inst board-finance
# AI 按指令通过 MCP 工具执行 MP 截图(手动步骤)
# scroll-view 页面使用 scroll-into-view 模式:
# page.setData({ scrollIntoView: '' }) // 先清空
# page.setData({ scrollIntoView: '<id>' }) // 设目标
# 等待 1000ms → 截图
# 逐段配对 + 对比
python scripts/ops/anchor_compare.py compare board-finance
```
#### board-finance 实测数据2026-03-08 验证)
| 段 | 区域 | H5 scrollY | 差异率 | 说明 |
|---|---|---|---|---|
| seg-0 | 经营一览 | 16 | 11.77% | 首屏,数据内容差异 |
| seg-1 | 预收资产 | 668 | 8.79% | 良好 |
| seg-2 | 应计收入确认 | 1393 | 8.62% | 良好 |
| seg-3 | 现金流入 | 2757 | 15.04% | 数据行数差异较大 |
| seg-4 | 现金流出 | 3233 | 8.46% | 良好 |
| seg-5 | 助教分析 | 4042 | 5.46% | 优秀 |
差异来源分析:
- 数据内容差异H5 是静态 mock 数据MP 是真实 API 数据,金额/文本不同
- 字体渲染差异Chromium vs 微信 WebView 的字体 hinting 和亚像素抗锯齿不同
- 非对齐问题所有段截图尺寸完全一致1290×2256对齐精度 ~1px
#### scroll-view 页面的 MP 截图要点
board-finance 等使用 `<scroll-view scroll-into-view>` 的页面MP 截图时:
1. 先清空 scrollIntoView`page.setData({ scrollIntoView: '' })`
2. 设置目标:`page.setData({ scrollIntoView: 'section-xxx' })`
3. 等待 1000ms 让滚动和渲染完成
4. 截图645×1128 物理像素 = 430×752 逻辑像素)
两端截图从顶部自然对齐,无需裁剪。
#### filter-bar 高度统一约束
所有带 filter-bar 的看板页面board-finance、board-coach、board-customerfilter-bar 统一高度为 70 逻辑像素。这确保两端 sticky 区域高度一致(~116px截图自然对齐。
编辑小程序前端页面时须 check 并修正不符合此高度的 filter-bar见需求 11.8)。
#### 短页面对比
短页面或无锚点配置的页面仍可使用全页面对比:
```bash
python scripts/ops/resize_and_compare_v2.py
```
### 14.5 长页面性能注意事项
| 风险 | 触发条件 | 解决方案 |
|------|----------|----------|
| setData 数据量过大 | 列表 > 50 条 | 只传变化字段:`'list[2].name': 'new'` |
| 首屏渲染慢 | 页面 DOM 节点 > 500 | 非首屏内容用 `wx:if` 延迟渲染 |
| 滚动卡顿 | 大量图片 + 复杂布局 | `<image lazy-load>` + 简化嵌套层级 |
| 内存溢出 | 无限滚动列表 | 本项目列表数据量有限(< 200 条),暂不需要虚拟列表 |
当前 17 个页面中,数据量最大的是 board-coach 和 board-customer助教/客户卡片列表),单店通常 < 100 条,不需要虚拟列表。如果未来数据量增长,优先考虑分页加载(`onReachBottom` 触发加载更多)。
---
## 十五、交互弹窗与状态管理专题
> 17 个迁移页面中涉及大量交互弹窗:筛选下拉、长按菜单、备注弹窗、放弃弹窗、指标弹窗、目录面板等。
> 本章统一规范弹窗实现模式、z-index 分层、动画过渡、三态处理。
### 15.1 弹窗类型与实现方案
| 弹窗类型 | 涉及页面 | 实现方案 | 说明 |
|----------|---------|---------|------|
| 筛选下拉面板 | board-finance/coach/customer | 自定义组件 `filter-dropdown` | 全屏宽面板 + 遮罩,已有组件规范(第八章 8.3 |
| 长按浮层菜单 | task-list、board-finance | 自定义 view + 遮罩 | 黑底圆角菜单,定位在长按元素附近 |
| 备注弹窗 | task-list/detail、coach-detail | `<t-popup>` + 自定义内容 | 底部弹出,含 textarea + 提交按钮 |
| 确认弹窗 | task-detail放弃任务 | `<t-dialog>` | 标准确认/取消双按钮 |
| 指标说明弹窗 | board-finance | 自定义 view + 遮罩 | 居中卡片,展示指标解释文本 |
| 目录导航面板 | board-finance | 自定义 view + 遮罩 | 右侧滑出面板 |
| Toast 提示 | 所有页面 | `<t-toast>``wx.showToast` | 操作成功/失败的轻量反馈 |
决策规则:
- 标准确认/取消 → `<t-dialog>`
- 底部弹出表单/选择器 → `<t-popup position="bottom">`
- 需要高度定制样式的浮层(筛选下拉、长按菜单、目录面板)→ 自定义 view + 遮罩
- 轻量提示 → `<t-toast>``wx.showToast`
### 15.2 z-index 分层策略(全局统一)
```
z-index 层级分配(从低到高):
10-29 页面内 sticky 元素
20 顶部 Tab 栏
15 筛选栏容器
30-49 页面内浮动元素
30 AI 悬浮按钮
50 底部固定操作栏
100-199 自定义底部导航栏
100 board-tab-bar
900-999 弹窗遮罩层
999 遮罩背景rgba(0,0,0,0.5)
1000+ 弹窗内容层
1000 筛选下拉面板
1000 长按浮层菜单
1000 备注弹窗
1000 指标说明弹窗
1000 目录导航面板
9999 Toast / Loading
```
规则:
- 同一时刻只允许一个弹窗打开(互斥),因此弹窗内容层统一用 1000
- 如果未来需要弹窗叠加(如弹窗中再弹确认框),内层用 1100
- 遮罩和内容必须成对出现,遮罩 z-index = 内容 z-index - 1
### 15.3 遮罩 + 弹窗通用实现模式
```xml
<!-- WXML 通用模式 -->
<!-- 遮罩层 -->
<view
wx:if="{{showPopup}}"
class="overlay"
bindtap="onClosePopup"
catchtouchmove="preventScroll"
></view>
<!-- 弹窗内容 -->
<view
wx:if="{{showPopup}}"
class="popup popup--bottom"
catchtouchmove="preventScroll"
>
<!-- 弹窗内容 -->
</view>
```
```css
/* WXSS 通用样式 */
.overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
}
/* 底部弹出 */
.popup--bottom {
position: fixed;
left: 0; right: 0; bottom: 0;
z-index: 1000;
background: #ffffff;
border-radius: 28rpx 28rpx 0 0;
padding-bottom: env(safe-area-inset-bottom);
animation: slideUp 220ms ease;
}
/* 居中弹窗 */
.popup--center {
position: fixed;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
z-index: 1000;
background: #ffffff;
border-radius: 28rpx;
animation: fadeIn 200ms ease;
}
/* 右侧滑出 */
.popup--right {
position: fixed;
top: 0; right: 0; bottom: 0;
width: 560rpx;
z-index: 1000;
background: #ffffff;
animation: slideLeft 220ms ease;
}
@keyframes slideUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
@keyframes fadeIn {
from { opacity: 0; transform: translate(-50%, -48%); }
to { opacity: 1; transform: translate(-50%, -50%); }
}
@keyframes slideLeft {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
```
```typescript
// TS 通用逻辑
Page({
data: {
showPopup: false,
},
onOpenPopup() {
this.setData({ showPopup: true });
},
onClosePopup() {
this.setData({ showPopup: false });
},
// 阻止遮罩层下方页面滚动
preventScroll() {},
});
```
关键点:
- `catchtouchmove="preventScroll"` 阻止弹窗打开时背景页面滚动空函数即可catch 会阻止事件冒泡)
- 遮罩 `bindtap` 关闭弹窗,弹窗内容用 `catchtap` 防止点击穿透
- 动画时长统一 200-220ms缓动函数 `ease`
- 底部弹窗必须加 `env(safe-area-inset-bottom)`
### 15.4 长按菜单实现
task-list 的长按卡片菜单是特殊交互,小程序中用 `bindlongpress` 实现:
```xml
<!-- WXML -->
<view
wx:for="{{taskList}}"
wx:key="id"
class="task-card"
bindtap="onTaskTap"
bindlongpress="onTaskLongPress"
data-id="{{item.id}}"
data-index="{{index}}"
>
<!-- 卡片内容 -->
</view>
<!-- 长按菜单(定位在长按位置附近) -->
<view wx:if="{{longPressMenu.visible}}" class="overlay" bindtap="closeLongPressMenu"></view>
<view
wx:if="{{longPressMenu.visible}}"
class="longpress-menu"
style="top: {{longPressMenu.y}}px; left: {{longPressMenu.x}}px;"
>
<view class="menu-item" bindtap="onMenuAction" data-action="pin_bottom">任务置底</view>
<view class="menu-item" bindtap="onMenuAction" data-action="ask_ai">问问助手</view>
<view class="menu-item" bindtap="onMenuAction" data-action="add_note">备注</view>
</view>
```
```css
.longpress-menu {
position: fixed;
z-index: 1000;
background: rgba(0, 0, 0, 0.85);
border-radius: 16rpx;
padding: 8rpx 0;
min-width: 200rpx;
}
.menu-item {
padding: 20rpx 32rpx;
color: #ffffff;
font-size: 28rpx;
}
.menu-item + .menu-item {
border-top: 1rpx solid rgba(255, 255, 255, 0.1);
}
```
```typescript
onTaskLongPress(e: WechatMiniprogram.TouchEvent) {
const { id } = e.currentTarget.dataset;
// 使用触摸点坐标定位菜单
const touch = e.touches[0];
this.setData({
'longPressMenu.visible': true,
'longPressMenu.taskId': id,
'longPressMenu.x': touch.clientX,
'longPressMenu.y': touch.clientY,
});
},
```
注意:`bindlongpress` 触发后会同时触发 `bindtap`,需要在 `onTaskTap` 中判断:
```typescript
onTaskTap(e: WechatMiniprogram.TouchEvent) {
// 如果长按菜单正在显示,不处理 tap
if (this.data.longPressMenu.visible) return;
// 正常跳转逻辑
const { id } = e.currentTarget.dataset;
wx.navigateTo({ url: `/pages/task-detail/task-detail?id=${id}` });
},
```
### 15.5 三态处理标准模板
每个页面必须处理 loading / empty / error / normal 四种状态。统一模式:
```typescript
// TS — data 定义
data: {
pageState: 'loading' as 'loading' | 'empty' | 'error' | 'normal',
errorMessage: '',
// ... 业务数据
},
async onLoad() {
await this.loadData();
},
async loadData() {
this.setData({ pageState: 'loading' });
try {
const data = await api.fetchXxx();
if (!data || data.length === 0) {
this.setData({ pageState: 'empty' });
} else {
this.setData({ pageState: 'normal', /* 业务数据 */ });
}
} catch (err) {
this.setData({ pageState: 'error', errorMessage: '加载失败,请点击重试' });
}
},
onRetry() {
this.loadData();
},
```
```xml
<!-- WXML — 三态切换 -->
<!-- 加载态 -->
<view wx:if="{{pageState === 'loading'}}" class="state-container">
<t-loading theme="circular" size="40rpx" />
<text class="state-text">加载中...</text>
</view>
<!-- 空数据态 -->
<view wx:elif="{{pageState === 'empty'}}" class="state-container">
<text class="state-text">暂无数据</text>
</view>
<!-- 错误态 -->
<view wx:elif="{{pageState === 'error'}}" class="state-container">
<text class="state-text">{{errorMessage}}</text>
<t-button theme="primary" size="small" bindtap="onRetry">重试</t-button>
</view>
<!-- 正常态 -->
<view wx:else>
<!-- 页面主体内容 -->
</view>
```
```css
/* WXSS — 三态容器 */
.state-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 200rpx;
gap: 24rpx;
}
.state-text {
font-size: 28rpx;
color: #a6a6a6;
}
```
各页面空状态文案:
| 页面 | 空状态文案 |
|------|-----------|
| task-list | 暂无任务 |
| board-coach | 暂无助教数据 |
| board-customer | 暂无客户数据 |
| board-finance | 暂无财务数据 |
| chat-history | 暂无对话记录 |
| notes | 暂无备注记录 |
| performance-records | 暂无业绩明细 |
| customer-service-records | 暂无服务记录 |
| 其他详情页 | 暂无数据 |
### 15.6 弹窗内表单处理
备注弹窗task-list、task-detail、coach-detail的统一实现
```xml
<!-- 备注弹窗 -->
<view wx:if="{{showNotePopup}}" class="overlay" bindtap="closeNotePopup"></view>
<view wx:if="{{showNotePopup}}" class="popup popup--bottom" catchtap>
<view class="popup-header">
<text class="popup-title">添加备注</text>
<view class="popup-close" bindtap="closeNotePopup">
<t-icon name="close" size="40rpx" color="#a6a6a6" />
</view>
</view>
<t-textarea
value="{{noteContent}}"
placeholder="请输入备注内容"
maxlength="500"
bind:change="onNoteInput"
autosize="{{ { minRows: 3, maxRows: 6 } }}"
t-class="note-textarea"
/>
<view class="popup-footer">
<t-button
theme="primary"
block
disabled="{{!noteContent.trim()}}"
loading="{{noteSubmitting}}"
bindtap="onSubmitNote"
>提交</t-button>
</view>
</view>
```
关键点:
- 弹窗内容区用 `catchtap`(无处理函数)防止点击穿透到遮罩
- textarea 用 TDesign `<t-textarea>`,事件是 `bind:change`
- 提交按钮在内容为空时 `disabled`,提交中显示 `loading`
- 提交成功后 `wx.showToast({ title: '备注已添加', icon: 'success' })` + 关闭弹窗
### 15.7 交互态验证清单
每个页面迁移完成后,按此清单逐项验证交互态:
| # | 验证项 | 验证方法 | 合格标准 |
|---|--------|----------|----------|
| 1 | 弹窗打开/关闭 | 点击触发元素 → 弹窗出现;点击遮罩/关闭按钮 → 弹窗消失 | 动画流畅,无闪烁 |
| 2 | 背景滚动锁定 | 弹窗打开时尝试滑动背景 | 背景不滚动 |
| 3 | 点击穿透 | 点击弹窗内容区域 | 不触发遮罩关闭 |
| 4 | 长按菜单定位 | 长按列表不同位置的卡片 | 菜单出现在触摸点附近,不超出屏幕 |
| 5 | 长按 vs 点击 | 短按卡片 → 跳转;长按卡片 → 菜单 | 两种手势互不干扰 |
| 6 | 三态切换 | 模拟 loading/empty/error | 三种状态 UI 正确显示 |
| 7 | 错误重试 | 点击重试按钮 | 重新加载数据 |
| 8 | 表单提交 | 填写内容 → 提交 | loading 态 → 成功 toast → 弹窗关闭 |
| 9 | 键盘适配 | 弹窗内 textarea 获取焦点 | 键盘弹起不遮挡输入区 |
| 10 | safe-area | 底部弹窗在刘海屏设备 | 底部内容不被遮挡 |
---
## 十六、产品需求摘要PRD
> ⚠️ 本章仅为迁移时的速查索引,详细度约为原 spec 的 15-20%。
> 完整需求验收标准、状态变量、操作→响应映射、API 依赖、样式细节、边界条件等)
> 必须参考原 PRD spec 文件:
> - 任务模块:`docs/prd/specs/P6-miniapp-fe-tasks.md`
> - 业绩模块:`docs/prd/specs/P7-miniapp-fe-performance.md`
> - 看板模块:`docs/prd/specs/P8-miniapp-fe-boards.md`
> - 详情页模块:`docs/prd/specs/P9-miniapp-fe-details.md`
>
> 每个页面的交互子状态(弹窗、下拉、长按菜单、星星评分等)详见:
> `docs/h5_ui/interactions/<page>.md`21 个文件,覆盖全部页面)
>
> PRD spec 原文件未被删除,完整保留在 `docs/prd/specs/` 目录中。
### 16.1 全局设计规范
- 语言:简体中文;金额元取整,万元保留两位小数
- 底部 TabBar任务 / 看板 / 我的
- 二级/详情页隐藏原生导航栏,使用自定义头部(左上角返回图标)
- 悬浮助手按钮:所有业务页面右下角显示,点击进入助手对话页(详见第八章 8.1 节)
- 错误态:`加载失败,请点击重试` + 重试按钮
- 空数据态:`暂无数据` / `暂无任务`(纯文字,无插画)
- 加载态:`加载中...`(纯文字,无骨架屏)
### 16.2 页面级需求
#### task-list任务列表页默认首页
- 顶部自定义导航栏(标题"任务",无返回按钮)
- Banner 区:用户名+身份+业绩概览+预计收入,整块可点击跳转业绩详情页
- 任务列表:单列,按紧急程度排序(红→橙→粉→蓝),不分组
- 单条卡片:类型标签(带颜色)+ 客户姓名 + 右箭头 + 补充信息行
- 长按卡片:黑底浮层菜单(任务置底 / 问问助手 / 备注)
- 空状态:`暂无任务`
#### task-detail任务详情页
- 模块:客户基本信息 → 消费习惯 → 与我的关系(等级+说明)→ 任务建议
- 底部固定栏:问问助手 + 备注
#### task-detail-callback / task-detail-priority / task-detail-relationship
- task-detail 的主题色变体,数据结构和页面布局完全以 task-detail 为准
- 变体仅差异banner 背景色、按钮配色、页面主题色(对照 H5 原型截图校准色值即可)
- 迁移策略:先完成 task-detail再复制并替换主题色变量
#### performance业绩详情页
- 顶部 Banner用户名+身份+本月业绩进度+预计收入
- 下方:多组指标,两列卡片网格(收入构成/台球助教业绩/充值业绩/酒水业绩)
- 指标卡片:名称+当前值+目标值+完成度%
- 仅展示本月数据,无时间切换
#### performance-records业绩明细页
- 业绩详情的明细列表
#### board-finance看板-财务视图)
- 筛选时间月份9 选项+指定周期,最大 366 天)+ 区域(全部/大厅ABC/麻将房/团建房/具体台桌)
- 财务汇总行:实际收入/支出/净利润(三列)
- 四个板块:营业数据 / 收入构成 / 支出构成 / 利润构成,每行 3 个指标卡片
- 长按指标卡片:启动助手对话
#### board-coach看板-助教视图)
- 筛选排序维度7 选项)+ 擅长项目 + 时间月份
- 助教卡片:姓名+等级+擅长项目 | 关系最好客户前三 | 排序维度数值
- 点击进入助教详情页
#### board-customer看板-客户视图)
- 筛选客户类型8 维度)+ 偏爱项目
- 客户卡片:名称+等级+VIP标识 | 最喜欢助教前三 | 核心指标+最近到店
- 助教身份默认过滤(后台行为,前端不显式展示)
- 点击进入客户详情页
#### coach-detail助教详情页
- 模块:基本信息 → 流水与业绩 → 工资与上课时长 → 前 10 客户指数列表
- 底部固定栏:问问助手 + 备注
#### customer-detail客户详情页
- 模块:基本信息 → 消费习惯(标签+文本)→ 与我的关系(等级+说明)
- 底部固定栏:问问助手 + 备注
#### customer-service-records客户服务记录页
- 客户的服务记录列表
#### my-profile我的首页
- 顶部用户信息区 + 列表菜单(备注记录/助手对话记录/首页设置/退出账号)
#### chat助手对话页
- 仿微信对话界面(左助手气泡/右用户气泡)
- 引用内容:灰底小卡片(来源类型+标题+摘要)
- 输入区:文本框 + 按住说话(语音转文字)+ 发送按钮
- 会话管理:超 1 小时提示"新对话主题/继续对话"
#### chat-history助手对话记录页
- 列表:对话标题+最近时间+消息条数,按更新时间倒序
- 点击打开对应会话,滚动到最后一条
#### notes备注记录页
- 列表按时间倒序平铺,每条:备注全文+关联对象+创建时间
- 不支持编辑/删除
---
## 十七、单页收尾清单
每个页面迁移完成后,逐项检查:
| # | 检查项 | 合格标准 | 验证方式 |
|---|--------|----------|----------|
| 1 | 编译零报错 | 开发者工具无 error/warning | 编译面板 |
| 2 | 默认态像素对比 | 差异率 ≤ 10% | `scripts/ops/page_compare.py` |
| 3 | 关键交互态截图 | 弹窗/筛选/空状态均有截图 | `docs/h5_ui/screenshots/` |
| 4 | 87.5% 缩放一致 | 所有 rpx 值 = H5 px × 2 × 0.875 | 抽查 3 个关键元素 |
| 5 | 颜色 Token 一致 | 使用 design-tokens.json 中定义的颜色 | 全局搜索硬编码色值 |
| 6 | TDesign 组件正确 | 按第九章规范使用 | 代码审查 |
| 7 | 共享组件复用 | filter-bar / card 等按第八章规范 | 代码审查 |
| 8 | 交互逻辑完整 | 所有状态变量和操作→响应已实现 | 对照 interactions/*.md |
| 9 | 空/错误/加载态 | 三种状态均已处理 | 手动触发验证 |
| 10 | 真机预览 | iOS + Android 各一台无异常 | 扫码预览 |
| 11 | 导航正确 | 返回/跳转/TabBar 行为符合 PRD | 手动操作 |
| 12 | 认证守卫 | 未登录自动跳转登录页 | 清除 token 后访问 |
---
## 附录:目录结构参考
迁移完成后,`docs/h5_ui/` 目录结构应如下:
```
docs/h5_ui/
├── css/ # H5 自定义 CSS
├── img/ # H5 图片资源
├── js/ # H5 JS 文件
├── pages/ # H5 原型页面(迁移输入源)
├── rendered/ # 渲染后 DOM可选
├── computed-styles.json # 计算样式(按页面名分 key
├── screenshots/ # 页面截图(必须)
│ ├── <page>--default.png
│ ├── <page>--<state>.png
│ └── ...
├── interactions/ # 交互说明(必须)
│ ├── <page>.md
│ └── ...
├── design-tokens.json # 设计 Token必须做一次
├── icon-mapping.md # 图标映射表(必须,做一次)
└── index.html # 入口页
```
---
> 本文档整合自原 `apps/miniprogram/doc/` 下 9 个文件,为 H5→小程序迁移的唯一权威参考。
> 原文件已废弃,不再维护。