Vue之网易云音乐横向菜单的实现

https://rain120.github.io/2018/06/30/horizontal-menu/

之前在学习的时候有稍微捣鼓一下网易云音乐,主要是为了学习Vue,巩固基础知识,然后看到这个横向菜单,当然个人也喜欢看球,所以每次看腾讯NBA的时候总是会想这个是这样实现的,于是借助之前还没写完的demo,完成这个横向菜单的实现,废话不多说,先上效果图

Vue之网易云音乐横向菜单的实现

从使用虎牙直播横向菜单的体验得到,我们的横向菜单的业务逻辑如下:

  1. 滑动菜单,并选择菜单项;
  2. 选择某个菜单项,该菜单项居中(去除边界菜单项)

我们的使用的better-scroll这个插件来实现,具体安装请参考BetterScroll

前端DOM结构

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
  <div class="mv-tabs">
    <div class="tabs" ref="tabsWrapper">
      <ul ref="tab">
        <li v-for="(item, index) in tabs" :key="index" @click="selectItem(index)">
          <router-link tag="div" :to="item.to" class="tab-item">
            <span class="tab-link">{{item.title}}</span>
          </router-link>
        </li>
      </ul>
    </div>
  </div>
</template>

 

通过使用插件Vue来调试项目
Vue之网易云音乐横向菜单的实现

