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;
}
}
ref
为outBox
的节点包含箭头和tag内容区ref
为box
的节点只包含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的。害 不容易呀。如果对你有帮助,希望各位能点赞、收藏们谢谢!