# 间距测量与 rpx 换算

## 核心换算公式

```
小程序 viewport 宽 = 750rpx = 430px
RPX_FACTOR = 750 / 430 = 1.7442

精确公式：rpx = ceil(H5_px × 1.7442 / 2) × 2
简化心算：rpx ≈ H5_px × 2 × 0.875 取偶（6px、14px 处有 2rpx 偏差，以精确公式为准）

圆角例外：border-radius = px × 2（不乘系数），数值更整洁
```

---

## 结构化审计方法论（三阶段）

本方法论适用于两个场景：
- **迁移前**：对 H5 页面做详细审计，产出映射表，指导 MP 开发
- **迁移后**：基于映射表做双端对比，逐元素验证偏差

### 阶段 0：结构拆解（自顶向下）

目标：将页面拆解为树状元素清单，建立 H5↔MP 映射表。

#### 0.1 拆解流程

1. 读取 H5 源码（`docs/h5_ui/pages/<page>.html`），识别页面首层容器
2. 对每个首层容器，递归拆解子元素，直到到达叶子节点
3. 叶子节点定义：**单一样式属性可描述**的最小视觉单元
   - 文字 span（font-size + color + weight 即可描述）
   - 图标 image / SVG
   - 纯色/渐变 view（background 即可描述）
   - 分隔线（border 即可描述）
4. 为每个节点记录：层级深度、CSS 选择器、Tailwind 类名、角色描述

#### 0.2 H5 审计映射表（迁移前必做）

拆解完成后，产出结构化映射表。此表是后续所有工作的基准：

```markdown
## <page> H5 审计映射表

| # | 层级 | 元素描述 | H5 选择器 | Tailwind 类名 | 理论 rpx 值 | MP 选择器 | 备注 |
|---|------|---------|-----------|--------------|------------|-----------|------|
| 1 | L0 | 页面根容器 | .page | px-4 bg-gray-1 | padding: 0 14rpx | .page | — |
| 2 | L1 | Banner 区域 | .banner-bg | h-40 rounded-2xl | height: 140rpx, radius: 32rpx | .banner-bg | 渐变需简化 |
| 3 | L1 | 统计卡片 | .summary-card | p-4 rounded-xl | padding: 28rpx, radius: 24rpx | .summary-card | — |
| 4 | L2 | 卡片标题 | .summary-card h3 | text-base font-semibold | font-size: 28rpx, weight: 600 | .card-title | — |
| 5 | L2 | 金额数值 | .amount | text-2xl font-semibold | font-size: 42rpx, weight: 600 | .amount | — |
| ... | ... | ... | ... | ... | ... | ... | ... |
```

列说明：
- **层级**：L0=页面根, L1=首层区块, L2=区块内组件, L3=组件内元素...
- **理论 rpx 值**：从 Tailwind 类名查速查表换算，不是实测值
- **MP 选择器**：迁移前留空，迁移后填入对应的 WXML 选择器
- **备注**：CSS 风险特性、不可消除差异、特殊处理方案

#### 0.3 结构完整性校验

映射表同时用于校验 MP 端结构完整性：

| 校验项 | 方法 | 判定 |
|--------|------|------|
| 元素缺失 | H5 映射表中有、MP 选择器为空或不存在 | P0 |
| 元素多余 | MP 中存在映射表未列出的视觉元素 | 检查是否为调试元素，否则标注 |
| 顺序错误 | MP 元素 DOM 顺序与映射表不一致 | P0 |
| 嵌套层级不一致 | MP 的父子关系与 H5 不同 | P1（可能影响间距计算） |

校验时机：
- **迁移前**：映射表产出后，MP 选择器列全部留空，无需校验
- **迁移后**：填入 MP 选择器，逐行校验存在性和顺序
- **对比审计时**：先跑结构完整性校验，通过后再进入测量阶段


### 阶段 1：逐级测量（自底向上）