其中tabs包括菜单项名和它的路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
data () {
    return {
      tabs: [
        {
          to: '/mv/recommend-mv',
          title: '推荐'
        },
        {
          to: '/mv/music-mv',
          title: '音乐'
        },
        {
          to: 'show-mv',
          title: 'Show'
        },
        {
          to: '/mv/acg-mv',
          title: '二次元'
        },
        {
          to: '/mv/dance-mv',
          title: '舞蹈'
        },
        {
          to: '/mv/game-mv',
          title: '游戏'
        },
        {
          to: '/mv/mvs',
          title: 'mv'
        }
      ],
      mX: 0, // tab移动的距离
      tabWidth: 80 // 每个tab的宽度
    }

 

样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
.mv-tabs
    position relative
    top -5.5rem
    bottom 0
    width 100%
    .tabs
      margin-top 3rem
      height 2.5rem
      width 100%
      line-height 2.5rem
      box-sizing border-box
      overflow hidden
      white-space nowrap
      .tab-item
        float left
        width 80px
        height 40px
        text-align center
        .tab-link
          padding-bottom 5px
          color #333333
        &.router-link-active
          color #d33a31
          border-bottom 2px solid #d33a31
          box-sizing border-box

 

样式和DOM结构就不详细讲了,具体讲实现吧
首先需要计算出这个菜单中所有内容的width,也就是包裹这个菜单的容器;接着初始化better-scroll,并忽略该实例对象的垂直方向的滑动.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
methods: {
    _initMenu () {
      let tabsWidth = this.tabWidth
      let width = this.tabs.length * tabsWidth
      this.$refs.tab.style.width = `${width}px`
      this.$nextTick(() => {
        if (!this.scroll) {
          this.scroll = new BScroll(this.$refs.tabsWrapper, {
            scrollX: true,
            eventPassthrough: 'vertical' // 忽略这个实例对象的垂直滑动事件
          })
        } else {
          this.scroll.refresh()
        }
      })
    }
  }

 

这里是第二个业务逻辑的思路(应该会有更好的思路,求大佬指点)

我的思路是这样的:每一个菜单项都会有各自的点击移动操作,所以我是根据当前tabs的位置,通过点击事件将tabs移动到它相应的位置,例如,中间菜单项在点击时会移动到居中的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
methods: {
    selectItem (index) {
      let tabs = this.$refs.tab
      let moveX = +tabs.style.transform.replace(/[^0-9\-,]/g, '').split(',')[0]
      switch (index) {
        case 0:
          if (moveX <= 0 && moveX > -this.tabWidth) {
            this.mX = 0
          }
          break
        case 1:
          if (moveX <= 0 && moveX > -this.tabWidth * 2) {
            this.mX = 0
          }
          break
        case 2:
          if (moveX < 0 && moveX >= -this.tabWidth * 2) {
            this.mX = 0
          }
          break
        case 3:
          if (moveX <= 0 && moveX >= -this.tabWidth * 2) {
            this.mX = -this.tabWidth
          }
          break
        case 4:
          if (moveX <= 0 && moveX >= -this.tabWidth * 2) {
            this.mX = -this.tabWidth * 2
          } else if (moveX === 0) {
            this.mX = -this.tabWidth * 2
          }
          break
        case 5:
          if (moveX < 0 && moveX > -this.tabWidth * 2) {
            this.mX = -this.tabWidth * 2
          }
          break
        case 6:
          if (moveX > -this.tabWidth * 2 && moveX < -this.tabWidth * 3 / 2) {
            this.mX = -this.tabWidth * 2 + 10
          }
          break
        default:
          break
      }
      tabs.style.transform = `translate(${this.mX}px, 0)`
    }
  }

 

Vue之网易云音乐横向菜单的实现

很多时候我们在使用better-scroll的时候,发现这个实例对象已经初始化,但是不能滑动,是因为,Vue是异步更新数据的,所以我们需要异步计算它实际内容的宽度或者高度,Vue提供一个了this.$nextTick()这个hock来实现,这个API是在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

官方解释:$nextTick

当生命钩子mounted触发时,初始化better-scroll

1
2
3
4
5
mounted () {
    this.$nextTick(() => {
      this._initMenu()
    })
}

 

全部代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
<template>
  <div class="mv-tabs">
    <div class="tabs" ref="tabsWrapper">
      <ul ref="tab">
        <li v-for="(item, index) in tabs" :key="index" @click="selectItem(index)">
          <router-link tag="div" :to="item.to" class="tab-item">
            <span class="tab-link">{{item.title}}</span>
          </router-link>
        </li>
      </ul>
    </div>
  </div>
</template>

<script>
import BScroll from 'better-scroll'

export default {
  data () {
    return {
      tabs: [
        {
          to: '/mv/recommend-mv',
          title: '推荐'
        },
        {
          to: '/mv/music-mv',
          title: '音乐'
        },
        {
          to: 'show-mv',
          title: 'Show'
        },
        {
          to: '/mv/acg-mv',
          title: '二次元'
        },
        {
          to: '/mv/dance-mv',
          title: '舞蹈'
        },
        {
          to: '/mv/game-mv',
          title: '游戏'
        },
        {
          to: '/mv/mvs',
          title: 'mv'
        }
      ],
      mX: 0,
      tabWidth: 80
    }
  },
  mounted () {
    this.$nextTick(() => {
      this._initMenu()
    })
  },
  methods: {
    selectItem (index) {
      let tabs = this.$refs.tab
      let moveX = +tabs.style.transform.replace(/[^0-9\-,]/g, '').split(',')[0]
      switch (index) {
        case 0:
          if (moveX <= 0 && moveX > -this.tabWidth) {
            this.mX = 0
          }
          break
        case 1:
          if (moveX <= 0 && moveX > -this.tabWidth * 2) {
            this.mX = 0
          }
          break
        case 2:
          if (moveX < 0 && moveX >= -this.tabWidth * 2) {
            this.mX = 0
          }
          break
        case 3:
          if (moveX <= 0 && moveX >= -this.tabWidth * 2) {
            this.mX = -this.tabWidth
          }
          break
        case 4:
          if (moveX <= 0 && moveX >= -this.tabWidth * 2) {
            this.mX = -this.tabWidth * 2
          } else if (moveX === 0) {
            this.mX = -this.tabWidth * 2
          }
          break
        case 5:
          if (moveX < 0 && moveX > -this.tabWidth * 2) {
            this.mX = -this.tabWidth * 2
          }
          break
        case 6:
          if (moveX > -this.tabWidth * 2 && moveX < -this.tabWidth * 3 / 2) {
            this.mX = -this.tabWidth * 2 + 10
          }
          break
        default:
          break
      }
      tabs.style.transform = `translate(${this.mX}px, 0)`
    },
    _initMenu () {
      let tabsWidth = this.tabWidth
      let width = this.tabs.length * tabsWidth
      this.$refs.tab.style.width = `${width}px`
      this.$nextTick(() => {
        if (!this.scroll) {
          this.scroll = new BScroll(this.$refs.tabsWrapper, {
            scrollX: true,
            eventPassthrough: 'vertical'
          })
        } else {
          this.scroll.refresh()
        }
      })
    }
  }
}
</script>

<style lang="stylus" scoped>
  .mv-tabs
    position relative
    top -5.5rem
    bottom 0
    width 100%
    .tabs
      margin-top 3rem
      height 2.5rem
      width 100%
      line-height 2.5rem
      box-sizing border-box
      overflow hidden
      white-space nowrap
      .tab-item
        float left
        width 80px
        height 40px
        text-align center
        .tab-link
          padding-bottom 5px
          color #333333
        &.router-link-active
          color #d33a31
          border-bottom 2px solid #d33a31
          box-sizing border-box
</style>

 

Vue better-scroll 网易云音乐 横向菜单