my-marquee/src/Marquee.vue

<template>
  <div class="my-marquee"
       :class="classes"
       @mouseenter="handleMouseEnter"
       @mouseleave="handleMouseLeave">
    <div class="my-marquee__scroll" :style="scrollStyle">
      <div ref="content" class="my-marquee__content">
        <slot></slot>
      </div>
      <div class="my-marquee__copy-content" v-html="copyHtml"></div>
    </div>
  </div>
</template>

<script>
  /**
   * 无缝滚动
   * @module $ui/components/my-marquee
   */

  import {requestAnimationFrame, cancelAnimationFrame} from '$ui/utils/util'

  /**
   * 插槽
   * @member slots
   * @property {string} default 默认插槽,放置滚动内容
   */
  export default {
    name: 'MyMarquee',
    /**
     * 属性参数
     * @member props
     * @property {Array} [data] 滚动数据,用作监听数据变更,更新html
     * @property {Boolean} [disabled] 禁用
     * @property {string} [direction=up] 滚动方向,可选'up', 'down', 'left', 'right'
     * @property {boolean} [auto=true] 自动开始
     * @property {number} [speed=1] 滚动速度,数值越大速度越快
     * @property {number} [waitTime=3000] 间隔滚动停顿等待时间
     * @property {number} [scrollLength=0] 间隔滚动,每次滚动的长度,单位px
     *
     */
    props: {
      // 滚动数据
      data: {
        type: Array,
        default() {
          return []
        }
      },
      disabled: Boolean,
      // 滚动方向
      direction: {
        type: String,
        default: 'up',
        validator(val) {
          return ['up', 'down', 'left', 'right'].includes(val)
        }
      },
      // 自动开始
      auto: {
        type: Boolean,
        default: true
      },
      // 滚动速度,数值越大速度越快
      speed: {
        type: Number,
        default: 1
      },
      // 间隔滚动停顿等待时间
      waitTime: {
        type: Number,
        default: 3000
      },
      // 间隔滚动,每次滚动的长度,单位px
      scrollLength: Number
    },
    data() {
      this.timerId = null
      return {
        copyHtml: '',
        contentWidth: 0,
        contentHeight: 0,
        scrollPosition: 0,
        isPause: false
      }
    },
    computed: {
      isHorizontal() {
        return ['left', 'right'].includes(this.direction)
      },
      classes() {
        return {
          'my-marquee--horizontal': this.isHorizontal,
          'is-disabled': this.disabled
        }
      },
      scrollStyle() {
        return {
          transform: this.isHorizontal
            ? `translate3d(-${this.scrollPosition}px, 0, 0)`
            : `translate3d(0, -${this.scrollPosition}px, 0)`,
          width: this.isHorizontal ? `${2 * this.contentWidth}px` : '100%'
        }
      }
    },
    watch: {
      data: {
        immediate: true,
        handler() {
          this.$nextTick(this.renderCopyHtml)
        }
      },
      disabled(val) {
        this.stop()
        if (val) {
          this.copyHtml = ''
          this.scrollPosition = 0
        } else {
          this.$nextTick(this.renderCopyHtml)
          this.auto && this.start()
        }
      }
    },
    methods: {
      renderCopyHtml() {
        if (!this.$refs.content || this.disabled) return
        this.copyHtml = this.$refs.content.innerHTML
        this.updateView()
      },
      updateView() {
        if (!this.$refs.content) return
        const rect = this.$refs.content.getBoundingClientRect()
        // 内容必须要撑开高度宽度
        this.contentHeight = rect.height
        this.contentWidth = rect.width
        this.$emit('resize', rect, this)
      },
      /**
       * 开始滚动
       * @method start
       */
      start() {
        this.isPause = false
        switch (this.direction) {
          case 'up':
            this.scrollPosition = this.scrollPosition || 0
            this.scrollUp()
            break;
          case 'down':
            this.scrollPosition = this.scrollPosition || this.contentHeight
            this.scrollDown()
            break;
          case 'left':
            this.scrollPosition = this.scrollPosition || 0
            this.scrollLeft()
            break;
          case 'right':
            this.scrollPosition = this.scrollPosition || this.contentWidth
            this.scrollRight()
            break;
        }
      },
      /**
       * 停止
       * @method stop
       */
      stop() {
        this.timerId && cancelAnimationFrame(this.timerId)
        this.resumeId && clearTimeout(this.resumeId)
      },
      /**
       * 暂停
       * @method pause
       */
      pause() {
        this.isPause = true
      },
      scrollUp() {
        this.timerId = requestAnimationFrame(() => {
          if (this.isPause) return
          this.scrollPosition += this.speed
          if (this.resume()) {
            this.isPause = true
            return;
          }
          if (this.scrollPosition >= this.contentHeight) {
            this.scrollPosition = 0
          }
          this.scrollUp()
        })
      },
      scrollDown() {
        this.timerId = requestAnimationFrame(() => {
          if (this.isPause) return
          this.scrollPosition -= this.speed
          if (this.resume()) {
            this.isPause = true
            return;
          }
          if (this.scrollPosition <= 0) {
            this.scrollPosition = this.contentHeight
          }
          this.scrollDown()
        })
      },
      scrollLeft() {
        this.timerId = requestAnimationFrame(() => {
          if (this.isPause) return
          this.scrollPosition += this.speed
          if (this.resume()) {
            this.isPause = true
            return;
          }
          if (this.scrollPosition >= this.contentWidth) {
            this.scrollPosition = 0
          }
          this.scrollLeft()
        })
      },
      scrollRight() {
        this.timerId = requestAnimationFrame(() => {
          if (this.isPause) return
          this.scrollPosition -= this.speed
          if (this.resume()) {
            this.isPause = true
            return;
          }
          if (this.scrollPosition <= 0) {
            this.scrollPosition = this.contentWidth
          }
          this.scrollRight()
        })
      },
      resume() {
        const value = Math.floor(this.scrollPosition) % this.scrollLength
        const match = value === 0
        if (match) {
          this.resumeId && clearTimeout(this.resumeId)
          this.resumeId = setTimeout(this.start, this.waitTime)
        }
        return match

      },
      handleMouseEnter() {
        this.stop()
        this.isPause = true
      },
      handleMouseLeave() {
        this.start()
      }
    },
    mounted() {
      (this.auto && !this.disabled) && this.start()
    },
    beforeDestroy() {
      this.stop()
      this.copyHtml = ''
    }
  }
</script>