第1章 1.5 样式与 rpx 自适应
上一章我们搞定了 Vue3 语法和组件,写出了第一个能交互的页面。但你有没有这种感觉:页面在开发工具里看着挺漂亮,到了真机上不是字挤成一团,就是按钮大得离谱?
这不是你的问题,是屏幕尺寸的锅。
这一章我们来解决这个问题——学会用 rpx 让你的页面在所有设备上都长得刚刚好。
🎯 开场 3 分钟:为什么你的页面会"变形"?
想象你是个餐厅老板,要给三种大小的桌子印桌布:
- 小桌子(iPhone SE):宽 50cm
- 中桌子(iPhone 12):宽 60cm
- 大桌子(iPad):宽 80cm
如果你只印一种尺寸的桌布,肯定有的盖不满、有的太大。你会怎么做?
聪明的做法是:用「相对于桌子宽度的比例」来算桌布大小——桌布宽度 = 桌子宽度 × 0.6。这样不管桌子多大,桌布都刚刚好。
rpx 就是这个原理。它是小程序/uniapp 里的「相对单位」——rpx = responsive pixel,会根据屏幕宽度自动换算成真实像素。
学完这一章,你就能写出「一\n\n
\n\n
\n\n次编写,完美适配所有手机」的页面了。
🧱 基础 25 分钟:核心概念
1.5.1 rpx 是什么?
生活类比:rpx 就像是服装的「尺码」——S/M/L 不是固定厘米数,而是相对于身材的比例。所以同是 L 码,175cm 和 185cm 的人穿都合适。
为什么用 rpx:
- 不用写一堆 @media 查询
- 不用记每个机型的具体尺寸
- 设计稿给的 750px 宽度,直接除以 2就是 rpx 值
怎么用:
<template>
<view class="container">
<text class="title">小明的水果店</text>
<view class="price">原价:¥12.5</view>
</view>
</template>
<style>
.container {
width: 750rpx; /* 整个屏幕宽度(设计稿标准) */
padding: 20rpx;
background-color: #f5f5f5;
}
.title {
font-size: 32rpx; /* 标题大小 */
color: #333;
}
.price {
font-size: 28rpx; /* 价格大小 */
color: #ff6600;
margin-top: 10rpx;
}
</style>
解释:.container 设为 750rpx 就是占满整个屏幕宽度。设计稿上量出来多少 px,除以 2 直接写 rpx 就行。
1.5.2 upx2px 和 px2rpx 的换算
有时候你需要手动算一下尺寸,uniapp 提供了两个工具函数。
生活类比:就像Currency换算——你知道「人民币」的值,但需要算出「美元」的值,这时候查一下汇率就行。
为什么用:
- rpx 是写代码用的单位
- px 是系统 API 用的单位
- 有时候需要互相转换
怎么用:
<template>
<view>
<text>屏幕宽度:{{ screenWidth }} px</text>
<text>换算成 rpx:{{ widthInRpx }} rpx</text>
</view>
</template>
<script>
export default {
data() {
return {
screenWidth: 375,
widthInRpx: 0
}
},
onLoad() {
// 获取屏幕宽度(px)
const info = uni.getSystemInfoSync()
this.screenWidth = info.windowWidth
// px 转 rpx:750 是设计稿标准宽度
// 公式:rpx = px * (750 / 设计稿宽度)
this.widthInRpx = this.screenWidth * (750 / 375)
}
}
</script>
解释:uniapp 默认设计稿宽度是 375px(iPhone 6/7/8 标准),所以 px × 2 = rpx。上面代码 widthInRpx 算出来应该是 750。
1.5.3 rpx 和 rem 的区别(避坑)
新手最常搞混的两个单位,记住一个口诀:
rpx = 固定参照屏幕宽度,rem = 固定参照根字体大小
生活类比:
- rpx 像「按比例分配」——不管桌子多大,布都占桌子的 60%
- rem 像「按固定步长」——不管桌子多大,布永远增加固定的 5cm
什么时候用谁:
- rpx:页面布局、盒子大小(推荐,响应式更强)
- rem:字体大小(有时候需要统一缩放比例)
<style>
/* rpx 用在布局上 */
.box {
width: 200rpx;
height: 200rpx;
margin: 20rpx;
}
/* rem 用在字体上(如果需要整体缩放) */
.text {
font-size: 1.2rem;
}
</style>
1.5.4 scss 变量和混合器
写过 CSS 的同学都知道,改一个颜色要改十几处。scss 的变量就是来解决这个麻烦的。
生活类比:就像你给常用联系人建了个「快捷方式」——打「老妈」比记一串手机号方便多了,而且要换号只改一处就行。
为什么用 scss:
- 变量一次定义,到处使用
- 颜色/间距统一管理
- 代码更 DRY(Don't Repeat Yourself)
怎么用:
<template>
<view class="card">
<text class="title">苹果</text>
<text class="price">¥5.00</text>
</view>
</template>
<style lang="scss">
/* 定义变量 */
$primary-color: #07c160;
$secondary-color: #646464;
$spacing-base: 20rpx;
$border-radius: 12rpx;
.card {
background-color: #fff;
border-radius: $border-radius;
padding: $spacing-base;
margin: $spacing-base;
.title {
color: $primary-color;
font-size: 32rpx;
}
.price {
color: $secondary-color;
font-size: 28rpx;
margin-top: 10rpx;
}
}
</style>
解释:把颜色、间距这些抽成变量,以后要换主题色只改 $primary-color 一处就行。
🔥 实战 35 分钟:3 个递进小项目
项目 1:自适应购物卡片(5 分钟)
目标:写一个商品卡片,在任何屏幕上都比例协调。
<template>
<view class="product-card">
<view class="image-placeholder">🍎</view>
<view class="info">
<text class="name">红富士苹果</text>
<text class="desc">新鲜采摘 脆甜多汁</text>
<view class="bottom-row">
<text class="price">¥12.80</text>
<view class="btn">加入购物车</view>
</view>
</view>
</view>
</template>
<style lang="scss">
$card-width: 710rpx;
$card-padding: 20rpx;
$primary: #07c160;
$text-dark: #333333;
$text-gray: #888888;
.product-card {
width: $card-width;
margin: 20rpx auto;
background: #ffffff;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
display: flex;
padding: $card-padding;
.image-placeholder {
width: 160rpx;
height: 160rpx;
background: #f0f0f0;
border-radius: 12rpx;
font-size: 80rpx;
text-align: center;
line-height: 160rpx;
}
.info {
flex: 1;
margin-left: 20rpx;
display: flex;
flex-direction: column;
.name {
font-size: 32rpx;
color: $text-dark;
font-weight: bold;
}
.desc {
font-size: 24rpx;
color: $text-gray;
margin-top: 8rpx;
}
.bottom-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
.price {
font-size: 36rpx;
color: #ff4d4f;
font-weight: bold;
}
.btn {
background: $primary;
color: #fff;
font-size: 26rpx;
padding: 12rpx 24rpx;
border-radius: 30rpx;
}
}
}
}
</style>
预期输出:一个卡片,左边图片右边信息,价格和按钮在底部对齐。
解释:卡片宽度 710rpx(留 20rpx 边距),图片固定 160rpx 宽,内容区 flex: 1 自适应。
项目 2:商品列表 + 动态计算价格(15 分钟)
目标:读取一组商品数据,根据屏幕宽度动态决定每行显示几个。
<template>
<view class="page">
<view class="header">🍓 小明的水果店</view>
<view class="grid" :style="gridStyle">
<view
v-for="item in products"
:key="item.id"
class="product-item"
>
<view class="emoji">{{ item.emoji }}</view>
<text class="name">{{ item.name }}</text>
<text class="price">¥{{ item.price.toFixed(2) }}</text>
</view>
</view>
<view class="summary">
共 {{ products.length }} 件商品,总价 ¥{{ totalPrice.toFixed(2) }}
</view>
</view>
</template>
<script>
export default {
data() {
return {
products: [
{ id: 1, name: '红富士', emoji: '🍎', price: 5.5 },
{ id: 2, name: '香蕉', emoji: '🍌', price: 3.2 },
{ id: 3, name: '橙子', emoji: '🍊', price: 4.8 },
{ id: 4, name: '葡萄', emoji: '🍇', price: 12.0 },
{ id: 5, name: '草莓', emoji: '🍓', price: 15.0 },
{ id: 6, name: '西瓜', emoji: '🍉', price: 8.5 },
]
}
},
computed: {
// 根据屏幕宽度动态计算每行几个
columnsCount() {
const width = uni.getSystemInfoSync().windowWidth
if (width >= 500) return 4 // 大屏(平板)
if (width >= 375) return 3 // 标准屏
return 2 // 小屏
},
// 生成 grid 样式
gridStyle() {
return `grid-template-columns: repeat(${this.columnsCount}, 1fr);`
},
// 计算总价
totalPrice() {
return this.products.reduce((sum, p) => sum + p.price, 0)
}
}
}
</script>
<style lang="scss">
$primary: #07c160;
$bg-color: #f5f5f5;
.page {
min-height: 100vh;
background: $bg-color;
padding: 20rpx;
}
.header {
font-size: 36rpx;
font-weight: bold;
color: #333;
text-align: center;
padding: 30rpx 0;
}
.grid {
display: grid;
gap: 20rpx;
}
.product-item {
background: #fff;
border-radius: 12rpx;
padding: 20rpx;
text-align: center;
.emoji {
font-size: 60rpx;
height: 80rpx;
line-height: 80rpx;
}
.name {
display: block;
font-size: 28rpx;
color: #333;
margin-top: 10rpx;
}
.price {
display: block;
font-size: 26rpx;
color: #ff4d4f;
margin-top: 8rpx;
}
}
.summary {
text-align: center;
padding: 30rpx;
font-size: 30rpx;
color: #666;
}
</style>
预期输出:商品按网格排列,小屏 2 列、中屏 3 列、大屏 4 列。底部显示总价。
解释:用 CSS grid + columnsCount 动态计算实现自适应,不需要写媒体查询。
项目 3:优惠券领取页面(15 分钟)
目标:做一个优惠券展示 + 领取的页面,模拟真实场景。
<template>
<view class="container">
<view class="title-section">
<text class="main-title">🎁 限时优惠券</text>
<text class="sub-title">全场水果通用</text>
</view>
<view class="coupon-list">
<view
v-for="coupon in coupons"
:key="coupon.id"
class="coupon"
:class="{ disabled: coupon.claimed || !coupon.available }"
>
<view class="coupon-left">
<text class="amount">{{ coupon.discount }}</text>
<text class="unit">元</text>
</view>
<view class="coupon-right">
<text class="coupon-name">{{ coupon.name }}</text>
<text class="coupon-desc">满 {{ coupon.threshold }} 元可用</text>
<text class="coupon-date">有效期至 {{ coupon.expireDate }}</text>
</view>
<view class="coupon-action">
<view
v-if="!coupon.claimed && coupon.available"
class="claim-btn"
@tap="claimCoupon(coupon)"
>
立即\n领取
</view>
<text v-else-if="coupon.claimed" class="claimed-text">已领取</text>
<text v-else class="unavailable-text">已抢光</text>
</view>
</view>
</view>
<view class="toast" v-if="toastVisible">
{{ toastMessage }}
</view>
</view>
</template>
<script>
export default {
data() {
return {
toastVisible: false,
toastMessage: '',
coupons: [
{ id: 1, name: '新人专享', discount: 5, threshold: 20, expireDate: '2024-12-31', claimed: false, available: true },
{ id: 2, name: '满减券', discount: 10, threshold: 50, expireDate: '2024-12-31', claimed: false, available: true },
{ id: 3, name: '限时特惠', discount: 20, threshold: 100, expireDate: '2024-12-31', claimed: false, available: false },
{ id: 4, name: '生日福利', discount: 15, threshold: 30, expireDate: '2024-12-31', claimed: true, available: true },
]
}
},
methods: {
claimCoupon(coupon) {
coupon.claimed = true
this.showToast(`恭喜获得${coupon.discount}元优惠券!`)
},
showToast(message) {
this.toastMessage = message
this.toastVisible = true
setTimeout(() => {
this.toastVisible = false
}, 2000)
}
}
}
</script>
<style lang="scss">
$primary: #07c160;
$danger: #ff4d4f;
$gray: #999;
$bg: #f5f5f5;
.container {
min-height: 100vh;
background: $bg;
padding: 30rpx;
}
.title-section {
text-align: center;
margin-bottom: 40rpx;
.main-title {
display: block;
font-size: 40rpx;
font-weight: bold;
color: #333;
}
.sub-title {
display: block;
font-size: 26rpx;
color: $gray;
margin-top: 10rpx;
}
}
.coupon-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.coupon {
display: flex;
background: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
&.disabled {
opacity: 0.6;
}
}
.coupon-left {
width: 180rpx;
background: linear-gradient(135deg, #ff6b6b, #ff4d4f);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 30rpx 0;
.amount {
font-size: 56rpx;
font-weight: bold;
color: #fff;
}
.unit {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.9);
}
}
.coupon-right {
flex: 1;
padding: 24rpx;
display: flex;
flex-direction: column;
justify-content: center;
.coupon-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.coupon-desc {
font-size: 24rpx;
color: $gray;
margin-top: 8rpx;
}
.coupon-date {
font-size: 22rpx;
color: #bbb;
margin-top: 10rpx;
}
}
.coupon-action {
width: 140rpx;
display: flex;
align-items: center;
justify-content: center;
.claim-btn {
background: $primary;
color: #fff;
font-size: 26rpx;
padding: 16rpx 20rpx;
border-radius: 30rpx;
text-align: center;
line-height: 1.4;
}
.claimed-text {
font-size: 24rpx;
color: $gray;
}
.unavailable-text {
font-size: 24rpx;
color: $danger;
}
}
.toast {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.75);
color: #fff;
padding: 30rpx 60rpx;
border-radius: 12rpx;
font-size: 28rpx;
z-index: 999;
}
</style>
预期输出:优惠券列表,每张券有金额、名称、使用条件、有效期。状态分三种:可领取、已领取、已抢光。
解释:用 flex 布局实现券的左中右三栏结构,用 linear-gradient 做优惠券的红色背景。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:设计稿尺寸搞混
/* ❌ 错误:直接把设计稿 px 当 rpx 用 */
.box {
width: 375px; /* 设计稿上标的 px */
}
/* ✅ 正确:除以 2 转 rpx */
.box {
width: 375rpx; /* 或者直接量设计稿的 rpx 值 */
}
原因:设计稿一般基于 750px 宽(iPhone 6 基准),rpx 是设计稿数值的 1/2。
坑 2:flex 布局里忘了给子元素设置宽度
/* ❌ 错误:flex 子元素没有明确宽度,导致混乱 */
.container {
display: flex;
.item {
padding: 20rpx; /* 可能不生效或表现不一致 */
}
}
/* ✅ 正确:明确告诉子元素该怎么分配空间 */
.container {
display: flex;
.item {
flex: 1; /* 或者 flex: none + width */
padding: 20rpx;
}
}
坑 3:scoped 样式影响了子组件
<!-- ❌ 错误:scoped 的 deep 选择器写错了 -->
<style scoped>
.parent :global(.child) { /* 错:不是 global */
color: red;
}
</style>
<!-- ✅ 正确:使用 ::v-deep 或 /deep/ -->
<style scoped>
.parent ::v-deep .child {
color: red;
}
</style>
坑 4:rpx 在旧版 uniapp 不支持
/* 如果发现 rpx 不生效,检查是否用了不支持的编译器版本 */
@import './variable.scss'; /* 确保 scss 文件里也是 rpx */
/* 或者降级使用 px(但会失去自适应能力) */
坑 5:图片用 rpx 导致模糊
/* ❌ 错误:小图标用 rpx 宽高可能模糊 */
.icon {
width: 100rpx;
height: 100rpx;
}
/* ✅ 正确:小图标建议用固定 px,或者用 iconfont */
.icon {
width: 50px;
height: 50px;
}
调试技巧:实时查看当前设备的 rpx 换算
// 在控制台执行,获取当前设备信息
uni.getSystemInfo({
success: (res) => {
console.log('屏幕宽度(px):', res.windowWidth)
console.log('屏幕宽度(rpx):', res.windowWidth * (750 / 375))
console.log('设备型号:', res.model)
console.log('系统:', res.system)
}
})
✏️ 练习题 + 作业题
练习题(10 分钟)
练习 1(2 分钟):换个商品
- 输入:把项目 1 的商品从「苹果」改成「香蕉」
- 预期输出:卡片里显示香蕉的 emoji 和名称
- 提示:改两处——emoji 和 name
练习 2(2 分钟):加个折扣标签
- 输入:在项目 1 的卡片右上角加个「8折」标签
- 预期输出:卡片右上角显示红色「8折」
- 提示:用 position: absolute 定位到右上角
练习 3(3 分钟):添加新商品
- 输入:用项目 2 的数据结构,添加两种新水果(榴莲、火龙果)
- 预期输出:列表里多出两个商品
- 提示:在 products 数组里 push 新对象
练习 4(3 分钟):改价格保留小数
- 输入:把项目 2 里所有价格改成保留 2 位小数显示
- 预期输出:¥3.00 而不是 ¥3
- 提示:使用 toFixed(2) 方法
练习 5(5 分钟):分析报错
- 输入:以下代码为什么会抖动?修复它
<template>
<view class="container">
<text class="price">¥{{ price }}</text>
</view>
</template>
<script>
export default {
data() {
return {
price: 12.5
}
},
onLoad() {
// 模拟延迟加载
setTimeout(() => {
this.price = 9.9
}, 1000)
}
}
</script>
<style>
.container {
width: 750rpx;
}
.price {
font-size: 32rpx;
}
</style>
- 预期输出:价格变化时页面不会抖动
- 提示:给价格区域一个固定最小宽度
作业题(30 分钟 - 2 小时)
作业:做一个「账单记录工具」
-
需求描述:小明每天要记花了多少钱,今天买了啥。用这个工具记录一周的消费。
-
功能点:
1. 显示本周消费汇总(总金额、笔数、平均每笔)
2. 列出消费记录列表,每条包含:日期、商品、金额、分类(餐饮/水果/其他)
3. 根据金额自动变色(>100 红色,<50 绿色,中间蓝色)
4. 页面自适应,在不同屏幕都好看 -
加分项:
1. 用 scss 变量统一管理颜色
2. 底部有个「本周节省」的计算显示 -
验收标准:
- 能跑起来看到消费列表
- 不同屏幕宽度下列表布局合理
-
金额颜色按规则变色
-
提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
一句话总结:rpx 是 uniapp 的自适应神器——用 750rpx 标设计稿尺寸,你的页面就能在所有设备上自动适配。
延伸学习:
- uniapp 官方样式文档——官方出品的样式指南
- CSS Flex 布局教程——flex 布局的原理讲得很清楚
- Sass 官方文档中文版——scss 变量、混合器的完整参考
互动钩子:你在写页面的时候,有没有被「在不同手机上长得不一样」坑过?是用什么方法解决的?评论区聊聊,老粉优先回复!
下章预告:学会了样式自适应,下一章我们要解决一个更底层的问题——页面什么时候该干什么事。比如页面加载时该做什么、返回时该做什么、隐藏时又要做什么。这就是「生命周期」,下一章揭晓。

评论(0)