62 KiB
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 文档
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
理由:
- 小程序的主要使用场景是手机,屏幕宽度在 320-428px 之间,rpx 的等比缩放在这个范围内视觉差异可接受
- 微信官方推荐使用 rpx 做适配,但并未要求所有样式都必须用 rpx;WXSS 也支持直接写 px
- 412px 是验收基准,在该设备上 rpx 换算后的值与原型一致
- 固定 px 在小屏设备上可能导致内容溢出或布局挤压,但在细节尺寸上能保持质感
- 全面 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 是:
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> 设置。
正确做法:
/* 全局设置 */
page {
line-height: 1.5; /* Tailwind 默认行高 */
}
view {
line-height: inherit; /* view 继承 page 的 line-height */
}
/* text 会自动继承外层 view 的 line-height,不需要额外设置 */
局部覆盖:
.section-title {
font-size: 26rpx;
line-height: 36rpx; /* 在 view 的 class 上设置,text 会继承 */
font-weight: 600;
}
<view class="section-title">
<text>标题文本</text>
</view>
错误做法:
/* ❌ 直接在 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:部分机型不支持或性能差
降级策略:
/* 优先尝试 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 嵌套容易出问题
迁移规则:
- 页面级 sticky(如筛选栏吸顶):直接用
position: sticky; top: Nrpx scroll-view内的 sticky:改用 JS 监听bindscroll+ 条件渲染- 多层 sticky:只保留最外层,内层改为固定布局
env(safe-area-inset-*)(原型使用 9 次)
小程序不支持 env() CSS 函数。
替代方案:
// JS 获取安全区信息
const systemInfo = wx.getSystemInfoSync()
const statusBarHeight = systemInfo.statusBarHeight // 状态栏高度 px
const safeAreaBottom = systemInfo.screenHeight - systemInfo.safeArea.bottom // 底部安全区 px
/* 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 属性,可以实现按压态效果:
<view hover-class="pressed" hover-stay-time="100">
点击我
</view>
.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。
处理方式:
- 将 SVG 导出为独立
.svg文件,放入assets/icons/ - 用
<image src="/assets/icons/xxx.svg" mode="aspectFit">引用 - 高频图标考虑使用 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 写法:
// 显示弹窗
document.getElementById('modal').classList.remove('hidden')
document.getElementById('modal').classList.add('flex')
// 隐藏弹窗
document.getElementById('modal').classList.add('hidden')
document.getElementById('modal').classList.remove('flex')
小程序写法:
// JS
Page({
data: { showModal: false },
openModal() { this.setData({ showModal: true }) },
closeModal() { this.setData({ showModal: false }) }
})
<!-- 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 写法:
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')
}
小程序写法:
Page({
data: { activeTab: 'basic' },
onTabChange(e) {
this.setData({ activeTab: e.currentTarget.dataset.tab })
}
})
<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 写法:
function toggleExpand(id) {
const el = document.getElementById(id)
el.classList.toggle('hidden')
const btn = document.getElementById(id + '-btn')
btn.textContent = el.classList.contains('hidden') ? '展开更多' : '收起'
}
小程序写法:
Page({
data: { expandedSections: {} },
toggleSection(e) {
const key = e.currentTarget.dataset.key
this.setData({ [`expandedSections.${key}`]: !this.data.expandedSections[key] })
}
})
<view bindtap="toggleSection" data-key="detail">
<text>{{expandedSections.detail ? '收起' : '展开更多'}}</text>
</view>
<view wx:if="{{expandedSections.detail}}">
<!-- 展开内容 -->
</view>
8.5 模式 D:style 直接操作 → 动态 style 绑定(47 处)
H5 写法:
element.style.top = scrollY + 'px'
element.style.opacity = '0'
element.style.transform = 'translateY(-10px)'
小程序写法:
Page({
data: { dynamicTop: 0, fadeOpacity: 1 },
onScroll(e) {
this.setData({ dynamicTop: e.scrollTop })
}
})
<view style="top: {{dynamicTop}}px; opacity: {{fadeOpacity}}; transform: translateY({{offsetY}}px)">
注意:频繁 setData 驱动 style 变化会导致性能问题。对于动画场景,优先使用 WXSS transition / animation,或 wx.createAnimation()。
8.6 模式 E:内容动态更新(38 处)
H5 写法:
document.getElementById('total').textContent = '¥12,345'
document.getElementById('list').innerHTML = items.map(i => `<div>${i.name}</div>`).join('')
小程序写法:
Page({
data: { total: '¥12,345', items: [] },
loadData() {
this.setData({
total: '¥12,345',
items: [{ name: '台桌' }, { name: '酒水' }]
})
}
})
<text>{{total}}</text>
<view wx:for="{{items}}" wx:key="name">
<text>{{item.name}}</text>
</view>
8.7 模式 F:表单验证样式(~10 处)
H5 写法:
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')
}
小程序写法:
Page({
data: { errors: {} },
validate(field, value) {
this.setData({ [`errors.${field}`]: !value })
}
})
<input class="input {{errors.phone ? 'input--error' : ''}}" bindinput="onPhoneInput" />
<text wx:if="{{errors.phone}}" class="error-text">请输入手机号</text>
8.8 模式 G:滚动联动(~15 处)
原型中看板页的筛选栏隐藏/显示、吸顶标题切换、section 感知都依赖滚动事件。
H5 写法:
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
})
小程序写法:
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 })
}
}
})
关键规则:
- 滚动事件处理函数中必须做节流(100ms 以上)
- 只在状态真正变化时调用
setData,避免无意义的重渲染 - 不要在滚动事件中更新大量数据或触发复杂计算
- 吸顶标题切换:用
wx.createIntersectionObserver()监听 section 进出视口,比手动计算 scrollTop 更高效
8.9 模式 H:长按菜单(task-list 页面)
H5 写法:
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))
小程序写法:
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 })
}
})
<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 写法:
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)))
})
})
小程序写法:
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 写法:
navigator.clipboard.writeText(text).then(() => {
copyBtn.classList.add('copied')
setTimeout(() => copyBtn.classList.remove('copied'), 2000)
})
小程序写法:
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 写法:
window.location.href = 'task-detail.html?id=123'
history.back()
小程序写法:
// 普通页面跳转
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 写法:
toast.classList.remove('hidden')
setTimeout(() => toast.classList.add('hidden'), 2000)
小程序写法:
Page({
showToast(text) {
this.setData({ showToast: true, toastText: text })
setTimeout(() => {
this.setData({ showToast: false })
}, 2000)
}
})
或直接使用微信 API:
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 实现的动画。在小程序中:
- 优先用 WXSS transition/animation(性能最好)
- 其次用
wx.createAnimation() - 避免用连续
setData逐帧更新
wx.createAnimation() 示例:
const animation = wx.createAnimation({
duration: 300,
timingFunction: 'ease'
})
animation.opacity(0).translateY(-10).step()
this.setData({ fadeAnimation: animation.export() })
<view animation="{{fadeAnimation}}">内容</view>
9.5 @media (prefers-reduced-motion: reduce)
原型中有减少动画偏好的媒体查询。小程序 WXSS 支持 @media 查询,可以直接迁移:
@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 选择器中定义全局变量:
/* app.wxss */
page {
--color-primary: #0052d9;
--ai-from: #667eea;
/* ... */
}
组件内可以通过 class 覆盖变量值(与 H5 行为一致):
.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)