微信小程序页面迁移校验之前 P5任务处理之前

This commit is contained in:
Neo
2026-03-09 01:19:21 +08:00
parent 263bf96035
commit 6e20987d2f
1112 changed files with 153824 additions and 219694 deletions

View File

@@ -0,0 +1,476 @@
# H5 → 微信小程序转换避坑指南
> 基于本项目 `docs/h5_ui/` 原型与 `apps/miniprogram/miniprogram/pages/` 已转换页面的实际对比,结合微信小程序官方文档整理。
> 适用于后续页面开发(如 board-coach、task-list、customer-detail 等)的快速参考。
---
## 一、WXML vs HTML — 标签与结构
### 1.1 标签映射表
| H5 (HTML) | 小程序 (WXML) | 说明 |
|---------------------|----------------------|------|
| `<div>` | `<view>` | 最基础的容器 |
| `<span>` / `<p>` | `<text>` | 文本必须用 `<text>` 包裹才可选中/换行 |
| `<a href="...">` | `<navigator url="">` | 或用 `wx.navigateTo()` 编程式跳转 |
| `<img src="...">` | `<image src="" mode="">` | 必须指定 `mode`,默认 320×240 |
| `<input>` | `<input>``<t-input>` | 原生 input 事件名不同;推荐 TDesign |
| `<textarea>` | `<textarea>``<t-textarea>` | 同上 |
| `<button>` | `<button>``<t-button>` | 小程序 button 有 `open-type` 能力 |
| `<ul>/<li>` | `<view wx:for="{{list}}">` | 没有列表语义标签 |
| `<select>` | `<picker>``<t-picker>` | 完全不同的交互模式 |
| `<label for="id">` | `<label for="id">` | 支持,但 for 只能绑定 checkbox/radio/switch |
| `<svg>` 内联 | `<image src="xx.svg">` | 不支持内联 SVG只能作为图片引用 |
| `<iframe>` | `<web-view>` | 需配置业务域名白名单 |
### 1.2 实际对比login 页面
**H5 原型** — 内联 SVG 图标:
```html
<svg class="w-14 h-14 text-white" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="10" .../>
</svg>
```
**小程序转换** — 改为 image 引用:
```xml
<image class="logo-icon" src="/assets/icons/logo-billiard.svg" mode="aspectFit" />
```
> **坑**:小程序不支持内联 SVG。所有 SVG 图标需提取为独立 `.svg` 文件放到 `assets/icons/`,通过 `<image>` 引用。
### 1.3 不存在的标签/属性
| H5 特性 | 小程序替代方案 |
|---------|---------------|
| `<h1>`~`<h6>` | `<text>` + 样式类 |
| `<table>` | `<view>` 手动布局 |
| `<form>` + `<input name>` | 小程序 `<form>` 或直接 `setData` 收集 |
| `onclick="fn()"` | `bindtap="fn"``bind:tap="fn"` |
| `class="a b c"` 动态 | `class="base {{condition ? 'a' : 'b'}}"` |
| `innerHTML` | `<rich-text nodes="{{html}}">` |
---
## 二、WXSS vs CSS — 样式差异
### 2.1 支持的选择器(有限)
| 选择器 | 支持 | 说明 |
|--------|------|------|
| `.class` | ✅ | |
| `#id` | ✅ | |
| `element` (如 `view`) | ✅ | |
| `element, element` | ✅ | 群组选择器 |
| `::before` / `::after` | ✅ | |
| `*` 通配符 | ❌ | Tailwind 的 `*` reset 全部失效 |
| `>` 子选择器 | ⚠️ | 部分版本支持,不推荐依赖 |
| `+` / `~` 兄弟选择器 | ⚠️ | 同上 |
| `:nth-child()` | ⚠️ | 部分支持 |
| `@media` | ✅ | 支持,但用 rpx 更好 |
### 2.2 rpx 单位 — 最大差异
H5 用 `px`/`rem`/`vw`,小程序用 `rpx`responsive pixel
- 屏幕宽度固定 = 750rpx
- iPhone6 上 1rpx = 0.5px,即 1px = 2rpx
- 设计稿以 750px 宽为基准时,数值直接写 rpx
**实际对比login 页面**
H5 原型Tailwind
```html
<div class="w-24 h-24 rounded-3xl"> <!-- 96px × 96px -->
```
小程序转换:
```css
.logo-box {
width: 192rpx; /* 96px × 2 = 192rpx */
height: 192rpx;
border-radius: 48rpx;
}
```
> **换算规则**H5 的 px 值 × 2 = rpx 值(基于 750 宽设计稿)。
### 2.2.1 H5 → 小程序全局缩放比例87.5%
H5 原型基于 375px 视口设计iPhone SE/6/7/8直接 ×2 转 rpx 后在大屏手机iPhone 15 Pro Max430pt 宽)上元素偏大。经实测对比,对所有 rpx 值统一乘以 0.875 缩放系数后视觉效果最佳。
**规则**
- 尺寸、间距、圆角、阴影偏移:`H5 px × 2 × 0.875` = 最终 rpx取偶数
- 字号:同上规则,如 `text-2xl`(24px) → 48rpx × 0.875 = 42rpx
- t-icon size同上规则`w-5`(20px) → 40rpx × 0.875 = 35rpx
- `max-width` 等约束宽度:同上规则
- 背景纹理间距(如十字纹 `bg-pattern`):不缩放,保持原值
**换算速查**(常用 Tailwind 值):
| Tailwind | H5 px | 原始 rpx | ×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 |
| `w-10` / `h-10` (40px) | 40 | 80 | 70 | 70 |
| `w-14` / `h-14` (56px) | 56 | 112 | 98 | 98 |
| `w-24` / `h-24` (96px) | 96 | 192 | 168 | 168 |
| `w-28` / `h-28` (112px) | 112 | 224 | 196 | 196 |
| `rounded-xl` (12px) | 12 | 24 | 21 | 22 |
| `rounded-2xl` (16px) | 16 | 32 | 28 | 28 |
| `rounded-3xl` (24px) | 24 | 48 | 42 | 42 |
> **来源**no-permission 页面实测确定。先后尝试了非统一缩放、80%、87.5% 三种方案87.5% 在 iPhone 15 Pro Max 上与 H5 原型视觉一致度最高。后续所有页面转换统一使用此系数。
### 2.3 Tailwind CSS → 手写 WXSS
小程序不支持 Tailwind CSS无构建链集成所有 Tailwind 工具类必须手写为 WXSS。
| Tailwind 类 | WXSS 等价写法 |
|-------------|--------------|
| `flex flex-col items-center` | `display: flex; flex-direction: column; align-items: center;` |
| `gap-4` | `gap: 32rpx;`4 × 8px × 2rpx |
| `p-5` | `padding: 40rpx;` |
| `rounded-2xl` | `border-radius: 32rpx;` |
| `text-sm` | `font-size: 28rpx;` |
| `text-gray-7` | `color: #8b8b8b;` |
| `bg-white/60` | `background: rgba(255,255,255,0.6);` |
| `backdrop-blur-sm` | ❌ 不支持 `backdrop-filter` |
| `shadow-lg` | `box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.06);` |
| `min-h-screen` | `min-height: 100vh;` |
### 2.4 不支持/有差异的 CSS 特性
| CSS 特性 | 小程序支持情况 | 替代方案 |
|----------|---------------|---------|
| `backdrop-filter: blur()` | ❌ 不支持 | 用半透明背景色模拟 |
| `position: fixed` | ⚠️ 部分场景异常 | 用 `position: sticky` 或组件自带吸顶 |
| `@import url()` 远程 | ❌ | 只支持本地 `@import "path.wxss"` |
| `@font-face` 远程字体 | ⚠️ | 需 `wx.loadFontFace()` 动态加载 |
| CSS 变量 `var()` | ✅ 支持 | TDesign 大量使用 |
| `linear-gradient` | ✅ 支持 | 正常使用 |
| `animation` / `@keyframes` | ✅ 支持 | 正常使用 |
| `transition` | ✅ 支持 | 正常使用 |
| `env(safe-area-inset-*)` | ✅ 支持 | 刘海屏适配必用 |
### 2.5 样式作用域
- `app.wxss` = 全局样式
- 页面 `.wxss` = 仅当前页面生效(自动隔离)
- 组件 `.wxss` = 默认隔离(`styleIsolation: 'isolated'`
> **坑**H5 的全局 CSS reset如 `* { box-sizing: border-box; }`)在小程序中无效。需要在每个需要的元素上手动设置 `box-sizing`。
---
## 三、事件系统 — 最容易踩坑
### 3.1 事件绑定对比
| H5 | 小程序 | 说明 |
|----|--------|------|
| `onclick="fn()"` | `bindtap="fn"` | 不能传参! |
| `onclick="fn(1)"` | `data-id="1" bindtap="fn"` | 通过 dataset 传参 |
| `addEventListener` | 不支持 | 只能在 WXML 中声明式绑定 |
| `event.target.value` | `e.detail.value` | 取值路径不同 |
| `event.preventDefault()` | `catchtap` | 用 catch 前缀阻止冒泡 |
| `event.stopPropagation()` | `catchtap` | 同上 |
### 3.2 传参方式
**H5**
```html
<button onclick="handleClick(item.id, item.name)">点击</button>
```
**小程序**
```xml
<view data-id="{{item.id}}" data-name="{{item.name}}" bindtap="handleClick">点击</view>
```
```typescript
handleClick(e: WechatMiniprogram.TouchEvent) {
const { id, name } = e.currentTarget.dataset
}
```
> **坑**`data-` 属性名会自动转换 — 连字符转驼峰(`data-user-id` → `dataset.userId`),大写转小写(`data-userId` → `dataset.userid`)。
### 3.3 实际对比login 页面的协议勾选
**H5 原型**
```html
<input type="checkbox" id="agreeCheckbox" onchange="updateButtonState()">
<script>
checkbox.addEventListener('change', updateButtonState);
</script>
```
**小程序转换**
```xml
<view class="agreement" bindtap="onAgreeChange">
<view class="checkbox {{agreed ? 'checkbox--checked' : ''}}">
<t-icon wx:if="{{agreed}}" name="check" size="20rpx" color="#fff" />
</view>
</view>
```
```typescript
onAgreeChange() {
this.setData({ agreed: !this.data.agreed })
}
```
> **坑**:小程序没有原生 checkbox 的 `checked` 双向绑定,需要手动用 `setData` + 条件样式类模拟。
---
## 四、数据绑定与渲染
### 4.1 模板语法对比
| 功能 | H5 (原生/框架) | 小程序 WXML |
|------|----------------|-------------|
| 插值 | `${variable}` / `{{variable}}` | `{{variable}}` |
| 条件渲染 | `if/else` + DOM 操作 | `wx:if` / `wx:elif` / `wx:else` |
| 列表渲染 | `forEach` + `innerHTML` | `wx:for="{{list}}" wx:key="id"` |
| 显示/隐藏 | `style.display = 'none'` | `hidden="{{!show}}"``wx:if` |
| 动态 class | `classList.toggle()` | `class="base {{active ? 'on' : ''}}"` |
| 动态 style | `element.style.color = 'red'` | `style="color: {{color}};"` |
### 4.2 wx:if vs hidden
```xml
<!-- wx:if条件为 false 时不渲染 DOM切换时销毁/重建 -->
<view wx:if="{{status === 'pending'}}">审核中</view>
<!-- hidden始终渲染只切换 display -->
<view hidden="{{status !== 'pending'}}">审核中</view>
```
- 频繁切换 → 用 `hidden`(避免重复创建销毁)
- 初始条件不太可能变 → 用 `wx:if`(减少初始渲染量)
### 4.3 实际对比reviewing 页面的条件渲染
**H5 原型**:只有一种状态(审核中),用静态 HTML。
**小程序转换**:支持 pending/rejected 两种状态,用 `wx:if` 动态切换:
```xml
<view class="top-gradient top-gradient--{{status}}"></view>
<t-icon wx:if="{{status === 'pending'}}" name="time" size="112rpx" color="#fff" />
<t-icon wx:else name="close-circle" size="112rpx" color="#fff" />
<text class="main-title">{{status === 'pending' ? '申请审核中' : '申请未通过'}}</text>
```
> **坑**`wx:if` 中的表达式必须在 `{{}}` 内,且不支持复杂 JS 表达式(如函数调用)。需要复杂逻辑时用 WXS 或在 JS 中预处理好数据。
---
## 五、路由与导航
### 5.1 对比
| H5 | 小程序 | 说明 |
|----|--------|------|
| `window.location.href = 'xx.html'` | `wx.navigateTo({ url: '/pages/xx/xx' })` | 保留当前页,跳新页 |
| `window.location.replace()` | `wx.redirectTo()` | 关闭当前页,跳新页 |
| `history.back()` | `wx.navigateBack()` | 返回上一页 |
| 无直接等价 | `wx.reLaunch()` | 关闭所有页面,打开新页 |
| 无直接等价 | `wx.switchTab()` | 跳转 tabBar 页面 |
### 5.2 实际对比reviewing 页面的"更换账号"
**H5 原型**
```javascript
function switchAccount() {
localStorage.clear();
window.location.href = 'login.html';
}
```
**小程序转换**
```typescript
onSwitchAccount() {
const app = getApp<IAppOption>()
app.globalData.token = undefined
wx.removeStorageSync("token")
wx.removeStorageSync("refreshToken")
wx.reLaunch({ url: "/pages/login/login" }) // 清空页面栈
}
```
> **坑**
> - 页面栈最多 10 层,`navigateTo` 超过会静默失败
> - 跳转 tabBar 页面必须用 `switchTab`,用 `navigateTo` 会报错
> - 路径必须以 `/` 开头,且不带 `.wxml` 后缀
> - `reLaunch` 会销毁所有页面,适合登录/登出等场景
---
## 六、存储与网络
### 6.1 本地存储
| H5 | 小程序 | 说明 |
|----|--------|------|
| `localStorage.setItem(k, v)` | `wx.setStorageSync(k, v)` | 同步写入 |
| `localStorage.getItem(k)` | `wx.getStorageSync(k)` | 同步读取 |
| `localStorage.removeItem(k)` | `wx.removeStorageSync(k)` | 同步删除 |
| `localStorage.clear()` | `wx.clearStorageSync()` | 清空全部 |
| 上限 ~5MB | 上限 10MB | 小程序更大 |
> **坑**:小程序没有 Cookie登录态必须自行通过 Storage + header token 管理。
### 6.2 网络请求
| H5 | 小程序 | 说明 |
|----|--------|------|
| `fetch()` / `XMLHttpRequest` | `wx.request()` | 需配置域名白名单 |
| 无限制 | 并发上限 10 个 | 超出排队 |
| 任意域名 | 必须 HTTPS + 白名单 | 开发时可关闭校验 |
---
## 七、TDesign 组件替代 H5 原生元素
本项目使用 TDesign 小程序组件库,以下是常见替代关系:
| H5 原型元素 | TDesign 组件 | 注意事项 |
|-------------|-------------|---------|
| `<button>` | `<t-button>` | 用 CSS 变量定制样式,如 `--td-button-large-height` |
| `<input>` | `<t-input>` | 事件是 `bind:change` 而非 `bindinput` |
| `<textarea>` | `<t-textarea>` | 同上 |
| `<select>` | `<t-picker>` | 完全不同的交互 |
| `<checkbox>` | `<t-checkbox>` | 或手动实现(如 login 页) |
| `<radio>` | `<t-radio>` / `<t-radio-group>` | `bind:change` 取值 |
| SVG 图标 | `<t-icon name="xxx">` | TDesign 内置图标库 |
| 加载动画 | `<t-loading>` | 替代 CSS spinner |
| 弹窗 | `<t-dialog>` / `<t-toast>` | 替代 `alert()` / `confirm()` |
| 下拉刷新 | 页面 `onPullDownRefresh` | 在 page.json 中 `enablePullDownRefresh: true` |
### TDesign 样式覆盖 4 种方式
1. **CSS 变量**(推荐):`--td-button-large-height: 96rpx`
2. **外部样式类**`t-class="my-class"` + `.my-class { ... !important }`
3. **解除隔离**TDesign 已开启 `addGlobalClass`,页面样式可直接覆盖
4. **style 属性**`style="background: #f5f5f5; border-radius: 16rpx;"`
**实际示例**login 页面按钮定制):
```css
.login-btn {
--td-button-large-height: 96rpx !important;
--td-button-large-font-size: 32rpx !important;
--td-button-border-radius: 24rpx !important;
}
.login-btn--active {
background: linear-gradient(135deg, #0052d9, #3b82f6) !important;
box-shadow: 0 12rpx 32rpx rgba(0, 82, 217, 0.3);
}
```
---
## 八、高频踩坑清单
### 8.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>` |
### 8.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 变量覆盖 |
### 8.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 替代 |
### 8.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 |
---
## 九、转换 Checklist新页面开发用
开发新页面时,按此清单逐项检查:
- [ ] HTML 标签全部替换为 WXML 组件(`div→view``span→text``img→image`
- [ ] 内联 SVG 提取为文件,改用 `<image>``<t-icon>`
- [ ] Tailwind 类全部手写为 WXSSpx × 2 × 0.875 = rpx见 §2.2.1 缩放规则)
- [ ] `backdrop-filter` 等不支持的 CSS 改为替代方案
- [ ] 事件绑定改为 `bindtap` / `bind:change`,传参用 `data-*`
- [ ] `alert/confirm` 改为 `wx.showToast` / `wx.showModal`
- [ ] `localStorage` 改为 `wx.setStorageSync`
- [ ] 路由跳转改为 `wx.navigateTo` / `wx.reLaunch`
- [ ] 表单收集改为 `setData` + 事件回调
- [ ] 图片设置 `mode` 属性(`aspectFit` / `aspectFill` / `widthFix`
- [ ] 列表渲染加 `wx:key`
- [ ] 布尔属性用 `{{}}` 包裹(`checked="{{true}}"`
- [ ] TDesign 组件在页面 `.json` 中注册 `usingComponents`
- [ ] 安全区适配:`padding-top: env(safe-area-inset-top)`
- [ ] 页面配置:`enablePullDownRefresh``navigationBarTitleText`
---
## 十、board-customer 迁移经验补充
> 来源board-customer 页面 8 维度卡片迁移实战2026-03-07
### 10.1 复杂维度用独立布局
最专一维度的助教明细表不适合用通用的 `card-mid-row``card-grid`,直接用独立的 `loyal-table` 布局(左侧竖线 `border-left: 4rpx solid #eee` + 表头 + 数据行)。同时在助教行的 `wx:if` 中排除 `dimType !== 'loyal'`,避免信息重复。
**关键**:当某个维度的卡片结构与其他维度差异过大时,不要硬套通用模板,直接写独立布局更清晰。
### 10.2 heart-icon 组件TS observer 替代 WXS
小程序 WXS 不支持 emoji surrogate pair`\uD83D\uDC96`),渲染为乱码。解决方案:
- 用 TS `observers` 监听 `score` 属性变化,计算对应 emoji 字符串
- WXML 中用 `{{heartEmoji}}` 数据绑定渲染
- 样式:`font-size: 22rpx; line-height: 1; position: relative; top: -4rpx` 和文字对齐
### 10.3 助教字体颜色三态 + badge 渐变
助教名字颜色规则(通过 CSS class 控制):
- 跟 badge → `.assistant--assignee``color: #e34d59; font-weight: 700`(红色加粗)
- 弃 badge → `.assistant--abandoned``color: #a6a6a6`(灰色)
- 无 badge → `.assistant--normal``color: #242424`(黑色)
Badge 样式(白字 + 渐变背景 + 阴影):
- 跟:`background: linear-gradient(135deg, #e34d59, #f26a76); box-shadow: 0 2rpx 6rpx rgba(227,77,89,0.28)`
- 弃:`background: linear-gradient(135deg, #d4d4d4, #b6b6b6); box-shadow: 0 2rpx 6rpx rgba(0,0,0,0.14)`