前端【uniapp】02【全局文件】【组件【内置、扩展(uni-ui、uview-plus)】】【生命周期】【API调用】【条件编译】【屏幕安全区域】【节点信息获取】【自定义组件custom-tabs、custom-sticky】【组件样式隔离】【uniForms表单验证】【字体图标】
uni-app 是组合了 Vue 和微信小程序的相关技术知识,要求大家同时俱备 Vue 和原生小程序的开发基础。
1、全局文件
在小程序中有全局样式、全局配置等全局性的设置,为此在 uni-app 中也有一些与之相对应的全局性的文件。
uni.scss
uni-app 项目在运行时会自动将 uni.scss
会自动被注入到页面样式当中,根据这个特性可以在 uni.scss
中定义一些全局 SASS 变量,统一页面的样式风格,如主色调、边框圆角等。
1 /* uni.scss */ 2 /* 在原有 sass 变量基础上添加新的变量 */ 3 $uni-bg-color: #f4f4f4;
App.vue
App.vue 在 uni-app 中是一个特殊的文件
1 <script> 2 // 相当于小程序的 app.js 3 export default { 4 onLaunch: function () { 5 console.log('App Launch') 6 }, 7 onShow: function () { 8 console.log('App Show') 9 }, 10 onHide: function () { 11 console.log('App Hide') 12 }, 13 } 14 </script> 15 16 <!-- 组件模板需要被省略 --> 17 <template></template> 18 <!-- 组件模板需要被省略 --> 19 20 <style> 21 /* 相当于 app.wxss */ 22 page { 23 background-color: $uni-bg-color; 24 } 25 </style>
script
相当于小程序的app.js
style
相当于小程序的app.wxss
,为其指定lang="scss"
属性后,会自动安装dart-sass
插件
pages.json
pages.json 文件即包含了小程序的【全局配置】也包含了【页面配置】:
1 { 2 "pages": [ 3 { 4 "path": "pages/index/index", 5 "style": { 6 "navigationBarTitleText": "学习uni-app" 7 } 8 } 9 ], 10 "globalStyle": { 11 "navigationBarTextStyle": "black", 12 "navigationBarTitleText": "uni-app", 13 "navigationBarBackgroundColor": "#F8F8F8", 14 "backgroundColor": "#F8F8F8" 15 }, 16 "uniIdRouter": {} 17 }
上述是 pages.json 初始的代码,其中
pages
对应的是【页面配置】,path
指定页面的路径,style
为该路径的相关配置,如背景色、导航栏等globalStyle
对应【全局配置】的window
,用来全局配置页面背景色、导航栏等
那其它的全局配置如何定义呢?比如 tabBar
、subPackages
答案是其它的【全局配置】与 globalStyle
平级就可以了
重点:tabBar 的图片必须要放到 static 目录下,否则小程序中无法运行
关于tabBar
的配置
在pages.json文件中与globalStyle
平级
1 { 2 "pages": [], 3 "globalStyle": {}, 4 "tabBar": { 5 "color": "", 6 "selectedColor": "", 7 "borderStyle": "", 8 "position": "", 9 "list": [ 10 { 11 "text": "", 12 "pagePath": "", 13 "iconPath": "", 14 "selectedIconPath": "" 15 } 16 ] 17 }, 18 "uniIdRouter": {} 19 }
关于subPackages的配置
在pages.json文件中与globalStyle
平级
1 { 2 "pages": [], 3 "globalStyle": {}, 4 "tabBar": {}, 5 "subPackages": [ 6 { 7 "root": "", 8 "pages": [ 9 { 10 "path": "", 11 "style": { 12 13 } 14 } 15 ], 16 } 17 ], 18 "uniIdRouter": {} 19 }
其它的【全局配置】,也都采用与 globalStyle
平级的方式来写就可以了。
2、组件
在 uni-app 中组件分成内置组件和扩展组件。
内置组件
uni-app 把微信小程序的内置组件都做了重新实现,保证能够在不同的平台表现尽量一致,因此在学习使用uni-app 的组件时,只需要参照微信小程序内置组件即可。
1 <view class="message">hello uni-app!</view> 2 <image src="" mode="" /> 3 <swiper autoplay> 4 <swiper-item> 5 <image src="" /> 6 </swiper-item> 7 <swiper-item> 8 <image src="" /> 9 </swiper-item> 10 <swiper>
内置组件的使用与原生组件基本一致
扩展组件(uni-ui、uViewPlus)
在 uni-app 中的扩展组件(uni ui)大多是一些业务性与交互性比较强的组件,比如倒计时组件、日历组件、文件上传等,扩展组件是需要下载到项目录目录中才可以使用。
uni-app官方文档:https://uniapp.dcloud.net.cn/component/uniui/uni-ui.html#
uViewPlus文档:https://uiadmin.net/uview-plus/components/quickstart.html#google_vignette
参照文档来使用相应的组件,插件市场也有许多第三方的优秀组件库,如 uView(不支持 Vue3)
3、生命周期
在 uni-app 中生命周期也微信小程序一样也分成3个类别,分别是应用级生命周期、页面级生命周期和组件级生命周期,其支持情况可见下表:
应用级
名称 |
应用是否可用 |
页面是否可用 |
组件是否可用 |
onLaunch |
是 |
否 |
否 |
onShow |
是 |
否 |
否 |
onHide |
是 |
否 |
否 |
可以看出小程序应用级的生命周期只能用在应用级别,即只能在 App.vue 中应用。
页面级
名称 |
应用是否可用 |
页面是否可用 |
组件是否可用 |
onLoad |
否 |
是 |
否 |
onShow |
否 |
是 |
否 |
onReady |
否 |
是 |
否 |
onHide |
否 |
是 |
否 |
可以看出小程序页面级的生命周期只能用在页面中,组件中是不支持的。
组件级
名称 |
应用是否可用 |
页面是否可用 |
组件是否可用 |
beforeCreate |
是 |
是 |
是 |
created |
是 |
是 |
是 |
beforeMount |
是 |
是 |
是 |
mounted |
是 |
是 |
是 |
beforeUpdate |
是 |
是 |
是 |
updated |
是 |
是 |
是 |
activated |
仅H5 |
仅H5 |
仅H5 |
deactivated |
仅H5 |
仅H5 |
仅H5 |
beforeDestoryed |
是 |
是 |
是 |
destoryed |
是 |
是 |
是 |
当然上表是不需要大家死记硬背的,大家记这样一个原则即可:
1、【应用生命周期】和【页面生命周期】采用小程序的生命期,
2、自定义【组件生命周期】使用 Vue 的生命周期。
3、结合 Vue3 的 setup 语法使用【应用生命周期】和【页面生命周期】需要用到 @dcloudio/uni-app
包,这个包不需要单独安装,HBuilder X 中内置已经包含,在项目代码中直接使用即可。
1 <script setup> 2 // Vue 组件生命周期 3 import { onMounted } from 'vue' 4 // 应用/页面生命周期(小程序生命周期) 5 import { onLaunch, onLoad } from '@dcloudio/uni-app' 6 7 // ... 8 </script>
4、API 调用
命名空间
uni-app 把微信小程序绝大部分的 API 做了重新实现,使其尽量能在不同的平台(H5的限制比较多)中使用,所不同的是在调用这些 API 时,需要将命名空间换成 uni
,举例来说明,原来的调用方法为 wx.request
在 uni-app 中则换成 uni.request
即可。
1 <script setup> 2 import { onLoad } from '@dcloudio/uni-app' 3 4 // 页面加载后获取接口数据 5 onLoad(() => { 6 // 原来的 wx.request 换成 uni.request 7 uni.request({ 8 url: '', 9 success(result) { 10 console.log(result) 11 } 12 }) 13 }) 14 </script>
Promise
在原生小程序中有部分的 API 是不支持 Promise 的,比如 wx.request
、wx.uploadFile
等,在 uni-app 中对这些 API 的调用方法做了规订,使其即能支持 Promise 也可以支持 callback 方式,它是这样规定的:
-
在调用 API 时,如果传入
success
、fail
、complete
任意回调函数,即为callback方式,就不不能使用then方式拿结果
1 // 回调方式,不返回 Promise 2 uni.request({ 3 url: 'https://your.api.com/xxx', 4 success: (result) => { 5 console.log(result) 6 } 7 }) 8 9 // 如果在调用 API 时传入了 success fail complete 那么不会返回 Promise
-
在调用 API 时,没有传入任意回调函数,即为 Promise 方式,可以通过then方式拿结果,配合async/awit使用
1 // Promise 方式 2 const result = uni.request({ 3 url: 'https://your.api.com/xxx' 4 }) 5 // Promise 对象 6 console.log(result)
返回值为 Promise 时方便配置 async/wait 来获取结果。
5、条件编译
uni-app 目标是通过编写一套代码,实现跨端的开发,但是不同的平台之间存在的差异也是事实,很难做到完全一套代码在各个平台都能够兼容,比如 uni.login
这个 API 在 H5 平台就无法被支持,再比如 keep-alive
只能用在 H5 端。
为了解决平台的差异性,特殊情况下需要为不同平台编写合适的代码,且要保证这些代码只在某个的平台下运行,uni-app 提供了条件编译的技术解决方案。
基本语法
条件编译是用特殊的注释作为标记,在编译时根据这些特殊的注释,将注释里面的代码编译到不同平台。
语法格式为:
在不通的地方使用不同的注释,然后加上特定的语法 #ifedf,js中使用注释("//") html和vue模板中使用( <!-- --> ),css中使用原生注释( /* */)
1 #ifdef 平台名称 || 平台名称 2 特定平台要执行的代码 3 #endif 4 5 #ifndef 平台名称 6 除了特定平台之外其它平台要执行的代码 7 #endif
下面以 H5 平台来给大家演示具体的语法:
1 <script setup> 2 // #ifdef H5 js中使用 // 3 console.log('这段代码只在 H5 端才会运行') 4 // #endif 5 </script> 6 <template> 7 <!-- #ifdef H5 --> vue模板中使用html注释 <!-- --> 8 <view class="iconfont icon-search"></view> 9 <!-- #endif --> 10 11 <!-- #ifndef H5 --> 12 <view class="iconfont icon-scan"></view> 13 <!-- #endif --> 14 </template> 15 <style lang="scss" scoped> 16 .iconfont { 17 color: #666; 18 font-size: 30rpx; 19 /* #ifdef H5 */ css使用原生的注释 /* */ 20 font-size: 28rpx; 21 /* #endif */ 22 } 23 </style>
平台名称
不同的不台对应了不同的名称,这些名称都是 uni-app 内置提供的,比如 H5、MP-WEIXIN、APP-PLUS 等
值 | 生效条件 |
---|---|
VUE3 | Vue 版本为 Vue3 |
APP-PLUS | App 平台,包括 Android 和 IOS |
APP-ANDROID | Android 平台 |
APP-IOS | IOS 平台 |
H5 | H5 平台 |
MP | 小程序平台,包括所有小程序 |
MP-WEIXIN | 微信小程序 |
MP-ALIPAY | 阿里小程序 |
设置安全区域
1 .element { 2 /* 设置顶部的安全区域 */ 3 padding-top: env(safe-area-inset-top); 4 /* 设置底部的安全区域 */ 5 padding-bottom: env(safe-area-inset-bottom); 6 /* 设置左侧的安全区域 */ 7 padding-left: env(safe-area-inset-left); 8 /* 设置右侧的安全区域 */ 9 padding-right: env(safe-area-inset-right); 10 }
6、节点信息
原生小程序没有DOM操作相关内容,也因此在uni-app中也无法对DOM进行操作的,但是在实际开发过程中是有获取节点信息,例如宽高、位置等信息的需求的
① 创建查询器
在网页中可以直接使用 document.querySelector
来查找 DOM 节点,在 uni-app 或小程序中则没有这样一个方法,取而代之的是调用 API uni.createSelectorQuery
创建一个查询实例(查询器),进而调用该实例的方法来查询页面中的节点元素。
1 <script setup> 2 import { onMounted } from 'vue' 3 4 // 在生命周期中调用 5 onMounted(() => { 6 // 节点查询器(实例) 7 const selectorQuery = uni.createSelectorQuery() 8 console.log(selectorQuery) 9 }) 10 </script> 11 12 <template> 13 <view class="container"> 14 <view class="box">获取这个盒子的宽高、位置等信息</view> 15 </view> 16 </template> 17 18 <style lang="scss"> 19 page { 20 padding: 30rpx; 21 } 22 23 .box { 24 width: 300rpx; 25 height: 300rpx; 26 margin-top: 40rpx; 27 background-color: pink; 28 } 29 </style>
注意事项:
1、需要在onMounted或onReady生命周期中调用
2、选择之定义组件中的节点时,需要调用in方法并传入当前页面实例
② 节点对象
-
select
根据选择器的要求,只查找符合条件的第一个节点,结果是一个对象
-
selectAll
根据选择器的要求,查找符合条件的全部节点,结果是一个对象数组
-
selectViewport
特指获取视口,查找视口的尺寸、滚动位置等信息
1 <script setup> 2 import { onMounted } from 'vue' 3 4 onMounted(() => { 5 // 1. 节点查询器(实例) 6 const selectorQuery = uni.createSelectorQuery() 7 8 // 2. 查找节点 9 10 // 2.1 查找单个节点 11 selectorQuery.select('.box').boundingClientRect((rect) => { 12 // 获取宽高和位置 13 console.log(rect) 14 }) 15 16 // 2.2 查找全部节点 17 selectorQuery.selectAll('.box').boundingClientRect((rects) => { 18 // 获取宽高和位置 19 console.log(rects) 20 }) 21 22 // 2.3 查找视口信息 23 selectorQuery.selectViewport().boundingClientRect((rect) => { 24 console.log(rect) 25 }) 26 27 // 3. 执行请求结果 28 selectorQuery.exec() 29 }) 30 </script> 31 32 <template> 33 <view class="container"> 34 <view class="box">获取这个盒子的宽高、位置等信息</view> 35 <view class="box"> 类选择器名称一样的另一个盒子 </view> 36 </view> 37 </template> 38 39 <style lang="scss"></style>
注意事项:
不执行
exec
方法,将获取不到任何的节点信息
有多个查询步骤时,在结尾只执行一次
exec
即可,避免重复查询
exec
方法代表执行结束,因此务必保证最后再调用
③ 节点信息
节点信息对象中包含了若干的信息,根据需要调用不同的方法进行获取:
boundingClientRect
节点的宽高及位置,长度单位是px
scrollOffset
节点滚动的位置,仅支持scroll-view
组件或页面( viewport)
1 <template> 2 <view class="container"> 3 <view class="box">获取这个盒子的宽高、位置信息</view> 4 </view> 5 </template> 6 7 <script setup> 8 import { onMounted } from 'vue' 9 10 onMounted(() => { 11 12 // 1、创建查询器 13 const selectQuery = uni.createSelectQuery() 14 console.log("查询器对象: ", selectQuery) 15 16 // 2、查询元素 17 selectQuery.select(".box").boundingClientRect((res) => { 18 console.log("查询到的节点对象: ", res) 19 }) 20 21 // 查询当前节点滚动信息,当屏幕上下滑动到不同位置,然后调用该方法,可以拿到此时的屏幕滚动位置 22 selectQuery.selectViewport().scrollOffset((offset) => { 23 console.log("查询当前节点滚动信息: ", offset) 24 }) 25 26 // 3、执行查询 27 selectQuery.exec() 28 }) 29 </script> 30 31 <style lang="scss" scoped> 32 page { 33 height: 1500rpx; // 让屏幕高度高点,可以滚动 34 } 35 </style>
注意事项:
- 在获取元素的位置时是按已定位的祖先元素为参考,即大家平时理解的“子绝父相”方式
- 元素未定位时参视口(viewport)为参考
7、自定义组件
① easycom组件规范
easycom是uni-app自定义的加载组件的规范,按该规范定义的组件可以实现自动导入,其规范如下:
- 方式1、安装在项目根目录的components目录下,并符合components/组件名/组件名.vue
-
-
- 方式2、安装在uni_modules目录下,路径为uni_modules/插件ID/components/组件名/组件名.vue
扩展组件uni_ui就是没有引入的情况下自动导入的,因为其符合easycom组件规范
② 自定义组件custom-tabs
标签页(tabs)的切换,在开发中经常会使用到的一种交互方式,在【优医咨询】项目就用到这种交互方式,接下来封装一个标签页组件,按照easycom的规范创建组件和目录及文件:
在项目根目录下创建components目录,然后创建自定义组件,目录结构为:components/custom-tabs/custom-tabs.vue
组件内部,如果获取节点信息,传入this,但是使用的是组合式api,所以需要通过in(),然后通过getCurrentInstance()传入页面实例
1、custom-tabs组件布局
<!-- components/custom-tabs/custom-tabs.vue -->
1 <view class="custom-tabs"> 2 <view class="custom-tabs-bar active"> 3 <text class="tabbar-text">关注</text> 4 </view> 5 <view class="custom-tabs-bar"> 6 <text class="tabbar-text">推荐</text> 7 </view> 8 <view class="custom-tabs-bar"> 9 <text class="tabbar-text">护肤</text> 10 </view> 11 <view class="custom-tabs-bar"> 12 <text class="tabbar-text">减脂</text> 13 </view> 14 <view class="custom-tabs-bar"> 15 <text class="tabbar-text">饮食</text> 16 </view> 17 <view class="custom-tabs-cursor"></view> 18 </view>
样式
1 // 自定义tabbar 2 .custom-tabs { 3 display: flex; 4 position: relative; 5 padding: 0 30rpx; 6 } 7 8 .custom-tabs-bar { 9 height: 80rpx; 10 line-height: 80rpx; 11 color: #979797; 12 padding-right: 30rpx; 13 position: relative; 14 15 &.active { 16 color: #121826; 17 font-weight: 500; 18 } 19 } 20 21 .tabbar-text { 22 font-size: 30rpx; 23 } 24 25 .custom-tabs-cursor { 26 position: absolute; 27 bottom: 3px; 28 left: 20px; 29 30 width: 20px; 31 height: 2px; 32 border-radius: 2px; 33 background-color: #2cb5a5; 34 transition: all 0.3s ease-out; 35 }
2、引用 custom-tabs
组件,由于符合 easycom 组件规范,可以省略组件的导入步骤
1 <!-- pages/index/index.vue --> 2 <template> 3 ... 4 <!-- 信息流 --> 5 <view class="doctor-feeds"> 6 <custom-tabs></custom-tabs> 7 </view> 8 </template>
3、自定义组件属性,允许以属性的方式向组件传入数据
list
数据类型为对象数组,初始值为[]
list
数组中的对象单元结构为{label: '', rendered: false}
1 <!-- components/custom-tabs/custom-tabs.vue --> 2 <script setup> 3 // 接收组件外部传入的数据 4 const customTabsProps = defineProps({ 5 list: { 6 type: Array, 7 default: [], 8 }, 9 }) 10 </script> 11 12 <template> 13 <view class="custom-tabs"> 14 <view 15 v-for="tab in customTabsProps.list" 16 :key="tab.label" 17 class="custom-tabs-bar active" 18 > 19 <text class="tabbar-text">{{ tab.label }}</text> 20 </view> 21 <view class="custom-tabs-cursor"></view> 22 </view> 23 </template>
首页面index.vue中引入组件时为 custom-tabs
组件传入数据
1 <!-- pages/index/index.vue --> 2 <script setup> 3 import { ref } from 'vue' 4 // 获取安全区域尺寸 5 const { safeAreaInsets } = uni.getSystemInfoSync() 6 7 // 定义tabs组件的数据 8 const feedTabs = ref([ 9 { label: '关注', rendered: true }, 10 { label: '推荐', rendered: false }, 11 { label: '减脂', rendered: false }, 12 { label: '饮食', rendered: false }, 13 ]) 14 </script> 15 <template> 16 ... 17 <!-- 信息流 --> 18 <view class="doctor-feeds"> 19 <custom-tabs :list="feedTabs"></custom-tabs> 20 </view> 21 </template>
4、点击切换交互,此步骤要完成两个操作:一个是 tab 的高亮显示,另一个是指示游标位置的移动
先来看切换 tab 的高亮显示:
- 监听点击事件
- 根据点击元素的索引值来区分当前需要高亮的 tab 元素
- 高亮显示样式的类名为
active
1 <!-- components/custom-tabs/custom-tabs.vue --> 2 <script setup> 3 import { ref } from 'vue' 4 5 // 接收组件外部传入的数据 6 const customTabsProps = defineProps({ 7 list: { 8 type: Array, 9 default: [], 10 }, 11 }) 12 13 // 初始默认第一个 tab 高亮 14 const tabIndex = ref(0) 15 // 用户点击 tab 16 function onTabChange(index, tab) { 17 // 显示/隐藏组件 18 tabIndex.value = index 19 } 20 </script> 21 22 <template> 23 <view class="custom-tabs"> 24 <view 25 v-for="(tab, index) in customTabsProps.list" 26 :key="tab.label" 27 @click="onTabChange(index, tab)" 28 :class="{ active: tabIndex === index }" 29 class="custom-tabs-bar" 30 > 31 <text class="tabbar-text">{{ tab.label }}</text> 32 </view> 33 <view class="custom-tabs-cursor"></view> 34 </view> 35 </template>
再来看指示游标的位置移动,实现此交互功能需要获取用户点击元素的位置和尺寸,动画效果是通过 css 的过渡(transition)实现的。
- 获取每个 tab 的宽度和位置,此处的难点是要计算每个
tabbar-text
相对于custom-tabs
盒子的位置
- 计算游标的位置,根据布局结构看
custom-tabs-cursor
的定位是参照custom-tabs
1 <!-- components/custom-tabs/custom-tabs.vue --> 2 <script setup> 3 import { ref, onMounted, getCurrentInstance, computed } from 'vue' 4 5 // 接收组件外部传入的数据 6 const customTabsProps = defineProps({ 7 list: { 8 type: Array, 9 default: [], 10 }, 11 }) 12 13 // 初始默认第一个 tab 高亮 14 const tabIndex = ref(0) 15 // 记录节点信息,宽度和位置 16 const tabBarRect = ref([]) 17 18 // 生命周期 19 onMounted(() => { 20 // 在组件中应用,获取组件内部节点信息时需要调用 in 方法 21 // 传入当页面实例,通过 getCurrentInstance 获取,相当于选项 API 中的 this 22 const selectorQuery = uni.createSelectorQuery().in(getCurrentInstance()) 23 24 // 查找【所有节点】信息,用 selectAll 方法 25 selectorQuery 26 .selectAll('.custom-tabs, .tabbar-text') 27 .boundingClientRect(([parent, ...data]) => { 28 // 记录每个 tab 文字宽度和位置 29 tabBarRect.value = data.map(({ width, left }) => { 30 return { left: left - parent.left, width } 31 }) 32 }) 33 34 // 执行节点查询 35 selectorQuery.exec() 36 }) 37 38 // 计算游标的位置 39 const cursorPosition = computed(() => { 40 if (tabBarRect.value.length === 0) return 41 const { width, left } = tabBarRect.value[tabIndex.value] 42 // 计算游标要居中显示 43 return left + (width - 20) / 2 44 }) 45 46 // 用户点击 tab 47 function onTabChange(index, tab) { 48 // 显示/隐藏组件 49 tabIndex.value = index 50 } 51 </script> 52 53 <template> 54 <view class="custom-tabs"> 55 <view 56 v-for="(tab, index) in customTabsProps.list" 57 :key="tab.label" 58 @click="onTabChange(index, tab)" 59 :class="{ active: tabIndex === index }" 60 class="custom-tabs-bar" 61 > 62 <text class="tabbar-text">{{ tab.label }}</text> 63 </view> 64 <view 65 class="custom-tabs-cursor" 66 :style="{ left: cursorPosition + 'px' }" 67 ></view> 68 </view> 69 </template>
注意事项:通过 selectorQuery.selectAll 获取到的节点的位置和宽高时长度单位是 px
,不是 rpx
5、自定义事件,定义事件的目的是通知组件外部,组件内部的 tab 发生了切换
- 定义事件名称
- 触发事件并传递数据
1 <!-- components/custom-tabs/custom-tabs.vue --> 2 <script setup> 3 import { ref, onMounted, getCurrentInstance, computed } from 'vue' 4 5 // 省略前面步骤的代码... 6 7 // 自定义事件 8 const customTabsEmit = defineEmits(['click']) 9 10 // 省略前面步骤的代码... 11 12 // 用户点击 tab 13 function onTabChange(index, tab) { 14 // 显示/隐藏组件 15 tabIndex.value = index 16 // 触发自定义事件 17 customTabsEmit('click', { index, ...tab }) 18 } 19 </script> 20 21 <template> 22 ... 23 </template>
首页面中引入组件时为 custom-tabs
组件时监听 click
事件
1 <!-- pages/index/index.vue --> 2 <script setup> 3 import { ref } from 'vue' 4 // 获取安全区域尺寸 5 const { safeAreaInsets } = uni.getSystemInfoSync() 6 7 // 定义tabs组件的数据 8 const feedTabs = ref([ 9 { label: '关注', rendered: true }, 10 { label: '推荐', rendered: false }, 11 { label: '减脂', rendered: false }, 12 { label: '饮食', rendered: false }, 13 ]) 14 15 // 监听 custom-tabs 组件的变化 16 function onFeedTabChange(ev) { 17 console.log(ev) 18 } 19 </script> 20 <template> 21 ... 22 <!-- 信息流 --> 23 <view class="doctor-feeds"> 24 <custom-tabs :list="feedTabs" @click="onFeedTabChange"></custom-tabs> 25 </view> 26 </template>
完整代码
1 <!-- components/custom-tabs/custom-tabs.vue --> 2 <script setup> 3 import { ref, onMounted, getCurrentInstance, computed } from 'vue' 4 // 接收组件外部传入的数据 5 const customTabsProps = defineProps({ 6 list: { 7 type: Array, 8 default: [], 9 }, 10 }) 11 // 自定义事件 12 const customTabsEmit = defineEmits(['click']) 13 // 初始默认第一个 tab 高亮 14 const tabIndex = ref(0) 15 // 记录节点信息,宽度和位置 16 const tabBarRect = ref([]) 17 // 生命周期 18 onMounted(() => { 19 // 在组件中应用,获取组件内部节点信息时需要调用 in 方法 20 // 传入当页面实例,通过 getCurrentInstance 获取,相当于选项 API 中的 this 21 const selectorQuery = uni.createSelectorQuery().in(getCurrentInstance()) 22 // 查找【所有节点】信息,用 selectAll 方法 23 selectorQuery 24 .selectAll('.custom-tabs, .tabbar-text') 25 .boundingClientRect(([parent, ...data]) => { 26 // 记录每个 tab 文字宽度和位置 27 tabBarRect.value = data.map(({ width, left }) => { 28 return { left: left - parent.left, width } 29 }) 30 }) 31 // 执行节点查询 32 selectorQuery.exec() 33 }) 34 // 计算游标的位置 35 const cursorPosition = computed(() => { 36 if (tabBarRect.value.length === 0) return 37 const { width, left } = tabBarRect.value[tabIndex.value] 38 return left + (width - 20) / 2 39 }) 40 // 用户点击 tab 41 function onTabChange(index, tab) { 42 // 显示/隐藏组件 43 tabIndex.value = index 44 // 触发自定义事件 45 customTabsEmit('click', { index, ...tab }) 46 } 47 </script> 48 49 <template> 50 <view class="custom-tabs"> 51 <view 52 v-for="(tab, index) in customTabsProps.list" 53 :key="tab.label" 54 @click="onTabChange(index, tab)" 55 :class="{ active: tabIndex === index }" 56 class="custom-tabs-bar" 57 > 58 <text class="tabbar-text">{{ tab.label }}</text> 59 </view> 60 <view 61 class="custom-tabs-cursor" 62 :style="{ left: cursorPosition + 'px' }" 63 ></view> 64 </view> 65 </template> 66 67 <style lang="scss"> 68 // 自定义tabbar 69 .custom-tabs { 70 display: flex; 71 position: relative; 72 padding: 0 30rpx; 73 } 74 75 .custom-tabs-bar { 76 height: 80rpx; 77 line-height: 80rpx; 78 color: #979797; 79 padding-right: 30rpx; 80 position: relative; 81 82 &.active { 83 color: #121826; 84 font-weight: 500; 85 } 86 } 87 88 .tabbar-text { 89 font-size: 30rpx; 90 } 91 92 .custom-tabs-cursor { 93 position: absolute; 94 bottom: 3px; 95 left: 20px; 96 97 width: 20px; 98 height: 2px; 99 border-radius: 2px; 100 background-color: #2cb5a5; 101 transition: all 0.3s ease-out; 102 } 103 </style>
③ 自定义组件custom-sticky
在页面滚动过程中常常要求实现某个节点能固定在距离页面顶部的某个位置,我们称这种效果为吸顶,为了方便以后重复使用吸顶的效果,我们来封装一个组件,核心实现思路为组件插槽 slot
和 postion: sticky
。
1、按 easycom 组件创建组件 custom-sticky,在项目根目录下创建components目录,然后创建自定义组件,目录结构为:components/custom-tabs/custom-tabs.vue,这样组件符合easycom规范,会自动导入,在其他页面可以直接使用
2、搭建基本结构并定义插槽
1 <!-- components/custom-sticky/custom-sticky.vue --> 2 <script setup></script> 3 4 <template> 5 <view class="custom-sticky"> 6 <slot></slot> 7 </view> 8 </template> 9 10 <style lang="scss"> 11 .custom-sticky { 12 position: sticky; 13 z-index: 100; 14 top: 0; 15 } 16 </style>
3、允许自定义距离顶部的距离和组件的背景颜色,通过自定义属性方式传入数据
1 <!-- components/custom-sticky/custom-sticky.vue --> 2 <script setup> 3 import { computed } from 'vue' 4 5 // 接收组件外部传入的数据 6 const stickyProps = defineProps({ 7 offsetTop: { 8 type: [String, Number], 9 default: 0, 10 }, 11 backgroundColor: { 12 type: String, 13 default: '#fff', 14 }, 15 }) 16 17 // 自定义组件样式 18 const stickStyle = computed(() => { 19 return { 20 paddingTop: stickyProps.offsetTop, 21 backgroundColor: stickyProps.backgroundColor, 22 } 23 }) 24 </script> 25 26 <template> 27 <view :style="stickStyle" class="custom-sticky"> 28 <slot></slot> 29 </view> 30 </template> 31 32 <style lang="scss"> 33 .custom-sticky { 34 position: sticky; 35 z-index: 100; 36 top: 0; 37 } 38 </style>
在首页面的 custom-tabs
组件上应用吸顶的效果
1 <!-- pages/index/index.vue --> 2 <script setup> 3 import { ref } from 'vue' 4 // 获取安全区域尺寸 5 const { safeAreaInsets } = uni.getSystemInfoSync() 6 7 // 定义tabs组件的数据 8 const feedTabs = ref([ 9 { label: '关注', rendered: true }, 10 { label: '推荐', rendered: false }, 11 { label: '减脂', rendered: false }, 12 { label: '饮食', rendered: false }, 13 ]) 14 15 // 监听 custom-tabs 组件的变化 16 function onFeedTabChange(ev) { 17 console.log(ev) 18 } 19 </script> 20 <template> 21 ... 22 <!-- 信息流 --> 23 <view class="doctor-feeds"> 24 <custom-sticky :offset-top="safeAreaInsets.top + 'px'"> 25 <custom-tabs :list="feedTabs" @click="onFeedTabChange"></custom-tabs> 26 </custom-sticky> 27 </view> 28 </template>
注意事项:
在设置吸顶距离时使用的是 padding-top
属性,而不是 top
,原因是这样可以避免刘海屏或灵动岛部分无法被覆盖的情况。
但上述设置会导致 custom-sticky
组件默认情况间距变大,为了解决这个问题我们采取这样一个技巧,通过 为margin-top
指定一个负值,将 padding-top
的间距抵消。
index.vue
1 <template> 2 ... 3 <!-- 信息流 --> 4 <view :style="{ marginTop: -safeAreaInsets.top + 'px' }" class="doctor-feeds"> 5 <custom-sticky :offset-top="safeAreaInsets.top + 'px'"> 6 <custom-tabs :list="feedTabs" @click="onFeedTabChange"></custom-tabs> 7 </custom-sticky> 8 </view> 9 </template>
这种处理方式虽不是很优雅,但相对算是比较高效的方式了,省去了监听滚动的事件,更重要的是在自定义组件内部很难监听页面的滚动。
完整代码
1 <!-- components/custom-sticky/custom-sticky.vue --> 2 <script setup> 3 import { computed } from 'vue' 4 5 // 接收组件外部传入的数据 6 const stickyProps = defineProps({ 7 offsetTop: { 8 type: [String, Number], 9 default: 0, 10 }, 11 backgroundColor: { 12 type: String, 13 default: '#fff', 14 }, 15 }) 16 17 // 组件样式 18 const stickStyle = computed(() => { 19 return { 20 paddingTop: stickyProps.offsetTop, 21 backgroundColor: stickyProps.backgroundColor, 22 } 23 }) 24 </script> 25 26 <template> 27 <view :style="stickStyle" class="custom-sticky"> 28 <slot></slot> 29 </view> 30 </template> 31 32 <style lang="scss"> 33 .custom-sticky { 34 position: sticky; 35 z-index: 100; 36 top: 0; 37 } 38 </style>
完整 index.vue
1 <!-- pages/index/index.vue --> 2 <script> 3 export default { name: 'IndexPage' } 4 </script> 5 6 <script setup> 7 import { computed, ref, onMounted } from 'vue' 8 import FeedList from './components/feed-list.vue' 9 import { patientHomeKnowledgeApi } from '@/services/doctor.js' 10 11 // 获取安全区域数据 12 const { safeAreaInsets } = uni.getSystemInfoSync() 13 14 // 标签页索引值 15 const tabIndex = ref(0) 16 17 // 标签页数据 18 const feedTabs = ref([ 19 { 20 label: '推荐', 21 type: 'recommend', 22 current: 1, 23 hasMore: true, 24 list: [], // 每个list管理自己的数据列表 25 rendered: true, 26 }, 27 { 28 label: '关注', 29 type: 'like', 30 current: 1, 31 hasMore: true, 32 list: [], 33 rendered: false, 34 }, 35 { 36 label: '减脂', 37 type: 'fatReduction', 38 current: 1, 39 hasMore: true, 40 list: [], 41 rendered: false, 42 }, 43 { 44 label: '饮食', 45 type: 'food', 46 current: 1, 47 hasMore: true, 48 list: [], 49 rendered: false, 50 }, 51 ]) 52 53 // 当前的tab配置对象 54 const feedTab = computed(() => feedTabs.value[tabIndex.value]) 61 62 // 切换标签页 63 function onFeedTabChange({ index }) { 64 tabIndex.value = index 65 66 // 如果当前 tab 配置中的 rendered 为 false,那么就需要发一次请求 67 if (!feedTab.value.rendered) { 68 loadList() 69 } 70 71 // 只要点击过这个tab,就将其 是否渲染过 标记为 true 72 feedTab.value.rendered = true 73 } 74 75 const onScrolltolower = () => { 76 console.log('onScrolltolower') 77 // 加载之前,要先判断,当前列表是否还有下一页 78 if (feedTab.value.hasMore) { 79 loadList() 80 } 81 } 82 83 const loadList = async () => { 84 const res = await patientHomeKnowledgeApi({ 85 type: feedTab.value.type, 86 current: feedTab.value.current, 87 pageSize: 5, 88 }) 89 90 const { pageTotal, rows } = res.data 91 92 // 设置列表数据 93 feedTab.value.list.push(...rows) 94 95 // 判断是否还有下一页 96 if (feedTab.value.current >= pageTotal) { 97 feedTab.value.hasMore = false 98 } 99 100 feedTab.value.current++ 101 } 102 103 onMounted(() => { 104 loadList() 105 }) 106 </script> 107 108 <template> 109 <scroll-page @scrolltolower="onScrolltolower"> 229 230 <!-- 标签切换 --> 231 <view 232 class="doctor-feeds" 233 :style="{ marginTop: -safeAreaInsets.top + 'px' }" 234 > 235 <custom-sticky :offset-top="safeAreaInsets.top + 'px'"> 236 <custom-tabs @click="onFeedTabChange" :list="feedTabs" /> 237 </custom-sticky> 238 <!-- 这里的 v-show 和 v-if 配合使用,实现了一个懒渲染和缓存的功能 --> 239 <!-- 关注知识列表 --> 240 <!-- v-show只会控制样式显示隐藏,并不会删结构 --> 241 <view 242 v-for="(feed, index) in feedTabs" 243 :key="feed.type" 244 v-show="tabIndex === index" 245 > 246 <!-- v-if会控制结构来显示隐藏,rendered 在首页初次加载时,只有推荐是true --> 247 <feed-list :list="feed.list" v-if="feed.rendered" /> 248 </view> 249 </view> 250 </view> 251 </scroll-page> 252 </template> 253 <style lang="scss"> 254 @import './index.scss'; 255 </style>
④ 组件样式隔离
在原生小程序中自定义组件中如果引用其它的自定义组件时,通过 :deep
也无法对组件内部样式进行修改,通过设置原生小程序的样式隔离可以解决这个问题。
具体的设置方式如下代码所示:
1 <script setup> 2 // 组件式 setup 语法糖,在页面中下面为了小程序样式隔离设置,这个script通过组合式API写页面的逻辑 3 </script> 4 5 <script> 6 // 选项式 7 export default { 8 options: { 9 styleIsolation: 'shared', 10 }, 11 } 12 </script>
可以同时在 vue 组件中使用选项式和组合式 setup 语法糖。
创建符合 easycom 组件规范的组件 custom-form
,以表单相关组件为例来进行演示:
1 <!-- components/custom-form/custom-form.vue --> 2 <template> 3 <uni-forms label-width="0"> 4 <uni-forms-item> 5 <uni-easyinput 6 type="text" 7 :clearable="false" 8 placeholder="请输入手机号" 9 /> 10 </uni-forms-item> 11 <uni-forms-item> 12 <uni-easyinput 13 type="password" 14 :clearable="false" 15 placeholder="请输入密码" 16 /> 17 </uni-forms-item> 18 <uni-forms-item> 19 <uni-easyinput type="textarea" :clearable="false" /> 20 </uni-forms-item> 21 <uni-forms-item> 22 <label style="display: flex; align-items: center" class="radio"> 23 <radio style="transform: scale(0.7)" value="" /> 24 <text>我已同意用户协议及隐私协议</text> 25 </label> 26 </uni-forms-item> 27 </uni-forms> 28 <button type="primary">提交</button> 29 </template>
在修改输入框 uni-easyinput
组件内部样式时就必须要指定 styleIsolation: 'shared'
否则在小程序中样式并不会生效。
官方文档:小程序组件样式隔离
1 <!-- components/custom-form/custom-form.vue --> 2 <script> 3 export default { 4 options: { 5 styleIsolation: 'shared', 6 }, 7 } 8 </script> 9 10 <style lang="scss"> 11 .uni-button { 12 background-color: #2cb5a5; 13 } 14 15 :deep(.uni-easyinput__content-textarea) { 16 padding: 5px 10px; 17 } 18 19 :deep(.uniui-eye-filled), 20 :deep(.uniui-eye-slash-filled) { 21 color: #c0c4cc !important; 22 } 23 </style>
⑤ uniForms 表单验证
官方文档:https://uniapp.dcloud.net.cn/component/uniui/uni-forms.html
表单数据
原生小程序组件中关于表单数据的获取只能支持简易双向数据绑定,由于这个局限性,在 uni-app 开发中经常使用 uni-easyinput
增强组件替代 input
和 textarea
,通过 v-model
来获取表单的数据:
1 <!-- components/custom-form/custom-form.vue --> 2 <script setup> 3 import { ref } from 'vue' 4 5 // 表单数据 6 const formData = ref({ 7 mobile: '13212345678', 8 password: 'abc12345', 9 alt: '关于我的描述', 10 }) 11 </script> 12 13 <template> 14 <uni-forms label-width="0"> 15 <uni-forms-item> 16 <uni-easyinput 17 type="text" 18 :clearable="false" 19 :input-border="false" 20 v-model="formData.mobile" 21 placeholder="请输入手机号" 22 /> 23 </uni-forms-item> 24 <uni-forms-item> 25 <uni-easyinput 26 type="password" 27 :clearable="false" 28 :input-border="false" 29 v-model="formData.password" 30 placeholder="请输入密码" 31 /> 32 </uni-forms-item> 33 <uni-forms-item> 34 <uni-easyinput 35 type="textarea" 36 v-model="formData.alt" 37 :clearable="false" 38 :input-border="false" 39 /> 40 </uni-forms-item> 41 <uni-forms-item> 42 <label style="display: flex; align-items: center" class="radio"> 43 <radio color="#2cb5a5" style="transform: scale(0.7)" value="" /> 44 <text>我已同意用户协议及隐私协议</text> 45 </label> 46 </uni-forms-item> 47 </uni-forms> 48 <button class="uni-button" type="primary">提交</button> 49 </template>
验证规则
在对表单数据进行验证时不同的表单项,验证规则各不相同,在 uniForms 中通过 rules
属性来指定验证规则,语法格式如下:
1 <!-- components/custom-form/custom-form.vue --> 2 <script setup> 3 import { ref } from 'vue' 4 5 // 表单数据 6 const formData = ref({ 7 mobile: '13212345678', 8 password: 'abc12345', 9 alt: '关于我的描述', 10 }) 11 12 // 表单验证规则 13 const formRules = { 14 mobile: { 15 rules: [ 16 { required: true, errorMessage: '请填写手机号码' }, 17 { pattern: '^1\\d{10}$', errorMessage: '手机号码格式不正确' }, 18 ], 19 }, 20 password: { 21 rules: [ 22 { required: true, errorMessage: '请输入密码' }, 23 { pattern: '^[a-zA-Z0-9]{8}$', errorMessage: '密码格式不正确' }, 24 ], 25 }, 26 } 27 </script>
mobile
、password
表示要验证的数据名称,该名称需要在组件中定义(后面步骤会介绍)
rules
表示具体验证数据的规则,规则可以有多条
required
表示是否必填(不能为空)
pattern
自定义正则表达式进行验证,正则中的\
需要进行转义,即要写成\\
errorMessage
数据验证不合法时的提示文字
验证规则定义好之后,还有3件事需要处理:一是通过 rules
应用规则,二是为通过 name
为待验证数据命名,三是通过 model
指定验证的数据
1 <!-- components/custom-form/custom-form.vue --> 2 <template> 3 <uni-forms label-width="0" :model="formData" :rules="formRules"> 4 <uni-forms-item name="mobile"> 5 <uni-easyinput 6 type="text" 7 :clearable="false" 8 :input-border="false" 9 v-model="formData.mobile" 10 placeholder="请输入手机号" 11 /> 12 </uni-forms-item> 13 <uni-forms-item name="password"> 14 <uni-easyinput 15 type="password" 16 :clearable="false" 17 :input-border="false" 18 v-model="formData.password" 19 placeholder="请输入密码" 20 /> 21 </uni-forms-item> 22 <uni-forms-item name="alt"> 23 <uni-easyinput 24 type="textarea" 25 v-model="formData.alt" 26 :clearable="false" 27 :input-border="false" 28 /> 29 </uni-forms-item> 30 <uni-forms-item> 31 <label style="display: flex; align-items: center" class="radio"> 32 <radio color="#2cb5a5" style="transform: scale(0.7)" value="" /> 33 <text>我已同意用户协议及隐私协议</text> 34 </label> 35 </uni-forms-item> 36 </uni-forms> 37 <button class="uni-button" type="primary">提交</button> 38 </template>
触发验证
- 通过 ref 获取组件实例
- 调用组件暴漏出来的方法
1 <!-- components/custom-form/custom-form.vue --> 2 <script setup> 3 import { ref } from 'vue' 4 5 // 组件 ref 6 const formRef = ref() 7 8 // 省略前面部分代码... 9 10 // 表单提交 11 async function onFormSubmit() { 12 try { 13 // 执行表单的验证 14 await formRef.value.validate() 15 } catch(error) { 16 console.log(err) 17 } 18 } 19 </script> 20 21 <template> 22 <uni-forms label-width="0" ref="formRef" :model="formData" :rules="formRules"> 23 ... 24 </uni-forms> 25 <button class="uni-button" @click="onFormSubmit" type="primary">提交</button> 26 </template>
⑥ 自定义字体图标
扩展组件中的 uni-icons
内置了许多的图标,在内置的图标不能满足要求时还可以使用自定义图标。
单色图标
自定义单色图标的制作和使用与网页面几乎是一致的,首先在 iconfont.cn 平台制作字体图标,其次下载字体文件及配套的样式文件。
1 // static/fonts/iconfont.scss 2 @font-face { 3 font-family: 'iconfont'; 4 // 修改这里字体文件的引入路径,只保留 .ttf 格式字体文件即可(可以减少文件体积) 5 src: url('/static/fonts/iconfont.ttf') format('truetype'); 6 } 7 8 .iconfont { 9 font-family: 'iconfont' !important; 10 font-size: 16px; 11 font-style: normal; 12 -webkit-font-smoothing: antialiased; 13 -moz-osx-font-smoothing: grayscale; 14 }
1 <!-- App.vue --> 2 <style lang="scss"> 3 // 将下载的字体文件及样式表放到 static/fonts 目录中 4 // 将 iconfont.css 改成 iconfont.scss 5 @import '@/static/fonts/iconfont.scss'; 6 </style>
在 App.vue 中引入字体文件后可以在任何页面使用字体图标了,使用的方式也网页中是一样的:
1 <!-- pages/my/index.vue --> 2 <view class="icons rows"> 3 <!-- 自定义字体图标 --> 4 <text class="iconfont icon-done"></text> 5 <text class="iconfont icon-box"></text> 6 </view>
上述用法是常规方式使用自定义图标,除此之个 uni-icons 也支持使用自定义图标:
1 <!-- pages/my/index.vue --> 2 <view class="icons rows"> 3 <!-- 常规方式使用自定义字体图标 --> 4 <text class="iconfont icon-done"></text> 5 <text class="iconfont icon-box"></text> 6 <!-- uni-icons 方式使用自定义字体图标 --> 7 <uni-icons size="30" custom-prefix="iconfont" type="icon-done" /> 8 <uni-icons size="30" custom-prefix="iconfont" type="icon-box" /> 9 </view> 10 11 <style lang="scss"> 12 page { 13 padding: 30px; 14 box-sizing: border-box; 15 } 16 17 .iconfont { 18 font-size: 30px; 19 } 20 </style>
custom-prefix
指定自定义图标的公共类名
type
指定自定义图标的名称
注意事项:原生小程序中是不支持引入本地字体图标文件,必须为网络地址或base64,在使用 uni-app 时引入的本地字体文件在打包后会处理成 base64,因此使用时可以引入本地字体文件。
多色图标
多色图标目通过 svg 来支持的,然而微信小程序目前还不支持 svg 格式图片,所以在 uni-app 中多色图标只能用普通的图片来代替。
虽然多色图标是用普通图片来实现的,但是我们可以让其的使用方式变得方便一些,即从形式上看仍是以字体图标的方式来使用。
- 安装 iconfont-tools 工具来处理多色图标,将图标转找成 base64 格式的图片
1 npm install -g iconfont-tools
- 通过命令行切换到多色字体文件所在目录,执行
iconfont-tools,如下图,设置相关名称信息
1 cd font_4186074_yi1taw43z // 进入到下载的iconfont字体图标目录下 2 iconfont-tools // 执行转换命令
![](https://img2024.cnblogs.com/blog/1388631/202405/1388631-20240501133316874-1102180910.png)
- 把生成的字体文件
color-fonts.css
放到项目中,然后在 App.vue 文件中全局引入
1 <!-- App.vue --> 2 <style lang="scss"> 3 // 单色图标 将下载的字体文件及样式表放到 static/fonts 目录中 4 // 将 iconfont.css 改成 iconfont.scss 5 @import '@/static/fonts/iconfont.scss'; 6 // 多色图标 7 @import 'color-fonts.scss'; 8 </style>
1 <!-- pages/my/index.vue --> 2 <view class="icons rows"> 3 <!-- 常规方式使用自定义字体图标 --> 4 <text class="icon-symbol icon-symbol-tool-03"></text> 5 <!-- uni-icons 方式使用自定义字体图标 --> 6 <uni-icons custom-prefix="icon-symbol" type="icon-symbol-tool-03" /> 7 </view> 8 9 <style lang="scss"> 10 page { 11 padding: 30px; 12 box-sizing: border-box; 13 } 14 15 .icon-symbol { 16 width: 30px; 17 height: 30px; 18 } 19 </style>
注意事项:转换后的图片虽然使用方式上与字体图标相似,但是本质是是 base64 格式的图片,因此无法修改颜色,并且修改尺寸要修改其宽高来实现。