【组件封装】uniapp vue3 封装一个自定义下拉刷新组件pullRefresh,带刷新时间和加载动画教程

embedded/2024/11/29 3:23:47/

文章目录

  • 前言
  • 一、实现原理
  • 二、组件样式和功能设计
  • 三、scroll-view 自定义下拉刷新使用回顾
    • 相关属性:
    • 最终版完整代码:


前言

手把手教你封装一个移动端 自定义下拉刷新组件带更新时间和加载动画(PullRefresh),以uniapp vue3为代码示例。

在这里插入图片描述

请添加图片描述

在这里插入图片描述


一、实现原理

基于系统自带组件scroll-view封装,开启组件refresher-enabled属性支持自定义下拉刷新功能。下拉自定义UI容器默认状态下位于页面顶部外不可视区域,下拉时候自定义区域进入页面显示。并通过多个自定义下拉刷新回调函数refresherpulling、refresherrefresh、refresherrestore进行逻辑和UI控制。

二、组件样式和功能设计

请添加图片描述
1、如上述动图所示,下拉未达到阈值提示文字显示下拉可以刷新,到达或超过阈值显示释放立即刷新
2、释放后进入刷新状态,提示文字显示正在刷新,左边出现加载转圈动画,下拉区域卡住不动
3、数据刷新完成后设置更新时间,恢复到初始状态,再次下拉会显示上次更新时间

三、scroll-view 自定义下拉刷新使用回顾

相关属性:

属性名类型默认值说明
refresher-enabledBooleanfalse开启自定义下拉刷新
refresher-thresholdNumber45(单位px)设置自定义下拉刷新阈值
refresher-default-styleString“black”设置自定义下拉刷新默认样式,支持设置 black,white,none,none 表示不使用默认样式
refresher-backgroundString“#FFF”设置自定义下拉刷新区域背景颜色
refresher-triggeredBooleanfalse设置当前下拉刷新状态,true 表示下拉刷新已经被触发,false 表示下拉刷新未被触发
@refresherpullingEventHandle自定义下拉刷新控件被下拉
@refresherrefreshEventHandle自定义下拉刷新被触发
@refresherrestoreEventHandle自定义下拉刷新被复位

上面比较重要一个关系就是下拉松手那一刻下拉高度大于或等于refresher-threshold时(刷新阈值)会触发刷新事件@refresherrefresh

refresher-enabled用来控制下拉区域是否复位,当值从true转变为false才能复位。

分析下拉过程属性值变化和回调函数的触发时机:

1、初始状态refresher-triggered=false
2、 开始下拉@refresherpulling一直持续被触发,下拉松手那一刻当下拉距离大于或等于refresher-threshold(刷新阈值)时,@refresherrefresh 被触发一次,此时设置refresher-triggered=true,下拉区域卡住不复位进入加载中状态,数据加载完成设置refresher-triggered=false,下拉复位,触发@refresherrestore。

按上面描述我们很容易实现一个自定义下拉刷新简易版如下:

pullRefresh.vue

javascript"><template><view class="pull-refresh"><scroll-view style="height: 100%;" scroll-y refresher-enabled refresher-default-style="none"refresher-background="#ffffff" :refresher-threshold="threshold" :refresher-triggered="loading"@refresherpulling="onRefresherpulling" @refresherrefresh="onRefresherrefresh"@refresherrestore="onRefresherrestore" ><view class="main"><!-- 下拉提示内容 --><view class="content" :style="{top:`-${threshold}px`,height:`${threshold}px`}"><view class="tip-view"><view class="text">{{tipText}}</view></view></view></view></scroll-view></view>
</template><script setup>import {ref} from 'vue'//拉刷新阈值const threshold=ref(80)//是否正在刷新(加载数据)const loading = ref(false)//提示文字const tipText = ref('下拉可以刷新')//控件被下拉触发回调const onRefresherpulling = (e) => {console.log(e,'正在下拉')}//下拉刷新被触发回调const onRefresherrefresh = (e) => {loading.value=true;tipText.value="正在刷新..."//模拟接口请求数据setTimeout(()=>{loading.value=false uni.showToast({title:'刷新成功',icon:'none'})},1000)}//下拉刷新被复位回调const onRefresherrestore = () => {tipText.value = "下拉可以刷新"}</script><style lang="scss" scoped>.pull-refresh {height: 100vh;width: 100%;position: relative;}.main {position: relative;}.content {width: 100%;display: flex;align-items: center;justify-content: center;position: absolute;top: -80px;height: 80px;color: #000;z-index: 999;padding-bottom: 15px;box-sizing: border-box;.arrow {width: 45rpx;height: auto;}.tip-view {width: 10em;display: flex;font-size: 22rpx;flex-direction: column;align-items: center;margin-left: 20rpx;.text {font-size: 28rpx;color: #666;}}}</style>

