2097 lines
87 KiB
Markdown
2097 lines
87 KiB
Markdown
# 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:\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→小程序迁移的唯一权威参考。
|
||
> 原文件已废弃,不再维护。 |