VUE实现上拉加载下拉刷新组件,移动端&PC端通用
其实写这个组件之前是因为在使用某UI组件库的loadmore时踩了不少坑,到最后还是会出现各种问题,至此就放弃了踩坑,决定自己封装一个类似组件。不敢说自己写的没问题,但是目前运用到项目中,各手机设备上都能正常操作,所以今天把自己封装的这个组件分享出来,如果大家在使用时遇到问题可以随时向我反馈,我这边也会第一时间去修复。废话就不多说了,这里我先贴出组件代码
<style lang="scss" scoped>
.loadmore {
overflow-y: auto;
position: fixed;
height: 100%;
width: 100%;
left: 0;
top: 0;
bottom: 0;
right: 0;
z-index: 10;
user-select: none;
.loadmore-content {
backface-visibility: hidden;
transform-style: preserve-3d;
background: #f5f5f5;
position: relative;
}
.loading, .pull, .drop {
position: fixed;
width: 100%;
left: 0;
right: 0;
text-align: center;
line-height: 40px;
z-index: -1;
font-size: 14px;
}
.top-loading-text {
top: -40px;
}
.bottom-loading-text {
bottom: 0px;
}
}
</style>
<template>
<section class="loadmore" ref="loadmore">
<section class="loadmore-content" ref="list" :style="style">
<section class="loading top-loading-text" v-if="topLoading" v-html="topLoadingText"></section>
<section class="pull" :style="{top: `-40px`}" v-if="topPull" v-html="topPullText"></section>
<section class="drop" :style="{top: `-40px`}" v-if="topDrop" v-html="topDropText"></section>
<slot></slot>
</section>
<section v-if="bottomDrop" class="drop" :style="{bottom: `${0}px`}" v-html="bottomDropText"></section>
<section v-if="bottomPull" class="pull" :style="{bottom: `${0}px`}" v-html="bottomPullText"></section>
<section v-if="bottomLoading" class="loading bottom-loading-text" v-html="bottomLoadingText"></section>
</section>
</template>
<script>
export default {
name: 'loadmore',
data () {
return {
style: {},
direction: '',
touchstart: 'touchstart',
touchmove: 'touchmove',
touchend: 'touchend',
loadmore: null,
list: null,
dY: 0,
mY: 0,
bottomLoading: false,
bottomPull: false,
bottomDrop: false,
topLoading: false,
topPull: false,
topDrop: false,
topOn: false,
bottomOn: false,
scrollOn: true
}
},
computed: {
isMobile () {
return /Mobile/ig.test(window.navigator.userAgent)
}
},
props: {
//提前触发到底事件的高度,默认到底触发(手机端建议默认此参数)
earlyTrigger: {
type: Number,
validater (v) {
return v >= 0
},
default () {
return 0
}
},
//手指移动距离和组件滚动之间的比率
distanceIndex: {
type: Number,
default () {
return 3
}
},
//更新完,设置此参数不再触发 topMethod 方法
topAllLoaded: {
type: Boolean,
default () {
return true
}
},
//下拉刷新的高度
topDistance: {
type: Number,
validater (v) {
return v > 40
},
default () {
return 60
}
},
//上拉加载的高度
bottomDistance: {
type: Number,
validater (v) {
return v > 40
},
default () {
return 60
}
},
//加载完,设置此参数不再触发 bottomMethod 方法
bottomAllLoaded: {
type: Boolean,
default () {
return false
}
},
//是否自动填充内容
autoFill: {
type: Boolean,
default () {
return true
}
},
//下拉刷新提示内容,可传入HTML
topPullText: {
type: String,
default () {
return '↓ 下拉刷新'
}
},
//下拉刷新到达指定高度提示内容,可传入HTML
topDropText: {
type: String,
default () {
return '释放更新'
}
},
//下拉更新中提示内容,可传入HTML
topLoadingText: {
tye: String,
default () {
return '更新中...'
}
},
//上拉加载提示内容,可传入HTML
bottomPullText: {
type: String,
default () {
return '↑ 上拉加载'
}
},
//上拉加载到达指定高度提示内容,可传入HTML
bottomDropText: {
type: String,
default () {
return '释放更新'
}
},
//上拉加载中提示内容,可传入HTML
bottomLoadingText: {
type: String,
default () {
return '加载中...'
}
},
//下拉刷新,触发方法
topMethod: {
type: Function,
default: () => {}
},
//上拉加载,触发方法
bottomMethod: {
type: Function,
default: () => {}
}
},
methods: {
//偏移元素
deviationElement (distance, isAnimation) {
this.style = isAnimation && {
transform: `translate3d(0, ${ distance }px, 0)`,
transition: 'transform .3s ease'
} || {
transform: `translate3d(0, ${ distance }px, 0)`
}
},
//底部加载完成后调用,关闭加载状态
onBottomLoadedSuccess () {
this.bottomLoading = this.bottomDrop = this.bottomPull = false
this.deviationElement(0, true)
},
//顶部加载完成后调用,关闭加载状态
onTopLoadedSuccess () {
this.topLoading = this.topDrop = this.topPull = false
this.deviationElement(0, true)
},
//手指(鼠标按键)抬起
removeEv () {
//上拉释放
if (this.direction == 'up') {
if (this.bottomDrop) {
this.deviationElement(-40, true)
setTimeout(() => {
this.bottomDrop = this.bottomPull = false
this.bottomLoading = true
this.bottomMethod()
}, 300)
} else {
this.deviationElement(0, true)
setTimeout(() => {
this.bottomLoading = this.bottomDrop = this.bottomPull = false
}, 300)
}
} else if (this.direction == 'down') {//下拉释放
if (this.topDrop) {
this.deviationElement(40, true)
setTimeout(() => {
this.topDrop = this.topPull = false
this.topLoading = true
this.topMethod()
}, 300)
} else {
this.deviationElement(0, true)
setTimeout(() => {
this.topLoading = this.topDrop = this.topPull = false
}, 300)
}
}
this.loadmore.removeEventListener(this.touchmove, this.moveEv)
this.loadmore.removeEventListener(this.touchend, this.removeEv)
},
//手指(鼠标)按下事件
startEv (e) {
e = e || window.event
this.dY = this.isMobile && e.touches[0].pageY || e.pageY
this.topOn = this.bottomOn = true
this.loadmore.addEventListener(this.touchmove, this.moveEv)
this.loadmore.addEventListener(this.touchend, this.removeEv)
},
//手指(鼠标按住拖动)滑动事件
moveEv (ev) {
ev = ev || window.event
this.mY = this.isMobile && ev.touches[0].pageY || ev.pageY
let sT = this.loadmore.scrollTop,
prevent = false,
h = this.loadmore.clientHeight - this.list.scrollHeight
//下拉更新
if ((this.mY - this.dY) > 0) {
this.direction = 'down'
if (sT <= 0) {
prevent = true
if (this.topOn) {
this.topOn = false
this.dY = this.mY
}
let top = (this.mY - this.dY) / this.distanceIndex
this.deviationElement(top)
if ((top > 0) && (top < this.topDistance) && !this.topAllLoaded) {
this.topLoading = this.topDrop = false
this.topPull = true
} else if ((top > this.topDistance) && !this.topAllLoaded) {
this.topLoading = this.topPull = false
this.topDrop = true
}
}
} else {//上拉加载
this.direction = 'up'
if (Math.abs(Math.abs(h) - sT) < 10) {
prevent = true
if (this.bottomOn) {
this.bottomOn = false
this.dY = this.mY
this.$emit('bottomStatusChange', '到底啦')
}
let bottom = (this.mY - this.dY) / this.distanceIndex,
absBottom = Math.abs(bottom)
this.deviationElement(bottom)
if ((absBottom > 0) && (absBottom < this.bottomDistance) && !this.bottomAllLoaded) {
this.bottomLoading = this.bottomDrop = false
this.bottomPull = true
} else if ((absBottom > this.bottomDistance) && !this.bottomAllLoaded) {
this.bottomLoading = this.bottomPull = false
this.bottomDrop = true
}
}
}
prevent && ev.preventDefault()
},
//scorll 监听事件
scrollEv () {
let sH = this.list.scrollHeight,
cH = this.loadmore.clientHeight,
top = this.loadmore.scrollTop
if ((sH - cH - this.earlyTrigger) <= top && this.scrollOn) {
this.scrollOn = false
this.$emit('bottomStatusChange', '到底啦')
}
}
},
mounted () {
this.$nextTick(() => {
if (!this.isMobile) {
this.touchstart = 'mousedown'
this.touchmove = 'mousemove'
this.touchend = 'mouseup'
}
this.loadmore = this.$refs.loadmore
this.list = this.$refs.list
//自动填充
if ((this.loadmore.clientHeight > this.list.scrollHeight) && this.autoFill) {
this.bottomMethod(!0)
}
!this.earlyTrigger && this.loadmore.addEventListener(this.touchstart, this.startEv)
this.earlyTrigger && this.loadmore.addEventListener('scroll', this.scrollEv)
})
},
deactivated () {
!this.earlyTrigger && this.loadmore.removeEventListener(this.touchstart, this.startEv)
this.earlyTrigger && this.loadmore.removeEventListener('scroll', this.scrollEv)
}
}
</script>
上面是这个组件的全部代码,这里不解读代码,我大概说一下实现思路:
首先我利用了addEventListener做了主要三个事件的绑定,按下事件,滑动事件,抬起事件,通过滑动事件内首先判断了用户的滑动方向,往上还是往下。往上拉的逻辑,要获取可视区高度,整个页面内容的高度,当前滚动条的位置,这样就能计算出当前用户是否滑动到了底部,计算公式:
scorllHeight-clientHeight == scrollTop
当以上条件成立,既滑动到了底部,这时就可以做相关上拉效果的逻辑。同理,向下拉逻辑,计算公式:
scrollTop <= 0
当以上条件成立,既滑动到了顶部,同样可做相关下拉效果逻辑,组件整个拉动效果都是运用的css3的动画,js计算了滑动位置,这里说一下拉动的效果逻辑,例如:当拉动到了底部,如果再往上拉,效果会给人一种好像越拉越难拉的感觉,其实这里也是运用了一个公式:
//(手指移动的ev.pageY - 手指按下的ev.pageY) / 系数
(movePageY - downPageY) / coefficient
根据上方公式就能实现拉动效果,最后当抬起事件触发时即归位元素位置,触发更新事件实现加载数据的回调。上面简单的说了下实现思路,当然也可以不利用滚动条去写,全部滑动效果都可自己用计算方式实现,这里也就提一下这个,因为个人只是觉得没有这个必要去整个滑动都自己计算写,当然如果你如果非要自己做处理,可以屏蔽掉滚动条,获取滚动条位置改写成获取元素顶部距离,然后在滑动事件内加一个没有到底部或者顶部的滑动处理。
组件使用方式请移步GitHub把Demo拉到本地去跑一下,体验一下效果,地址:https://github.com/wujiabk/vue-loadmore
如果感觉不错,可以帮我Star一下哦~
扫码关注后,回复“资源”免费领取全套视频教程
12
发表评论(共0条评论)