运行效果:
请添加图片描述

通过运行效果可以看出除了图标、加载动画、更新时间外,和我们设想的效果最关键的区别在于释放刷新这个状态实现,组件无提供此状态,此时需要我们自己判断定义出来。

我们打印下来下拉刷新控件被下拉回调函数的参数

javascript">	//控件被下拉触发回调const onRefresherpulling = (e) => {console.log(e,'下拉触发')}

小程序端:
在这里插入图片描述
H5或APP:
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/3051ac201bdc4ba395b784f56521f24a.png

可以看到下拉过程中该回调一直被触发,小程序端内部有个dy字段而H5或APP端却变成了deltaY,dy或deltaY就是我们下拉区域的高度(单位px),当这个值大于等于下拉刷新阈值( refresher-threshold)将触发下拉刷新回调(@refresherrefresh)

所以是否达到释放立即刷新这个状态就很容易判断,也即dy或deltaY>=refresher-threshold进入释放立即刷新状态

javascript">	//控件被下拉触发回调const onRefresherpulling = (e) => {//微信小程dy字段,H5和app deltaY字段//#ifdef H5||APPif (e.detail.deltaY >= threshold ) {tipText.value = props.loosingText || "释放立即刷新"}// #endif//#ifndef H5||APPif (e.detail.dy >= threshold) {tipText.value = props.loosingText || "释放立即刷新"}// #endif		else {tipText.value = props.pullingText || "下拉可以刷新"}}

为了更好控制下拉区域图标和文字我们定义3中状态:

javascript">/*** 状态:0:下拉状态 1:可释放刷新状态 2:正在刷新状态*/const status = ref(0)

通过不同状态来改变UI和执行逻辑。

完整代码如下:
pullRefresh.vue

