Files
Neo-ZQYY/_DEL/h5-to-mp-bridge.md
2026-03-15 10:15:02 +08:00

1569 lines
62 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# H5 原型 → 微信小程序迁移桥接规范
版本v1.0
基于:`docs/h5_ui/` 原型代码穷举审阅 + Tailwind CSS v3 CDN 默认配置 + 微信 WXSS rpx 官方定义
适用:本项目所有 H5 原型页面到小程序页面的迁移工作
---
## 0. 尺寸换算基础(核心依据)
### 0.1 Tailwind CSS 在 412px 设备上的渲染行为
本项目 H5 原型使用 `<script src="https://cdn.tailwindcss.com"></script>`Tailwind CSS v3 CDN Play 模式)。
Tailwind v3 默认主题中,大量 spacing、font-size、border-radius token 主要使用 `rem` 体系(默认 spacing scale 中除 `0`0px`px`1px其余均为 rem但 Tailwind 并非所有尺寸都基于 rem仍可能出现 px、百分比`w-1/2`)、分数、视口单位(`min-h-screen`)、自定义 arbitrary 值(`h-[38px]`)等。
在浏览器默认根字号 `1rem = 16px` 且页面未主动改写 `html { font-size }` 的前提下,这些 rem 体系的 utility 会计算成固定 CSS px 值。这个值不随设备宽度变化——在 320px、375px、412px 的设备上,`p-4``1rem`)始终渲染为 `16px`
原型的 `<meta name="viewport" content="width=device-width, initial-scale=1.0">` 意味着:在浏览器按 `width=device-width` 建立布局视口、且该设备当前等效布局宽度为 412 CSS px 的前提下,页面宽度按 412 CSS px 参与布局。
因此:**Tailwind 默认 rem 体系的 utility class 在任何设备上都会计算为固定的 CSS px 值**,与设备布局宽度无关。但百分比、视口单位、自定义值等非 rem 单位仍会随设备变化。
### 0.2 微信 rpx 的精确定义
来源:[微信官方 WXSS 文档](https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxss.html)
> rpxresponsive pixel可以根据屏幕宽度进行自适应。规定屏幕宽为 750rpx。
换算公式:
```
1rpx = 屏幕宽度px / 750
1px = 750 / 屏幕宽度px × rpx
```
官方示例:
| 设备 | 屏幕宽度 | 1rpx = ?px | 1px = ?rpx |
|------|----------|-----------|-----------|
| iPhone 5 | 320px | 0.42px | 2.34rpx |
| iPhone 6 | 375px | 0.5px | 2rpx |
| iPhone 6 Plus | 414px | 0.552px | 1.81rpx |
### 0.3 本项目的换算链路
验收基准设备:等效宽度 412px
完整链路:`Tailwind class → CSS 计算值(px) → rpx`
```
rpx = CSS_px × (750 / 412)
= CSS_px × 1.82039...
```
但这里有一个关键问题:**rpx 是响应式单位px 是固定单位**。
在 412px 设备上:`750rpx = 412px`,所以 `1rpx ≈ 0.549px`
在 375px 设备上:`750rpx = 375px`,所以 `1rpx = 0.5px`
在 320px 设备上:`750rpx = 320px`,所以 `1rpx ≈ 0.427px`
而 H5 原型中 Tailwind 的 `p-4 = 16px` 在所有设备上都是 16px。如果我们把它转成 `29rpx`16 × 1.82),那么:
- 在 412px 设备上29rpx = 29 × 0.549 = 15.93px ≈ 16px ✓
- 在 375px 设备上29rpx = 29 × 0.5 = 14.5px ✗(比原型小 9.4%
- 在 320px 设备上29rpx = 29 × 0.427 = 12.38px ✗(比原型小 22.6%
这意味着:**用 rpx 无法在所有设备上精确还原 H5 原型的固定 px 值**。rpx 的本质是按屏宽缩放适合保持比例px 的本质是固定绝对尺寸,适合贴近 H5 原型的绝对视觉值。二者不能在所有设备上同时做到完全等价。
### 0.4 本项目的尺寸单位策略
H5 原型是移动端单栏布局,没有使用响应式断点。在 412px 设备上验收。
用 rpx 可以在不同宽度设备上保持相对比例一致;用固定 px 更接近 H5 原型的绝对尺寸视觉。二者不能在所有设备上同时做到完全等价。
**推荐策略:以 rpx 为主、px 为辅的混合单位方案。**
rpx 适用场景(跟屏宽强相关的尺寸):
- 页面外层容器宽度、横向间距、卡片宽度、栅格
- 吸顶区高度、块级间距
- 大尺寸元素的 padding / margin
px 适用场景(需要绝对精度或极小尺寸):
- `1px` 发丝线边框(`border: 1px solid`
- 阴影的模糊半径和扩散半径(`box-shadow` 的 blur/spread
- 极小的装饰元素2px 以下的点、线)
- icon 实际绘制尺寸12/14/16/18/20px 等常见图标尺寸)
- 绝对定位微调badge 偏移 `top`/`right`
- 弹层阴影、蒙层模糊参数
- 评分星星、勾选框、状态点等小控件
- 自定义导航栏中的状态栏补偿(通常结合 `wx.getSystemInfoSync()` 动态计算,不是纯 rpx 公式能解决)
按实际效果决定(不一刀切):
- 文本字号、图标尺寸、圆角、局部 padding
理由:
1. 小程序的主要使用场景是手机,屏幕宽度在 320-428px 之间rpx 的等比缩放在这个范围内视觉差异可接受
2. 微信官方推荐使用 rpx 做适配,但并未要求所有样式都必须用 rpxWXSS 也支持直接写 px
3. 412px 是验收基准,在该设备上 rpx 换算后的值与原型一致
4. 固定 px 在小屏设备上可能导致内容溢出或布局挤压,但在细节尺寸上能保持质感
5. 全面 rpx 在小尺寸上容易出现"比例上没错,但质感不对"的问题
**换算公式rpx 场景使用):**
```
rpx = H5_CSS_px × (750 / 412)
≈ H5_CSS_px × 1.8204
```
取整规则:
- 常规布局值:四舍五入到整数 rpx
- 高频出现的设计 token沉淀成统一变量表如 §1.1 换算表),保持全项目一致
- 对视觉特别敏感的值:以真机截图比对微调,不强行套公式
---
## 1. Tailwind Spacing → rpx 完整换算表
Tailwind v3 的 spacing scale 基于 `0.25rem = 4px` 步进。以下是本项目原型中实际使用的所有 spacing 值的精确换算。这些 rpx 值已在已迁移页面中使用,作为项目统一 token后续页面应保持一致。
### 1.1 Tailwind spacing scale默认
| Tailwind 值 | rem | CSS px | rpx项目 token | 常见用途 |
|-------------|-----|--------|---------------------|---------|
| `0.5` | 0.125rem | 2px | 4rpx | gap-0.5, p-0.5, mt-0.5 |
| `px` | 1px | 1px | 2rpx | py-px, w-px |
| `1` | 0.25rem | 4px | 8rpx | gap-1, p-1, mt-1, mb-1 |
| `1.5` | 0.375rem | 6px | 12rpx | gap-1.5, p-1.5, mt-1.5 |
| `2` | 0.5rem | 8px | 14rpx | gap-2, p-2, mt-2, mb-2 |
| `2.5` | 0.625rem | 10px | 18rpx | gap-2.5, p-2.5, mb-2.5 |
| `3` | 0.75rem | 12px | 22rpx | gap-3, p-3, mb-3, pl-3 |
| `3.5` | 0.875rem | 14px | 26rpx | p-3.5, h-3.5, w-3.5 |
| `4` | 1rem | 16px | 30rpx | gap-4, p-4, px-4, mb-4 |
| `5` | 1.25rem | 20px | 36rpx | p-5, px-5, mb-5, pb-5 |
| `6` | 1.5rem | 24px | 44rpx | gap-6, p-6, px-6, mb-6 |
| `7` | 1.75rem | 28px | 52rpx | h-7, w-7 |
| `8` | 2rem | 32px | 58rpx | px-8, mb-8, pb-8, h-8 |
| `9` | 2.25rem | 36px | 66rpx | h-9, w-9 |
| `10` | 2.5rem | 40px | 72rpx | h-10, w-10, pb-10 |
| `11` | 2.75rem | 44px | 80rpx | h-11, w-11, ml-11 |
| `12` | 3rem | 48px | 88rpx | h-12, w-12, pb-12, left-12 |
| `14` | 3.5rem | 56px | 102rpx | h-14, w-14 |
| `16` | 4rem | 64px | 116rpx | h-16, w-16 |
| `20` | 5rem | 80px | 146rpx | h-20, w-20, top-20 |
| `24` | 6rem | 96px | 174rpx | h-24, w-24 |
| `28` | 7rem | 112px | 204rpx | h-28, w-28 |
| `32` | 8rem | 128px | 232rpx | h-32, w-32 |
| `40` | 10rem | 160px | 292rpx | bottom-40, top-40 |
| `64` | 16rem | 256px | 466rpx | h-64 |
| `72` | 18rem | 288px | 524rpx | w-72 |
### 1.2 Arbitrary spacing 值
| Tailwind class | CSS 值 | rpx |
|---------------|--------|-----|
| `h-[38px]` / `w-[38px]` | 38px | 70rpx |
| `top-[44px]` | 44px | 80rpx |
| `min-h-[2.5rem]` | 40px | 72rpx |
| `max-h-[70vh]` | 70vh | 70vh保留 vh |
---
## 2. Tailwind 字号 → WXSS font-size + line-height 完整换算表
### 2.1 核心事实
Tailwind v3 的字号类(`text-xs``text-sm` 等)同时设置 `font-size``line-height`。这是 Tailwind 的内置行为,不是可选的。
例如 `text-sm` 生成的 CSS 是:
```css
font-size: 0.875rem; /* 14px */
line-height: 1.25rem; /* 20px */
```
如果 WXSS 只写了 `font-size` 而没写 `line-height`,小程序会使用默认行高(约 1.2 倍字号),导致每行文字的垂直空间不足,整体高度偏矮。
**强制规则:每个 Tailwind 字号类转 WXSS 时,必须同时写 `font-size` 和 `line-height`。**
### 2.1.1 微信小程序 text 组件的 line-height 限制 ⚠️
**关键发现**:微信小程序的 `<text>` 组件不能直接设置 `line-height`,必须通过外层 `<view>` 设置。
**正确做法:**
```wxss
/* 全局设置 */
page {
line-height: 1.5; /* Tailwind 默认行高 */
}
view {
line-height: inherit; /* view 继承 page 的 line-height */
}
/* text 会自动继承外层 view 的 line-height不需要额外设置 */
```
**局部覆盖:**
```wxss
.section-title {
font-size: 26rpx;
line-height: 36rpx; /* 在 view 的 class 上设置text 会继承 */
font-weight: 600;
}
```
```wxml
<view class="section-title">
<text>标题文本</text>
</view>
```
**错误做法:**
```wxss
/* ❌ 直接在 text 上设置 line-height 无效 */
text {
line-height: 36rpx; /* 不会生效 */
}
/* ❌ 添加 display: inline-block 也无效 */
text {
display: inline-block;
line-height: 36rpx; /* 仍然不会生效 */
}
```
**原因**:微信小程序的 `<text>` 是特殊的内联组件,直接设置 `line-height` 不生效,必须在外层 `<view>` 上设置。
**验证方法**:在开发者工具的 Computed 面板中text 元素不会显示 `line-height` 属性,但外层 view 的 `height` 值会包含行高效果。
### 2.2 标准字号类换算表
Tailwind v3 默认字号定义来源Tailwind CSS v3 源码 `defaultTheme.js`
| Tailwind class | font-size (rem) | font-size (px) | line-height (rem) | line-height (px) | WXSS font-size | WXSS line-height |
|---------------|----------------|---------------|-------------------|-----------------|----------------|-----------------|
| `text-xs` | 0.75rem | 12px | 1rem | 16px | 22rpx | 30rpx |
| `text-sm` | 0.875rem | 14px | 1.25rem | 20px | 26rpx | 36rpx |
| `text-base` | 1rem | 16px | 1.5rem | 24px | 30rpx | 44rpx |
| `text-lg` | 1.125rem | 18px | 1.75rem | 28px | 32rpx | 52rpx |
| `text-xl` | 1.25rem | 20px | 1.75rem | 28px | 36rpx | 52rpx |
| `text-2xl` | 1.5rem | 24px | 2rem | 32px | 44rpx | 58rpx |
| `text-3xl` | 1.875rem | 30px | 2.25rem | 36px | 54rpx | 66rpx |
### 2.3 Arbitrary 字号换算表
原型中使用的 arbitrary 字号(`text-[Npx]`)没有 Tailwind 内置的 line-height 绑定。浏览器会使用默认行高(通常为 `normal`,约 1.2 倍字号)。
| Tailwind class | font-size (px) | 默认 line-height (px, ×1.2) | WXSS font-size | WXSS line-height |
|---------------|---------------|----------------------------|----------------|-----------------|
| `text-[9px]` | 9px | ~10.8px | 16rpx | 20rpx |
| `text-[10px]` | 10px | ~12px | 18rpx | 22rpx |
| `text-[11px]` | 11px | ~13.2px | 20rpx | 24rpx |
| `text-[12px]` | 12px | ~14.4px | 22rpx | 26rpx |
| `text-[13px]` | 13px | ~15.6px | 24rpx | 28rpx |
注意arbitrary 字号在原型中使用频率极高(`text-[10px]` 157 次、`text-[11px]` 230 次),是本项目的主力小字号。
### 2.4 font-weight 映射
| Tailwind class | CSS font-weight | WXSS |
|---------------|----------------|------|
| `font-normal` | 400 | `font-weight: 400` |
| `font-medium` | 500 | `font-weight: 500` |
| `font-semibold` | 600 | `font-weight: 600` |
| `font-bold` | 700 | `font-weight: 700` |
**注意:`font-medium` 是 500 不是 600。** 这是常见错误。
### 2.5 其他文本属性
| Tailwind class | CSS | WXSS |
|---------------|-----|------|
| `text-center` | `text-align: center` | `text-align: center` |
| `text-right` | `text-align: right` | `text-align: right` |
| `text-left` | `text-align: left` | `text-align: left` |
| `truncate` | `overflow: hidden; text-overflow: ellipsis; white-space: nowrap` | 同左 |
| `line-clamp-2` | `-webkit-line-clamp: 2; display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden` | 同左(小程序支持) |
| `whitespace-nowrap` | `white-space: nowrap` | `white-space: nowrap` |
| `leading-normal` | `line-height: 1.5` | `line-height: 1.5` |
| `leading-relaxed` | `line-height: 1.625` | `line-height: 1.625` |
| `leading-snug` | `line-height: 1.375` | `line-height: 1.375` |
| `tracking-wide` | `letter-spacing: 0.025em` | `letter-spacing: 0.025em` |
| `underline` | `text-decoration: underline` | `text-decoration: underline` |
| `line-through` | `text-decoration: line-through` | `text-decoration: line-through` |
---
## 3. Tailwind 颜色 → WXSS 颜色完整映射
### 3.1 项目自定义颜色tailwind.config.theme.extend.colors
每个 HTML 页面的 `<script>` 中定义了相同的自定义颜色扩展:
| Tailwind 名称 | 色值 | WXSS 变量 |
|--------------|------|----------|
| `primary` | `#0052d9` | `var(--color-primary)` |
| `primary-light` | `#ecf2fe` | `var(--color-primary-light)` |
| `success` | `#00a870` | `var(--color-success)` |
| `warning` | `#ed7b2f` | `var(--color-warning)` |
| `error` | `#e34d59` | `var(--color-error)` |
| `gray-1` | `#f3f3f3` | `var(--color-gray-1)` |
| `gray-2` | `#eeeeee` | `var(--color-gray-2)` |
| `gray-3` | `#e7e7e7` | `var(--color-gray-3)` |
| `gray-4` | `#dcdcdc` | `var(--color-gray-4)` |
| `gray-5` | `#c5c5c5` | `var(--color-gray-5)` |
| `gray-6` | `#a6a6a6` | `var(--color-gray-6)` |
| `gray-7` | `#8b8b8b` | `var(--color-gray-7)` |
| `gray-8` | `#777777` | `var(--color-gray-8)` |
| `gray-9` | `#5e5e5e` | `var(--color-gray-9)` |
| `gray-10` | `#4b4b4b` | `var(--color-gray-10)` |
| `gray-11` | `#393939` | `var(--color-gray-11)` |
| `gray-12` | `#2c2c2c` | `var(--color-gray-12)` |
| `gray-13` | `#242424` | `var(--color-gray-13)` |
### 3.2 Tailwind 内置颜色(原型中实际使用的)
以下是原型中使用的 Tailwind 内置颜色的精确色值Tailwind v3 默认 palette
| Tailwind 色阶 | 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 |
|--------------|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|
| `red` | #fef2f2 | #fee2e2 | #fecaca | #fca5a5 | #f87171 | #ef4444 | #dc2626 | #b91c1c | #991b1b | #7f1d1d |
| `orange` | #fff7ed | #ffedd5 | #fed7aa | #fdba74 | #fb923c | #f97316 | #ea580c | #c2410c | — | — |
| `amber` | #fffbeb | #fef3c7 | #fde68a | #fcd34d | #fbbf24 | #f59e0b | #d97706 | #b45309 | — | — |
| `yellow` | #fefce8 | #fef9c3 | #fef08a | #fde047 | #facc15 | #eab308 | #ca8a04 | #a16207 | — | — |
| `green` | #f0fdf4 | #dcfce7 | #bbf7d0 | #86efac | #4ade80 | #22c55e | #16a34a | #15803d | — | — |
| `emerald` | #ecfdf5 | #d1fae5 | — | #6ee7b7 | — | — | #059669 | #047857 | — | — |
| `teal` | #f0fdfa | — | #99f6e4 | #5eead4 | #2dd4bf | #14b8a6 | — | — | — | — |
| `cyan` | — | #cffafe | — | — | #22d3ee | #06b6d4 | — | #0e7490 | — | — |
| `sky` | — | — | — | #7dd3fc | #38bdf8 | #0ea5e9 | — | — | — | — |
| `blue` | #eff6ff | #dbeafe | — | #93c5fd | #60a5fa | #3b82f6 | — | #1d4ed8 | — | — |
| `indigo` | — | — | — | #a5b4fc | #818cf8 | — | — | — | — | — |
| `violet` | — | — | — | #c4b5fd | #a78bfa | — | — | — | — | — |
| `purple` | — | #f3e8ff | — | — | — | #a855f7 | #9333ea | #7e22ce | — | — |
| `fuchsia` | — | — | — | #f0abfc | #e879f9 | — | — | — | — | — |
| `pink` | — | #fce7f3 | — | #f9a8d4 | #f472b6 | #ec4899 | #db2777 | #be185d | — | — |
| `rose` | — | — | — | #fda4af | #fb7185 | — | — | — | — | — |
### 3.3 透明度变体
原型大量使用 Tailwind 的透明度修饰符(`/N`),如 `bg-primary/10``text-white/60`
WXSS 映射方式:使用 `rgba()` 或直接写带透明度的色值。
| 透明度后缀 | alpha 值 | 示例 |
|-----------|---------|------|
| `/5` | 0.05 | `bg-primary/5``rgba(0, 82, 217, 0.05)` |
| `/10` | 0.10 | `bg-primary/10``rgba(0, 82, 217, 0.10)` |
| `/15` | 0.15 | `bg-white/15``rgba(255, 255, 255, 0.15)` |
| `/20` | 0.20 | `bg-white/20``rgba(255, 255, 255, 0.20)` |
| `/25` | 0.25 | `bg-white/25``rgba(255, 255, 255, 0.25)` |
| `/30` | 0.30 | `bg-primary/30``rgba(0, 82, 217, 0.30)` |
| `/40` | 0.40 | `bg-primary/40``rgba(0, 82, 217, 0.40)` |
| `/50` | 0.50 | `bg-black/50``rgba(0, 0, 0, 0.50)` |
| `/60` | 0.60 | `text-white/60``rgba(255, 255, 255, 0.60)` |
| `/70` | 0.70 | `text-white/70``rgba(255, 255, 255, 0.70)` |
| `/80` | 0.80 | `bg-white/80``rgba(255, 255, 255, 0.80)` |
| `/85` | 0.85 | `text-white/85``rgba(255, 255, 255, 0.85)` |
| `/90` | 0.90 | `text-white/90``rgba(255, 255, 255, 0.90)` |
| `/95` | 0.95 | `bg-white/95``rgba(255, 255, 255, 0.95)` |
---
## 4. Tailwind 布局类 → WXSS 完整映射
### 4.1 Flexbox
| Tailwind class | CSS | WXSS | 频率 |
|---------------|-----|------|------|
| `flex` | `display: flex` | `display: flex` | 1530 |
| `flex-col` | `flex-direction: column` | `flex-direction: column` | 14 |
| `flex-1` | `flex: 1 1 0%` | `flex: 1` | 358 |
| `flex-[2]` | `flex: 2` | `flex: 2` | 1 |
| `flex-shrink-0` | `flex-shrink: 0` | `flex-shrink: 0` | 527 |
| `shrink-0` | `flex-shrink: 0` | `flex-shrink: 0` | 72 |
| `flex-wrap` | `flex-wrap: wrap` | `flex-wrap: wrap` | 26 |
| `items-center` | `align-items: center` | `align-items: center` | 1432 |
| `items-start` | `align-items: flex-start` | `align-items: flex-start` | 56 |
| `items-end` | `align-items: flex-end` | `align-items: flex-end` | 9 |
| `items-baseline` | `align-items: baseline` | `align-items: baseline` | 8 |
| `items-stretch` | `align-items: stretch` | `align-items: stretch` | 1 |
| `justify-between` | `justify-content: space-between` | `justify-content: space-between` | 395 |
| `justify-center` | `justify-content: center` | `justify-content: center` | 341 |
| `justify-end` | `justify-content: flex-end` | `justify-content: flex-end` | 55 |
| `justify-start` | `justify-content: flex-start` | `justify-content: flex-start` | 3 |
### 4.2 Grid
| Tailwind class | CSS | WXSS | 频率 |
|---------------|-----|------|------|
| `grid` | `display: grid` | `display: grid` | 48 |
| `grid-cols-2` | `grid-template-columns: repeat(2, minmax(0, 1fr))` | `grid-template-columns: repeat(2, minmax(0, 1fr))` | 8 |
| `grid-cols-3` | `grid-template-columns: repeat(3, minmax(0, 1fr))` | `grid-template-columns: repeat(3, minmax(0, 1fr))` | 11 |
| `grid-cols-4` | `grid-template-columns: repeat(4, minmax(0, 1fr))` | `grid-template-columns: repeat(4, minmax(0, 1fr))` | 29 |
### 4.3 Gap间距
| Tailwind class | CSS | WXSS |
|---------------|-----|------|
| `gap-0.5` | `gap: 2px` | `gap: 4rpx` |
| `gap-1` | `gap: 4px` | `gap: 8rpx` |
| `gap-1.5` | `gap: 6px` | `gap: 12rpx` |
| `gap-2` | `gap: 8px` | `gap: 14rpx` |
| `gap-2.5` | `gap: 10px` | `gap: 18rpx` |
| `gap-3` | `gap: 12px` | `gap: 22rpx` |
| `gap-4` | `gap: 16px` | `gap: 30rpx` |
| `gap-5` | `gap: 20px` | `gap: 36rpx` |
| `gap-6` | `gap: 24px` | `gap: 44rpx` |
| `gap-x-4` | `column-gap: 16px` | `column-gap: 30rpx` |
| `gap-y-1` | `row-gap: 4px` | `row-gap: 8rpx` |
### 4.4 定位
| Tailwind class | CSS | WXSS | 频率 |
|---------------|-----|------|------|
| `relative` | `position: relative` | `position: relative` | 64 |
| `absolute` | `position: absolute` | `position: absolute` | 56 |
| `fixed` | `position: fixed` | `position: fixed` | 23 |
| `sticky` | `position: sticky` | 见 §6.3 兼容性说明 | 11 |
| `inset-0` | `inset: 0` | `top: 0; right: 0; bottom: 0; left: 0` | 14 |
| `top-0` | `top: 0` | `top: 0` | 10 |
| `bottom-0` | `bottom: 0` | `bottom: 0` | 33 |
| `left-0` | `left: 0` | `left: 0` | 8 |
| `right-0` | `right: 0` | `right: 0` | 35 |
| `top-1/2` | `top: 50%` | `top: 50%` | 5 |
| `left-1/2` | `left: 50%` | `left: 50%` | 5 |
| `-translate-x-1/2` | `transform: translateX(-50%)` | `transform: translateX(-50%)` | 5 |
| `-translate-y-1/2` | `transform: translateY(-50%)` | `transform: translateY(-50%)` | 5 |
| `z-10` | `z-index: 10` | `z-index: 10` | 13 |
| `z-20` | `z-index: 20` | `z-index: 20` | 3 |
| `z-50` | `z-index: 50` | `z-index: 50` | 12 |
| `z-[100]` | `z-index: 100` | `z-index: 100` | 5 |
### 4.5 尺寸
| Tailwind class | CSS | WXSS |
|---------------|-----|------|
| `w-full` | `width: 100%` | `width: 100%` |
| `h-full` | `height: 100%` | `height: 100%` |
| `w-1/2` | `width: 50%` | `width: 50%` |
| `min-w-0` | `min-width: 0` | `min-width: 0` |
| `min-h-screen` | `min-height: 100vh` | `min-height: 100vh` |
| `max-w-sm` | `max-width: 24rem (384px)` | `max-width: 700rpx` |
| `max-w-xs` | `max-width: 20rem (320px)` | `max-width: 582rpx` |
固定尺寸参照 §1.1 spacing 换算表。
### 4.6 显示与溢出
| Tailwind class | CSS | WXSS |
|---------------|-----|------|
| `block` | `display: block` | `display: block` |
| `inline-block` | `display: inline-block` | `display: inline-block` |
| `hidden` | `display: none` | `display: none` |
| `overflow-hidden` | `overflow: hidden` | `overflow: hidden` |
| `overflow-x-auto` | `overflow-x: auto` | `overflow-x: auto` |
| `overflow-y-auto` | `overflow-y: auto` | `overflow-y: auto` |
### 4.7 space-y子元素间距
`space-y-N` 通过给子元素(除第一个外)添加 `margin-top` 实现。WXSS 中需要手动实现。
| Tailwind class | CSS 效果 | WXSS 实现 |
|---------------|---------|----------|
| `space-y-0` | 子元素间距 0 | 子元素 `margin-top: 0` |
| `space-y-1` | 子元素间距 4px | 子元素 `margin-top: 8rpx`(首个除外) |
| `space-y-1.5` | 子元素间距 6px | 子元素 `margin-top: 12rpx`(首个除外) |
| `space-y-2` | 子元素间距 8px | 子元素 `margin-top: 14rpx`(首个除外) |
| `space-y-2.5` | 子元素间距 10px | 子元素 `margin-top: 18rpx`(首个除外) |
| `space-y-3` | 子元素间距 12px | 子元素 `margin-top: 22rpx`(首个除外) |
| `space-y-4` | 子元素间距 16px | 子元素 `margin-top: 30rpx`(首个除外) |
**实现方式**:在 WXSS 中用 `.parent > view + view { margin-top: Nrpx; }` 或对每个子元素单独加 class。小程序支持 `>``+` 选择器。
### 4.8 divide-y分割线
| Tailwind class | CSS 效果 | WXSS 实现 |
|---------------|---------|----------|
| `divide-y` | 子元素之间 1px 上边框 | 子元素 `border-top: 1px solid` (首个除外) |
| `divide-gray-100` | 分割线颜色 #f3f4f6 | `border-color: #f3f4f6` |
---
## 5. Tailwind 装饰类 → WXSS 完整映射
### 5.1 圆角
| Tailwind class | CSS | WXSS |
|---------------|-----|------|
| `rounded-sm` | `border-radius: 2px` | `border-radius: 4rpx` |
| `rounded` | `border-radius: 4px` | `border-radius: 8rpx` |
| `rounded-lg` | `border-radius: 8px` | `border-radius: 14rpx` |
| `rounded-xl` | `border-radius: 12px` | `border-radius: 22rpx` |
| `rounded-2xl` | `border-radius: 16px` | `border-radius: 30rpx` |
| `rounded-3xl` | `border-radius: 24px` | `border-radius: 44rpx` |
| `rounded-full` | `border-radius: 9999px` | `border-radius: 50%``border-radius: 999rpx` |
| `rounded-t` | `border-top-left/right-radius: 4px` | `border-top-left-radius: 8rpx; border-top-right-radius: 8rpx` |
| `rounded-t-3xl` | `border-top-left/right-radius: 24px` | `border-top-left-radius: 44rpx; border-top-right-radius: 44rpx` |
| `rounded-b-2xl` | `border-bottom-left/right-radius: 16px` | `border-bottom-left-radius: 30rpx; border-bottom-right-radius: 30rpx` |
| `rounded-tl-sm` | `border-top-left-radius: 2px` | `border-top-left-radius: 4rpx` |
| `rounded-tr-sm` | `border-top-right-radius: 2px` | `border-top-right-radius: 4rpx` |
### 5.2 阴影
| Tailwind class | CSS | WXSS |
|---------------|-----|------|
| `shadow-sm` | `box-shadow: 0 1px 2px 0 rgba(0,0,0,0.05)` | `box-shadow: 0 2rpx 4rpx 0 rgba(0,0,0,0.05)` |
| `shadow-lg` | `box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1)` | `box-shadow: 0 18rpx 28rpx -6rpx rgba(0,0,0,0.1), 0 8rpx 12rpx -8rpx rgba(0,0,0,0.1)` |
| `shadow-xl` | `box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1)` | `box-shadow: 0 36rpx 46rpx -10rpx rgba(0,0,0,0.1), 0 14rpx 18rpx -12rpx rgba(0,0,0,0.1)` |
带颜色的阴影(原型中使用的):
| Tailwind class | WXSS |
|---------------|------|
| `shadow-primary/20` | `box-shadow: 0 2rpx 4rpx 0 rgba(0,82,217,0.2)` |
| `shadow-primary/30` | `box-shadow: 0 2rpx 4rpx 0 rgba(0,82,217,0.3)` |
| `shadow-error/30` | `box-shadow: 0 2rpx 4rpx 0 rgba(227,77,89,0.3)` |
| `shadow-warning/30` | `box-shadow: 0 2rpx 4rpx 0 rgba(237,123,47,0.3)` |
| `shadow-gray-200/50` | `box-shadow: 0 2rpx 4rpx 0 rgba(229,231,235,0.5)` |
| `shadow-orange-500/30` | `box-shadow: 0 2rpx 4rpx 0 rgba(249,115,22,0.3)` |
| `shadow-pink-500/30` | `box-shadow: 0 2rpx 4rpx 0 rgba(236,72,153,0.3)` |
| `shadow-teal-500/30` | `box-shadow: 0 2rpx 4rpx 0 rgba(20,184,166,0.3)` |
### 5.3 边框
| Tailwind class | CSS | WXSS |
|---------------|-----|------|
| `border` | `border: 1px solid` | `border: 1px solid` |
| `border-0` | `border: 0` | `border: 0` |
| `border-2` | `border: 2px solid` | `border: 2px solid` |
| `border-b` | `border-bottom: 1px solid` | `border-bottom: 1px solid` |
| `border-t` | `border-top: 1px solid` | `border-top: 1px solid` |
| `border-r` | `border-right: 1px solid` | `border-right: 1px solid` |
| `border-l-2` | `border-left: 2px solid` | `border-left: 2px solid` |
| `border-transparent` | `border-color: transparent` | `border-color: transparent` |
边框颜色:直接使用 §3 中的颜色映射,加上 `border-color: xxx`
### 5.4 渐变
原型大量使用渐变背景(`bg-gradient-to-br` 190 次、`bg-gradient-to-r` 62 次)。
| Tailwind class | CSS | WXSS |
|---------------|-----|------|
| `bg-gradient-to-r` | `background-image: linear-gradient(to right, ...)` | `background: linear-gradient(to right, ...)` |
| `bg-gradient-to-br` | `background-image: linear-gradient(to bottom right, ...)` | `background: linear-gradient(to bottom right, ...)` |
| `bg-gradient-to-b` | `background-image: linear-gradient(to bottom, ...)` | `background: linear-gradient(to bottom, ...)` |
| `from-X` | 渐变起始色 | 渐变第一个色值 |
| `to-X` | 渐变结束色 | 渐变最后一个色值 |
示例:`bg-gradient-to-br from-blue-400 to-blue-500`
→ WXSS: `background: linear-gradient(to bottom right, #60a5fa, #3b82f6)`
### 5.5 透明度与模糊
| Tailwind class | CSS | WXSS | 小程序兼容性 |
|---------------|-----|------|-------------|
| `opacity-30` | `opacity: 0.3` | `opacity: 0.3` | ✓ |
| `opacity-55` | `opacity: 0.55` | `opacity: 0.55` | ✓ |
| `opacity-60` | `opacity: 0.6` | `opacity: 0.6` | ✓ |
| `opacity-90` | `opacity: 0.9` | `opacity: 0.9` | ✓ |
| `backdrop-blur-sm` | `backdrop-filter: blur(4px)` | 见 §6.2 | ⚠️ 部分支持 |
| `backdrop-blur-md` | `backdrop-filter: blur(12px)` | 见 §6.2 | ⚠️ 部分支持 |
| `backdrop-blur-lg` | `backdrop-filter: blur(16px)` | 见 §6.2 | ⚠️ 部分支持 |
| `blur-xl` | `filter: blur(24px)` | `filter: blur(24px)` | ✓ |
### 5.6 其他装饰
| Tailwind class | CSS | WXSS |
|---------------|-----|------|
| `object-cover` | `object-fit: cover` | `mode="aspectFill"`image 组件属性) |
| `ring-2` | `box-shadow: 0 0 0 2px` | `box-shadow: 0 0 0 2px` |
| `appearance-none` | `appearance: none` | 不需要(小程序无浏览器默认样式) |
| `outline-none` | `outline: none` | 不需要 |
| `resize-none` | `resize: none` | 不需要(小程序 textarea 默认不可拖拽) |
| `cursor-pointer` | `cursor: pointer` | 不需要(触屏无光标) |
---
## 6. CSS 特性兼容性H5 → 小程序
### 6.1 完全支持(直接迁移)
以下 CSS 特性在小程序 WXSS 中完全支持,可直接使用:
| CSS 特性 | 说明 |
|---------|------|
| `display: flex / grid / block / none` | 布局核心 |
| `position: relative / absolute / fixed` | 定位 |
| `box-shadow` | 阴影 |
| `border-radius` | 圆角 |
| `linear-gradient()` | 线性渐变 |
| `radial-gradient()` | 径向渐变 |
| `opacity` | 透明度 |
| `transform: translate / scale / rotate` | 变换 |
| `transition` | 过渡动画 |
| `animation` + `@keyframes` | 关键帧动画 |
| `::before` / `::after` | 伪元素 |
| `overflow: hidden / auto` | 溢出控制 |
| `text-overflow: ellipsis` | 文本省略 |
| `-webkit-line-clamp` | 多行省略 |
| `calc()` | 计算函数 |
| `var()` | CSS 变量 |
| `z-index` | 层级 |
| `white-space` | 空白处理 |
| `word-break` / `word-wrap` | 换行控制 |
| `filter: blur()` | 模糊滤镜 |
| `clip-path` | 裁剪路径 |
| `will-change` | 性能提示 |
| `font-variant-numeric` | 数字字体变体 |
### 6.2 部分支持(需要降级方案)
#### `backdrop-filter: blur()`(原型使用 35 次)
小程序对 `backdrop-filter` 的支持取决于基础库版本和设备:
- iOS基础库 2.9.0+ 通常支持
- Android部分机型不支持或性能差
**降级策略**
```css
/* 优先尝试 backdrop-filter */
.blur-bg {
background: rgba(255, 255, 255, 0.80);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
/* 如果不生效,半透明背景本身也能接受 */
```
对于原型中的三种 blur 级别:
| H5 | 降级 WXSS |
|----|----------|
| `backdrop-blur-sm`blur 4px | `background: rgba(255,255,255,0.85)` + 可选 `backdrop-filter: blur(4px)` |
| `backdrop-blur-md`blur 12px | `background: rgba(255,255,255,0.90)` + 可选 `backdrop-filter: blur(12px)` |
| `backdrop-blur-lg`blur 16px | `background: rgba(255,255,255,0.92)` + 可选 `backdrop-filter: blur(16px)` |
#### `position: sticky`(原型使用 11 次)
小程序支持 `position: sticky`,但有限制:
- 必须在页面自然滚动中使用(不能在 `scroll-view` 内部)
- `top` 值必须明确指定
- 多层 sticky 嵌套容易出问题
**迁移规则**
1. 页面级 sticky如筛选栏吸顶直接用 `position: sticky; top: Nrpx`
2. `scroll-view` 内的 sticky改用 JS 监听 `bindscroll` + 条件渲染
3. 多层 sticky只保留最外层内层改为固定布局
#### `env(safe-area-inset-*)`(原型使用 9 次)
**小程序不支持 `env()` CSS 函数。**
替代方案:
```javascript
// JS 获取安全区信息
const systemInfo = wx.getSystemInfoSync()
const statusBarHeight = systemInfo.statusBarHeight // 状态栏高度 px
const safeAreaBottom = systemInfo.screenHeight - systemInfo.safeArea.bottom // 底部安全区 px
```
```css
/* WXSS 中通过 style 动态绑定 */
/* WXML: <view style="padding-top: {{statusBarHeight}}px"> */
```
#### `color-mix()`(原型 CSS 中使用)
小程序不支持 `color-mix()`。替代方案:预计算混合后的色值,直接写 `rgba()`
### 6.3 不支持(必须替代)
| H5 CSS 特性 | 原型使用场景 | 小程序替代方案 |
|-------------|------------|--------------|
| `env(safe-area-inset-*)` | 顶部/底部安全区 | JS `wx.getSystemInfoSync()` 动态获取 |
| `:hover` 伪类 | 悬停高亮45 次) | 不需要(触屏无 hover如需按压反馈用 `hover-class` 属性 |
| `:focus` 伪类 | 输入框聚焦样式11 次) | `bindfocus` 事件 + `setData` 切换 class |
| `:active` 伪类 | 按压缩放1 次) | `hover-class` 属性或 `bindtouchstart/end` |
| `group-hover:*` | 父元素 hover 时子元素变化1 次) | 不需要(触屏无 hover |
| `:first-child` / `:last-child` | 首尾元素特殊样式 | 用 `wx:if` + index 判断,或手动加 class |
| `:checked` | 复选框选中态 | `setData` + 条件 class |
| `history.back()` | 返回上一页 | `wx.navigateBack()` |
| `window.scrollY` | 滚动位置 | `onPageScroll(e)``e.scrollTop` |
| `scrollIntoView()` | 滚动到元素 | `scroll-view``scroll-into-view` 属性 |
| `navigator.clipboard` | 复制文本 | `wx.setClipboardData()` |
| `navigator.vibrate` | 触觉反馈 | `wx.vibrateShort()` |
| `localStorage` / `sessionStorage` | 本地存储 | `wx.setStorageSync()` / `wx.getStorageSync()` |
| `confirm()` / `alert()` | 对话框 | `wx.showModal()` / `wx.showToast()` |
### 6.4 hover-class 属性说明
小程序的 `view` 组件支持 `hover-class` 属性,可以实现按压态效果:
```xml
<view hover-class="pressed" hover-stay-time="100">
点击我
</view>
```
```css
.pressed {
opacity: 0.7;
/* 或 background-color 变化 */
}
```
原型中 `hover:bg-primary/5`45 次)和 `hover:bg-gray-100`4 次)统一改为 `hover-class`
---
## 7. HTML 标签 → WXML 组件映射
### 7.1 基础标签映射
| H5 标签 | WXML 组件 | 说明 |
|--------|----------|------|
| `<div>` | `<view>` | 通用容器 |
| `<span>` | `<text>` | 行内文本。注意:`<text>` 内只能嵌套 `<text>`,不能嵌套 `<view>` |
| `<p>` | `<view>``<text>` | 段落 |
| `<img>` | `<image>` | 图片。必须指定 `mode` 属性 |
| `<a>` | `<navigator>``<view bindtap>` | 链接/跳转 |
| `<button>` | `<button>``<view bindtap>` | 按钮 |
| `<input>` | `<input>` | 输入框 |
| `<textarea>` | `<textarea>` | 多行输入 |
| `<svg>` | `<image src="xxx.svg">` | 小程序不支持内联 SVG |
| `<ul>` / `<ol>` / `<li>` | `<view>` | 列表容器 |
| `<h1>`~`<h6>` | `<view>` + 样式 class | 标题 |
| `<label>` | `<label>``<view>` | 表单标签 |
| `<select>` | `<picker>` | 选择器 |
### 7.2 image 组件的 mode 属性
原型中 `<img>` 使用 `object-cover` 的场景,对应小程序 `<image mode="aspectFill">`
| H5 CSS | image mode |
|--------|-----------|
| `object-fit: cover` | `mode="aspectFill"` |
| `object-fit: contain` | `mode="aspectFit"` |
| 无特殊设置 | `mode="widthFix"`(宽度撑满,高度自适应) |
### 7.3 SVG 处理规则
原型中大量使用内联 SVG图标、装饰图形。小程序不支持内联 SVG。
处理方式:
1. 将 SVG 导出为独立 `.svg` 文件,放入 `assets/icons/`
2.`<image src="/assets/icons/xxx.svg" mode="aspectFit">` 引用
3. 高频图标考虑使用 iconfont 或 TDesign 内置图标
### 7.4 事件属性映射
| H5 事件 | WXML 事件 | 说明 |
|--------|----------|------|
| `onclick` | `bindtap` | 点击 |
| `onchange` | `bindchange` | 值变化 |
| `oninput` | `bindinput` | 输入 |
| `onscroll` | `bindscroll`scroll-view`onPageScroll`(页面) | 滚动 |
| `ontouchstart` | `bindtouchstart` | 触摸开始 |
| `ontouchmove` | `bindtouchmove` | 触摸移动 |
| `ontouchend` | `bindtouchend` | 触摸结束 |
| `oncontextmenu` | `bindlongpress` | 长按 |
`bind` 前缀事件会冒泡,`catch` 前缀事件会阻止冒泡:
- `bindtap`:点击事件,冒泡
- `catchtap`:点击事件,阻止冒泡(等同于 `e.stopPropagation()`
- `catchtouchmove`:阻止触摸移动冒泡(常用于遮罩层阻止背景滚动)
---
## 8. JS DOM 操作 → 小程序数据驱动 完整映射
本章是整份文档的核心。H5 原型中所有 JS 交互都基于直接操作 DOM小程序必须改为数据驱动模式。
### 8.1 总体原则
H5 模式:`用户操作 → JS 查询 DOM → 修改 DOM 属性/样式/内容 → 视觉变化`
小程序模式:`用户操作 → 事件处理函数 → setData 更新数据 → WXML 自动重渲染 → 视觉变化`
**禁止在小程序中使用的 H5 API**(原型中全部出现过):
- `document.getElementById()` — 175 处
- `document.querySelector()` / `querySelectorAll()` — 多处
- `element.classList.add/remove/toggle/contains` — 109 处
- `element.style.xxx = yyy` — 47 处
- `element.innerHTML` / `textContent` — 38 处
- `document.createElement()` / `appendChild()` — 18 处
- `element.getAttribute()` — 1 处
### 8.2 模式 A显示/隐藏切换(最高频,~80 处)
H5 原型中最常见的 DOM 操作模式。
**H5 写法**
```javascript
// 显示弹窗
document.getElementById('modal').classList.remove('hidden')
document.getElementById('modal').classList.add('flex')
// 隐藏弹窗
document.getElementById('modal').classList.add('hidden')
document.getElementById('modal').classList.remove('flex')
```
**小程序写法**
```javascript
// JS
Page({
data: { showModal: false },
openModal() { this.setData({ showModal: true }) },
closeModal() { this.setData({ showModal: false }) }
})
```
```xml
<!-- WXML -->
<view wx:if="{{showModal}}" class="modal-overlay" catchtouchmove bindtap="closeModal">
<view class="modal-content" catchtap>
<!-- 弹窗内容 -->
</view>
</view>
```
**原型中的具体实例**(穷举):
| 状态变量 | 控制对象 | 出现页面 |
|---------|---------|---------|
| `showModal` | 通用弹窗/确认框 | task-detail 系列, notes |
| `showFilter` / `showFilterOverlay` | 筛选下拉面板 | board-finance, board-customer, board-coach |
| `showToc` / `showTocDropdown` | 目录导航浮层 | board-finance, board-customer, board-coach |
| `showContextMenu` | 长按上下文菜单 | task-list |
| `showToast` | Toast 提示 | 多个页面 |
| `showNoteModal` | 备注弹窗 | notes, task-detail 系列 |
| `showAbandonModal` | 放弃客户确认框 | task-detail 系列 |
| `showTipOverlay` | 帮助提示浮层 | board-finance |
| `showCopied` | 复制成功提示 | chat, chat-history |
### 8.3 模式 BTab/状态切换(~40 处)
**H5 写法**
```javascript
function switchTab(tabName) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'))
document.getElementById('tab-' + tabName).classList.add('active')
document.querySelectorAll('.tab-content').forEach(c => c.classList.add('hidden'))
document.getElementById('content-' + tabName).classList.remove('hidden')
}
```
**小程序写法**
```javascript
Page({
data: { activeTab: 'basic' },
onTabChange(e) {
this.setData({ activeTab: e.currentTarget.dataset.tab })
}
})
```
```xml
<view class="tab {{activeTab === 'basic' ? 'tab--active' : ''}}"
data-tab="basic" bindtap="onTabChange">基础课</view>
<view class="tab {{activeTab === 'vip' ? 'tab--active' : ''}}"
data-tab="vip" bindtap="onTabChange">VIP课</view>
<view wx:if="{{activeTab === 'basic'}}">基础课内容</view>
<view wx:if="{{activeTab === 'vip'}}">VIP课内容</view>
```
**原型中的具体实例**
| 状态变量 | 选项值 | 出现页面 |
|---------|-------|---------|
| `activeTab` (看板) | `'finance'` / `'customer'` / `'coach'` | board-* |
| `activeTab` (任务详情) | `'basic'` / `'vip'` / `'incentive'` | task-detail 系列 |
| `activeTab` (业绩) | `'income'` / `'service'` | performance |
| `selectedTime` | `'thisMonth'` / `'lastMonth'` / `'last3Months'` | board-* |
| `selectedArea` | `'all'` / `'area1'` / `'area2'` | board-* |
| `selectedSort` | `'default'` / `'amount'` / `'count'` | board-customer, board-coach |
### 8.4 模式 C展开/收起(~20 处)
**H5 写法**
```javascript
function toggleExpand(id) {
const el = document.getElementById(id)
el.classList.toggle('hidden')
const btn = document.getElementById(id + '-btn')
btn.textContent = el.classList.contains('hidden') ? '展开更多' : '收起'
}
```
**小程序写法**
```javascript
Page({
data: { expandedSections: {} },
toggleSection(e) {
const key = e.currentTarget.dataset.key
this.setData({ [`expandedSections.${key}`]: !this.data.expandedSections[key] })
}
})
```
```xml
<view bindtap="toggleSection" data-key="detail">
<text>{{expandedSections.detail ? '收起' : '展开更多'}}</text>
</view>
<view wx:if="{{expandedSections.detail}}">
<!-- 展开内容 -->
</view>
```
### 8.5 模式 Dstyle 直接操作 → 动态 style 绑定47 处)
**H5 写法**
```javascript
element.style.top = scrollY + 'px'
element.style.opacity = '0'
element.style.transform = 'translateY(-10px)'
```
**小程序写法**
```javascript
Page({
data: { dynamicTop: 0, fadeOpacity: 1 },
onScroll(e) {
this.setData({ dynamicTop: e.scrollTop })
}
})
```
```xml
<view style="top: {{dynamicTop}}px; opacity: {{fadeOpacity}}; transform: translateY({{offsetY}}px)">
```
**注意**:频繁 `setData` 驱动 style 变化会导致性能问题。对于动画场景,优先使用 WXSS `transition` / `animation`,或 `wx.createAnimation()`
### 8.6 模式 E内容动态更新38 处)
**H5 写法**
```javascript
document.getElementById('total').textContent = '¥12,345'
document.getElementById('list').innerHTML = items.map(i => `<div>${i.name}</div>`).join('')
```
**小程序写法**
```javascript
Page({
data: { total: '¥12,345', items: [] },
loadData() {
this.setData({
total: '¥12,345',
items: [{ name: '台桌' }, { name: '酒水' }]
})
}
})
```
```xml
<text>{{total}}</text>
<view wx:for="{{items}}" wx:key="name">
<text>{{item.name}}</text>
</view>
```
### 8.7 模式 F表单验证样式~10 处)
**H5 写法**
```javascript
if (!value) {
input.classList.add('ring-2', 'ring-error/30', 'border-error/40')
errorMsg.classList.remove('hidden')
} else {
input.classList.remove('ring-2', 'ring-error/30', 'border-error/40')
errorMsg.classList.add('hidden')
}
```
**小程序写法**
```javascript
Page({
data: { errors: {} },
validate(field, value) {
this.setData({ [`errors.${field}`]: !value })
}
})
```
```xml
<input class="input {{errors.phone ? 'input--error' : ''}}" bindinput="onPhoneInput" />
<text wx:if="{{errors.phone}}" class="error-text">请输入手机号</text>
```
### 8.8 模式 G滚动联动~15 处)
原型中看板页的筛选栏隐藏/显示、吸顶标题切换、section 感知都依赖滚动事件。
**H5 写法**
```javascript
let lastScrollY = 0
window.addEventListener('scroll', () => {
const currentY = window.scrollY
if (currentY > lastScrollY && currentY > 100) {
filterBar.classList.add('filter-bar-hidden')
} else {
filterBar.classList.remove('filter-bar-hidden')
}
lastScrollY = currentY
})
```
**小程序写法**
```javascript
Page({
data: { filterBarVisible: true, stickyTitle: '' },
_lastScrollTop: 0,
_throttleTimer: null,
onPageScroll(e) {
// 节流:避免每帧 setData
if (this._throttleTimer) return
this._throttleTimer = setTimeout(() => {
this._throttleTimer = null
}, 100)
const scrollTop = e.scrollTop
const goingDown = scrollTop > this._lastScrollTop
this._lastScrollTop = scrollTop
// 只在状态真正变化时 setData
const shouldHide = goingDown && scrollTop > 100
if (shouldHide !== !this.data.filterBarVisible) {
this.setData({ filterBarVisible: !shouldHide })
}
}
})
```
**关键规则**
1. 滚动事件处理函数中必须做节流100ms 以上)
2. 只在状态真正变化时调用 `setData`,避免无意义的重渲染
3. 不要在滚动事件中更新大量数据或触发复杂计算
4. 吸顶标题切换:用 `wx.createIntersectionObserver()` 监听 section 进出视口,比手动计算 scrollTop 更高效
### 8.9 模式 H长按菜单task-list 页面)
**H5 写法**
```javascript
let pressTimer = null
let startX, startY
card.addEventListener('touchstart', (e) => {
startX = e.touches[0].clientX
startY = e.touches[0].clientY
pressTimer = setTimeout(() => showContextMenu(e), 500)
})
card.addEventListener('touchmove', (e) => {
if (Math.abs(e.touches[0].clientX - startX) > 10 ||
Math.abs(e.touches[0].clientY - startY) > 10) {
clearTimeout(pressTimer)
}
})
card.addEventListener('touchend', () => clearTimeout(pressTimer))
```
**小程序写法**
```javascript
Page({
data: { contextMenu: { visible: false, taskId: null } },
onLongPress(e) {
const taskId = e.currentTarget.dataset.id
this.setData({
contextMenu: { visible: true, taskId }
})
},
closeContextMenu() {
this.setData({ 'contextMenu.visible': false })
}
})
```
```xml
<view wx:for="{{tasks}}" wx:key="id"
bindlongpress="onLongPress" data-id="{{item.id}}">
<!-- 任务卡片内容 -->
</view>
<!-- 上下文菜单(建议改为底部 action sheet -->
<view wx:if="{{contextMenu.visible}}" class="menu-overlay" bindtap="closeContextMenu">
<view class="action-sheet" catchtap>
<view class="action-item" bindtap="onMenuAction" data-action="abandon">放弃客户</view>
<view class="action-item" bindtap="onMenuAction" data-action="note">添加备注</view>
</view>
</view>
```
**建议**:原型中的锚点浮出菜单改为底部 action sheet兼容性和可维护性更高。
### 8.10 模式 I评分拖动notes 页面)
**H5 写法**
```javascript
stars.forEach((star, index) => {
star.addEventListener('touchstart', () => setRating(index + 1))
star.addEventListener('touchmove', (e) => {
const rect = container.getBoundingClientRect()
const x = e.touches[0].clientX - rect.left
const newRating = Math.ceil(x / (rect.width / 5))
setRating(Math.max(1, Math.min(5, newRating)))
})
})
```
**小程序写法**
```javascript
Component({
properties: { value: { type: Number, value: 0 } },
methods: {
onStarTap(e) {
const score = e.currentTarget.dataset.score
this.triggerEvent('change', { score })
},
onTouchMove(e) {
// 用 selectorQuery 获取容器位置
const query = this.createSelectorQuery()
query.select('.star-container').boundingClientRect((rect) => {
const x = e.touches[0].clientX - rect.left
const score = Math.ceil(x / (rect.width / 5))
this.triggerEvent('change', { score: Math.max(1, Math.min(5, score)) })
}).exec()
}
}
})
```
**注意**`createSelectorQuery` 是异步的,频繁调用会有延迟。建议在 `touchstart` 时缓存 `rect``touchmove` 中直接用缓存值计算。
### 8.11 模式 J复制到剪贴板chat 页面)
**H5 写法**
```javascript
navigator.clipboard.writeText(text).then(() => {
copyBtn.classList.add('copied')
setTimeout(() => copyBtn.classList.remove('copied'), 2000)
})
```
**小程序写法**
```javascript
Page({
onCopy(e) {
const text = e.currentTarget.dataset.text
wx.setClipboardData({
data: text,
success() {
wx.showToast({ title: '已复制', icon: 'success', duration: 1500 })
}
})
}
})
```
### 8.12 模式 K页面导航
**H5 写法**
```javascript
window.location.href = 'task-detail.html?id=123'
history.back()
```
**小程序写法**
```javascript
// 普通页面跳转
wx.navigateTo({ url: '/pages/task-detail/task-detail?id=123' })
// 返回上一页
wx.navigateBack()
// TabBar 页面跳转task-list、board-finance、my-profile
wx.switchTab({ url: '/pages/task-list/task-list' })
// 重定向(替换当前页)
wx.redirectTo({ url: '/pages/login/login' })
```
**关键规则**TabBar 页面(在 `app.json``tabBar.list` 中定义的)必须用 `wx.switchTab`,用 `navigateTo` 会静默失败。
### 8.13 模式 L定时器Toast 自动隐藏等)
**H5 写法**
```javascript
toast.classList.remove('hidden')
setTimeout(() => toast.classList.add('hidden'), 2000)
```
**小程序写法**
```javascript
Page({
showToast(text) {
this.setData({ showToast: true, toastText: text })
setTimeout(() => {
this.setData({ showToast: false })
}, 2000)
}
})
```
或直接使用微信 API
```javascript
wx.showToast({ title: '操作成功', icon: 'success', duration: 2000 })
```
---
## 9. 动画与过渡迁移
### 9.1 CSS transition → WXSS transition直接迁移
原型中的 transition 定义可以直接迁移到 WXSS时间和缓动函数不变其中涉及的 px 值按 §0.4 策略决定是否换算为 rpx。
| H5 transition | WXSS transition |
|--------------|----------------|
| `transition: all 0.2s ease` | `transition: all 0.2s ease` |
| `transition: all 0.3s ease` | `transition: all 0.3s ease` |
| `transition: opacity 0.2s ease` | `transition: opacity 0.2s ease` |
| `transition: transform 0.15s ease` | `transition: transform 0.15s ease` |
| `transition: opacity 0.15s ease, transform 0.15s ease` | `transition: opacity 0.15s ease, transform 0.15s ease` |
| `transition: transform 220ms ease, opacity 220ms ease, max-height 220ms ease` | 同左 |
| `transition: background 0.2s, opacity 0.2s` | `transition: background 0.2s, opacity 0.2s` |
| `transition: width 0.6s ease-out` | `transition: width 0.6s ease-out` |
| `transition: clip-path 0.12s ease` | `transition: clip-path 0.12s ease` |
### 9.2 @keyframes → WXSS @keyframes直接迁移
原型中的关键帧动画可以直接迁移,其中涉及的 px 值按 §0.4 策略决定是否换算为 rpx。
| 动画名 | 用途 | 迁移方式 |
|-------|------|---------|
| `ai-shimmer` | AI 标识微光扫过12s/14s 周期) | 直接复制,已在 app.wxss 中实现 |
| `ai-pulse` | AI 标识呼吸脉冲3s 周期) | 直接复制,已在 app.wxss 中实现 |
| `float` | 浮动装饰动画3s/4s 周期) | 直接复制 |
| `pulse-soft` | 柔和脉冲装饰2s/3s 周期) | 直接复制 |
| `pulse-glow` | 奖励闪烁2s 周期) | 直接复制 |
| `shake` | 抖动反馈0.5s | 直接复制 |
| `stampDown` | 红戳盖章0.5scubic-bezier | 直接复制 |
| `texture-shift` | Banner 纹理位移20s 周期) | 直接复制 |
| `filterBarDrop` | 筛选栏下滑出现 | 直接复制 |
### 9.3 Tailwind transition 类
| Tailwind class | CSS | WXSS |
|---------------|-----|------|
| `transition-all` | `transition-property: all; transition-timing-function: cubic-bezier(0.4,0,0.2,1); transition-duration: 150ms` | `transition: all 150ms cubic-bezier(0.4,0,0.2,1)` |
| `transition-colors` | `transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; ...150ms` | `transition: color 150ms, background-color 150ms, border-color 150ms` |
| `transition-transform` | `transition-property: transform; ...150ms` | `transition: transform 150ms cubic-bezier(0.4,0,0.2,1)` |
| `duration-200` | `transition-duration: 200ms` | 覆盖 duration 为 200ms |
### 9.4 JS 驱动动画的替代
原型中有少量通过 JS 连续修改 style 实现的动画。在小程序中:
1. 优先用 WXSS transition/animation性能最好
2. 其次用 `wx.createAnimation()`
3. 避免用连续 `setData` 逐帧更新
`wx.createAnimation()` 示例:
```javascript
const animation = wx.createAnimation({
duration: 300,
timingFunction: 'ease'
})
animation.opacity(0).translateY(-10).step()
this.setData({ fadeAnimation: animation.export() })
```
```xml
<view animation="{{fadeAnimation}}">内容</view>
```
### 9.5 `@media (prefers-reduced-motion: reduce)`
原型中有减少动画偏好的媒体查询。小程序 WXSS 支持 `@media` 查询,可以直接迁移:
```css
@media (prefers-reduced-motion: reduce) {
.animated-element {
animation: none;
transition: none;
}
}
```
---
## 10. CSS 变量迁移
### 10.1 原型中定义的 CSS 变量
| 变量名 | 用途 | 定义位置 |
|-------|------|---------|
| `--ai-from` | AI 图标渐变起始色 | ai-icons.css |
| `--ai-to` | AI 图标渐变结束色 | ai-icons.css |
| `--ai-from-deep` | AI 图标深色起始 | ai-icons.css |
| `--ai-to-deep` | AI 图标深色结束 | ai-icons.css |
| `--ai-pulse-r/g/b` | AI 脉冲动画 RGB 分量 | ai-icons.css |
| `--banner-color-start/mid/end` | Banner 渐变三色 | banner.css |
| `--star-color` | 评分星星颜色 | notes.css |
| `--texture-opacity` | 纹理透明度 | banner.css |
### 10.2 小程序 CSS 变量支持
小程序 WXSS 支持 CSS 变量(`var()`),在 `page` 选择器中定义全局变量:
```css
/* app.wxss */
page {
--color-primary: #0052d9;
--ai-from: #667eea;
/* ... */
}
```
组件内可以通过 class 覆盖变量值(与 H5 行为一致):
```css
.ai-color-red {
--ai-from: #e74c3c;
--ai-to: #f39c9c;
}
```
**注意**`color-mix()` 函数在小程序中不支持。原型中使用 `color-mix()` 的地方需要预计算为 `rgba()` 值。
---
## 11. 自定义 CSS 类 → WXSS 迁移指引
原型中有 212 个非 Tailwind 的自定义类,定义在各页面的 `<style>` 标签和独立 CSS 文件中。这些类的迁移策略是:逐个检查其 CSS 定义,按 §0.4 混合单位策略决定 px 值是否换算为 rpx将不兼容的 CSS 特性按 §6 替换。
### 11.1 全局自定义类(跨页面复用)
以下类在多个页面中出现,应提取到 `app.wxss` 或公共样式文件中:
| 类名 | 用途 | 出现频率 | 迁移要点 |
|------|------|---------|---------|
| `safe-area-top` | 顶部安全区 padding | 9 页面 | 改为 JS 动态获取 statusBarHeight |
| `banner-bg` | Banner 渐变背景 | 10 页面 | 直接迁移 linear-gradientpx→rpx |
| `texture-aurora` | Banner 纹理动画 | 10 页面 | 直接迁移 @keyframes |
| `card-section` | 卡片区块容器 | 5 页面 | px→rpx |
| `card-header` / `card-header-content` / `card-header-emoji` / `card-header-title` / `card-header-desc` | 卡片头部组件 | 5 页面 | px→rpx |
| `section-title` | 区块标题 | 17 处 | px→rpx |
| `compare-up` / `compare-down` / `compare-data` | 环比数据标签 | 85+70+9 处 | px→rpx颜色直接映射 |
| `page-label` | 页面标签 | 22 处 | px→rpx |
| `phone-frame` / `phone-screen` | 手机框架容器 | 22 处 | 仅 index.html 用,小程序不需要 |
| `ai-inline-icon` / `ai-title-badge` / `ai-title-badge-icon` | AI 图标系统 | 16+22+22 处 | 已在 app.wxss 中实现 |
| `show` | 显示状态 | 74 处 | 改为 `wx:if` 或条件 class |
| `active` | 激活状态 | 32 处 | 改为条件 class `{{xxx ? 'active' : ''}}` |
### 11.2 页面专属自定义类
以下类仅在特定页面中使用,应放在对应页面的 WXSS 文件中:
| 类名 | 页面 | 用途 |
|------|------|------|
| `task-card` / `task-name` / `task-desc` / `task-tag-wrap` / `task-tag-text` | task-list | 任务卡片 |
| `customer-card` / `customer-item` | board-customer, customer-detail | 客户卡片 |
| `coach-card` | board-coach, coach-detail | 助教卡片 |
| `record-card` / `record-item` / `record-table` / `record-date` / `record-time` / `record-type` / `record-duration` / `record-income` | performance-records | 业绩明细 |
| `svc-record` / `svc-table` / `svc-date` / `svc-type` / `svc-duration` / `svc-income` / `svc-drinks` | customer-service-records | 服务记录 |
| `message-bubble` / `chat-container` / `chat-item` / `copy-btn` / `copied` | chat, chat-history | 聊天界面 |
| `note-card-wrap` / `note-expand-btn` / `note-indicator` / `note-tag` / `note-rating-*` | notes | 备注系统 |
| `nr-star` / `nr-filled` / `nr-empty` / `star-rating` / `star` / `star-fill` / `star-empty` | notes | 评分星级 |
| `context-menu` / `context-overlay` / `ctx-item` | task-list | 长按菜单 |
| `modal-overlay` / `modal-card` / `modal-submit-btn` / `modal-error` | notes, task-detail | 弹窗 |
| `toc-dropdown` / `toc-overlay` / `toc-item` / `toc-item-emoji` / `toc-item-text` | board-* | 目录导航 |
| `filter-overlay` / `filter-dropdown` | board-* | 筛选面板 |
| `date-divider` / `dd-date` / `dd-line` / `dd-stats` | performance-records, customer-service-records | 日期分割线 |
| `dim-container` | board-customer | 维度容器 |
| `assistant-badge` / `assistant-tag` / `assistant-sep` / `assistant-normal` / `assistant-abandoned` / `assistant-assignee` / `assistant-badge-drop` / `assistant-badge-follow` | board-coach, coach-detail | 助教标签系统 |
| `speech-bubble` | task-detail | 对话气泡 |
| `stamp-badge` / `stamp-text` / `stamp-animate` / `red-stamp` | task-detail | 红戳动画 |
| `tier-badge` / `tier-progress` / `tier-fill` / `tier-segment` | performance | 等级进度条 |
| `income-tab` / `income-tier` | performance | 收入层级 |
| `service-card` | customer-service-records | 服务卡片 |
| `info-card` | customer-detail | 信息卡片 |
| `option-card` | home-settings | 选项卡片 |
| `checkbox-custom` / `radio-checked` | apply, home-settings | 自定义表单控件 |
| `field-error` / `error-border` / `btn-disabled` | apply | 表单验证 |
| `voice-btn` | chat | 语音按钮 |
| `progress-sm` / `ruler-bar` / `ruler-strip` | performance | 进度条 |
| `tip-overlay` / `tip-toast` / `tip-toast-content` | board-finance | 帮助提示 |
| `summary-header` / `summary-content` / `summary-gradient` | board-customer | 摘要区 |
| `float-animation` | login, reviewing | 浮动装饰 |
### 11.3 主题色类
| 类名 | 用途 | 迁移方式 |
|------|------|---------|
| `theme-blue` / `theme-blue-light` / `theme-coral` / `theme-dark-gold` / `theme-orange` / `theme-pink` / `theme-red` / `theme-teal` | Banner 主题色 | 通过 CSS 变量 `--banner-color-start/mid/end` 控制 |
| `blue` / `green` / `orange` / `pink` / `purple` / `red` / `teal` | 标签/徽章颜色 | 直接映射为对应色值 |
### 11.4 状态类
| 类名 | 用途 | 小程序迁移 |
|------|------|-----------|
| `show` | 显示元素 | `wx:if="{{visible}}"` 或条件 class |
| `active` | 激活态 | `class="{{isActive ? 'active' : ''}}"` |
| `selected` | 选中态 | `class="{{isSelected ? 'selected' : ''}}"` |
| `pinned` | 置顶态 | `class="{{isPinned ? 'pinned' : ''}}"` |
| `completed` | 完成态 | `class="{{isCompleted ? 'completed' : ''}}"` |
| `abandoned` | 放弃态 | `class="{{isAbandoned ? 'abandoned' : ''}}"` |
| `current` | 当前项 | `class="{{isCurrent ? 'current' : ''}}"` |
| `high-priority` | 高优先级 | `class="{{isHighPriority ? 'high-priority' : ''}}"` |
---
## 12. 浏览器 API → 微信小程序 API 完整映射
### 12.1 导航
| H5 API | 小程序 API | 说明 |
|--------|-----------|------|
| `window.location.href = url` | `wx.navigateTo({ url })` | 普通页面跳转 |
| `window.location.replace(url)` | `wx.redirectTo({ url })` | 替换当前页 |
| `history.back()` | `wx.navigateBack()` | 返回上一页 |
| — | `wx.switchTab({ url })` | TabBar 页面跳转(必须用这个) |
| — | `wx.reLaunch({ url })` | 关闭所有页面,打开指定页面 |
### 12.2 滚动
| H5 API | 小程序 API | 说明 |
|--------|-----------|------|
| `window.scrollY` | `onPageScroll(e)``e.scrollTop` | 页面滚动位置 |
| `window.scrollTo(0, y)` | `wx.pageScrollTo({ scrollTop: y })` | 滚动到指定位置 |
| `element.scrollIntoView()` | `scroll-view``scroll-into-view="{{id}}"` | 滚动到指定元素 |
| `element.getBoundingClientRect()` | `wx.createSelectorQuery().select('.cls').boundingClientRect()` | 获取元素位置(异步) |
### 12.3 存储
| H5 API | 小程序 API | 说明 |
|--------|-----------|------|
| `localStorage.setItem(k, v)` | `wx.setStorageSync(k, v)` | 同步写入 |
| `localStorage.getItem(k)` | `wx.getStorageSync(k)` | 同步读取 |
| `localStorage.removeItem(k)` | `wx.removeStorageSync(k)` | 同步删除 |
| `localStorage.clear()` | `wx.clearStorageSync()` | 清空全部 |
| `sessionStorage.*` | 无直接等价 | 用页面 data 或 app.globalData 替代 |
### 12.4 剪贴板
| H5 API | 小程序 API |
|--------|-----------|
| `navigator.clipboard.writeText(text)` | `wx.setClipboardData({ data: text })` |
| `navigator.clipboard.readText()` | `wx.getClipboardData()` |
### 12.5 触觉反馈
| H5 API | 小程序 API |
|--------|-----------|
| `navigator.vibrate(15)` | `wx.vibrateShort({ type: 'light' })` |
| `navigator.vibrate(100)` | `wx.vibrateLong()` |
### 12.6 对话框
| H5 API | 小程序 API |
|--------|-----------|
| `alert(msg)` | `wx.showModal({ title: '', content: msg, showCancel: false })` |
| `confirm(msg)` | `wx.showModal({ title: '提示', content: msg })` |
| — | `wx.showToast({ title: msg, icon: 'success' })` |
| — | `wx.showActionSheet({ itemList: [...] })` |
### 12.7 系统信息
| 需求 | 小程序 API |
|------|-----------|
| 状态栏高度 | `wx.getSystemInfoSync().statusBarHeight` |
| 屏幕宽度 | `wx.getSystemInfoSync().windowWidth` |
| 安全区域 | `wx.getSystemInfoSync().safeArea` |
| 底部安全区高度 | `screenHeight - safeArea.bottom` |
| 胶囊按钮位置 | `wx.getMenuButtonBoundingClientRect()` |
### 12.8 定时器
| H5 API | 小程序 API | 说明 |
|--------|-----------|------|
| `setTimeout(fn, ms)` | `setTimeout(fn, ms)` | 完全相同 |
| `setInterval(fn, ms)` | `setInterval(fn, ms)` | 完全相同 |
| `clearTimeout(id)` | `clearTimeout(id)` | 完全相同 |
| `clearInterval(id)` | `clearInterval(id)` | 完全相同 |
| `requestAnimationFrame(fn)` | 不推荐使用 | 用 WXSS animation 或 `wx.createAnimation()` 替代 |
---
## 13. WXSS 选择器支持范围
微信官方文档明确列出的支持选择器:
| 选择器 | 示例 | 支持 |
|-------|------|------|
| `.class` | `.card` | ✓ |
| `#id` | `#header` | ✓ |
| `element` | `view` | ✓ |
| `element, element` | `view, text` | ✓ |
| `::after` | `view::after` | ✓ |
| `::before` | `view::before` | ✓ |
实测额外支持(非官方文档,但可用):
| 选择器 | 示例 | 支持 |
|-------|------|------|
| 后代选择器 | `.parent .child` | ✓ |
| 子选择器 | `.parent > .child` | ✓ |
| 相邻兄弟 | `.item + .item` | ✓ |
| 通用兄弟 | `.item ~ .item` | ✓ |
| 属性选择器 | `[data-type="vip"]` | ⚠️ 部分支持 |
**不支持或不推荐**
- 复杂的后代/兄弟选择器链(性能差)
- `:nth-child()` / `:nth-of-type()`(不稳定)
- `:hover`(触屏无意义)
- `:focus`(用事件替代)
- `:first-child` / `:last-child`(用条件 class 替代更可靠)
**建议**:样式选择器尽量简单,优先使用 class 选择器。复杂的结构关系用 WXML 条件渲染 + 显式 class 替代。
---
## 14. 迁移检查清单(逐页面执行)
每个页面迁移完成后,按以下清单逐项检查:
### 14.1 结构检查
- [ ] 所有 `<div>` 已替换为 `<view>`
- [ ] 所有 `<span>` 已替换为 `<text>`(且 `<text>` 内无 `<view>` 嵌套)
- [ ] 所有 `<img>` 已替换为 `<image>`,且指定了 `mode` 属性
- [ ] 所有内联 SVG 已导出为文件,用 `<image>` 引用
- [ ]`<a>` 标签残留(改为 `<navigator>``bindtap`
- [ ] 页面 JSON 中 `navigationStyle` 配置正确
### 14.2 样式检查
- [ ] 无 Tailwind class 残留(所有 utility 已转为 WXSS
- [ ] 布局尺寸(容器宽度、间距、卡片)已按 §0.4 策略换算为 rpx
- [ ] 细节尺寸发丝线、阴影、小图标、badge 偏移)按 §0.4 策略保留 px 或按效果决定
- [ ] 所有字号同时写了 `font-size``line-height`§2
- [ ] `font-medium` 对应 500不是 600
- [ ]`env(safe-area-inset-*)` 残留(改为 JS 动态获取)
- [ ]`cursor: pointer` 残留
- [ ] 渐变色值正确from/to 色值已查表确认)
- [ ] 透明度值正确(`/N` 后缀已转为 `rgba()`
### 14.3 交互检查
- [ ]`document.querySelector` 等 DOM 操作残留
- [ ]`classList.add/remove` 残留
- [ ]`element.style.xxx = ` 残留
- [ ] 所有显示/隐藏改为 `wx:if` 或条件 class + `setData`
- [ ] 所有 Tab 切换改为 `setData` + 条件渲染
- [ ] 所有页面跳转改为 `wx.navigateTo` / `wx.switchTab` / `wx.navigateBack`
- [ ] TabBar 页面跳转使用 `wx.switchTab`(不是 `navigateTo`
- [ ] 滚动事件有节流处理
- [ ] 弹窗遮罩有 `catchtouchmove` 阻止背景滚动
### 14.4 性能检查
- [ ] 无连续 `setData` 驱动动画(改用 WXSS transition/animation
- [ ] 滚动事件处理函数中只更新必要的状态位
- [ ] 长列表考虑分页或虚拟列表
- [ ] 无不必要的 `setData`(状态未变化时不调用)
### 14.5 间距校验规则
- [ ] 相邻元素间距确认归属(上方 padding-bottom 还是下方 margin-top不能两端都加
- [ ] `space-y-N` 的间距值正确(`space-y-2` = 8px = 14rpx不是 4px
- [ ] `gap-N` 的间距值正确(`gap-1` = 4px = 8rpx不是 2px