vue基于elementui的无限滚动组件

组件代码如下

<template>
  <div :class="wrapClasses" style="touch-action: none;">
    <div
      :class="scrollContainerClasses"
      :style="{height: height + 'px'}"
      @scroll="handleScroll"
      @wheel="onWheel"
      @touchstart="onPointerDown"
      ref="scrollContainer"
    >
      <div :class="loaderClasses" :style="{paddingTop: wrapperPadding.paddingTop}" ref="toploader"
           v-loading.body="showTopLoader"
           :element-loading-text="loadingText"
           :element-loading-spinner="loadingSpinner">
      </div>
      <div :class="slotContainerClasses" ref="scrollContent">
        <slot></slot>
      </div>
      <div :class="loaderClasses" :style="{paddingBottom: wrapperPadding.paddingBottom}" ref="bottomLoader"
           v-loading.body="showBottomLoader"
           :element-loading-text="loadingText"
           :element-loading-spinner="loadingSpinner">
      </div>
    </div>
  </div>
</template>

<script>
  import throttle from 'lodash.throttle'
 // 这个是element-ui框架自带的,需要安装element-ui
  import { on, off } from 'element-ui/lib/utils/dom'

  const prefixCls = 'xdh-scroll'
  const dragConfig = {
    sensitivity: 10,
    minimumStartDragOffset: 5 // minimum start drag offset
  }
  const noop = () => Promise.resolve()

  export default {
    name: 'MyScroll',
    props: {
      height: {
        type: [Number, String],
        default: 300
      },
      onReachTop: {
        type: Function
      },
      onReachBottom: {
        type: Function
      },
      onReachEdge: {
        type: Function
      },
      loadingText: {
        type: String,
        default: '加载中...'
      },
      loadingSpinner: {
        type: String,
        default: 'el-icon-loading'
      },
      distanceToEdge: [Number, Array]
    },
    data () {
      const distanceToEdge = this.calculateProximityThreshold()
      return {
        showTopLoader: false,
        showBottomLoader: false,
        showBodyLoader: false,
        lastScroll: 0,
        reachedTopScrollLimit: true,
        reachedBottomScrollLimit: false,
        topRubberPadding: 0,
        bottomRubberPadding: 0,
        rubberRollBackTimeout: false,
        isLoading: false,
        pointerTouchDown: null,
        touchScroll: false,
        handleScroll: () => {},
        pointerUpHandler: () => {},
        pointerMoveHandler: () => {},

        // near to edge detectors
        topProximityThreshold: distanceToEdge[0],
        bottomProximityThreshold: distanceToEdge[1]
      }
    },
    computed: {
      wrapClasses () {
        return `${prefixCls}-wrapper`
      },
      scrollContainerClasses () {
        return `${prefixCls}-container`
      },
      slotContainerClasses () {
        return [
          `${prefixCls}-content`,
          {
            [`${prefixCls}-content-loading`]: this.showBodyLoader
          }
        ]
      },
      loaderClasses () {
        return `${prefixCls}-loader`
      },
      wrapperPadding () {
        return {
          paddingTop: this.topRubberPadding + 'px',
          paddingBottom: this.bottomRubberPadding + 'px'
        }
      }
    },
    methods: {
      // just to improve feeling of loading and avoid scroll trailing events fired by the browser
      waitOneSecond () {
        return new Promise(resolve => {
          setTimeout(resolve, 1000)
        })
      },

      calculateProximityThreshold () {
        const dte = this.distanceToEdge
        if (typeof dte === 'undefined') return [20, 20]
        return Array.isArray(dte) ? dte : [dte, dte]
      },

      onCallback (dir) {
        this.isLoading = true
        this.showBodyLoader = true
        if (dir > 0) {
          this.showTopLoader = true
          this.topRubberPadding = 20
        } else {
          this.showBottomLoader = true
          this.bottomRubberPadding = 20

          // to force the scroll to the bottom while height is animating
          let bottomLoaderHeight = 0
          const container = this.$refs.scrollContainer
          const initialScrollTop = container.scrollTop
          for (let i = 0; i < 20; i++) {
            setTimeout(() => {
              bottomLoaderHeight = Math.max(
                bottomLoaderHeight,
                this.$refs.bottomLoader.getBoundingClientRect().height
              )
              container.scrollTop = initialScrollTop + bottomLoaderHeight
            }, i * 50)
          }
        }

        const callbacks = [this.waitOneSecond(), this.onReachEdge ? this.onReachEdge(dir) : noop()]
        callbacks.push(dir > 0 ? this.onReachTop ? this.onReachTop() : noop() : this.onReachBottom ? this.onReachBottom() : noop())

        let tooSlow = setTimeout(() => {
          this.reset()
        }, 5000)

        Promise.all(callbacks).then(() => {
          clearTimeout(tooSlow)
          this.reset()
        })
      },

      reset () {
        [
          'showTopLoader',
          'showBottomLoader',
          'showBodyLoader',
          'isLoading',
          'reachedTopScrollLimit',
          'reachedBottomScrollLimit'
        ].forEach(prop => (this[prop] = false))

        this.lastScroll = 0
        this.topRubberPadding = 0
        this.bottomRubberPadding = 0
        clearInterval(this.rubberRollBackTimeout)

        // if we remove the handler too soon the screen will bump
        if (this.touchScroll) {
          setTimeout(() => {
            off(window, 'touchend', this.pointerUpHandler)
            this.$refs.scrollContainer.removeEventListener('touchmove', this.pointerMoveHandler)
            this.touchScroll = false
          }, 500)
        }
      },

      onWheel (event) {
        if (this.isLoading) return

        // get the wheel direction
        const wheelDelta = event.wheelDelta ? event.wheelDelta : -(event.detail || event.deltaY)
        this.stretchEdge(wheelDelta)
      },

      stretchEdge (direction) {
        clearTimeout(this.rubberRollBackTimeout)

        // check if set these props
        if (!this.onReachEdge) {
          if (direction > 0) {
            if (!this.onReachTop) return
          } else {
            if (!this.onReachBottom) return
          }
        }

        // if the scroll is not strong enough, lets reset it
        this.rubberRollBackTimeout = setTimeout(() => {
          if (!this.isLoading) this.reset()
        }, 250)

        // to give the feeling its ruberish and can be puled more to start loading
        if (direction > 0 && this.reachedTopScrollLimit) {
          this.topRubberPadding += 5 - this.topRubberPadding / 5
          if (this.topRubberPadding > this.topProximityThreshold) this.onCallback(1)
        } else if (direction < 0 && this.reachedBottomScrollLimit) {
          this.bottomRubberPadding += 6 - this.bottomRubberPadding / 4
          if (this.bottomRubberPadding > this.bottomProximityThreshold) this.onCallback(-1)
        } else {
          this.onScroll()
        }
      },

      onScroll () {
        if (this.isLoading) return
        const el = this.$refs.scrollContainer
        const scrollDirection = Math.sign(this.lastScroll - el.scrollTop) // IE has no Math.sign, check that webpack polyfills this
        const displacement = el.scrollHeight - el.clientHeight - el.scrollTop

        const topNegativeProximity = this.topProximityThreshold < 0 ? this.topProximityThreshold : 0
        const bottomNegativeProximity = this.bottomProximityThreshold < 0 ? this.bottomProximityThreshold : 0
        if (scrollDirection === -1 && displacement + bottomNegativeProximity <= dragConfig.sensitivity) {
          this.reachedBottomScrollLimit = true
        } else if (scrollDirection >= 0 && el.scrollTop + topNegativeProximity <= 0) {
          this.reachedTopScrollLimit = true
        } else {
          this.reachedTopScrollLimit = false
          this.reachedBottomScrollLimit = false
          this.lastScroll = el.scrollTop
        }
      },

      getTouchCoordinates (e) {
        return {
          x: e.touches[0].pageX,
          y: e.touches[0].pageY
        }
      },

      onPointerDown (e) {
        // we just use scroll and wheel in desktop, no mousedown
        if (this.isLoading) return
        if (e.type === 'touchstart') {
          // if we start do touchmove on the scroll edger the browser will scroll the body
          // by adding 5px margin on pointer down we avoid this behaviour and the scroll/touchmove
          // in the component will not be exported outside of the component
          const container = this.$refs.scrollContainer
          if (this.reachedTopScrollLimit) container.scrollTop = 5
          else if (this.reachedBottomScrollLimit) container.scrollTop -= 5
        }
        if (e.type === 'touchstart' && this.$refs.scrollContainer.scrollTop === 0) {
          this.$refs.scrollContainer.scrollTop = 5
        }

        this.pointerTouchDown = this.getTouchCoordinates(e)
        on(window, 'touchend', this.pointerUpHandler)
        this.$refs.scrollContainer.parentElement.addEventListener('touchmove', e => {
          e.stopPropagation()
          this.pointerMoveHandler(e)
        }, {passive: false, useCapture: true})
      },

      onPointerMove (e) {
        if (!this.pointerTouchDown) return
        if (this.isLoading) return

        const pointerPosition = this.getTouchCoordinates(e)
        const yDiff = pointerPosition.y - this.pointerTouchDown.y

        this.stretchEdge(yDiff)

        if (!this.touchScroll) {
          const wasDragged = Math.abs(yDiff) > dragConfig.minimumStartDragOffset
          if (wasDragged) this.touchScroll = true
        }
      },

      onPointerUp () {
        this.pointerTouchDown = null
      }
    },
    created () {
      this.handleScroll = throttle(this.onScroll, 150, {leading: false})
      this.pointerUpHandler = this.onPointerUp.bind(this) // because we need the same function to add and remove event handlers
      this.pointerMoveHandler = throttle(this.onPointerMove, 50, {leading: false})
    }
  }
