/** * better-scroll vue封装 * @Author momoko * @Date 2018/05 */ <template> <div ref="scroll" :class="[c(),{'h-ios': isIos}]" class="content scroll-content"> <div :class="c('__wrapper')"> <div ref="scrollContent" class="scrollContent"> <slot/> </div> <slot :pullUp="pullUp" :pullUpNow="pullUpNow" name="pullup" > <div v-if="pullUp" :class="c('__pullup')"> <div v-if="!pullUpNow"> <span>{{ pullUpTxt }}</span> </div> <div v-else> <Loading/> </div> </div> </slot> </div> <slot :pullDown="pullDown" :pullDownStyle="pullDownStyle" :pullDownBefore="pullDownBefore" :pullDownNow="pullDownNow" :bubbleY="bubbleY" name="pulldown" > <div v-if="pullDown" ref="pulldown" :style="pullDownStyle" :class="c('__pulldown')"> <div v-if="pullDownBefore" :class="c('__pulldown__before')"> <Bubble :y="bubbleY"/> </div> <div v-else :class="c('__pulldown__after')"> <div v-if="pullDownNow"> <Loading/> </div> <div v-else><span>{{ pullDownTxt }}</span></div> </div> </div> </slot> </div> </template> <script type="text/ecmascript-6"> import BScroll from 'better-scroll' import Loading from './loading' import Bubble from './bubble' import mixin from './mixins' import {timeout} from './utils' import { detectOS } from '../../common/utils/index' export default { name: 'Scroll', components: { Loading, Bubble, }, mixins: [mixin], props: { probeType: { // 滚动事件监听类型 type: Number, default: 1, }, click: { // 开启点击事件代理 type: Boolean, default: true, }, listenScroll: { // 监听滚动 type: Boolean, default: false, }, listenBeforeScrollStart: { // 监听滚动开始前 type: Boolean, default: false, }, scrollX: { // 开启X轴滚动 type: Boolean, default: false, }, scrollY: { // 开启Y轴滚动 type: Boolean, default: true, }, scrollbar: { // 开启滚动条 type: null, default: false, }, pullDown: { // 启用下拉刷新 type: Boolean, default: false, }, pullDownConfig: { // 下拉刷新配置 type: Object, default: () => ({ threshold: 90, // 触发 pullingDown 的距离 stop: 40, // pullingDown 正在刷新 hold 时的距离 txt: '刷新成功', }), }, pullUp: { // 启用上拉加载 type: Boolean, default: false, }, pullUpConfig: { // 上拉加载配置 type: Object, default: () => ({ threshold: 100, // 提前触发 pullingUp 的距离 txt: {more: '上拉加载', noMore: '— 我是有底线的 —'}, }), }, startY: { // 起始Y位置 type: Number, default: 0, }, bounce: { // 回弹效果 type: Boolean, default: true, }, bounceTime: { // 回弹时间 type: Number, default: 500, }, preventDefaultException: { // 不阻止默认行为 type: Object, default: () => ({ tagName: /^(INPUT|TEXTAREA|BUTTON|SELECT)$/, }), }, autoUpdate: { // 自动刷新高度:适用于简单场景,复杂场景请使用updateData/refreshData type: Boolean, default: false, }, updateData: { // 引起更新上拉/下拉加载状态的数据(下拉刷新/上拉加载相关的数据) type: Array, default: null, }, refreshData: { // 引起刷新高度的数据(不包含 updateData 内的数据) type: Array, default: null, }, hasFoot: { // 底部按钮的配置 type: Object, default: () => ({ footFlag: false, // 提前触发 pullingUp 的距离 height: 44, }), }, }, data () { return { pullDownBefore: true, // 下拉之前 pullDownNow: false, // 正在下拉 pullDownStyle: '', // 下拉样式 pullUpNow: false, // 正在上拉 pullUpFinally: false, // true表示到了上拉加载到了最底部 isRebounding: false, // 正在回弹 bubbleY: 0, // 气泡y坐标, isIos: false, fullScreen: true, } }, computed: { // 下拉的文本 pullDownTxt () { return this.pullDownConfig && this.pullDownConfig.txt }, // 上拉的文本 pullUpTxt () { const moreTxt = this.pullUpConfig && this.pullUpConfig.txt && this.pullUpConfig.txt.more const noMoreTxt = this.pullUpConfig && this.pullUpConfig.txt && this.pullUpConfig.txt.noMore return this.pullUpFinally ? noMoreTxt : moreTxt }, }, watch: { updateData () { this.update() }, async refreshData () { if (this.updateState) return await this.$nextTick() this.refresh() }, }, created () { this.fullScreen && detectOS() === 'ios' && (this.isIos = true) }, async mounted () { this.pullDownInitTop = parseInt(this.$refs.pulldown && getComputedStyle(this.$refs.pulldown).top) || -100 await this.$nextTick() this.initScroll() // 自动刷新高度:深监视 $data,发生改变时更新高度 this.autoUpdate && this.$parent && this.$parent.$data && this.$watch(() => this.$parent.$data, (val) => { this.update() }, { deep: true, }) }, methods: { // 初始化scroll initScroll () { let vm = this if (!this.$refs.scroll) return // 设置scrollContent的最小高,实现高度不足时也有回弹效果 if (this.$refs.scrollContent) { this.$refs.scrollContent.style.minHeight = `${this.$refs.scroll.getBoundingClientRect().height + 1}px` if (vm.hasFoot.footFlag) { let height = vm.hasFoot.height || 88 // this.$refs.scrollContent.style.minHeight = `${this.$refs.scroll.getBoundingClientRect().height - height}px` this.$refs.scrollContent.style.paddingBottom = height + 'px' } } const options = { probeType: this.probeType, click: this.click, scrollX: this.scrollX, scrollY: this.scrollY, scrollbar: this.scrollbar, pullDownRefresh: this.pullDown && this.pullDownConfig, pullUpLoad: this.pullUp && this.pullUpConfig, startY: this.startY, bounce: this.bounce, bounceTime: this.bounceTime, preventDefaultException: this.preventDefaultException, } this.scroll = new BScroll(this.$refs.scroll, options) this.listenScroll && this.scroll.on('scroll', pos => { this.$emit('scroll', pos) }) this.listenBeforeScroll && this.scroll.on('beforeScrollStart', () => { this.$emit('beforeScrollStart') }) this.pullDown && this._initPullDown() this.pullUp && this._initPullUp() }, // 初始化下拉刷新 _initPullDown () { let vm = this this.scroll.on('pullingDown', () => { this.pullDownBefore = false this.pullDownNow = true setTimeout(function () { vm.$emit('pullingDown') vm.scroll.closePullDown() // 防止在 bounce 前二次触发 }, 500) }) this.scroll.on('scroll', pos => { if (!this.pullDown || pos.y < 0) return const posY = Math.floor(pos.y) // 滚动的y轴位置:Number if (this.pullDownBefore) { this.bubbleY = Math.max(0, posY + this.pullDownInitTop) this.pullDownStyle = `transform: translateY(${Math.min(posY, -this.pullDownInitTop)}px)` } else { this.bubbleY = 0 } if (this.isRebounding) { this.pullDownStyle = `transform: translateY(${Math.min(posY, this.pullDownConfig.stop)}px)` } }) }, // 初始化上拉加载 _initPullUp () { this.scroll.on('pullingUp', () => { let vm = this if (this.pullUpFinally) { this.scroll.finishPullUp() } else { vm.pullUpNow = true setTimeout(function () { vm.$emit('pullingUp') }, 500) } }) }, // 关闭滚动 disable () { this.scroll && this.scroll.disable() }, // 开启滚动 enable () { this.scroll && this.scroll.enable() }, // 销毁滚动示例 destroy () { this.scroll && this.scroll.destroy() }, // 刷新滚动高度 refresh () { this.scroll && this.scroll.refresh() }, // 更新加载状态,下拉/下拉成功后使用 async update (final) { if (this.updateState) return this.updateState = true // 正在update状态 if (this.pullDown && this.pullDownNow) { // 下拉刷新触发成功后 this.pullDownNow = false await timeout(this.bounceTime / 2) // 刷新成功hold this.isRebounding = true this.scroll.finishPullDown() // 开始回弹 await timeout(this.bounceTime) this.pullDownBefore = true this.isRebounding = false this.scroll.openPullDown(this.pullDownConfig) this.pullUpFinally = false } else if (this.pullUp && this.pullUpNow) { // 上拉加载触发成功后 this.pullUpNow = false this.scroll.finishPullUp() } typeof final !== 'undefined' && (this.pullUpFinally = !!final) await this.$nextTick() this.refresh() this.updateState = false }, /** * 每次滚动多少距离 * @param {Number} x x轴位置 * @param {Number} y y轴位置 * @param {Number} time 滚动时间 * @return {Void} */ scrollBy (x = 0, y = 0, time = this.bounceTime) { this.scroll && this.scroll.scrollTo((this.scroll.absStartX - x), (this.scroll.absStartY - y), time) }, /** * 滚动到指定位置 * @param {Number} x x轴位置 * @param {Number} y y轴位置 * @param {Number} time 滚动时间 * @return {Void} */ scrollTo (x = 0, y = 0, time = this.bounceTime) { this.scroll && this.scroll.scrollTo(x, y, time) }, // 滚动到元素 scrollToElement () { this.scroll && this.scroll.scrollToElement.apply(this.scroll, arguments) }, // 滚动到顶部 scrollToTop () { this.scroll && this.scrollTo(0, 0) }, // 滚动到底部 scrollToBottom () { this.scroll && this.scrollTo(0, this.scroll.maxScrollY) }, }, } </script> <style lang="stylus"> //$ = vue-better-scroll .vue-better-scroll { width 100% //height 100% overflow hidden box-sizing border-box position absolute &__wrapper { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; -webkit-touch-callout: none; -webkit-text-size-adjust: none; -moz-text-size-adjust: none; text-size-adjust: none; -webkit-transform-origin: left top; transform-origin: left top; } &__pullup { width 100% height 25px display flex justify-content center align-items center font-size 14px color rgb(153, 153, 153) } &__pulldown { position absolute left 0 top -50px; /*no*/ width 100% display flex justify-content center align-items center transition all font-size 14px color rgb(153, 153, 153) &__before { display flex } &__after { width 100% height 40px; /*no*/ display flex justify-content center align-items center color #666 } } } // iPhoneX适配 @media (device-width: 375px) and (device-height: 812px) and (-webkit-min-device-pixel-ratio: 3) { .platform-ios { .has-header { top: 84px; } } } // iPhoneX Max适配 @media (device-width: 414px) and (device-height: 896px) { .platform-ios { .has-header { top: 84px; } } } </style>