<template> <div class="scroll" :class="{ 'pull-down': (state === 0), 'pull-up': (state === 1), refreshing: (state === 2), touching: touching }" @touchstart="onRefresh ? touchStart($event) : undefined" @touchmove="onRefresh ? touchMove($event) : undefined" @touchend="onRefresh ? touchEnd($event) : undefined" @mousedown="onRefresh ? mouseDown($event) : undefined" @mousemove="onRefresh ? mouseMove($event) : undefined" @mouseup="onRefresh ? mouseUp($event) : undefined" @scroll="(onInfinite || infiniteLoading) ? onScroll($event) : undefined" > <div class="scroll-inner" :style="{ transform: 'translate3d(0, ' + top + 'px, 0)', webkitTransform: 'translate3d(0, ' + top + 'px, 0)' }" > <div class="pull-to-refresh-layer" v-if="!!onRefresh"> <slot name="refresh"> <div class="preloader"></div> <div class="pull-to-refresh-arrow"></div> <span class="label-down">下拉刷新</span> <span class="label-up">释放刷新</span> <span class="label-refresh">正在刷新..</span> </slot> </div> <slot></slot> <div class="infinite-layer" v-if="onInfinite"> <slot name="infinite"> <div class="infinite-preloader"></div> <span class="label-loading">正在加载..</span> </slot> </div> </div> </div> </template> <style lang="less" scoped> @layer-height: 80px; @color-text-gray: #aaa; @keyframes preloader-spin { 100% { transform: rotate(360deg); } } .pull-to-refresh-layer { position: relative; left: 0; top: 0; width: 100%; height: @layer-height; color: @color-text-gray; .preloader { visibility: hidden; width: 40px; height: 40px; animation: preloader-spin 1s steps(12, end) infinite; &:after { display: block; width: 100%; height: 100%; content: ""; background-image: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 120 120' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'><defs><line id='l' x1='60' x2='60' y1='7' y2='27' stroke='#6c6c6c' stroke-width='11' stroke-linecap='round'/></defs><g><use xlink:href='#l' opacity='.27'/><use xlink:href='#l' opacity='.27' transform='rotate(30 60,60)'/><use xlink:href='#l' opacity='.27' transform='rotate(60 60,60)'/><use xlink:href='#l' opacity='.27' transform='rotate(90 60,60)'/><use xlink:href='#l' opacity='.27' transform='rotate(120 60,60)'/><use xlink:href='#l' opacity='.27' transform='rotate(150 60,60)'/><use xlink:href='#l' opacity='.37' transform='rotate(180 60,60)'/><use xlink:href='#l' opacity='.46' transform='rotate(210 60,60)'/><use xlink:href='#l' opacity='.56' transform='rotate(240 60,60)'/><use xlink:href='#l' opacity='.66' transform='rotate(270 60,60)'/><use xlink:href='#l' opacity='.75' transform='rotate(300 60,60)'/><use xlink:href='#l' opacity='.85' transform='rotate(330 60,60)'/></g></svg>"); background-repeat: no-repeat; background-position: center; background-size: 100%; } } .pull-to-refresh-arrow { width: 40px; height: 40px; background: no-repeat center; background-image: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 26 40'><polygon points='9,22 9,0 17,0 17,22 26,22 13.5,40 0,22' fill='#8c8c8c'/></svg>"); background-size: 20.8px 32px; z-index: 10; transform: rotate(0deg) translate3d(0, 0, 0); transition-duration: 300ms; margin-left: -40px; } } .scroll { position: absolute; top: -@layer-height; right: 0; bottom: 0; left: 0; overflow-x: hidden; overflow-y: auto; -webkit-overflow-scrolling: touch; &.touching .scroll-inner { transition-duration: 0ms; } &:not(.refreshing) { .pull-to-refresh-layer .preloader { animation: none; } } &.refreshing { .pull-to-refresh-arrow { visibility: hidden; transition-duration: 0ms; } .preloader { visibility: visible; } } &.pull-up { .pull-to-refresh-arrow { transform: rotate(180deg) translate3d(0, 0, 0); } } } .scroll-inner { position: absolute; /* top: -@layer-height; */ top: 0; width: 100%; transition-duration: 300ms; } .label-down, .label-up, .label-refresh { display: none; text-align: center; } .pull-down .label-down, .pull-up .label-up, .refreshing .label-refresh { display: block; width: 5.5em; } .pull-to-refresh-layer { display: flex; align-items: center; justify-content: center; } .infinite-layer { height: @layer-height; display: flex; align-items: center; justify-content: center; color: @color-text-gray; } .infinite-preloader { width: 40px; height: 40px; animation: preloader-spin 1s steps(12, end) infinite; &:after { display: block; width: 100%; height: 100%; content: ""; background-image: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 120 120' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'><defs><line id='l' x1='60' x2='60' y1='7' y2='27' stroke='#6c6c6c' stroke-width='11' stroke-linecap='round'/></defs><g><use xlink:href='#l' opacity='.27'/><use xlink:href='#l' opacity='.27' transform='rotate(30 60,60)'/><use xlink:href='#l' opacity='.27' transform='rotate(60 60,60)'/><use xlink:href='#l' opacity='.27' transform='rotate(90 60,60)'/><use xlink:href='#l' opacity='.27' transform='rotate(120 60,60)'/><use xlink:href='#l' opacity='.27' transform='rotate(150 60,60)'/><use xlink:href='#l' opacity='.37' transform='rotate(180 60,60)'/><use xlink:href='#l' opacity='.46' transform='rotate(210 60,60)'/><use xlink:href='#l' opacity='.56' transform='rotate(240 60,60)'/><use xlink:href='#l' opacity='.66' transform='rotate(270 60,60)'/><use xlink:href='#l' opacity='.75' transform='rotate(300 60,60)'/><use xlink:href='#l' opacity='.85' transform='rotate(330 60,60)'/></g></svg>"); background-repeat: no-repeat; background-position: center; background-size: 100%; } } .label-loading { display: block; width: 5.5em; text-align: center; } </style> <script> export default { props: { offset: { type: Number, default: 88 }, onRefresh: { type: Function, default: undefined, required: false }, onInfinite: { type: Function, default: undefined, require: false } }, data() { return { top: 0, state: 0, // 0:down, 1: up, 2: refreshing startY: 0, touching: false, infiniteLoading: false } }, methods: { touchStart(e) { this.startY = e.targetTouches[0].pageY this.touching = true }, mouseDown(e) { this.startY = e.pageY this.touching = true }, touchMove(e) { if (this.$el.scrollTop > 0 || !this.touching) { return } let diff = e.targetTouches[0].pageY - this.startY if (diff > 0) e.preventDefault() this.top = Math.pow(diff, 0.8) + (this.state === 2 ? this.offset : 0) if (this.state === 2) { // in refreshing return } if (this.top >= this.offset) { this.state = 1 } else { this.state = 0 } }, mouseMove(e) { if (this.$el.scrollTop > 0 || !this.touching) { return } let diff = e.pageY - this.startY if (diff > 0) e.preventDefault() this.top = Math.pow(diff, 0.8) + (this.state === 2 ? this.offset : 0) if (this.state === 2) { // in refreshing return } if (this.top >= this.offset) { this.state = 1 } else { this.state = 0 } }, touchEnd(e) { this.touching = false if (this.state === 2) { // in refreshing this.state = 2 this.top = this.offset return } if (this.top >= this.offset) { // do refresh this.refresh() } else { // cancel refresh this.state = 0 this.top = 0 } }, mouseUp(e) { this.touching = false if (this.state === 2) { // in refreshing this.state = 2 this.top = this.offset return } if (this.top >= this.offset) { // do refresh this.refresh() } else { // cancel refresh this.state = 0 this.top = 0 } }, refresh() { this.state = 2 this.top = this.offset this.onRefresh(this.refreshDone) }, refreshDone() { this.state = 0 this.top = 0 }, infinite() { this.infiniteLoading = true this.onInfinite(this.infiniteDone) }, infiniteDone() { this.infiniteLoading = false }, onScroll(e) { if (this.infiniteLoading) { return } let outerHeight = this.$el.clientHeight let innerHeight = this.$el.querySelector('.scroll-inner').clientHeight let scrollTop = this.$el.scrollTop let ptrHeight = this.onRefresh ? this.$el.querySelector('.pull-to-refresh-layer').clientHeight : 0 let infiniteHeight = this.$el.querySelector('.infinite-layer').clientHeight let bottom = innerHeight - outerHeight - scrollTop - ptrHeight if (bottom < infiniteHeight) this.infinite() } } } </script>