The implementation of netease Cloud Music Horizontal menu in Vue

  • Vue
  • better-scroll
  • Horizontal menu Top: 100 Copyright: true

Before at the time of learning just dig in a little bit about the netease cloud music, mainly in order to study the Vue, consolidate the basic knowledge, and then see the horizontal menus, of course the individual also like to watch, so each time you look at tencent, the NBA will always think this is true, then with the help of before haven’t finished the demo, the realization of the complete the horizontal menus, nonsense not much said, Let’s start with the renderings

From the experience of using Tiger Teeth to broadcast the horizontal menu, the business logic of our horizontal menu is as follows:

  1. Slide menu and select menu item;
  2. Select a menu item that is centered (removes bounding menu items)

We use the better-Scroll plugin to achieve this, please refer to BetterScroll for installation

Front-end DOM structure

<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>
Copy the code

Debug the project by using the plug-in Vue

The tabs contains the menu item name and its route

data () {
    return {
      tabs: [
        {
          to: '/mv/recommend-mv',
          title: 'recommendations'
        },
        {
          to: '/mv/music-mv',
          title: 'music'
        },
        {
          to: 'show-mv',
          title: 'Show'
        },
        {
          to: '/mv/acg-mv',
          title: 'quadratic'
        },
        {
          to: '/mv/dance-mv',
          title: 'dance'
        },
        {
          to: '/mv/game-mv',
          title: 'games'
        },
        {
          to: '/mv/mvs',
          title: 'mv'}], mX: 0, // tabWidth: 80 // width of each TAB}Copy the code

style

.mv-tabs position relative top-5.5 REM bottom 0 width 100%. Tabs margin-top 3REM height 2.5 REM width 100% line-height 2.5 REM box-sizing border-box overflow Hidden white-space nowrap.tab-itemfloat 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
Copy the code

I’m not going to go into style and DOM structure, but I’m going to implement it because I need to figure out the width of everything in this menu, which is the container that wraps this menu; Then initialize the better-scroll and ignore the vertical slide of the instance object.

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'// Ignore the vertical slide event of this instance object})}else {
          this.scroll.refresh()
        }
      })
    }
  }
Copy the code

Here is the idea of the second business logic (there should be a better idea, ask the boss for advice)

The way I think about it is that each menu item has its own click-and-move action, so I move the tabs to the corresponding position by clicking on the event. For example, the middle menu item moves to the middle position when clicked.

methods: {
    selectItem (index) {
      let tabs = this.$refs.tab
      let moveX = +tabs.style.transform.replace(/[^0-9\-,]/g, ' ').split(', ')[0]
      switch (index) {
        caseZero: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)`
    }
  }
Copy the code

Most of the time when we use better scroll, we will find that the instance object has been initialized, but can not slide, because Vue updates data asynchronously, so we need to asynchronously calculate the width or height of its actual content, Vue provides a hock to implement this.$nextTick(). This API performs deferred callbacks after the next DOM update loop ends. Use this method immediately after modifying the data to get the updated DOM.

Official explanation: $nextTick

When health hook Mounted is triggered, better Scroll is initialized

mounted () {
    this.$nextTick(() => {
      this._initMenu()
    })
}
Copy the code

All the code

<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: 'recommendations'
        },
        {
          to: '/mv/music-mv',
          title: 'music'
        },
        {
          to: 'show-mv',
          title: 'Show'
        },
        {
          to: '/mv/acg-mv',
          title: 'quadratic'
        },
        {
          to: '/mv/dance-mv',
          title: 'dance'
        },
        {
          to: '/mv/game-mv',
          title: 'games'
        },
        {
          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) {
        caseZero: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.5 REM bottom 0 width 100%. Tabs margin-top 3rem height 2.5 REM width 100% Word-height 2.5 REM box-sizing border-box overflow hidden white-space nowrap.tab-itemfloat 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>
Copy the code

zhihu

Personal blog

Github