目标：从叶子节点开始，逐级向上测量每个元素的尺寸和间距。

#### 1.1 测量顺序

```
叶子节点（L3/L4）→ 组件容器（L2）→ 区块容器（L1）→ 页面根（L0）
```

自底向上的原因：内层元素的实测尺寸决定了外层容器的实际高度。先测内层，才能判断外层的 padding/gap 是否正确。

#### 1.2 每个元素的标准测量维度

| 维度 | 测量方法 | 适用节点 |
|------|---------|---------|
| width / height | `getBoundingClientRect()` | 所有 |
| padding（四方向） | `getComputedStyle().paddingTop/Right/Bottom/Left` | 容器节点 |
| margin（四方向） | `getComputedStyle().marginTop/Right/Bottom/Left` | 所有 |
| font-size | `getComputedStyle().fontSize` | 文字节点 |
| line-height | `getComputedStyle().lineHeight` | 文字节点 |
| border-radius | `getComputedStyle().borderRadius` | 有圆角的节点 |
| 与父容器的 offset | `child.rect.top - parent.rect.top - parent.paddingTop` | 所有子节点 |
| 与相邻兄弟的 gap | `next.rect.top - current.rect.bottom` | 同级相邻节点 |

#### 1.3 H5 端测量（Playwright MCP / measure_gaps.py）

方式 A：Playwright MCP `browser_evaluate`

```javascript
// 测量单个元素的完整属性
async (selector) => {
  const el = document.querySelector(selector);
  if (!el) return { error: 'not found' };
  const rect = el.getBoundingClientRect();
  const cs = getComputedStyle(el);
  return {
    selector,
    width: rect.width, height: rect.height,
    top: rect.top, left: rect.left,
    paddingTop: parseFloat(cs.paddingTop),
    paddingRight: parseFloat(cs.paddingRight),
    paddingBottom: parseFloat(cs.paddingBottom),
    paddingLeft: parseFloat(cs.paddingLeft),
    marginTop: parseFloat(cs.marginTop),
    marginBottom: parseFloat(cs.marginBottom),
    fontSize: parseFloat(cs.fontSize),
    lineHeight: cs.lineHeight === 'normal' ? 'normal' : parseFloat(cs.lineHeight),
    fontWeight: cs.fontWeight,
    borderRadius: cs.borderRadius,
    color: cs.color,
    backgroundColor: cs.backgroundColor
  };
}
```

> Playwright MCP 不支持 `file://` 协议。需先起本地 HTTP 服务：
> `python -m http.server 8765 --directory docs/h5_ui/pages`
> 然后用 `browser_navigate("http://localhost:8765/<page>.html")` 访问。

方式 B：`scripts/ops/measure_gaps.py`（批量测量）

```bash
uv run python scripts/ops/measure_gaps.py <page> --selectors "<sel1>" "<sel2>" ...
```

#### 1.4 MP 端测量（SelectorQuery）

```javascript
// 微信开发者工具 MCP evaluate_script
async (selector) => {
  return new Promise(resolve => {
    const query = wx.createSelectorQuery();
    query.select(selector).boundingClientRect(rect => {
      query.select(selector).fields({
        computedStyle: ['paddingTop','paddingRight','paddingBottom','paddingLeft',
                        'marginTop','marginBottom','fontSize','lineHeight',
                        'fontWeight','borderRadius','color','backgroundColor']
      }, style => {
        resolve({ rect, style });
      }).exec();
    }).exec();
  });
}
```

#### 1.5 测量结果记录格式

每个元素的测量结果追加到映射表：

