视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
vue移动端UI框架实现QQ侧边菜单组件
2020-11-27 22:17:43 责编:小采
文档


最近面试发现很多前端程序员都从来没有写过插件的经验,基本上都是网上百度。所以打算写一系列文章,手把手的教一些没有写过组件的兄弟们如何去写插件。本系列文章都基于VUE,核心内容都一样,会了之后大家可以快速的改写成react、angular或者是小程序等组件。这篇文章是第一篇,写的是一个类似QQ的侧边菜单组件。

效果展示

先让大家看个效果展示,知道咱们要做的东西是个怎么样的样子,图片有点模糊,大家先将就点:

开始制作

DOM结构

整体结构中应该存在两个容器:1. 菜单容器 2. 主页面容器;因此当前DOM结构如下:

<template>
 <div class="r-slide-menu">
 <div class="r-slide-menu-wrap"></div>
 <div class="r-slide-menu-content"></div>
 </div>
</template>

为了使得菜单内容和主题内容能够定制,我们再给两个容器中加入两个slot插槽:默认插槽中放置主体内容、菜单放置到menu插槽内:

<template>
 <div class="r-slide-menu">
 <div class="r-slide-menu-wrap">
 <slot name="menu"></slot>
 </div>
 <div class="r-slide-menu-content">
 <slot></slot>
 </div>
 </div>
</template>

css样式

我项目中使用了scss,代码如下:

<style lang="scss">
@mixin one-screen {
 position: absolute;
 left:0;
 top:0;
 width:100%;
 height:100%;
 overflow: hidden;
}

.r-slide-menu{
 @include one-screen;
 &-wrap, &-content{
 @include one-screen;
 }
 &-transition{
 -webkit-transition: transform .3s;
 transition: transform .3s;
 }
}
</style>

此时我们就得到了两个绝对定位的容器

javascript

