如何封装一个滚动选择组件

1,550 阅读1分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

TIP 👉 冤家宜解不宜结,各自回头看后头。明·冯梦龙《古今小说》

前言

在我们日常项目开发中,我们在做移动端的时候会涉及到滚动选择功能,所以封装了这个滚动选择组件。

滚动选择组件

属性

1. value
  • 当前选中值,可以为options中选项对象,也可以为选项的value
  • 值类型为:字符串(选项value)、数值(选项value)或对象(选项对象)
2. options
  • 选项数组
  • 值类型为数组,示例:[{ value: '01', text: '男'}, { value: '02', text: '女'}]

事件

1. change
  • 值发生改变时触发的事件
  • 参数:
    • currentOption:当前选中选项对象,例:{ value: '01', text: '男'}
2. input
  • 值发生改变时触发的事件
  • 参数:
    • value:当前选中选项的value,示例:'01'

实现scrollSelect.vue

<template>
  <div>
      <div class="f-flex f-flext scroll-select" @touchmove.prevent="" @mousewheel.prevent="">
        <div class="f-flex1 f-tac f-oh">
          <ul :class="{'dragging': dragging}" @touchstart.stop="handleTouchStart($event)" @mousedown.stop="handleTouchStart($event)" :style="{'transform' : 'translate3d(0,' + translateY + 'px, 0)'}">
            <li ref="li"></li>
            <li class="f-toe" v-for="(item, index) in options" :key="item.value" :class="{
                'current': currentOption ? item.value === currentOption.value : false,
                'node1':  Math.abs(index - currentIndex) == 1,
                'node2':  Math.abs(index - currentIndex) == 2,
                'node3':  Math.abs(index - currentIndex) >= 3
            }">{{item.text}}</li>
            <li></li>
            <li></li>
            <li></li>
          </ul>
        </div>
        <div class="net-monitor"></div>
      </div>
  </div>
</template>
<script>
export default {
  name: 'ScrollSelect',
  props: {
    // 当前选中值
    value: [String, Number, Object],
    /**
     * 选择项列表数组
     * 数组中对象示例:{ value: '01', text: '男'}
     *  value: 值
     *  text: 显示的文字
     **/
    options: {
      type: Array,
      default: () => []
    }
  },
  data () {
    return {
      // 当前选中值的索引位置
      currentIndex: 0,
      // 当前选中值的选项信息,例:{ value: '01', text: '男'}
      currentOption: {},
      dragging: false,
      distanceY: 0,
      translateY: 0
    }
  },
  watch: {
    value (val) {
      this.initValue(val)
      this.initTranslateY()
    },
    options (val) {
      if (val && val instanceof Array) {
        this.initValue(this.currentOption)
        this.initTranslateY()
      }
    }
  },
  created () {
    this.initValue(this.value)
  },
  mounted () {
    this.initTranslateY()
    if (typeof this.value === 'string' || typeof this.value === 'number') {
      this.$emit('change', this.currentOption)
    }
  },
  beforeDestroy () {
    document.removeEventListener('touchmove', this.handleTouchMove)
    document.removeEventListener('touchend', this.handleTouchEnd)
    document.removeEventListener('mousemove', this.handleTouchMove)
    document.removeEventListener('mouseup', this.handleTouchEnd)
  },
  methods: {
    initValue (value) {
      let currentIndex = 0
      let currentOption = this.options.length > 0 ? this.options[0] : null
      if (value !== null && typeof value !== 'undefined') {
        this.options.forEach((val, idx) => {
          if (typeof value === 'string' || typeof value === 'number') {
            if (val.value === value) {
              currentOption = val
              currentIndex = idx
            }
          } else if (val.value === value.value) {
            currentOption = val
            currentIndex = idx
          }
        })
      }
      this.currentIndex = currentIndex
      this.currentOption = currentOption
    },
    initTranslateY () {
      let clientHeight = this.$refs.li.offsetHeight
      this.translateY = -clientHeight * this.currentIndex
    },
    setPage () {
      let clientHeight = this.$refs.li.offsetHeight
      let total = this.options.length
      let goPage = Math.round((this.translateY / clientHeight).toFixed(1))
      if (goPage > 0) {
        goPage = 0
      }
      goPage = total - 1 >= Math.abs(goPage) ? goPage : -(total - 1)
      let index = Math.abs(goPage)
      this.currentOption = this.options[index]
      this.currentIndex = index
      this.translateY = goPage * clientHeight
      this.$emit('change', this.currentOption)
      this.$emit('input', this.currentOption.value)
    },
    getPageY (e) {
      return e.changedTouches ? e.changedTouches[0]['pageY'] : e['pageY']
    },
    handleTouchStart (e) {
      this.distanceY = 0
      this.startY = this.getPageY(e)
      this.startTranslateY = this.translateY
      this.dragging = true
      document.addEventListener('touchmove', this.handleTouchMove, false)
      document.addEventListener('touchend', this.handleTouchEnd, false)
      document.addEventListener('mousemove', this.handleTouchMove, false)
      document.addEventListener('mouseup', this.handleTouchEnd, false)
    },
    handleTouchMove (e) {
      this.distanceY = this.getPageY(e) - this.startY
      this.translateY = this.startTranslateY + this.distanceY
    },
    handleTouchEnd (e) {
      this.dragging = false
      this.setPage()
      document.removeEventListener('touchmove', this.handleTouchMove)
      document.removeEventListener('touchend', this.handleTouchEnd)
      document.removeEventListener('mousemove', this.handleTouchMove)
      document.removeEventListener('mouseup', this.handleTouchEnd)
    },
    // 获取默认值
    getDefaultValue (value) {
      if (this.options.length > 0) {
        return this.options[0]
      } else {
        return {}
      }
    }
  }
}
</script>
<style lang="scss" scoped>
.scroll-select {
  position: relative;
  width: 100%;
  margin: 0 auto;
  background: transparent;
  height: 350px;
  overflow: hidden;
  color: #2D3859;
  ul {
    transition: all .4s ease;
    padding-top: 27px;
    &.dragging {
      transition: none;
    }
    li {
      line-height: 98px;
      height: 98px;
      font-size: 36px;
      text-align: center;
      color: #a8a8a8;
      transition: .3s ease;
      &.current {
        font-size: 40px;
        color: #333333;
      }
      &.node1 {
        font-size: 36px;
        opacity: .7;
      }
      &.node2 {
        font-size: 32px;
        opacity: .5;
      }
      &.node3 {
        font-size: 28px;
        opacity: .3;
      }
    }
  }
}

.net-monitor {
  width: 100%;
  height: 94px;
  border: 1px solid $base-color;
  border-left-width: 0;
  border-right-width: 0;
  @include base-border-color(.5);
  position: absolute;
  bottom: 125px;
  @include base-background-color(.1);
  z-index: -1;
}
.f-toe {
  overflow: hidden;
  word-wrap: normal;
  white-space: nowrap;
  text-overflow: ellipsis;
}
</style>

index.js

/**
 * 滚动选择组件
 */
import ScrollSelect from './ScrollSelect.vue'
export default ScrollSelect

「欢迎在评论区讨论」

希望看完的朋友可以给个赞,鼓励一下