</script>

使用方法

<template>
    <my-scroll :on-reach-bottom="handleReachBottom">
        <el-card v-for="(item, index) in list1" :key="index" style="margin: 32px 0">
            Content {{ item }}
        </el-card>
    </my-scroll>
</template>
<script>
    export default {
        data () {
            return {
                list1: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
            }
        },
        methods: {
            handleReachBottom () {
                return new Promise(resolve => {
                    setTimeout(() => {
                        const last = this.list1[this.list1.length - 1];
                        for (let i = 1; i < 11; i++) {
                            this.list1.push(last + i);
                        }
                        resolve();
                    }, 2000);
                });
            }
        }
    }
</script>
const raFrame =
  window.requestAnimationFrame ||
  window.webkitRequestAnimationFrame ||
  function(callback) {
    return window.setTimeout(callback, 1000 / 60);
  };
export function throttle(fn, context = this, isImmediate = false) {
  let isLocked;
  return function() {
    const _args = arguments;

    if (isLocked) return;

    isLocked = true;
    raFrame(function() {
      isLocked = false;
      fn.apply(context, _args);
    });

    isImmediate && fn.apply(context, _args);
  };
}

属性

参数 说明 类型 可选值 默认值
height 滚动区域的高度,单位像素 String/Number - 300
loading-text 加载中的文案 String - 加载中...
loading-spinner 自定义加载图标类名 String - el-icon-loading
on-reach-top 滚动至顶部时触发,需返回 Promise Function - -
on-reach-bottom 滚动至底部时触发,需返回 Promise Function - -
on-reach-edge 滚动至顶部或底部时触发,需返回 Promise Function - -
distance-to-edge 从边缘到触发回调的距离。如果是负的,回调将在到达边缘之前触发。值最好在 24 以下。 Number/Array - [20, 20]
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353

推荐阅读更多精彩内容