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