现在开始正式的代码编写了,首先我们理清下交互逻辑:

  • 手指左右滑动的时候主体容器和菜单容器都跟着手指运动运动
  • 当手指移动的距离超过菜单容器宽度的时候页面不能继续向右滑动
  • 当手指向左移动使得菜单和页面的移动距离归零的时候页面不能继续向左移动
  • 当手指释放离开屏幕的时候,页面滑动如果超过一定的距离(整个菜单宽度的比例)则打开整个菜单,如果小于一定距离则关闭菜单
  • 所以现在咱们需要在使用组件的时候能够入参定制菜单宽度以及触发菜单收起关闭的临界值和菜单宽度的比例,同时需要给主体容器添加touch事件,最后我们给菜单容器和主体容器添加各自添加一个控制他们运动的style,通过控制这个style来控制容器的移动

    <template>
     <div class="r-slide-menu">
     <div class="r-slide-menu-wrap" :style="wrapStyle">
     <slot name="menu"></slot>
     </div>
     <div class="r-slide-menu-content" :style="contentStyle"
     @touchstart="touchstart"
     @touchmove="touchmove"
     @touchend="touchend">
     <slot></slot>
     </div>
     </div>
    </template>
    <script>
    export default {
     props: {
     width: {
     type: String,
     default: '250'
     },
     ratio: {
     type: Number,
     default: 2
     }
     },
     data () {
     return {
     isMoving: false,
     transitionClass: '',
     startPoint: {
     X: 0,
     y: 0
     },
     oldPoint: {
     x: 0,
     y: 0
     },
     move: {
     x: 0,
     y: 0
     }
     }
     },
     computed: {
     wrapStyle () {
     let style = {
     width: `${this.width}px`,
     left: `-${this.width / this.ratio}px`,
     transform: `translate3d(${this.move.x / this.ratio}px, 0px, 0px)`
     }
     return style
     },
     contentStyle () {
     let style = {
     transform: `translate3d(${this.move.x}px, 0px, 0px)`
     }
     return style
     }
     },
     methods: {
     touchstart (e) {},
     touchmove (e) {},
     touchend (e) {}
     }
    }

    接下来,我们来实现我们最核心的touch事件处理函数,事件的逻辑如下:

    1. 手指按下瞬间,记录下当前手指所触摸的点,以及当前主容器的位置
    2. 手指移动的时候,获取到移动的点的位置
    3. 计算当前手指所在点移动的X、Y轴距离,如果X移动的距离大于Y移动的距离则判定为横向运动,否则为竖向运动
    4. 如果横向运动则判断当前移动的距离是在合理的移动区间(0到菜单宽度)移动,如果是则改变两个容器的位置(移动过程中阻止页面中其他的事件触发)
    5. 手指离开屏幕:如果累计移动距离超过临界值则运用动画打开菜单,否则关闭菜单
    touchstart (e) {
     this.oldPoint.x = e.touches[0].pageX
     this.oldPoint.y = e.touches[0].pageY
     this.startPoint.x = this.move.x
     this.startPoint.y = this.move.y
     this.setTransition()
    },
    touchmove (e) {
     let newPoint = {
     x: e.touches[0].pageX,
     y: e.touches[0].pageY
     }
     let moveX = newPoint.x - this.oldPoint.x
     let moveY = newPoint.y - this.oldPoint.y
     if (Math.abs(moveX) < Math.abs(moveY)) return false
     e.preventDefault()
     this.isMoving = true
     moveX = this.startPoint.x * 1 + moveX * 1
     moveY = this.startPoint.y * 1 + moveY * 1
     if (moveX >= this.width) {
     this.move.x = this.width
     } else if (moveX <= 0) {
     this.move.x = 0
     } else {
     this.move.x = moveX
     }
    },
    touchend (e) {
     this.setTransition(true)
     this.isMoving = false
     this.move.x = (this.move.x > this.width / this.ratio) ? this.width : 0
    },
    setTransition (isTransition = false) {
     this.transitionClass = isTransition ? 'r-slide-menu-transition' : ''
    }

    上面,这段核心代码中有一个setTransition 函数,这个函数的作用是在手指离开的时候给容器元素添加transition属性,让容器有一个过渡动画,完成关闭或者打开动画;所以在手指按下去的瞬间需要把容器上的这个transition属性去除,避免滑动过程中出现容器和手指滑动延迟的不良体验。 最后提醒下,代码中使用translate3d而非translate的原因是为了启动移动端手机的动画3D加速,提升动画流畅度。最终代码如下:

    <template>
     <div class="r-slide-menu">
     <div class="r-slide-menu-wrap" :class="transitionClass" :style="wrapStyle">
     <slot name="menu"></slot>
     </div>
     <div class="r-slide-menu-content" :class="transitionClass" :style="contentStyle"
     @touchstart="touchstart"
     @touchmove="touchmove"
     @touchend="touchend">
     <slot></slot>
     </div>
     </div>
    </template>
    <script>
    export default {
     props: {
     width: {
     type: String,
     default: '250'
     },
     ratio: {
     type: Number,
     default: 2
     }
     },
     data () {
     return {
     isMoving: false,
     transitionClass: '',
     startPoint: {
     X: 0,
     y: 0
     },
     oldPoint: {
     x: 0,
     y: 0
     },
     move: {
     x: 0,
     y: 0
     }
     }
     },
     computed: {
     wrapStyle () {
     let style = {
     width: `${this.width}px`,
     left: `-${this.width / this.ratio}px`,
     transform: `translate3d(${this.move.x / this.ratio}px, 0px, 0px)`
     }
     return style
     },
     contentStyle () {
     let style = {
     transform: `translate3d(${this.move.x}px, 0px, 0px)`
     }
     return style
     }
     },
     methods: {
     touchstart (e) {
     this.oldPoint.x = e.touches[0].pageX
     this.oldPoint.y = e.touches[0].pageY
     this.startPoint.x = this.move.x
     this.startPoint.y = this.move.y
     this.setTransition()
     },
     touchmove (e) {
     let newPoint = {
     x: e.touches[0].pageX,
     y: e.touches[0].pageY
     }
     let moveX = newPoint.x - this.oldPoint.x
     let moveY = newPoint.y - this.oldPoint.y
     if (Math.abs(moveX) < Math.abs(moveY)) return false
     e.preventDefault()
     this.isMoving = true
     moveX = this.startPoint.x * 1 + moveX * 1
     moveY = this.startPoint.y * 1 + moveY * 1
     if (moveX >= this.width) {
     this.move.x = this.width
     } else if (moveX <= 0) {
     this.move.x = 0
     } else {
     this.move.x = moveX
     }
     },
     touchend (e) {
     this.setTransition(true)
     this.isMoving = false
     this.move.x = (this.move.x > this.width / this.ratio) ? this.width : 0
     },
     // 点击切换
     switch () {
     this.setTransition(true)
     this.move.x = (this.move.x === 0) ? this.width : 0
     },
     setTransition (isTransition = false) {
     this.transitionClass = isTransition ? 'r-slide-menu-transition' : ''
     }
     }
    }
    </script>
    <style lang="scss">
    @mixin one-screen {
     position: absolute;
     left:0;
     top:0;
     width:100%;
     height:100%;
     overflow: hidden;
    }
    .r-slide-menu{
     @include one-screen;
     &-wrap, &-content{
     @include one-screen;
     }
     &-transition{
     -webkit-transition: transform .3s;
     transition: transform .3s;
     }
    }
    </style>

    总结

    以上所述是小编给大家介绍的vue移动端UI框架实现QQ侧边菜单组件,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对脚本之家网站的支持!

    下载本文
    显示全文
    专题