1569 lines
62 KiB
Markdown
1569 lines
62 KiB
Markdown
# 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)
|
||
|