javascript"><template><view class="pull-refresh"><scroll-view style="height: 100%;" scroll-y refresher-enabled refresher-default-style="none"refresher-background="#ffffff" :refresher-threshold="threshold" :refresher-triggered="loading"@refresherpulling="onRefresherpulling" @refresherrefresh="onRefresherrefresh"@refresherrestore="onRefresherrestore" @scrolltolower="onScrolltolower"><view class="main"><!-- 下拉提示内容 --><view class="content" :style="{top:`-${threshold}px`,height:`${threshold}px`}"><!-- 下拉或可释放状态 --><image v-if="status<2" class="arrow":src="status===0 ? '/static/arrow_down.png':'/static/arrow_up.png'" mode="widthFix"></image><!-- 正在刷新中 --><image v-else-if="status==2" class="arrow loading" src="/static/loading.png" mode="widthFix"></image><view class="tip-view"><view :class="['text',{start:!updateTime}]">{{tipText}}</view><view v-if="updateTime" class="update-time">上次更新 {{updateTime}}</view></view></view><slot></slot></view></scroll-view></view>
</template><script setup>import {ref,nextTick,computed} from 'vue'const props = defineProps({//下拉刷新阈值threshold: {type: Number,default: 80},//下拉刷新接口方法,cb:接口请求完成回调refreshMethod: {type: Function,default: cb => cb()},//下拉过程文案pullingText: {type: String,default: '下拉可以刷新'},//释放过程文案loosingText: {type: String,default: '释放立即刷新'},//刷新中文案loadingText: {type: String,default: '正在刷新...'},})const emits = defineEmits(['scrolltolower'])//是否正在刷新(加载数据)const loading = ref(false)/*** 状态:0:下拉状态 1:可释放刷新状态 2:正在刷新状态*/const status = ref(0)//提示文字const tipText = computed(()=>{let tips={0:props.pullingText,1:props.loosingText,2:props.loadingText}return tips[status.value]||props.pullingText})//上一次刷新时间const updateTime = ref('')//控件被下拉触发回调const onRefresherpulling = (e) => {//微信小程dy字段,H5和app deltaY字段//#ifdef H5||APPif (e.detail.deltaY >= props.threshold ) {status.value = 1}// #endif//#ifndef H5||APPif (e.detail.dy >= props.threshold) {status.value = 1}// #endifelse {status.value = 0}}//下拉刷新被触发回调const onRefresherrefresh = (e) => {//到达下拉阈值刷新数据if (status.value === 1) {loading.value = truestatus.value = 2;//接口获取数据props.refreshMethod(() => {nextTick(() => {uni.showToast({title: '刷新成功',icon: 'none'})updateTime.value = formatDateTime()loading.value = false})})}}//下拉刷新被复位回调const onRefresherrestore = () => {status.value = 0}//获取当前日期时间const formatDateTime = () => {let date = new Date()let month = (date.getMonth() + 1).toString().padStart(2, '0')let day = date.getDate().toString().padStart(2, '0')let hour = date.getHours().toString().padStart(2, '0')let minus = date.getMinutes().toString().padStart(2, '0')let second = date.getSeconds().toString().padStart(2, '0')return `${month}-${day} ${hour}:${minus}`}//触底const onScrolltolower = () => {emits('scrolltolower')}
</script><style lang="scss" scoped>.pull-refresh {height: 100%;width: 100%;position: relative;}.main {position: relative;}.content {width: 100%;display: flex;align-items: center;justify-content: center;position: absolute;top: -80px;height: 80px;color: #000;z-index: 999;padding-bottom: 15px;box-sizing: border-box;.arrow {width: 45rpx;height: auto;&.loading {animation: loadingFrames 1s linear infinite;}}.tip-view {width: 10em;display: flex;font-size: 22rpx;flex-direction: column;align-items: center;margin-left: 20rpx;.text {font-size: 28rpx;color: #666;&.start{align-self: flex-start;}}.update-time {font-size: 22rpx;margin-top: 10rpx;color: #808080;}}}@keyframes loadingFrames {from {transform: rotate(0deg);}to {transform: rotate(360deg);}}
</style>

页面调用:
index.vue

javascript"><template><view class="container"><PullRefresh :threshold="80"  :refreshMethod="getData" @scrolltolower="handleScrolltolower"><view class="item" v-for="(item,index) in 20">{{item}}</view></PullRefresh></view>
</template><script setup>import PullRefresh from '@/components/pullRefresh.vue';/*** 获取数据* cb:接口数据获取完成回调函数*/const getData=(cb)=>{//模拟接口请求数据setTimeout(()=>{cb()},2000)}//触底回调const handleScrolltolower=()=>{console.log('触底')}</script><style lang="scss" scoped>.container {height: 100vh;background-color: #f2f2f2;}.item {height: 90rpx;line-height: 90rpx;text-align: center;margin-top: 20rpx;}
</style>

说明:为了让用户自定义文字,我们提供了各种状态下的文案属性设置,下拉刷新阈值也暴露属性threshold给用户自定义,同时还定义了refreshMethod属性用来控制接口获取刷新数据是否完成,该属性是一个方法内部写入从接口获取刷新数据逻辑,入参cb是个回调函数,刷新数据获取完成调用回调函数组件即可复位。组件高度默认100%继承父容器,内部默认插槽可添加页面内容。

ps:scrollView组件外层必须设置高度或最大高度才能保证自定义下拉刷新或者滚动触底功能正常

在小程序端运行:
请添加图片描述

H5或APP运行:

请添加图片描述

从运行效果看出小程序运行正常,而H5或APP端存在bug,当下拉区域较长时正常,当下拉区域较短(刚处于立即释放刷新状态)就放开手指会出现异常,组件不进入刷新状态。

查阅uniapp官方文档,官方文档对下拉回调参数都是轻描淡写,没写详细,通过多次试验测试,发现当中的猫腻,原来是下拉触发回调@refresherpulling返回的当前下拉区域高度值deltaY在这2端是不准确的,经过测试发现跟实际误差10px左右(多了10px),因此判断这2端是否到达阈值需要减去10

javascript">	//控件被下拉触发回调const onRefresherpulling = (e) => {console.log(e,'e')//微信小程dy字段,H5和app deltaY字段//#ifdef H5||APP//H5或APP端值判断是否到达阈值需要额外加10if (e.detail.deltaY-10 >= props.threshold ) {status.value = 1}// #endif//#ifndef H5||APPif (e.detail.dy >= props.threshold) {status.value = 1}// #endifelse {status.value = 0}}

最终版完整代码:

pullRefresh.vue(自定义下拉刷新组件)

javascript"><template><view class="pull-refresh"><scroll-view style="height: 100%;" scroll-y refresher-enabled refresher-default-style="none"refresher-background="#ffffff" :refresher-threshold="threshold" :refresher-triggered="loading"@refresherpulling="onRefresherpulling" @refresherrefresh="onRefresherrefresh"@refresherrestore="onRefresherrestore" @scrolltolower="onScrolltolower"><view class="main"><!-- 下拉提示内容 --><view class="content" :style="{top:`-${threshold}px`,height:`${threshold}px`}"><!-- 下拉或可释放状态 --><image v-if="status<2" class="arrow":src="status===0 ? '/static/arrow_down.png':'/static/arrow_up.png'" mode="widthFix"></image><!-- 正在刷新中 --><image v-else-if="status==2" class="arrow loading" src="/static/loading.png" mode="widthFix"></image><view class="tip-view"><view :class="['text',{start:!updateTime}]">{{tipText}}</view><view v-if="updateTime" class="update-time">上次更新 {{updateTime}}</view></view></view><slot></slot></view></scroll-view></view>
</template><script setup>import {ref,nextTick,computed} from 'vue'const props = defineProps({//下拉刷新阈值threshold: {type: Number,default: 80},//下拉刷新接口方法,cb:接口请求完成回调refreshMethod: {type: Function,default: cb => cb()},//下拉过程文案pullingText: {type: String,default: '下拉可以刷新'},//释放过程文案loosingText: {type: String,default: '释放立即刷新'},//刷新中文案loadingText: {type: String,default: '正在刷新...'},})const emits = defineEmits(['scrolltolower'])//是否正在刷新(加载数据)const loading = ref(false)/*** 状态:0:下拉状态 1:可释放刷新状态 2:正在刷新状态*/const status = ref(0)//提示文字const tipText = computed(()=>{let tips={0:props.pullingText,1:props.loosingText,2:props.loadingText}return tips[status.value]||props.pullingText})//上一次刷新时间const updateTime = ref('')//控件被下拉触发回调const onRefresherpulling = (e) => {//微信小程dy字段,H5和app deltaY字段//#ifdef H5||APP//H5或APP端值判断是否到达阈值需要额外加10if (e.detail.deltaY-10 >= props.threshold ) {status.value = 1}// #endif//#ifndef H5||APPif (e.detail.dy >= props.threshold) {status.value = 1}// #endifelse {status.value = 0}}//下拉刷新被触发回调const onRefresherrefresh = (e) => {//到达下拉阈值刷新数据if (status.value === 1) {loading.value = truestatus.value = 2;//接口获取数据props.refreshMethod(() => {nextTick(() => {uni.showToast({title: '刷新成功',icon: 'none'})updateTime.value = formatDateTime()loading.value = false})})}//发现误差在1左右未到达下拉刷新阈值H5或APP也会触发,兼容处理恢复初态else {loading.value = falsestatus.value = 0}}//下拉刷新被复位回调const onRefresherrestore = () => {status.value = 0}//获取当前日期时间const formatDateTime = () => {let date = new Date()let month = (date.getMonth() + 1).toString().padStart(2, '0')let day = date.getDate().toString().padStart(2, '0')let hour = date.getHours().toString().padStart(2, '0')let minus = date.getMinutes().toString().padStart(2, '0')let second = date.getSeconds().toString().padStart(2, '0')return `${month}-${day} ${hour}:${minus}`}//触底const onScrolltolower = () => {emits('scrolltolower')}
</script><style lang="scss" scoped>.pull-refresh {height: 100%;width: 100%;position: relative;}.main {position: relative;}.content {width: 100%;display: flex;align-items: center;justify-content: center;position: absolute;top: -80px;height: 80px;color: #000;z-index: 999;padding-bottom: 15px;box-sizing: border-box;.arrow {width: 45rpx;height: auto;&.loading {animation: loadingFrames 1s linear infinite;}}.tip-view {width: 10em;display: flex;font-size: 22rpx;flex-direction: column;align-items: center;margin-left: 20rpx;.text {font-size: 28rpx;color: #666;&.start{align-self: flex-start;}}.update-time {font-size: 22rpx;margin-top: 10rpx;color: #808080;}}}@keyframes loadingFrames {from {transform: rotate(0deg);}to {transform: rotate(360deg);}}
</style>

页面调用:

javascript"><template><view class="container"><PullRefresh :threshold="80" :refreshMethod="getData" @scrolltolower="handleScrolltolower"><view class="item" v-for="(item,index) in 20">{{item}}</view></PullRefresh></view>
</template><script setup>import PullRefresh from '@/components/pullRefresh.vue';/*** 获取数据* cb:接口数据获取完成回调函数*/const getData=(cb)=>{//模拟接口请求数据setTimeout(()=>{cb()},2000)}//触底回调const handleScrolltolower=()=>{console.log('触底')}</script><style lang="scss" scoped>.container {height: 100vh;background-color: #f2f2f2;}.item {height: 90rpx;line-height: 90rpx;text-align: center;margin-top: 20rpx;}
</style>

H5或APP端运行效果:
请添加图片描述


http://www.ppmy.cn/embedded/141338.html

相关文章

能源电力企业安全数据内外网文件交换

在数字化浪潮的推动下&#xff0c;能源电力行业数据交换的频率急剧上升&#xff0c;涉及的视频、研发、设计等各类文件数量庞大。这些文件的内外网传输不仅要追求速度&#xff0c;更要确保数据安全。随着国家对数据安全重视程度的提高&#xff0c;《网络安全法》和《数据安全法…

【SpringBoot】28 API接口防刷(Redis + 拦截器)

Gitee仓库 https://gitee.com/Lin_DH/system 介绍 常用的 API 安全措施包括&#xff1a;防火墙、验证码、鉴权、IP限制、数据加密、限流、监控、网关等&#xff0c;以确保接口的安全性。 常见措施 1&#xff09;防火墙 防火墙是网络安全中最基本的安全设备之一&#xff0c…

arm rk3588 onnx转rknn

一、环境部署&#xff1a; https://github.com/airockchip/rknn_model_zoo/tree/main/examples/yolo11 从该网址下载yolo11的模型。支持80种类型检测 二、下载模型 进入examples/yolo11/model文件夹&#xff0c;执行 ./download_model.sh 如图&#xff1a; 三、模型转换…

Vue.js 中 v-for 指令与 JavaScript 数组方法

简介 在 Vue.js 中&#xff0c;v-for 指令是渲染列表数据的利器。它能够让你轻松地根据数组或对象渲染一个列表。本文将首先展示 v-for 的基本用法&#xff0c;然后详细介绍 JavaScript 数组的常用方法&#xff0c;并提供示例&#xff0c;展示如何在 Vue.js 应用中操作数组。 …

基于Springboot的流浪宠物管理系统

基于javaweb的流浪宠物管理系统 介绍 基于javaweb的流浪宠物管理系统的设计与实现&#xff0c;后端框架使用Springbootmybatis&#xff0c;前端框架使用Vuehrml&#xff0c;数据库使用mysql&#xff0c;使用B/S架构实现前台用户系统和后台管理员系统&#xff0c;和不同权限级别…

2022年全国职业院校技能大赛(中职组)网络安全竞赛试题解析

2022年全国职业院校技能大赛&#xff08;中职组&#xff09; 网络安全竞赛试题 &#xff08;1&#xff09; &#xff08;总分100分&#xff09; 赛题说明 一、竞赛项目简介 “网络安全”竞赛共分A.基础设施设置与安全加固&#xff1b;B.网络安全事件响应、数字取证调查和应用安…

当我重构时,我在想些什么

文章目录 1 讨论1.1 什么是重构1.2 重构与性能优化是一件事吗 2 重构的目的、时机、难点2.1 重构的目的2.2 何时重构2.2.1 添加新功能时对周边历史代码进行小型重构2.2.2 code review时2.2.3 有计划有目的的重构2.2.4 出现线上问题2.2.5 何时不该重构 2.3 重构的难点2.3.1 如何…

【Python爬虫五十个小案例】爬取猫眼电影Top100

博客主页&#xff1a;小馒头学python 本文专栏: Python爬虫五十个小案例 专栏简介&#xff1a;分享五十个Python爬虫小案例 &#x1f40d;引言 猫眼电影是国内知名的电影票务与资讯平台&#xff0c;其中Top100榜单是影迷和电影产业观察者关注的重点。通过爬取猫眼电影Top10…