Bootstrap

使用elementUI的el-tag实现菜单tab标签,实现右键菜单 关闭当前,关闭其他、关闭全部以及超出长度是隐藏并可左右滑动功能。

1. 效果展示

效果展示

2. 功能分析

2.1 点击左侧菜单时添加标签

标签要以数组形式存在vuex里进行管理。数组里每一个对象里面都有三个属性,如下:

tagList: [
	{
		path: '/home',
		name: 'home',
		label: '首页'
	}
]

添加时我们要判断该标签是否存在,不存在就push进tagList数组。
首页是默认存在的!不可删除也不可添加。

2.2 标签右键菜单的刷新功能

this.$router.go(0) 或者 window.reload()

2.3 标签右键菜单的关闭功能

这个功能是关闭单个标签,所以我们只需获取到当前右键点击标签的索引即可,使用数组的splice方法即可实现删除,Array.splice(index, 1)

2.4 标签右键菜单的关闭其他功能

此功能在标签数组长度tagList.length ≤ 2时不生效。并且要做多种情况判断。

  • 一般情况:
    假设此时数组长度为5,我们点击的第3个标签的关闭全部,那么执行完成后标签数组就会剩下 首页第三个。具体逻辑是,假设index是第3个标签的索引,先执行splice(1, index - 1),再执行 splice(index, tagList.length)。要执行两次splice才有效果,这里需要细品~
  • 点击的第2个标签的关闭全部:
    这个时候要加一个判断,当点击标签的索引index为1时,执行splice(index + 1, tagList.length),并且return 不执行后面的一般情况。

2.5 标签右键菜单的关闭所有功能

从数组的第2个开始删就完事了。执行 splice(1, tagList.length)

2.6 监听存放标签的节点长度,超出容器宽度时出现箭头,实现左右滑动

要实现试试监听节点的宽度,可以用到 element-resize-detector 这个插件
npm install element-resize-detector --save-dev
定义一个arrowVisible属性,通过它来控制显示,默认值为false

3. 上代码

3.1 dom结构

  • 文件 MentTag.vue
 <div class="tags" @contextmenu.prevent="" ref="tags" :style="{'padding': arrowVisible ? '12px 0' : '12px 10px'}">
    <div class="tags-out-box" ref="outBox">
      <div class="svg arrow" v-if="arrowVisible" @click="handleClickToLeft">
          <svg-icon icon-class='arrow-left'/>
        </div>
      <div class="tags-box" ref="box" :style="{'padding-left': arrowVisible ? '30px' : '0', left: `${left}px`}">
        <el-tag
          ref="tag"
          size="small"
          v-for="(tag, i) in tags"
          :data-index='i'
          :data-id='tag.menuId'
          :key="tag.name"
          :closable="tag.name !== 'home'"
          :effect="$route.name === tag.name ? 'dark' : 'plain'"
          @contextmenu.native.prevent="handleClickContextMenu($event)"
          @click="handleTagClick($event, tag)"
          @close="handleTagClose(tag, i)"
          >
          {{tag.label}}
        </el-tag>
      </div>
      <div class="svg arrow arrow-right" v-if="arrowVisible" @click="handleClickToRight">
          <svg-icon icon-class='arrow-right'/>
        </div>
    </div>

    <ul class="right-menu" :style="{left: menuLeft, top: menuTop}" v-show="contextMenuVisible">
      <a href="javascript:;" @click="refresh">刷新</a>
      <a href="javascript:;" @click="closeTag" v-if="tagIndex !== 0">关闭</a>
      <a href="javascript:;" @click="closeOtherTag" v-if="tagIndex !== 0">关闭其它</a>
      <a href="javascript:;" @click="closeAllTag">关闭所有</a>
    </ul>
  </div>
// tag组件样式
.tags{
  width: 100%;
  white-space: nowrap;
  background-color: $white-color;
  padding: 12px 10px;
  &-out-box{
    display: inline-block;
    position: relative;
  }
  &-box{
    position: relative;
    transition: .3s;
  }
}
  • refoutBox的节点包含箭头tag内容区
  • refbox的节点只包含tag内容区
  • 我项目图片/图标使用的是svg,所以自定义了个svg-icon组件。你可以用font-awesome或者其他图标。

3.2 数据定义

data () {
	 contextMenuVisible: false, // 是否显示菜单
     menuLeft: '', // 右键菜单距离浏览器左边的距离
     menuTop: '', // 右键菜单距离浏览器上边的距离
     tagIndex: 0, // 当前点击的tag的索引
     tag: {}, // 当前右键点击的tag对象
     arrowVisible: false, // 是否显示箭头
     tagsBoxWidth: 0, // ref 为 outBox 的长度
     tagsWidth: 0, // ref 为 tags 的长度
     left: 0, // ref 为 box 节点相对于左边的距离,用于箭头点击
}
  • 获取vuex里的tagList
computed: {
  ...mapState({
    tags: state => state.menuTag.tagList
  })
}

3.3 右键点击出现菜单

  • el-tag上绑定事件@contextmenu.native.prevent="handleClickContextMenu($event)"
