干货--手把手撸vue移动UI框架: 滑动删除

前言

前几天因为项目需要,用jquery写了一个swiperOut组件,然后我就随便把这个组件翻译成基于Vue的了,有兴趣的朋友可以看下。Github源码(不麻烦的话帮忙start,请各位大爷赏个星星) demo展示

效果展示

老规矩,先上效果,效果不是很好,大家如果有什么生成gif的好用的软件可以推荐下:

开始制作

DOM结构

分析效果途中的结构,我们可以得出每一项可滑动删除的节点的结构包含如下两部分:

  1. 正文部分,显示咱们的主内容
  2. 滑动出来的部分(如:删除按钮)
<li class="r-swiper-out-item">
  <div class="r-swiper-out-item-content"></div>
  <div class="r-swiper-out-item-btns"></div>
</li>

因为使用swiperOut的情景一般都是列表,所以,这里我们用li标签;在实际使用情况中,咱们的content和btns两个容器中的内容是经常会自定义的,所以在这里咱们使用两个插槽slot接受用户的自定义:

<li class="r-swiper-out-item">
  <div class="r-swiper-out-item-content">
    <slot></slot>
  </div>
  <div class="r-swiper-out-item-btns">
    <slot name="btns">
      <div class="r-swiper-out-item-btn" @click="delItem">删除</div>
    </slot>
  </div>
</li>

这个是咱们每一项的DOM结构,接下来,咱们还需要把这个列表放到一个UL容器中去,我们把UL父容器叫做swiperOut,子列表项li叫做swiperOutItem。swiperOut的DOM结构如下:

<ul class="r-swiper-out" ref="swiperOut">
  <slot></slot>
</ul>

css样式

swiperOutItem:

<style lang="scss">
.r-swiper-out-item{
  position: relative;
  &-btns{
    display: inline-block;
    position: absolute;
    right: 0;
    top:0;
    height: 100%;
    transform: translateX(100%);
  }
  &-btn{
    background-color: red;
    color: #fff;
    width: 100px;
    text-align: center;
  }
}
</style>

我们这里让正文content占据视图的100%,然后按钮容器btns靠右绝对定位,然后再把btns向右移动100%,这样btns就刚好衔接在content后面。当向左滑动的时候,item向左移动,btns显示出来。父容器swiperOut的样式如下:

<style lang="scss">
.r-swiper-out{
  position: relative;
  width: 100%;
  overflow: hidden;
}
</style>

javascript

该交互的具体逻辑如下:

  1. 自容器向左滑动,容器跟着移动
  2. 如果有已经处于打开状态的子项,需要把这个已经打开的子项关闭
  3. 当手指释放的时候,判断移动距离是否超过阀值
  4. 如果超过阀值,则运用动画,把btns完全显示出来
  5. 如果没有,则运用动画返回初始状态

首先当子项初始化的时候我们应该获取btns的宽度,因为这个宽度决定了我们向左最多能滑动多少

<script>
export default{
  data () {
    return {
      btnsWidth: 0
    }
  },
  mounted () {
    this.$nextTick(() => {
      this.btnsWidth = this.$refs.btns.offsetWidth
    })
  }
}
</script>

接下来,咱们应该给item绑定一个样式对象,用来动态控制,子项滑动的距离:

<script>
export default{
  data () {
    return {
      btnsWidth: 0
      startX: 0,
      translateX: 0,
    }
  },
  computed: {
    itemStyle () {
      return {
        transform: `translate3d(${this.translateX}px, 0, 0)`,
        transition: `all ${this.speed}ms`
      }
    }
  },
  /*...省略之前代码...*
}
</script>

接下来给content绑定滑动事件:

<script>
export default{
  /*...省略之前代码...*
  methods: {
    touchstart (e) {},
    touchmove (e) {},
    touchend (e) {}
  }
  /*...省略之前代码...*
}
</script>

接下来完善,touchstart函数:

  1. 记录手指开始滑动的坐标
  2. 将动画执行事件设置成零
  3. 记录当前item的X轴坐标
<script>
export default{
  data () {
    return {
      speed: 300,
      startX: 0,
      translateX: 0,
      oldPoint: null,
      btnsWidth: 0
    }
  },
  methods: {
    touchstart (e) {
      this.oldPoint = e.touches[0]
      this.speed = 0
      this.startX = this.translateX
    }
  }
  /*...省略之前代码...*
}
</script>

完善我们的核心函数touchmove函数,逻辑如下:

  1. 获取手指横向移动距离moveX以及纵向距离moveY
  2. 判断手指是横向滑动还是纵向滑动,如果是横向滑动才移动容器,否则假设用户在滚动列表
  3. 判定用户在横向滑动,计算出item当前在X轴应该滑动的距离
<script>
export default{
  /*...省略之前代码...*
  methods: {
    touchmove (e) {
      let moveX = e.touches[0].pageX - this.oldPoint.pageX
      let moveY = e.touches[0].pageY - this.oldPoint.pageY
      if (Math.abs(moveX) < Math.abs(moveY) || Math.abs(moveX) < 20 || Math.abs(moveY) > 30) return

      e.preventDefault()
      moveX = this.startX * 1 + moveX * 1

      if (moveX < -this.btnsWidth) {
        moveX = -this.btnsWidth
      } else if (moveX > 0) {
        moveX = 0
      }

      this.translateX = moveX
    }
  }
  /*...省略之前代码...*
}
</script>

当手指离开屏幕,触发touchend的时候:

  1. 判定当前滑动总距离是否超过阀值
  2. 超过阀值,显示btns,否则重置item的移动
  3. 给item一个动画,关闭或者打开btns
<script>
export default{
  /*...省略之前代码...*
  methods: {
    touchend (e) {
      let moveX = -this.translateX > 30 ? -this.btnsWidth : 0
      this.speed = 300
      this.translateX = moveX
    }
  }
  /*...省略之前代码...*
}
</script>

到目前为止,咱们完成了item本身的交互逻辑,但是下面这个功能咱们目前还没有实现:

  1. 如果有已经处于打开状态的子项,需要把这个已经打开的子项关闭

那么我们应该怎么实现这个功能呢,我的初步设想是:
当用户触发滑动的时候,咱们触发父组件中的一个事件,把自组件本身传递给父组件;父组件记录当前活动的组件,如果之前活动的组件和当前活动的组件不是同一个子项,那么调用子组件自己的重置函数,关闭上一个活动子项,然后根据之前的逻辑移动现在咱们手指触摸的这个组件。代码如下:
swiperOutItem:

<script>
export default{
  /*...省略之前代码...*
  methods: {
    touchmove (e) {
      let moveX = e.touches[0].pageX - this.oldPoint.pageX
      let moveY = e.touches[0].pageY - this.oldPoint.pageY
      if (Math.abs(moveX) < Math.abs(moveY) || Math.abs(moveX) < 20 || Math.abs(moveY) > 30) return

      e.preventDefault()
      this.$parent.$emit(‘changeActiveItem‘, this)
      moveX = this.startX * 1 + moveX * 1

      if (moveX < -this.btnsWidth) {
        moveX = -this.btnsWidth
      } else if (moveX > 0) {
        moveX = 0
      }

      this.translateX = moveX
    }
  }
  /*...省略之前代码...*
}
</script>

请注意,在touchmove中咱们增加了一句代码:this.$parent.$emit(‘changeActiveItem‘, this)
swiperOut:

<script>
export default{
  data () {
    return {
      activeItem: null
    }
  },
  methods: {
    changeActiveItem (item) {
      if (this.activeItem === item) return
      if (this.activeItem && this.activeItem.close) {
        this.activeItem.close()
      }
      this.activeItem = item
    }
  },
  created () {
    this.$on(‘changeActiveItem‘, this.changeActiveItem)
  }
}
</script>

到此为止,咱们这个组件基本上已经完成了,但是还有一个问题,就是咱们应该怎么去删除我们的子项呢?这里咱们还是和上面一样,点击子项的删除按钮,触发父组件,然后在父组件中调用removeChild方法删除子项,具体实现如下:
swiperOut:

<script>
export default{
  /*...省略之前代码...*
  methods: {
    childRemove (childNode) {
      this.$refs.swiperOut.removeChild(childNode)
    }
  },
  created () {
    this.$on(‘childRemove‘, this.childRemove)
  }
  /*...省略之前代码...*
}
</script>

swiperOutItem:

<script>
export default{
  /*...省略之前代码...*
  methods: {
    delItem () {
      this.$parent.$emit(‘childRemove‘, this.$el)
    }
  }
  /*...省略之前代码...*
}
</script>

咱们在这一小节中只写了各个事件的处理函数,但是在DOM中绑定事件的代码没有写出来大家在跟着写代码的时候,千万不要忘了在DOM中绑定事件哦!!,整理最终代码如下:
swiperOut:

<template>
  <ul class="r-swiper-out" ref="swiperOut">
    <slot></slot>
  </ul>
</template>

<script>
export default{
  data () {
    return {
      activeItem: null
    }
  },
  methods: {
    changeActiveItem (item) {
      if (this.activeItem === item) return
      if (this.activeItem && this.activeItem.close) {
        this.activeItem.close()
      }
      this.activeItem = item
    },
    childRemove (childNode) {
      this.$refs.swiperOut.removeChild(childNode)
    }
  },
  created () {
    this.$on(‘changeActiveItem‘, this.changeActiveItem)
    this.$on(‘childRemove‘, this.childRemove)
  }
}
</script>

<style lang="scss">
.r-swiper-out{
    position: relative;
    width: 100%;
    overflow: hidden;
}
</style>

swiperOutItem:

<template>
  <li class="r-swiper-out-item" :style="itemStyle">
    <div class="r-swiper-out-item-content" ref="content"
    @touchstart="touchstart"
    @touchmove="touchmove"
    @touchend="touchend">
      <slot></slot>
    </div>
    <div class="r-swiper-out-item-btns" ref="btns">
      <slot name="btns">
        <div class="r-swiper-out-item-btn" @click="delItem">删除</div>
      </slot>
    </div>
  </li>
</template>

<script>
export default{
  data () {
    return {
      speed: 300,
      startX: 0,
      translateX: 0,
      oldPoint: null,
      btnsWidth: 0
    }
  },
  computed: {
    itemStyle () {
      return {
        transform: `translate3d(${this.translateX}px, 0, 0)`,
        transition: `all ${this.speed}ms`
      }
    }
  },
  methods: {
    touchstart (e) {
      this.oldPoint = e.touches[0]
      this.speed = 0
      this.startX = this.translateX
    },
    touchmove (e) {
      let moveX = e.touches[0].pageX - this.oldPoint.pageX
      let moveY = e.touches[0].pageY - this.oldPoint.pageY
      if (Math.abs(moveX) < Math.abs(moveY) || Math.abs(moveX) < 20 || Math.abs(moveY) > 30) return

      e.preventDefault()
      this.$parent.$emit(‘changeActiveItem‘, this)
      moveX = this.startX * 1 + moveX * 1

      if (moveX < -this.btnsWidth) {
        moveX = -this.btnsWidth
      } else if (moveX > 0) {
        moveX = 0
      }

      this.translateX = moveX
    },
    touchend (e) {
      let moveX = -this.translateX > 30 ? -this.btnsWidth : 0
      this.speed = 300
      this.translateX = moveX
    },
    close () {
      this.translateX = 0
    },
    delItem () {
      this.$parent.$emit(‘childRemove‘, this.$el)
    }
  },
  mounted () {
    this.$nextTick(() => {
      this.btnsWidth = this.$refs.btns.offsetWidth
    })
  }
}
</script>

<style lang="scss">
.r-swiper-out-item{
  position: relative;
  &-btns{
    display: inline-block;
        position: absolute;
        right: 0;
        top:0;
        height: 100%;
        transform: translateX(100%);
  }
  &-btn{
    background-color: red;
        color: #fff;
        width: 100px;
        text-align: center;
  }
}
</style>

写在最后

最近好像懒劲犯了,好久都没有更新博客了,谨记谨记!!!!对了,还有就是如果大家对我写的文章有不懂的,或者说希望我怎么写的更通俗易懂的,可以在评论里面评论我!争取以后的写的更能让大家懂我实现的思路。

原文地址:https://www.cnblogs.com/baimeishaoxia/p/11962937.html

时间: 2024-10-08 00:40:56

干货--手把手撸vue移动UI框架: 滑动删除的相关文章

基于Vue的Ui框架

饿了么公司基于vue开的的vue的Ui组件库 Element Ui 基于vue pc端的UI框架 http://element.eleme.io/ MintUi 基于vue 移动端的ui框架 http://mint-ui.github.io/#!/en mintUI的使用: 1.找官网 2.安装 npm install mint-ui -S -S表示 --save 3.引入mint Ui的css 和 插件 import Mint from 'mint-ui'; Vue.use(Mint); im

常用vue前端UI框架

Element(Star-43.7k) 网站快速成型工具 Element,一套为开发者.设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库 官网地址 https://element.eleme.cn/#/zh-CN iview(Star-23k) View UI,即原先的 iView,是一套基于 Vue.js 的开源 UI 组件库,主要服务于 PC 界面的中后台产品. 官网地址 https://iviewui.com/ vuetify(Star-23.7k) 一个为 Vue JS 2.0

vue项目中使用了vw适配方案,引入第三方ui框架mint-ui时,适配问题解决

问题分析: 一般第三方ui框架用的都是不同的适配方式,如果我们使用了vw适配,那么在使用mint-ui框架时,就会发现px单位会被转换成vw,从而导致样式变小的问题,如图 解决方案 网上看到了很多种解决方案,这里推荐第四种 1.重写第三方组件ui样式大小 2.在postcss.config.js中的selectorBlackList选项中增加不需要vw转换的类名 selectorBlackList: ['.ignore', '.hairlines'], // (Array) The select

目前流行前端几大UI框架 ----vue Vue的UI组件库

在前端项目开发过程中,总是会引入一些UI框架,已为方便自己的使用,很多大公司都有自己的一套UI框架,下面就是最近经常使用并且很流行的UI框架. 一.Mint UI 屏幕快照 2019-01-18 下午3.03.59.png Mint UI是 饿了么团队开发基于vue .js的移动端UI框架,它包含丰富的 CSS 和 JS 组件,能够满足日常的移动端开发需要. 官网:https://mint-ui.github.io/#!/zh-cn Github: https://github.com/Elem

优秀的基于VUE移动端UI框架合集

1. vonic 一个基于 vue.js 和 ionic 样式的 UI 框架,用于快速构建移动端单页应用,很简约,是我喜欢的风格 star 2.3k 中文文档 在线预览 2.vux 基于WeUI和Vue(2.x)开发的移动端UI组件库 star 10k 基于webpack+vue-loader+vux可以快速开发移动端页面,配合vux-loader方便你在WeUI的基础上定制需要的样式. 中文文档 在线预览 3.Mint UI 由饿了么前端团队推出的 Mint UI 是一个基于 Vue.js 的

VUE PC端框架和移动端UI框架(收集)

在学习Vue的过程之中,我发现不管是 BAT 大厂,还是创业公司,Vue 都有着广泛的应用,而且框架层出不穷,学习文档也越来越多,Vue也越来越受欢迎.下面是我整理的 Vue PC端和移动端的UI框架,建议收藏,方便以后学习的时候拿出来查看. Vue PC端框架 1. Element 中文文档:http://element-cn.eleme.io/#/zh-CN github地址:https://github.com/ElemeFE/element 2. iView 中文文档:https://w

爱创课堂----推荐你们几个精致的前端Web UI框架 | 干货

1.Aliceui Aliceui是支付宝的样式解决方案,是一套精选的基于 spm 生态圈的样式模块集合,是 Arale 的子集,也是一套模块化的样式命名和组织规范,是写 CSS 的更好方式. 2.Amazeui Amaze UI 是一个轻量级. Mobile first 的前端框架, 基于开源社区流行前端框架编写的. 3.sui SUI是一套基于bootstrap开发的前端组件库,同时她也是一套设计规范. 通过SUI,可以非常方便的设计和实现精美的页面. 同时sui还有移动端版本msui,ms

Android酷炫实用的开源框架(UI框架)

前言 忙碌的工作终于可以停息一段时间了,最近突然有一个想法,就是自己写一个app,所以找了一些合适开源控件,这样更加省时,再此分享给大家,希望能对大家有帮助,此博文介绍的都是UI上面的框架,接下来会有其他的开源框架(如:HTTP框架.DB框架). 1.Side-Menu.Android分类侧滑菜单,Yalantis 出品.项目地址:https://github.com/Yalantis/Side-Menu.Android2.Context-Menu.Android可以方便快速集成漂亮带有动画效果

Chisel辅助iOS 应用程序调试,MusicApp模仿酷狗4.0 UI框架

本文转载至 http://www.cocoachina.com/ios/20140825/9446.html Chisel Chisel集合了大量的LLDB 命令来辅助iOS 应用程序调试,并支持添加本地和自定义的命令.以下是其中所包含的一些命令,并对其适用于iOS还是OS X进行了区分: M13ProgressSuite 该项目包含了多种不同的风格的进程指示图,比如普通圆环形.分段圆形加载.圆形饼图加载以及条形加载等等,比如其中UINavigationBar的进程动画非常像苹果的Messag