```markdown
| # | 元素 | 属性 | H5 实测(px) | 理论 rpx | MP 实测(rpx) | 偏差 | 级别 |
|---|------|------|------------|---------|-------------|------|------|
| 4 | 卡片标题 | font-size | 16px | 28rpx | 28rpx | 0 | ✅ |
| 4 | 卡片标题 | line-height | 24px | 42rpx | 36rpx | -6rpx | P3 |
| 4 | 卡片标题 | font-weight | 600 | 600 | 400 | — | P6 |
| 3 | 统计卡片 | padding-top | 16px | 28rpx | 30rpx | +2rpx | ✅ |
| 3→4 | 卡片→标题 gap | margin-top | 0px | 0rpx | 0rpx | 0 | ✅ |
```


### 阶段 2：偏差审计

目标：基于测量结果，逐属性判定偏差级别，输出结构化审计报告。

#### 2.1 偏差判定标准

| rpx 偏差 | 级别 | 说明 |
|----------|------|------|
| 0 | ✅ 通过 | 完全匹配 |
| ≤ 2rpx | ✅ 通过 | rpx 取偶导致的不可避免误差 |
| > 2rpx 且 ≤ 4rpx | ⚠️ 警告（P4-P7） | 像素级精调可修复 |
| > 4rpx | ❌ 不通过（P0-P3） | 必须修正 |

特殊属性判定：
- **font-weight**：不用 rpx 偏差，直接比对数值（400/500/600/700），不一致即 P6
- **color**：不用 rpx 偏差，比对 hex 值，不在标准色表中即 P5
- **border-radius**：使用 `px × 2` 公式（不乘 1.7442），偏差 > 2rpx 即 P4
- **结构缺失/顺序错误**：直接 P0，不进入测量

#### 2.2 审计报告模板

```markdown
# <page> 结构化审计报告

## 基本信息
- 页面：<page>
- 审计日期：YYYY-MM-DD
- H5 元素总数：N
- MP 元素总数：M
- 结构完整性：✅ 通过 / ❌ 缺失 K 个元素

## 结构完整性校验
| # | H5 元素 | MP 状态 | 问题 |
|---|---------|---------|------|
| 7 | .env-arrow svg | ❌ 缺失 | MP 不支持 inline SVG，需导出 |

## 偏差汇总
| 级别 | 数量 | 占比 |
|------|------|------|
| ✅ 通过 | XX | XX% |
| ⚠️ P4-P7 | XX | XX% |
| ❌ P0-P3 | XX | XX% |

## 逐元素偏差明细
（按映射表 # 排序，仅列出有偏差的属性）

| # | 元素 | 属性 | H5 值 | 理论 rpx | MP 值 | 偏差 | 级别 | 修正建议 |
|---|------|------|-------|---------|-------|------|------|---------|
| ... | ... | ... | ... | ... | ... | ... | ... | ... |

## 不可消除差异（不计入偏差）
- 字体渲染差异（Noto Sans SC vs 系统字体）
- 行内元素高度系统性偏小 ~7%
- 抗锯齿差异
```

#### 2.3 迁移前审计 vs 迁移后对比

| 维度 | 迁移前审计 | 迁移后对比 |
|------|-----------|-----------|
| 输入 | H5 源码 | H5 源码 + MP 源码 |
| 映射表 MP 列 | 留空 | 填入实际选择器 |
| 测量端 | 仅 H5 | H5 + MP 双端 |
| 产出 | 映射表 + 理论 rpx 值 | 偏差审计报告 |
| 用途 | 指导 WXSS 编写 | 验证迁移质量 |

迁移前审计产出的映射表直接作为迁移后对比的输入——MP 开发完成后，填入 MP 选择器列，跑结构完整性校验 + 逐级测量，即可得到偏差报告。

---

## Tailwind → WXSS 完整速查表

### 字号与行高

| Tailwind | H5 px | rpx | 说明 |
|----------|-------|-----|------|
| text-xs | 12px / 16px | 22rpx / 28rpx | 字号 / 行高 |
| text-sm | 14px / 20px | 26rpx / 36rpx | 字号 / 行高 |
| text-base | 16px / 24px | 28rpx / 42rpx | 字号 / 行高 |
| text-lg | 18px / 28px | 32rpx / 50rpx | 字号 / 行高 |
| text-xl | 20px / 28px | 36rpx / 50rpx | 字号 / 行高 |
| text-2xl | 24px / 32px | 42rpx / 56rpx | 字号 / 行高 |