handleClickContextMenu (event) {
  const e = event || window.event
  const target = e.target
  this.menuLeft = e.layerX + 20 + 'px' // 菜单出现的位置距离左侧的距离
  this.menuTop = e.layerY + 20 + 'px' // 菜单出现的位置距离顶部的距离
  this.tagIndex = Number(target.getAttribute('data-index')) // 获取当前右击菜单的索引。从0开始
  this.contextMenuVisible = true // 显示菜单

  this.tag = this.tags[this.tagIndex] // 当前右击的菜单信息
}
  • 全局绑定点击事件 可关闭右键菜单
window.addEventListener('click', (e) => {
  const target = e.target
  if (target.nodeName !== 'SPAN') { // 只要点击的不是el-tag就可以关闭,因为el-tag是用span标签实现的
    this.contextMenuVisible = false
  }
})

3.4 vuex里存/删标签

const mutations = {
  [types.SET_TAG] (state, payload) {
    if (payload.name === 'xxx') { // 这里可以用来过滤页面
      return
    }

    if (payload.name !== 'home') {
      const result = state.tagList.findIndex(item => item.name === payload.name)
      if (result === -1) state.tagList.push(payload)
    }
  },
  [types.CLOSE_TAG] (state, payload) {
    /**
     * payload 参数说明
     * index - 当前点击的tag的索引 也就是从tagList数组的第几个开始删除
     * delCount - 要删除的个数。点击关闭时默认为1,关闭全部时为tagList.length - 1
     */
    state.tagList.splice(payload.index, payload.delCount)
  }
}

const actions = {
  setTag: ({ commit }, payload) => {
    commit(types.SET_TAG, payload)
  },
  closeTag: ({ commit }, payload) => {
    commit(types.CLOSE_TAG, payload)
  }
}

3.5 路由跳转时存标签

  • router-view上绑定key 这样可以在菜单切换(路由跳转时)拿到路由信息
  • 要在路由的index.js里给每个路由定义meta属性,具体如下:
{
  path: '/home', // 首页
  name: 'home',
  component: () => import('@/views/home/Home'),
  meta: { label: '首页' }
}
  • 存标签
computed: {
	key () {
	  const tag = {}
	  tag.label = this.$route.meta.label
	  tag.name = this.$route.name
	  tag.path = this.$route.path
	  this.$store.dispatch('menuTag/setTag', tag)
	  return this.$route.path
	}
}

3.6 关闭标签的公共方法

/**
* 参数说明
* tag 当前右击的标签所存的对象
* index 当前右击标签的索引
* delCount 删除个数,默认删除1个
* isCloseOther 是否删除其他 默认不是
*/
handleTagClose (tag, index, delCount = 1, isCloseOther = false) {
  const length = this.tags.length - 1
  const payload = {
    index,
    delCount,
    isCloseOther
  }

  // 执行存标签
  this.$store.dispatch('menuTag/closeTag', payload)

  // 点击关闭其他,并且不是当前所在的页面
  if (tag.name !== this.$route.name && isCloseOther) {
    this.$router.push({ name: tag.name })
    return
  }
	
  // 右击的标签正是当前打开的页面并且不是最后一个
  if (tag.name === this.$route.name && index < this.tags.length - 1) {
    this.$router.push({ name: this.tags[index + 1].name })
  }

  // 关闭的标签是最右边的话,往左边跳转一个
  if (index === length && !isCloseOther) {
    this.$router.push({ name: this.tags[index - 1].name })
    return
  }
  if (index <= length && !isCloseOther) {
    // 否则往右边跳转
    if (index !== 1) this.$router.push({ name: this.tags[index].name })
  }
}

3.6.1 点击关闭

closeTag () {
  if (this.tags.length === 1) return
  this.handleTagClose(this.tag, this.tagIndex)
}

3.6.2 点击关闭其他

closeOtherTag () {
  if (this.tags.length === 2) return
  if (this.tagIndex === 1) {
    this.handleTagClose(this.tag, this.tagIndex + 1, this.tags.length, true)
    return
  }

  this.handleTagClose(this.tag, 1, this.tagIndex - 1, true)
  this.handleTagClose(this.tag, this.tagIndex, this.tags.length, true)
}

3.6.3 点击关闭全部

closeAllTag () {
  if (this.tags.length === 1) return
  this.handleTagClose(this.tag, 1, this.tags.length)
  this.$router.push({ name: 'home' })
}

4 超出长度时出现箭头可左右移动

  • 监听是否可移动
import elementResizeDetectorMaker from 'element-resize-detector'

mounted () {
 	this.len = this.tags.length
    this.tagsWidth = this.$refs.tags.offsetWidth
    const erd = elementResizeDetectorMaker()
    erd.listenTo(this.$refs.outBox, ele => {
      this.$nextTick(() => {      
        this.tagsBoxWidth = ele.offsetWidth
        if (this.tagsBoxWidth >= this.tagsWidth) {
          this.arrowVisible = true
        } else {
          this.arrowVisible = false
        }
      })
    })
}
  • 向左移动
handleClickToLeft () {
  this.left = 0
}
  • 向右移动
handleClickToLeft () {
  this.left = this.tagsWidth - this.tagsBoxWidth - 30
}

这个移动比较粗糙,直接是移到最右或最左

5 结语

这玩意儿还是踩了很多坑,改了很多bug的。害 不容易呀。如果对你有帮助,希望各位能点赞、收藏们谢谢!

;