第3章 3.4 uni-ui 官方组件库
⏱️ 阅读时间约 90 分钟 | 难度:进阶 ⭐⭐☆ | 配套视频:暂无
🎯 开场 3 分钟:为什么要学这个?
上一章我们学了 uView UI 这个第三方组件库,用起来确实香。但你有没有想过:万一哪天 uView 不维护了怎么办? 或者你进了个公司,项目里只让用官方的东西——这时候你就得认识认识 uni-app 的「亲儿子」了。
uni-ui 是 DCloud 官方出的组件库,跟 uni-app 是一家人,适配性更好,更新更及时。
举个例子:就像手机出厂自带的原生应用,虽然不一定最好用,但肯定最稳定、最不会被卸载。
本文学完,你能掌握:
- uni-ui 怎么装、怎么用
- 三个高频组件:uni-icons 图标、uni-data-select 数据选择器、uni-forms 表单
- 能独立把这些组件用到实战项目里
🧱 基础 25 分钟:核心概念
3.4.1 uni-ui 是什么?
uni-ui 是 DCloud 官方维\n\n
\n\n
\n\n护的 UI 组件库,专门为 uni-app 生态打造。
生活类比:把 uni-app 想象成手机系统,uni-ui 就是系统出厂自带的「原生应用」,而 uView 就像从应用商店下载的「第三方应用」。原生应用不一定功能最全,但一定最听话(兼容性最好)、最准时更新(官方维护)。
为什么要用 uni-ui?
1. 官方亲儿子 —— 跟 uni-app 内核同步更新,出问题找得到人
2. easycom 兼容 —— 不用手动 import,直接用标签就能跑
3. 全端支持 —— H5、微信小程序、安卓、iOS 一套代码走天下
3.4.2 安装 uni-ui
打开终端,在项目根目录执行:
npm install @dcloudio/uni-ui
安装完成后,在 pages.json 中确认是否有 easycom 配置(一般 uni-app 项目默认有):
{
"easycom": {
"autoscan": true,
"custom": {
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
}
}
}
如果没有,手动加进去。配置好之后,不需要再做任何 import,直接用 <uni-xxx> 标签就行。
3.4.3 uni-icons 图标组件
是什么:uni-ui 自带的图标库,内置了大量常用图标。
怎么用:直接上标签,图标名称从官方文档查。
<template>
<view class="container">
<text class="title">uni-icons 图标演示</text>
<!-- 基础用法:直接用图标名 -->
<uni-icons type="home" size="30" color="#333"></uni-icons>
<!-- 带点击事件 -->
<uni-icons type="search" size="30" @click="handleSearch"></uni-icons>
<!-- 配合文字使用 -->
<view class="icon-row">
<uni-icons type="plus" size="20"></uni-icons>
<text>添加</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {}
},
methods: {
handleSearch() {
console.log('点击了搜索图标')
uni.showToast({ title: '搜索图标被点击', icon: 'none' })
}
}
}
</script>
<style>
.container {
padding: 20px;
}
.icon-row {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
}
</style>
这行 type="home" 是图标名称,size="30" 控制大小,color="#333" 控制颜色。图标名去 uni-icons 官方文档 查表就行。
3.4.4 uni-data-select 数据选择器
是什么:一个带下拉搜索的数据选择器,比原生的 picker 好用太多。
生活类比:就像外卖点餐时点「收藏的商家」——你搜一个店名,下拉列表自动筛选,点一下就选中。
为什么用:原生 picker 不能搜索,数据多了很难找。uni-data-select 支持输入过滤,体验好很多。
<template>
<view class="container">
<text class="label">选择城市:</text>
<!-- 本地数据模式 -->
<uni-data-select
v-model="selectedCity"
:localdata="cityList"
placeholder="请选择城市"
@change="onCityChange"
></uni-data-select>
<text class="result">你选中的城市:{{ selectedCity || '暂无' }}</text>
</view>
</template>
<script>
export default {
data() {
return {
selectedCity: '',
// 城市列表,value 是实际值,text 是显示文字
cityList: [
{ value: 'beijing', text: '北京' },
{ value: 'shanghai', text: '上海' },
{ value: 'guangzhou', text: '广州' },
{ value: 'shenzhen', text: '深圳' },
{ value: 'hangzhou', text: '杭州' },
{ value: 'chengdu', text: '成都' }
]
}
},
methods: {
onCityChange(e) {
console.log('选中的值:', e)
const city = this.cityList.find(item => item.value === e)
uni.showToast({
title: `你选择了 ${city ? city.text : ''}`,
icon: 'none'
})
}
}
}
</script>
<style>
.container {
padding: 20px;
}
.label {
display: block;
margin-bottom: 10px;
font-weight: bold;
}
.result {
display: block;
margin-top: 15px;
color: #666;
}
</style>
localdata 传数组,每项有 value(实际值)和 text(显示文字)。v-model 绑定选中的值,变化时触发 @change。
3.4.5 uni-forms 表单组件
是什么:一套表单解决方案,支持表单验证、自动收集数据、报错提示。
生活类比:就像租房时的「模板合同」——房型、租金、押金都有固定格子,你只要往里填就行,漏填了还自动标红提醒。
为什么用:自己做表单验证要写一堆 if-else,用 uni-forms 只需要配置规则就行。
<template>
<view class="container">
<uni-forms ref="myForm" :modelValue="formData" :rules="rules">
<!-- 用户名输入 -->
<uni-forms-item label="用户名" name="username">
<uni-easyinput
v-model="formData.username"
placeholder="请输入用户名"
clearable
/>
</uni-forms-item>
<!-- 邮箱输入 -->
<uni-forms-item label="邮箱" name="email">
<uni-easyinput
v-model="formData.email"
placeholder="请输入邮箱"
type="text"
/>
</uni-forms-item>
<!-- 密码输入 -->
<uni-forms-item label="密码" name="password">
<uni-easyinput
v-model="formData.password"
placeholder="请输入密码"
type="password"
clearable
/>
</uni-forms-item>
<!-- 提交按钮 -->
<button @click="submitForm" class="submit-btn">提交</button>
</uni-forms>
</view>
</template>
<script>
export default {
data() {
return {
formData: {
username: '',
email: '',
password: ''
},
// 表单验证规则
rules: {
username: {
rules: [
{ required: true, errorMessage: '用户名不能为空' },
{ minLength: 3, errorMessage: '用户名至少3个字符' }
]
},
email: {
rules: [
{ required: true, errorMessage: '邮箱不能为空' },
{ format: 'email', errorMessage: '邮箱格式不正确' }
]
},
password: {
rules: [
{ required: true, errorMessage: '密码不能为空' },
{ minLength: 6, errorMessage: '密码至少6位' }
]
}
}
}
},
methods: {
submitForm() {
this.$refs.myForm.validate().then(res => {
console.log('表单数据:', res)
uni.showToast({
title: '提交成功!',
icon: 'success'
})
}).catch(err => {
console.log('校验失败:', err)
})
}
}
}
</script>
<style>
.container {
padding: 20px;
}
.submit-btn {
margin-top: 20px;
background-color: #007aff;
color: #fff;
}
</style>
uni-forms 包裹整个表单,uni-forms-item 包裹每个字段,uni-easyinput 是自带的输入框组件。规则写在 rules 里,required: true 表示必填,format: 'email' 表示要校验邮箱格式。
🔥 实战 35 分钟:3 个递进的小项目
📦 项目 1:图标工具栏(5 分钟)
目标:用 uni-icons 做一个底部工具栏,点击有反馈。
<template>
<view class="page">
<text class="title">我的工具栏</text>
<!-- 中间内容区 -->
<view class="content">
<text>当前选中:{{ currentTab }}</text>
</view>
<!-- 底部图标导航 -->
<view class="tab-bar">
<view
class="tab-item"
:class="{ active: currentTab === 'home' }"
@click="switchTab('home')"
>
<uni-icons :type="currentTab === 'home' ? 'home' : 'home'" size="24"></uni-icons>
<text class="tab-text">首页</text>
</view>
<view
class="tab-item"
:class="{ active: currentTab === 'list' }"
@click="switchTab('list')"
>
<uni-icons type="list" size="24"></uni-icons>
<text class="tab-text">列表</text>
</view>
<view
class="tab-item"
:class="{ active: currentTab === 'cart' }"
@click="switchTab('cart')"
>
<uni-icons type="cart" size="24"></uni-icons>
<text class="tab-text">购物车</text>
</view>
<view
class="tab-item"
:class="{ active: currentTab === 'user' }"
@click="switchTab('user')"
>
<uni-icons type="user" size="24"></uni-icons>
<text class="tab-text">我的</text>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
currentTab: 'home'
}
},
methods: {
switchTab(tab) {
this.currentTab = tab
uni.showToast({
title: `切换到${tab}`,
icon: 'none'
})
}
}
}
</script>
<style>
.page {
height: 100vh;
display: flex;
flex-direction: column;
}
.content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.tab-bar {
display: flex;
justify-content: space-around;
padding: 10px 0;
border-top: 1px solid #eee;
background: #fff;
}
.tab-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 5px 15px;
}
.tab-item.active {
color: #007aff;
}
.tab-text {
font-size: 12px;
margin-top: 4px;
}
</style>
预期输出:点击不同图标,顶部文字更新,底部高亮切换,弹出 toast 提示。
一句话解释:用 currentTab 状态控制哪个图标高亮,@click 切换状态。
📦 项目 2:城市选择器 + 缓存(15 分钟)
目标:用 uni-data-select 做城市选择,选中后存到本地,下次打开自动恢复。
<template>
<view class="container">
<text class="title">城市选择器</text>
<uni-data-select
v-model="selectedCity"
:localdata="cityList"
placeholder="请搜索或选择城市"
@change="onCityChange"
></uni-data-select>
<view class="info-card">
<text class="info-title">当前城市信息</text>
<text>城市代码:{{ selectedCity || '未选择' }}</text>
<text>城市名称:{{ cityName }}</text>
<text>选择次数:{{ selectCount }} 次</text>
</view>
<button @click="clearStorage" class="clear-btn">清除缓存</button>
</view>
</template>
<script>
export default {
data() {
return {
selectedCity: '',
selectCount: 0,
cityList: [
{ value: '110000', text: '北京' },
{ value: '120000', text: '天津' },
{ value: '130000', text: '石家庄' },
{ value: '140000', text: '太原' },
{ value: '150000', text: '呼和浩特' },
{ value: '210000', text: '沈阳' },
{ value: '220000', text: '长春' },
{ value: '230000', text: '哈尔滨' },
{ value: '310000', text: '上海' },
{ value: '320000', text: '南京' },
{ value: '330000', text: '杭州' },
{ value: '340000', text: '合肥' },
{ value: '350000', text: '福州' },
{ value: '360000', text: '南昌' },
{ value: '370000', text: '济南' },
{ value: '410000', text: '郑州' },
{ value: '420000', text: '武汉' },
{ value: '430000', text: '长沙' },
{ value: '440000', text: '广州' },
{ value: '450000', text: '南宁' },
{ value: '460000', text: '海口' },
{ value: '500000', text: '重庆' },
{ value: '510000', text: '成都' },
{ value: '520000', text: '贵阳' },
{ value: '530000', text: '昆明' },
{ value: '610000', text: '西安' },
{ value: '620000', text: '兰州' },
{ value: '630000', text: '西宁' },
{ value: '640000', text: '银川' },
{ value: '650000', text: '乌鲁木齐' }
]
}
},
computed: {
cityName() {
const city = this.cityList.find(item => item.value === this.selectedCity)
return city ? city.text : '未选择'
}
},
onLoad() {
// 页面加载时读取缓存
const cachedCity = uni.getStorageSync('lastCity')
const cachedCount = uni.getStorageSync('selectCount')
if (cachedCity) {
this.selectedCity = cachedCity
}
if (cachedCount) {
this.selectCount = parseInt(cachedCount)
}
},
methods: {
onCityChange(e) {
if (!e) return
this.selectCount += 1
// 保存到本地缓存
uni.setStorageSync('lastCity', e)
uni.setStorageSync('selectCount', this.selectCount.toString())
const city = this.cityList.find(item => item.value === e)
uni.showToast({
title: `已选择 ${city ? city.text : ''}`,
icon: 'none'
})
},
clearStorage() {
uni.removeStorageSync('lastCity')
uni.removeStorageSync('selectCount')
this.selectedCity = ''
this.selectCount = 0
uni.showToast({
title: '缓存已清除',
icon: 'success'
})
}
}
}
</script>
<style>
.container {
padding: 20px;
}
.title {
font-size: 18px;
font-weight: bold;
margin-bottom: 20px;
display: block;
}
.info-card {
margin-top: 30px;
padding: 15px;
background: #f5f5f5;
border-radius: 8px;
}
.info-title {
display: block;
font-weight: bold;
margin-bottom: 10px;
}
.info-card text {
display: block;
margin-bottom: 5px;
}
.clear-btn {
margin-top: 20px;
}
</style>
预期输出:选择城市后刷新页面,城市依然保持;清除缓存后重置。
一句话解释:用 uni.getStorageSync 和 uni.setStorageSync 做本地持久化,onLoad 时读取恢复。
📦 项目 3:注册表单 + 数据提交(15 分钟)
目标:用 uni-forms 做完整注册表单,包含验证、数据组装、模拟提交。
<template>
<view class="container">
<text class="title">用户注册</text>
<uni-forms ref="registerForm" :modelValue="formData" :rules="rules">
<!-- 用户名 -->
<uni-forms-item label="用户名" name="username">
<uni-easyinput
v-model="formData.username"
placeholder="3-10位字母或数字"
clearable
/>
</uni-forms-item>
<!-- 邮箱 -->
<uni-forms-item label="邮箱" name="email">
<uni-easyinput
v-model="formData.email"
placeholder="请输入邮箱"
type="text"
/>
</uni-forms-item>
<!-- 手机号 -->
<uni-forms-item label="手机号" name="phone">
<uni-easyinput
v-model="formData.phone"
placeholder="请输入手机号"
type="number"
maxlength="11"
/>
</uni-forms-item>
<!-- 所在城市 -->
<uni-forms-item label="城市" name="city">
<uni-data-select
v-model="formData.city"
:localdata="cityOptions"
placeholder="请选择城市"
></uni-data-select>
</uni-forms-item>
<!-- 密码 -->
<uni-forms-item label="密码" name="password">
<uni-easyinput
v-model="formData.password"
placeholder="6位以上密码"
type="password"
clearable
/>
</uni-forms-item>
<!-- 确认密码 -->
<uni-forms-item label="确认密码" name="confirmPassword">
<uni-easyinput
v-model="formData.confirmPassword"
placeholder="再次输入密码"
type="password"
clearable
/>
</uni-forms-item>
<button @click="handleRegister" class="submit-btn">注册</button>
</uni-forms>
</view>
</template>
<script>
export default {
data() {
// 自定义校验:确认密码
const validatePass = (rule, value, data, callback) => {
if (value !== this.formData.password) {
callback(new Error('两次输入的密码不一致'))
} else {
callback()
}
}
return {
formData: {
username: '',
email: '',
phone: '',
city: '',
password: '',
confirmPassword: ''
},
rules: {
username: {
rules: [
{ required: true, errorMessage: '用户名不能为空' },
{ minLength: 3, errorMessage: '用户名至少3个字符' },
{ maxLength: 10, errorMessage: '用户名最多10个字符' },
{ pattern: /^[a-zA-Z0-9]+$/, errorMessage: '只能输入字母或数字' }
]
},
email: {
rules: [
{ required: true, errorMessage: '邮箱不能为空' },
{ format: 'email', errorMessage: '邮箱格式不正确' }
]
},
phone: {
rules: [
{ required: true, errorMessage: '手机号不能为空' },
{ pattern: /^1[3-9]\d{9}$/, errorMessage: '手机号格式不正确' }
]
},
city: {
rules: [
{ required: true, errorMessage: '请选择城市' }
]
},
password: {
rules: [
{ required: true, errorMessage: '密码不能为空' },
{ minLength: 6, errorMessage: '密码至少6位' }
]
},
confirmPassword: {
rules: [
{ required: true, errorMessage: '请确认密码' },
{ validateFunction: validatePass }
]
}
},
cityOptions: [
{ value: 'beijing', text: '北京' },
{ value: 'shanghai', text: '上海' },
{ value: 'guangzhou', text: '广州' },
{ value: 'shenzhen', text: '深圳' },
{ value: 'hangzhou', text: '杭州' },
{ value: 'chengdu', text: '成都' },
{ value: 'wuhan', text: '武汉' }
]
}
},
methods: {
handleRegister() {
this.$refs.registerForm.validate().then(res => {
console.log('表单数据:', JSON.stringify(this.formData, null, 2))
// 模拟提交到后端
uni.showLoading({ title: '注册中...' })
setTimeout(() => {
uni.hideLoading()
uni.showModal({
title: '注册成功',
content: `欢迎 ${this.formData.username}!你的城市是 ${this.getCityName(this.formData.city)}`,
showCancel: false
})
}, 1000)
}).catch(err => {
console.log('校验失败:', err)
uni.showToast({
title: '请检查表单填写',
icon: 'none'
})
})
},
getCityName(cityValue) {
const city = this.cityOptions.find(item => item.value === cityValue)
return city ? city.text : cityValue
}
}
}
</script>
<style>
.container {
padding: 20px;
}
.title {
font-size: 20px;
font-weight: bold;
margin-bottom: 25px;
display: block;
text-align: center;
}
.submit-btn {
margin-top: 25px;
background-color: #007aff;
color: #fff;
}
</style>
预期输出:填完整表单点注册,通过验证后弹出成功对话框;填写错误会提示具体哪里错。
一句话解释:uni-forms 的 validate() 返回 Promise,then 里是校验通过的表单数据,catch 里是错误信息。
💪 进阶 20 分钟:常见坑 + 性能小贴士
⚠️ 坑 1:uni-data-select 的值是 string 不是 number
<!-- ❌ 错误示例:后端返回的是数字,但 select 要求字符串 -->
<uni-data-select v-model="formData.id" ...></uni-data-select>
<!-- 假设后端返回的 id 是 123 -->
data: { id: 123 } // 会导致选不上
<!-- ✅ 正确示例:统一转成字符串 -->
data: { id: '123' }
// 或者在数据返回时做转换
onLoad() {
const res = await getUserInfo()
res.data.id = String(res.data.id)
this.formData = res.data
}
原因:uni-data-select 内部用 === 比较,类型不一致就匹配不上。
⚠️ 坑 2:uni-forms 在 onLoad 时获取不到值
<!-- ❌ 错误示例:在 onLoad 里直接读 formData -->
onLoad() {
console.log(this.formData.username) // undefined,因为还没初始化完
}
<!-- ✅ 正确示例:用 nextTick 或 $nextTick -->
onLoad() {
this.$nextTick(() => {
console.log(this.formData.username)
})
}
// 或者在 mounted 生命周期
mounted() {
console.log(this.formData.username)
}
⚠️ 坑 3:uni-icons type 写错了不报错,静默失败
<!-- ❌ 错误示例:拼写错误,图标不显示但控制台没报错 -->
<uni-icons type="hom" size="30"></uni-icons> <!-- hom 不是有效图标名 -->
<!-- ✅ 正确示例:查文档确认图标名 -->
<uni-icons type="home" size="30"></uni-icons>
技巧:写图标名时打开 uni-icons 文档 对着抄。
⚠️ 坑 4:uni-forms 的 rules 写在 data 里拿不到 this
<!-- ❌ 错误示例:rules 里想用 this.cityList,但 this 是 undefined -->
data() {
return {
cityList: [...],
rules: {
city: {
rules: [
{ validateFunction: (rule, value, callback) => {
// 这里 this 是 undefined!
if (this.cityList.find(...)) callback()
}}
]
}
}
}
}
<!-- ✅ 正确示例:把 validateFunction 写到 methods 里 -->
methods: {
validateCity(rule, value, callback) {
if (this.cityList.find(item => item.value === value)) {
callback()
} else {
callback(new Error('城市不存在'))
}
}
}
// 在 data 的 rules 里引用
rules: {
city: {
rules: [{ validateFunction: this.validateCity }]
}
}
⚠️ 坑 5:v-model 绑定的值类型不对导致验证失效
<!-- ❌ 错误示例:初始值类型跟实际值类型不一致 -->
data() {
return {
formData: {
age: 0 // 数字类型
}
}
}
// 用户输入 "25",但表单验证时类型不一致
<!-- ✅ 正确示例:初始值类型保持一致 -->
data() {
return {
formData: {
age: '' // 空字符串,类型一致
}
}
}
🛠️ 调试技巧:console.log 打印表单数据
// 提交时打印完整表单
handleSubmit() {
this.$refs.myForm.validate().then(res => {
// 格式化打印,方便查看
console.log('提交数据:', JSON.stringify(this.formData, null, 2))
})
}
✏️ 练习题 + 作业题
练习题(5 道,10 分钟内完成)
练习 1(2 分钟):换一个图标类型
- 输入:把项目 1 中的 type="home" 改成 type="image",运行看效果
- 预期输出:图标从「家」变成「图片」图标
- 提示:去文档查「image」是不是有效图标名
练习 2(2 分钟):限制只能选特定城市
- 输入:在项目 2 的城市列表中,只保留「北京、上海、广州、深圳」
- 预期输出:下拉列表只有 4 个城市可选
- 提示:直接删掉 cityList 里不需要的项就行
练习 3(2 分钟):加一个必填校验
- 输入:给项目 3 的表单加一个「昵称」字段,要求必填
- 预期输出:不填昵称点提交,提示「昵称不能为空」
- 提示:在 formData 加 nickname,在 rules 加 nickname 的规则
练习 4(2 分钟):城市选择后显示 emoji
- 输入:在项目 2 中,选中城市后在 cityName 前面加个 🏠 emoji
- 预期输出:显示「🏠北京」而不是「北京」
- 提示:在 computed 的 cityName 前面拼接 emoji 字符串
练习 5(2 分钟):找到报错原因
- 输入:以下代码运行时报错,分析原因
<uni-data-select v-model="formData.id" :localdata="list"></uni-data-select>
<script>
export default {
data() {
return {
formData: { id: 123 }, // 后端返回的数字
list: [
{ value: '123', text: '选项1' }, // 字符串
{ value: '456', text: '选项2' }
]
}
}
}
</script>
- 预期输出:选择任意选项,v-model 的值不变
- 提示:类型不一致,
123 !== '123'
📝 作业题(30 分钟 - 2 小时)
作业:做一个「个人资料设置页」
-
需求描述:做一个完整的个人资料编辑页面,包含头像(用图标模拟)、昵称、性别、所在城市、生日、个性签名
-
功能点:
1. 用uni-icons模拟头像位置(一个大的 person 图标)
2. 用uni-forms+uni-easyinput做表单,支持验证
3. 性别用uni-data-select选择(男/女/保密)
4. 城市用uni-data-select选择
5. 个性签名用多行文本输入
6. 点保存时验证全部字段,通过后显示「保存成功」 -
加分项:
1. 页面加载时从本地缓存读取已有资料
2. 生日字段用uni-dateformat或自定义格式显示 -
验收标准:
- 能跑起来
- 所有字段都有验证
- 不填必填项点保存会报错
-
填写正确点保存会弹成功提示
-
提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
本章收获
本文我们学了 3 个核心知识点:
- uni-ui 是什么:DCloud 官方的组件库,兼容性好,easycom 免导入
- 三个高频组件:uni-icons(图标)、uni-data-select(下拉选择)、uni-forms(表单验证)
- 数据持久化:用 uni.getStorageSync 做本地缓存,刷新不丢失
延伸资源
- 📖 uni-ui 官方文档 —— 组件最全查这里
- 📖 uni-forms 校验规则详解 —— 进阶验证看这里
- 📖 uni-app 官方案例仓库 —— 官方示例代码
🎤 互动钩子
你在项目里更倾向用 uni-ui 还是 uView?为什么? 评论区聊聊,优缺点都说说,老粉优先回复!
📢 下章预告:
学会了组件库,是时候做个完整的页面了。下一章我们要用学到的组件知识,仿做一个掘金首页 + 文章列表,从 0 到 1 搭一个完整的资讯类 App 页面。敬请期待!

评论(0)