1、需求背景

产品需要在购物车加一个左右滑动更多的功能,由于是PC端,大致扫描了下使用的UI库,貌似没有单独提供此类组件,反正有时间,就自己造一个轮子试试

2、先看效果在这里插入图片描述

大致有一个橡皮筋的效果,可能没那么细致,凑合着用吧

3、思路分析

由于添加功能,所以最好不动以前代码,那么自然就想到单独封一个带插槽的组件,由上图效果我们可以大致得出几点

  1. 可分为三部,left、content、right,自然需要提供三个插槽
  2. 滑动方向,这个就比较简单了,记录一个鼠标按下初始位置,监听移动事件,同时计算移动位置与初始值之差,就可以得出移动方向
  3. 动画效果,我们可以使用css过渡实现,包括回弹效果

4、html结构确定

<template>
  <div class="swiper-cell-box"
    @mousedown="onMouseDown"
    @mousemove="onMouseMove"
    @mouseout="onMouseUp"
    @mouseup="onMouseUp" 
    @touchstart="onMouseDown"
    @touchmove="onMouseMove"
    @touchend="onMouseUp">
    <div class="wrapper" :style="getTransformStyle" >
        <div class="left" ref="left" >
          <slot name="left" ></slot> 
        </div>
        <div class="content">
          <slot name="content" ></slot> 
        </div>
        <div class="right" ref="right" >
          <slot name="right" ></slot> 
        </div>
    </div>
  </div>
</template>

我们第一个div块,用于我们事件监听(可以看到兼容了移动端),内部再用一个div,用于实现左右平移,最后内部就是三个插槽了

5、实现之滑动方向确定

这个,就不在累赘,简单代码如下

onMouseDown(e) {
	// 记录初始位置、及添加一个正在移动的标识
    this.startPageX = e.pageX || e.changedTouches[0].pageX
    this.isDragging = true
  },
 onMouseMove(e) {
   if(!this.isDragging) return 
   const pageX = e.pageX || e.changedTouches[0].pageX
   let offset = pageX - this.startPageX 
   // 确定方向
   this.direction = offset < 0 ? 'left' : 'right'
 },
 onMouseUp() {
   if(!this.isDragging) return 
   // 有开,就有关
   this.isDragging = false
   this.startPageX = 0
 },

6、实现之跟手移动

简单说就是,我边移动,它跟着走,我移动10px,它也得乖乖移动10px,那么问题自然变成了我计算移动的距离,再动态修改css位移量
这一块主要是 移动时的事情

onMouseMove(e) {
	if(!this.isDragging) return 
	const pageX = e.pageX || e.changedTouches[0].pageX
	let offset = pageX - this.startPageX 
	this.direction = offset < 0 ? 'left' : 'right'
	// 要动态赋值
	this.translateX = offset 
},

上述代码,当我们往左滑时,offset 值为负,div就会往左滑动,基本能实现跟手滑,不过一般这种滑动都会有一个最大响应距离,人话就是,不可能dom一直跟随你滑动吧!

所以,左右滑动的最大距离就是左右隐藏两块dom的宽度,这个好解决

data() {
    return {
      sidesWidth: {
        left: 0,
        right: 0
      }
    }
}
mounted() {
	// 获取一下,方便做边界值对比
	this.$nextTick(() => {
	  Object.keys(this.sidesWidth).forEach(key => {
	    this.sidesWidth[key] = this.getWidth(this.$refs[key])
	  })
	})
},

上面代码,我们能实现基本跟手动,同时也引入了边界的限定,但是还有几个小细节,比如:应该是轻轻滑动一下(或者超过明确的响应距离),就应该处于展开或者收起状态,这个就需要在滑动结束时,判断当前滑动的距离,以确定最终的状态

7、实现之滑动最终优化

问题一:最终状态的确定

我们可以通过滑动距离以及当前的状态(展开、收起),来确定最终是展开还是收起

问题二:滑动最大距离的限定

可以在移动时,拿滑动距离与边界值比较,不管是向左还是向右,均不能超过边界值,可以想象成坐标轴(-10,10),最大不能超过10,最小能小于-10

这个逻辑可以用这个公式得出translateX = Math.max(-10, Math.min(10, 当前计算出要位移的距离)),慢慢体会

onMouseMove(e) {
    if(!this.isDragging) return 
    const pageX = e.pageX || e.changedTouches[0].pageX
    let offset = pageX - this.startPageX 
    this.direction = offset < 0 ? 'left' : 'right'
    // 将要位移的距离 这里直接加,就不用考虑正负问题
    // 假设右边处于收起状态 this.currentX = 50 + 0
    // 假设右边处于展开状态(假设滑动方向往右,则offset 为正) this.currentX = 50 + -80 = -30
    this.currentX = offset + this.startTranslateX
    // 边界限定
    const min = Math.min(0, -this.sidesWidth.right)
    const max = Math.max(0, this.sidesWidth.left)
    this.currentX = Math.max(min, Math.min(max, this.currentX))
    this.translateX = this.currentX
  },
  onMouseUp() {
    if(!this.isDragging) return 
    const offset = Math.abs(Math.abs(this.translateX) - Math.abs(this.startTranslateX))
    // 设置展开、收起状态
    // 当为收起状态,偏移量大于30,则为展开状态
    // 当已经为展开状态时 偏移量小于30,应仍为展开状态
    let isExpanded = (!this.startTranslateX && offset > this.offsetValue) || (this.startTranslateX && offset < this.offsetValue) 
      ? true
      : false
    this.translateX = isExpanded
      ? Math.sign(this.currentX) * this.sidesWidth[this.direction === 'right' ? 'left' : 'right']
      : 0
    this.isDragging = false
    this.startPageX = 0
  },

