# 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)

> rpx（responsive 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 做适配，但并未要求所有样式都必须用 rpx；WXSS 也支持直接写 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 模式 B：Tab/状态切换（~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 模式 D：style 直接操作 → 动态 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.5s，cubic-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-gradient，px→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）

