87 KiB
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 导出规则
- 扫描目标页面 H5 源码中的所有
<svg>标签 - TDesign 有语义等价图标(如返回箭头 →
chevron-left)→ 用<t-icon>,不导出 - 品牌/自定义图标 → 导出为
apps/miniprogram/miniprogram/assets/icons/<name>.svg - 命名规则:
icon-<用途>.svg(如icon-wechat.svg);Logo 类用logo-<名称>.svg - 导出时保留原始
viewBox、fill、path - 小程序引用:
<image src="/assets/icons/<name>.svg" mode="aspectFit" />,必须指定宽高 - 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 导航页面强制)
// TS onLoad
const sysInfo = wx.getSystemInfoSync()
this.setData({ statusBarHeight: sysInfo.statusBarHeight || 20 })
<!-- WXML -->
<view class="page" style="padding-top: {{statusBarHeight}}px;">
/* WXSS */
.page { box-sizing: border-box; }
禁止使用 env(safe-area-inset-top)(部分机型不生效)。
3.8 一屏页面布局(不滚动的页面)
.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.wxssCSS 变量一致。 | 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 截图(一次性)
- 用户启动 Go Live(VS Code 插件),端口 5500
- Playwright MCP 导航到
http://127.0.0.1:5500/docs/h5_ui/pages/<page>.html - 设置视口
430×752(与 MP windowHeight 统一),DPR=3 - 逐段截图 →
docs/h5_ui/screenshots/h5-<page>--seg-<i>.png(每张 1290×2256) - 交互态截图(下拉、弹窗、面板等)→
<page>--<state>.png
微信开发者工具连接
# 用户手动启动自动化端口
& "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 种方式(优先级从高到低)
- CSS 变量(推荐):
--td-button-large-height: 96rpx - 外部样式类:
t-class="my-class"+.my-class { ... !important } - 解除隔离:TDesign 已开启
addGlobalClass,页面样式可直接覆盖 - style 属性:
style="background: #f5f5f5;"
原则:TDesign 组件适合"接近默认样式"的场景;与原型差异大时,原生 view 实现更可控。
十、认证联调指南
10.1 环境准备
后端:
# .env.local
WX_DEV_MODE=true # 开启开发模式,跳过真实微信 code2Session
JWT_SECRET_KEY=dev-secret
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 登录
# 默认创建 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 交互逻辑对小程序没有参考价值
推荐做法:
- 提供渲染后的最终 DOM(浏览器中 Copy outerHTML),保存为
docs/h5_ui/rendered/<page-name>.html - 去掉所有
<script>标签,交互逻辑单独用结构化文本描述 - 内联关键样式(二选一):
- 方式 A:手动在 Chrome DevTools → Computed 面板记录关键属性计算值
- 方式 B(推荐):在 Chrome Console 执行批量导出脚本
批量导出脚本:
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:
{
"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,格式:
# 页面名:<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 每页准备流程
- 在 Chrome 中打开 H5 页面,切换 iPhone 6/7/8 设备模式
- 运行
exportStyles()脚本,输出追加到computed-styles.json - 截图各状态,放入
screenshots/ - 写交互说明,放入
interactions/ - 检查图标,补充映射表
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 栏、筛选栏、分组标题。
/* 顶部 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值需要考虑状态栏高度(通过 JSwx.getSystemInfoSync().statusBarHeight获取) - 多个 sticky 元素叠加时,
z-index从上到下递减(20 → 15 → 10) - 如果页面使用
navigationStyle: 'custom',sticky top 需要加上statusBarHeight + 导航栏高度
14.3 底部固定栏实现
task-detail、coach-detail、customer-detail 等页面底部有固定操作栏(问问助手 + 备注)。
<!-- 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>
/* 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 逐段截图方案(推荐)
# 提取 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 截图时:
- 先清空 scrollIntoView:
page.setData({ scrollIntoView: '' }) - 设置目标:
page.setData({ scrollIntoView: 'section-xxx' }) - 等待 1000ms 让滚动和渲染完成
- 截图(645×1128 物理像素 = 430×752 逻辑像素)
两端截图从顶部自然对齐,无需裁剪。
filter-bar 高度统一约束
所有带 filter-bar 的看板页面(board-finance、board-coach、board-customer),filter-bar 统一高度为 70 逻辑像素。这确保两端 sticky 区域高度一致(~116px),截图自然对齐。
编辑小程序前端页面时须 check 并修正不符合此高度的 filter-bar(见需求 11.8)。
短页面对比
短页面或无锚点配置的页面仍可使用全页面对比:
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 遮罩 + 弹窗通用实现模式
<!-- WXML 通用模式 -->
<!-- 遮罩层 -->
<view
wx:if="{{showPopup}}"
class="overlay"
bindtap="onClosePopup"
catchtouchmove="preventScroll"
></view>
<!-- 弹窗内容 -->
<view
wx:if="{{showPopup}}"
class="popup popup--bottom"
catchtouchmove="preventScroll"
>
<!-- 弹窗内容 -->
</view>
/* 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); }
}
// 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 实现:
<!-- 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>
.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);
}
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 中判断:
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 四种状态。统一模式:
// 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();
},
<!-- 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>
/* 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)的统一实现:
<!-- 备注弹窗 -->
<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→小程序迁移的唯一权威参考。 原文件已废弃,不再维护。