8、技术总结

左右跟手滑动核心思路是通过计算滑动的距离,动态设置css位移量,这个过程看似简单,但也有几个小细节,比如边界值的限定弹性效果其实是设置的响应距离、元素移动的距离不是一味的使用偏移量(当处于收起状态时,移动距离 = 偏移量,当处于某一侧展开时,移动距离 = 初始位移距离 + 偏移量)

最后也可以再扩展一些api,比如:手动打开、关闭、以及结束后的回调

完整代码如下

<template>
  <div class="swiper-cell-box"
    @mousedown="onMouseDown"
    @mousemove="onMouseMove"
    @mouseout="onMouseUp"
    @mouseup="onMouseUp" 
    @touchstart="onMouseDown"
    @touchmove="onMouseMove"
    @touchend="onMouseUp">
    <div class="wrapper" :style="getTransformStyle" >
        <div class="left" ref="left" >
          <slot name="left" ></slot> 
        </div>
        <div class="content">
          <slot name="content" ></slot> 
        </div>
        <div class="right" ref="right" >
          <slot name="right" ></slot> 
        </div>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'RetailSiwperCell',
    props: {
      /** 偏移量 **/ 
      offsetValue: {
        type: [Number,String],
        default: 30
      }
    },
    data() {
      return {
        isDragging: false,
        startPageX: 0,
        translateX: 0,
        sidesWidth: {
          left: 0,
          right: 0
        },
        direction: '',
        startTranslateX: 0,
        currentX: 0
      }
    },
    mounted() {
      this.$nextTick(() => {
        Object.keys(this.sidesWidth).forEach(key => {
          this.sidesWidth[key] = this.getWidth(this.$refs[key])
        })
      })
    },
    computed: {
      getTransformStyle() {
        return {
          transform: `translate3d(${this.translateX}px, 0px, 0px)`,
          'transition-duration':  `${this.isDragging ? '0s' : '0.6s'}`
        }
      }
    },
    methods: {
      onMouseDown(e) {
        this.startPageX = e.pageX || e.changedTouches[0].pageX
        this.isDragging = true
        this.startTranslateX = this.translateX
      },
      onMouseMove(e) {
        if(!this.isDragging) return 
        const pageX = e.pageX || e.changedTouches[0].pageX
        let offset = pageX - this.startPageX 
        this.direction = offset < 0 ? 'left' : 'right'
        // 将要位移的距离
        this.currentX = offset + this.startTranslateX
        // 边界限定
        const min = Math.min(0, -this.sidesWidth.right)
        const max = Math.max(0, this.sidesWidth.left)
        this.currentX = Math.max(min, Math.min(max, this.currentX))
        this.translateX = this.currentX
      },
      onMouseUp() {
        if(!this.isDragging) return 
        const offset = Math.abs(Math.abs(this.translateX) - Math.abs(this.startTranslateX))
        // 设置展开、收起状态
        // 当为收起状态,偏移量大于30,则为展开状态
        // 当已经为展开状态时 偏移量小于30,应仍为展开状态
        let isExpanded = (!this.startTranslateX && offset > this.offsetValue) || (this.startTranslateX && offset < this.offsetValue) 
          ? true
          : false
        this.translateX = isExpanded
          ? Math.sign(this.currentX) * this.sidesWidth[this.direction === 'right' ? 'left' : 'right']
          : 0
        this.isDragging = false
        this.startPageX = 0
      },
      getWidth(el) {
        return el.getBoundingClientRect().width || 0
      },
      /**
       * @description: 关闭
       * @return {*}
       */      
      close() {
        this.translateX = 0
      },
      /**
       * @description: 打开
       * @param {*} position left | right
       * @return {*}
       */      
      open(position = '') {
        if(!position) return
        const width = this.sidesWidth[position]
        this.translateX = position === 'right' ? -width : width
      }
    }
  }
</script>

<style>
  .swiper-cell-box{
    overflow: hidden;
    border: 1px solid #eee;
  }
  .wrapper{
    position: relative;
    user-select: none;
  }
  .left{
    position: absolute;
    left: 0;
    top: 0;
    height: 100%;
    transform: translateX(-100%);
  }
  .right{
    position: absolute;
    right: 0;
    top: 0;
    height: 100%;
    transform: translateX(100%);
  }
</style>
GitHub 加速计划 / vu / vue
207.52 K
33.66 K
下载
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。
最近提交(Master分支:1 个月前 )
73486cb5 * chore: fix link broken Signed-off-by: snoppy <michaleli@foxmail.com> * Update packages/template-compiler/README.md [skip ci] --------- Signed-off-by: snoppy <michaleli@foxmail.com> Co-authored-by: Eduardo San Martin Morote <posva@users.noreply.github.com> 3 个月前
e428d891 Updated Browser Compatibility reference. The previous currently returns HTTP 404. 3 个月前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