# 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>.html（H5 源码 — 唯一结构真相）
   - 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` | 必须用 switchTab，navigateTo 会报错 |
| 替换当前页 | `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 注册 usingComponents（TDesign + 自定义组件）
3. 转换 WXML（标签映射，保持层级一致）
4. 转换 WXSS（Tailwind → 手写 WXSS，87.5% 缩放）
5. 转换 TS（DOM → 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` | 边框粗细或颜色错误 | 边框最小 2rpx（1rpx 部分设备不显示），颜色查 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-xl（12px × 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` = 700，H5 用的是 `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 Live（VS 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:\Project\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×2256（MP ×2 缩放）。
两端视口高度统一为 752px（MP 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×752（MP 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>.png（645×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 + compare（MP 截图仍需手动） |
| `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（默认）/ #0052d9（active） |
| active 字重 | 500 |
| gap（icon↔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（默认）/ 600（active） |
| tab 颜色 | #8b8b8b（默认）/ #0052d9（active） |
| 下划线宽 | 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 = 8rpx（750rpx 基准）", "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/8（375×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-customer），filter-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→小程序迁移的唯一权威参考。
> 原文件已废弃，不再维护。