betterScroll.vue
<template> <div ref="wrapper" class="list-wrapper"> <div class="scroll-content"> <div ref="listWrapper"> <slot> <ul class="list-content"> <li @click="clickItem($event,item)" class="list-item" v-for="item in data">{{item}}</li> </ul> </slot> </div> <slot name="pullup" :pullUpLoad="pullUpLoad" :isPullUpLoad="isPullUpLoad" > <div class="pullup-wrapper" v-if="pullUpLoad"> <div class="before-trigger" v-if="!isPullUpLoad"> <span>{{pullUpTxt}}</span> </div> <div class="after-trigger" v-else> <loading></loading> </div> </div> </slot> </div> <slot name="pulldown" :pullDownRefresh="pullDownRefresh" :pullDownStyle="pullDownStyle" :beforePullDown="beforePullDown" :isPullingDown="isPullingDown" :bubbleY="bubbleY" > <div ref="pulldown" class="pulldown-wrapper" :style="pullDownStyle" v-if="pullDownRefresh"> <div class="before-trigger" v-if="beforePullDown"> <bubble :y="bubbleY"></bubble> </div> <div class="after-trigger" v-else> <div v-if="isPullingDown" class="loading"> <loading></loading> </div> <div v-else><span>{{refreshTxt}}</span></div> </div> </div> </slot> </div> </template> <script> import BScroll from ‘better-scroll‘ import Loading from ‘../loading/loading.vue‘ import Bubble from ‘./bubble.vue‘ import { getRect } from ‘@/common/js/dom‘ const COMPONENT_NAME = ‘scroll‘ const DIRECTION_H = ‘horizontal‘ const DIRECTION_V = ‘vertical‘ export default { name: COMPONENT_NAME, props: { data: { type: Array, default: function () { return [] } }, probeType: { type: Number, default: 1 }, click: { type: Boolean, default: true }, listenScroll: { type: Boolean, default: false }, listenBeforeScroll: { type: Boolean, default: false }, direction: { type: String, default: DIRECTION_V }, scrollbar: { type: null, default: false }, pullDownRefresh: { type: null, default: false }, pullUpLoad: { type: null, default: false }, startY: { type: Number, default: 0 }, refreshDelay: { type: Number, default: 20 }, freeScroll: { type: Boolean, default: false }, mouseWheel: { type: Boolean, default: false }, bounce: { default: true } }, data() { return { beforePullDown: true, isRebounding: false, isPullingDown: false, isPullUpLoad: false, pullUpDirty: true, pullDownStyle: ‘‘, bubbleY: 0 } }, computed: { pullUpTxt() { const moreTxt = this.pullUpLoad && this.pullUpLoad.txt && this.pullUpLoad.txt.more || ‘加载更多‘ const noMoreTxt = this.pullUpLoad && this.pullUpLoad.txt && this.pullUpLoad.txt.noMore || ‘没有更多数据了‘ return this.pullUpDirty ? moreTxt : noMoreTxt }, refreshTxt() { return this.pullDownRefresh && this.pullDownRefresh.txt || ‘刷新成功‘ } }, created() { this.pullDownInitTop = -50 }, mounted() { setTimeout(() => { this.initScroll() }, 20) }, methods: { initScroll() { if (!this.$refs.wrapper) { return } if (this.$refs.listWrapper && (this.pullDownRefresh || this.pullUpLoad)) { this.$refs.listWrapper.style.minHeight = `${getRect(this.$refs.wrapper).height + 0.1}px` } let options = { probeType: this.probeType, click: this.click, scrollY: this.freeScroll || this.direction === DIRECTION_V, scrollX: this.freeScroll || this.direction === DIRECTION_H, scrollbar: this.scrollbar, pullDownRefresh: this.pullDownRefresh, pullUpLoad: this.pullUpLoad, startY: this.startY, freeScroll: this.freeScroll, mouseWheel: this.mouseWheel, bounce: this.bounce } this.scroll = new BScroll(this.$refs.wrapper, options) if (this.listenScroll) { this.scroll.on(‘scroll‘, (pos) => { this.$emit(‘scroll‘, pos) }) } if (this.listenBeforeScroll) { this.scroll.on(‘beforeScrollStart‘, () => { this.$emit(‘beforeScrollStart‘) }) } if (this.pullDownRefresh) { this._initPullDownRefresh() } if (this.pullUpLoad) { this._initPullUpLoad() } }, disable() { this.scroll && this.scroll.disable() }, enable() { this.scroll && this.scroll.enable() }, refresh() { this.scroll && this.scroll.refresh() }, scrollTo() { this.scroll && this.scroll.scrollTo.apply(this.scroll, arguments) }, scrollToElement() { this.scroll && this.scroll.scrollToElement.apply(this.scroll, arguments) }, clickItem(e, item) { // console.log(e) this.$emit(‘click‘, item) }, destroy() { // 销毁 better-scroll,解绑事件 this.scroll.destroy() }, forceUpdate(dirty) { if (this.pullDownRefresh && this.isPullingDown) { this.isPullingDown = false this._reboundPullDown().then(() => { this._afterPullDown() }) } else if (this.pullUpLoad && this.isPullUpLoad) { this.isPullUpLoad = false this.scroll.finishPullUp() this.pullUpDirty = dirty this.refresh() } else { this.refresh() } }, _initPullDownRefresh() { this.scroll.on(‘pullingDown‘, () => { this.beforePullDown = false this.isPullingDown = true this.$emit(‘pullingDown‘) }) this.scroll.on(‘scroll‘, (pos) => { if (!this.pullDownRefresh) { return } if (this.beforePullDown) { this.bubbleY = Math.max(0, pos.y + this.pullDownInitTop) this.pullDownStyle = `top:${Math.min(pos.y + this.pullDownInitTop, 10)}px` } else { this.bubbleY = 0 } if (this.isRebounding) { this.pullDownStyle = `top:${10 - (this.pullDownRefresh.stop - pos.y)}px` } }) }, _initPullUpLoad() { this.scroll.on(‘pullingUp‘, () => { this.isPullUpLoad = true this.$emit(‘pullingUp‘) }) }, _reboundPullDown() { const {stopTime = 600} = this.pullDownRefresh return new Promise((resolve) => { setTimeout(() => { this.isRebounding = true this.scroll.finishPullDown() resolve() }, stopTime) }) }, _afterPullDown() { setTimeout(() => { this.pullDownStyle = `top:${this.pullDownInitTop}px` this.beforePullDown = true this.isRebounding = false this.refresh() }, this.scroll.options.bounceTime) } }, watch: { data() { setTimeout(() => { this.forceUpdate(true) }, this.refreshDelay) } }, components: { Loading, Bubble } } </script> <style scoped> .list-wrapper{ position: relative; height: 100%; overflow: hidden; /* background: #fff; */ } .scroll-content{ position: relative; z-index: 1; } .list-content{ position: relative; z-index: 10; background: #fff; } .list-item{ height: 60px; line-height: 60px; font-size: 18px; padding-left: 20px; border-bottom: 1px solid #e5e5e5; } .pulldown-wrapper{ position: absolute; width: 100%; left: 0; display: flex; justify-content:center; align-items: center; transition: all; } .after-trigger{ /* margin-top: 10px; */ font-size: 16px; } .pullup-wrapper{ width: 100%; display: flex; justify-content: center; align-items: center; padding: 16px 0; } .before-trigger{ font-size: 16px; } </style>
bubble.vue
<template> <canvas ref="bubble" :width="width" :height="height" :style="style"></canvas> </template> <script> export default { props: { y: { type: Number, default: 0 } }, data() { return { width: 50, height: 80 } }, computed: { distance() { return Math.max(0, Math.min(this.y * this.ratio, this.maxDistance)) }, style() { return `width:${this.width / this.ratio}px;height:${this.height / this.ratio}px` } }, created() { this.ratio = window.devicePixelRatio this.width *= this.ratio this.height *= this.ratio this.initRadius = 18 * this.ratio this.minHeadRadius = 12 * this.ratio this.minTailRadius = 5 * this.ratio this.initArrowRadius = 10 * this.ratio this.minArrowRadius = 6 * this.ratio this.arrowWidth = 3 * this.ratio this.maxDistance = 40 * this.ratio this.initCenterX = 25 * this.ratio this.initCenterY = 25 * this.ratio this.headCenter = { x: this.initCenterX, y: this.initCenterY } }, mounted() { this._draw() }, methods: { _draw() { const bubble = this.$refs.bubble let ctx = bubble.getContext(‘2d‘) ctx.clearRect(0, 0, bubble.width, bubble.height) this._drawBubble(ctx) this._drawArrow(ctx) }, _drawBubble(ctx) { ctx.save() ctx.beginPath() const rate = this.distance / this.maxDistance const headRadius = this.initRadius - (this.initRadius - this.minHeadRadius) * rate this.headCenter.y = this.initCenterY - (this.initRadius - this.minHeadRadius) * rate // 画上半弧线 ctx.arc(this.headCenter.x, this.headCenter.y, headRadius, 0, Math.PI, true) // 画左侧贝塞尔 const tailRadius = this.initRadius - (this.initRadius - this.minTailRadius) * rate const tailCenter = { x: this.headCenter.x, y: this.headCenter.y + this.distance } const tailPointL = { x: tailCenter.x - tailRadius, y: tailCenter.y } const controlPointL = { x: tailPointL.x, y: tailPointL.y - this.distance / 2 } ctx.quadraticCurveTo(controlPointL.x, controlPointL.y, tailPointL.x, tailPointL.y) // 画下半弧线 ctx.arc(tailCenter.x, tailCenter.y, tailRadius, Math.PI, 0, true) // 画右侧贝塞尔 const headPointR = { x: this.headCenter.x + headRadius, y: this.headCenter.y } const controlPointR = { x: tailCenter.x + tailRadius, y: headPointR.y + this.distance / 2 } ctx.quadraticCurveTo(controlPointR.x, controlPointR.y, headPointR.x, headPointR.y) ctx.fillStyle = ‘rgb(170,170,170)‘ ctx.fill() ctx.strokeStyle = ‘rgb(153,153,153)‘ ctx.stroke() ctx.restore() }, _drawArrow(ctx) { ctx.save() ctx.beginPath() const rate = this.distance / this.maxDistance const arrowRadius = this.initArrowRadius - (this.initArrowRadius - this.minArrowRadius) * rate // 画内圆 ctx.arc(this.headCenter.x, this.headCenter.y, arrowRadius - (this.arrowWidth - rate), -Math.PI / 2, 0, true) // 画外圆 ctx.arc(this.headCenter.x, this.headCenter.y, arrowRadius, 0, Math.PI * 3 / 2, false) ctx.lineTo(this.headCenter.x, this.headCenter.y - arrowRadius - this.arrowWidth / 2 + rate) ctx.lineTo(this.headCenter.x + this.arrowWidth * 2 - rate * 2, this.headCenter.y - arrowRadius + this.arrowWidth / 2) ctx.lineTo(this.headCenter.x, this.headCenter.y - arrowRadius + this.arrowWidth * 3 / 2 - rate) ctx.fillStyle = ‘rgb(255,255,255)‘ ctx.fill() ctx.strokeStyle = ‘rgb(170,170,170)‘ ctx.stroke() ctx.restore() } }, watch: { y() { this._draw() } } } </script>
loading.vue
<template> <div class="mf-loading-container"> <img src="./loading.gif"> </div> </template> <script> const COMPONENT_NAME = ‘loading‘ export default { name: COMPONENT_NAME } </script> <style> .mf-loading-container img{ width: 20px; height: 20px; display: block; } </style>
dom.js
export function hasClass(el, className) { let reg = new RegExp(‘(^|\\s)‘ + className + ‘(\\s|$)‘) return reg.test(el.className) } export function addClass(el, className) { if (hasClass(el, className)) { return } let newClass = el.className.split(‘ ‘) newClass.push(className) el.className = newClass.join(‘ ‘) } export function removeClass(el, className) { if (!hasClass(el, className)) { return } let reg = new RegExp(‘(^|\\s)‘ + className + ‘(\\s|$)‘, ‘g‘) el.className = el.className.replace(reg, ‘ ‘) } export function getData(el, name, val) { let prefix = ‘data-‘ if (val) { return el.setAttribute(prefix + name, val) } return el.getAttribute(prefix + name) } export function getRect(el) { if (el instanceof window.SVGElement) { let rect = el.getBoundingClientRect() return { top: rect.top, left: rect.left, width: rect.width, height: rect.height } } else { return { top: el.offsetTop, left: el.offsetLeft, width: el.offsetWidth, height: el.offsetHeight } } }
封装 mixin/scroll.js
import Vue from ‘vue‘ import Scroll from ‘@/components/betterScroll/betterScroll‘ // better-scroll官网--https://ustbhuangyi.github.io/better-scroll/doc/zh-hans/api-specific.html#finishpullup export const Bscroll = { data() { return { scrollbar: true, // scrollbar 控制是否开启滚动条 scrollbarFade: true, // scrollbarFade 控制滚动条显示隐藏 pullDownRefresh: true, // pullDownRefresh 控制是否开启下拉刷新 pullDownRefreshThreshold: 90, pullDownRefreshStop: 40, pullUpLoad: true, // pullUpLoad 控制是否开启上拉加载 pullUpLoadThreshold: -1, // 在上拉到超过底部 1px 时,触发 上拉加载 事件 pullUpLoadMoreTxt: ‘加载更多‘, pullUpLoadNoMoreTxt: ‘没有更多数据了‘, startY: 0, itemIndex: 0, noData:false, pageNo: 1,//当前页 pageSize: 20,//每页显示数据条数 } }, created() {}, components: { Scroll }, activated(){ let _self = this this.$nextTick(() => { if(_self.$refs.scroll && _self.$refs.scroll.scroll) { _self.$refs.scroll.scroll.refresh(); } }) }, watch: { scrollbarObj: { // 显示滚动条 handler() { this.rebuildScroll() }, deep: true }, pullDownRefreshObj: { // 深度 watcher handler(val) { const scroll = this.$refs.scroll.scroll if (val) { scroll.openPullDown() } else { scroll.closePullDown() } }, deep: true }, pullUpLoadObj: { handler(val) { const scroll = this.$refs.scroll.scroll if (val) { scroll.openPullUp() } else { scroll.closePullUp() } }, deep: true }, startY() { this.rebuildScroll() } }, computed: { scrollbarObj: function () { return this.scrollbar ? {fade: this.scrollbarFade} : false }, pullDownRefreshObj: function () { // 下拉刷新---下拉距离与回弹距离 return this.pullDownRefresh ? { threshold: parseInt(this.pullDownRefreshThreshold), stop: parseInt(this.pullDownRefreshStop) } : false }, pullUpLoadObj: function () { // 上拉加载---显示提示 return this.pullUpLoad ? { threshold: parseInt(this.pullUpLoadThreshold), txt: {more: this.pullUpLoadMoreTxt, noMore: this.pullUpLoadNoMoreTxt} } : false } }, methods: { /*onPullingDown() { // 模拟更新数据 console.log(‘下拉刷新‘) setTimeout(() => { if (Math.random() > 1) { // 如果有新数据 this.massifList.unshift(‘我是新数据‘ + +new Date()) } else { // 如果没有新数据 this.$refs.scroll.forceUpdate() } }, 1500) }, onPullingUp() { // 更新数据 console.log(‘上拉加载‘) if (this.noData) { this.$refs.scroll.forceUpdate() return } setTimeout(() => { if (Math.random() > 1) { // 如果有新数据 let newPage = [] for (let i = 0; i < 10; i++) { newPage.push(‘第 ‘ + ++this.itemIndex + ‘ 行‘) } } else { // 如果没有新数据 this.noData = true this.$refs.scroll.forceUpdate() } }, 1500) },*/ onPullingDown() { // console.log(‘下拉刷新‘) setTimeout(() => { this._scrollList() }, 100) }, onPullingUp() { // console.log(‘上拉加载‘) if (this.noData) { this.$refs.scroll.forceUpdate() return } this.pageNo ++ setTimeout(() => { this._scrollList(‘onPullingUp‘); }, 500) }, clickItem() { this.$router.back() }, rebuildScroll() { Vue.nextTick(() => { this.$refs.scroll.destroy() this.$refs.scroll.initScroll() }) } } }
页面引用
<template> <div id="specialTask"> <div class="top-new-add-task dis-flex-center"> <div class="add-txt" @click="newAdd"><div class="new-add-icon"></div></div> </div> <div class="problem-box"> <scroll ref="scroll" :data="itemList" :scrollbar="scrollbarObj" :pullDownRefresh="pullDownRefreshObj" :pullUpLoad="pullUpLoadObj" :startY="parseInt(startY)" @pullingDown="onPullingDown" @pullingUp="onPullingUp" @click="clickItem"> <div class="scroll-list"> <div class="problem-li dis-flex-between" v-for="(item,i) in itemList" @click.stop="toUrlEdit(item,true)"> <div class="massif-num"></div> <div class="scoring">{{ i + 1}}</div> <div class="department over_elips">{{item.name}}</div> <div class="bg-state" @click.stop="toUrlEdit(item)">编辑</div> <div class="arrow-character-rg"></div> </div> <div class="no-data dis-flex-center" v-if="!itemList.length">暂无数据</div> </div> </scroll> </div> </div> </template> <script> import { Bscroll } from ‘@/mixin/scroll‘ import { specialList } from ‘@/http/api‘ import { mapActions } from ‘vuex‘ export default { name: ‘specialTask‘, mixins:[Bscroll], data(){ return { itemList: [] } }, created(){ isBack = false; this.addKeepAlive("index"); this._scrollList(); }, activated(){ isBack = false; }, methods: { _scrollList(directionPas){ this.pageNo = directionPas == ‘onPullingUp‘ ? this.pageNo : 1; let opt = { pageData: { pageNo: this.pageNo,//当前页 pageSize: this.pageSize,//每页显示数据条数 } } specialList(opt).then(res => { if(res.returnResult == 200){ if(directionPas == ‘onPullingUp‘){ this.itemList = this.itemList.concat(res.returnData.data.map(e=>{ return { name: e.name, id: e.id } })); } else { if(this.noData){ this.noData = false; this.$refs.scroll.scroll.openPullUp(); } this.itemList = res.returnData.data.map(e=>{ return { name: e.name, id: e.id } }); } if (this.itemList.length >= res.returnData.recordCount) { this.noData = true this.$refs.scroll.forceUpdate() } } }) }, newAdd(){ this.$router.push({ path: ‘/specialList‘ }) }, toUrlEdit(item,stateSee){ this.$router.push({ path: ‘/specialList‘, query: { id: item.id, stateSee } }) }, ...mapActions({ "cleanKeepAlive": "cleanKeepAlive", "addKeepAlive": "addKeepAlive" }) }, mounted() { //给当前页面顶层一个初始高度 document.getElementById("specialTask").style.minHeight = document.body.clientHeight + ‘px‘; } } </script> <style lang="less" scoped> #specialTask{ background-color: #F7F8FA; overflow: hidden; width:100%; .problem-box{ position: fixed; overflow-y: scroll; top: 0.70rem; left: 0; right: 0; bottom: 1.02rem; .scroll-list{ padding: 0.20rem; padding-top: 0; .problem-li{ margin-top: 0.20rem; background:rgba(255,255,255,1); box-shadow:0px 0.04rem 0.12rem rgba(61,180,248,0.25); border-radius: 0.08rem; color: #000; height: 1.00rem; padding-right: 0.36rem; &:first-child{ margin-top: 0; } .massif-num{ width: 0.10rem; height:100%; background:rgba(61,180,248,1); border-radius:0.08rem 0px 0px 0.08rem; } .scoring{ font-size: 0.36rem; width: 0.60rem; text-align: center; } .department{ flex: 1; margin: 0 0.15rem; line-height: 0.48rem; font-size: 0.30rem; } .bg-state{ background: url(../../assets/images/jdgl_bi@2x.png) 0 0 no-repeat; background-size: 0.29rem 0.30rem; padding: 0 0.20rem 0 0.40rem; color: #606060; font-size: 0.24rem; line-height: 0.30rem; } } } } } </style>
原文:https://www.cnblogs.com/lijh03/p/13566505.html