⚠️ Tailwind `text-*` 同时设置 font-size 和 line-height。MP 迁移时必须同时迁移两个属性，遗漏 line-height 会导致行高塌陷。

### 间距

| Tailwind | H5 px | rpx |
|----------|-------|-----|
| p-1 / gap-1 | 4px | 8rpx |
| p-1.5 / gap-1.5 | 6px | 12rpx |
| p-2 / gap-2 | 8px | 14rpx |
| p-3 / gap-3 / space-y-3 | 12px | 22rpx |
| p-4 / gap-4 | 16px | 28rpx |
| p-5 / gap-5 | 20px | 36rpx |
| p-6 / gap-6 | 24px | 42rpx |
| p-8 / gap-8 | 32px | 56rpx |

⚠️ `space-y-N` 换算陷阱：严格按精确公式 `ceil(px × 1.7442 / 2) × 2`，不要心算跳步。
例：`space-y-3` = 12px → 12×1.7442=20.93 → ceil(20.93/2)×2 = ceil(10.47)×2 = 22rpx（不是 20rpx）。

### 常用 px → rpx 对照

```
 2px →  4rpx    3px →  6rpx    4px →  8rpx
 5px → 10rpx    6px → 12rpx    8px → 14rpx
10px → 18rpx   11px → 20rpx   12px → 22rpx
14px → 26rpx   16px → 28rpx   18px → 32rpx
20px → 36rpx   24px → 42rpx   28px → 50rpx
32px → 56rpx   36px → 64rpx   40px → 70rpx
44px → 78rpx   48px → 84rpx
```



---

## 附录 A：measure_gaps.py 详细用法

路径：`scripts/ops/measure_gaps.py`

### 基本用法

```bash
# 测量页面内所有 .task-card 元素的尺寸和间距
uv run python scripts/ops/measure_gaps.py task-list --selectors ".task-card"

# 测量多个选择器（按 DOM 顺序，计算相邻间距）
uv run python scripts/ops/measure_gaps.py board-finance --selectors ".summary-header" ".summary-content" ".grid-cols-3"

# 指定两个元素的直接间距
uv run python scripts/ops/measure_gaps.py task-list --pairs ".sticky-header" ".task-card:first-child"

# 页面中下方元素（需 scrollTop）
uv run python scripts/ops/measure_gaps.py performance --selectors ".perf-section" --scroll 1200
```

### 输出内容

- 元素尺寸表：top_px, h_px, paddingT/B, marginT/B, gap, fontSize, lineHeight, h_rpx
- 相邻元素垂直间距表：gap_px 和 gap_rpx
- audit.md 可直接粘贴的 Markdown 表格

### 常用 CSS 选择器快查

| 元素类型 | 选择器示例 |
|---|---|
| 页面内边距容器 | `.px-4`, `.px-6`, `[class*="px-"]` |
| 卡片 | `.task-card`, `[class*="card"]` |
| 列表项间距 | `.list-item`, `li`, `[class*="item"]` |
| Sticky 头部 | `.sticky`, `.filter-bar`, `[class*="sticky"]` |
| Banner | `.banner-bg`, `[class*="banner"]` |
| 标签/徽章 | `.tag`, `.badge`, `[class*="tag"]` |

---

## 附录 B：图像反推验证（截图偏差定位）

当 diff 图显示某元素位置偏移时，用截图像素反推实际间距：

```
实际间距(px) = diff 图中偏移像素数 ÷ DPR
DPR = 1.5
示例：diff 图中元素 A 比 H5 下移 9 像素
      实际偏差 = 9 / 1.5 = 6px → 查速查表 → 12rpx
      应调整 WXSS 中对应 margin/padding
```
