实现效果如下:
一、点击左边菜单,实现右边tabs列表的加载
-
我们已经将左边菜单栏跟tabs列表分别单独抽成组件。这里暂且将左边叫做
菜单组件(nav-menu.vue)
。右边顶部tabs这是又是一个组件我们暂且叫tabs组件(tags.vue)
很明显这两个兄弟组件是有交互的,点击菜单的时候要加载tabs列表。实现原理如下:
-
方式1
1.左侧菜单组件中每次点击的时候使用调用了tabs组件的方法,给tabs组件里面
tagsList
数组赋值。
2.模板遍历tagsList生成tab
其实这种方式有点小问题,耦合性有点高,涉及到了兄弟组件直接的交互.所以我们接下来看下方式2
-
方式2:原理如下
1、点击菜单组件的时候会跳转路由
2、我们在tabs组件里面其实可以监控路由变化,然后再做tabsList赋值操作等。这样就实现了两个组件间的解耦。代码后面会贴出来。
二、 实现右键弹出菜单,必须先要禁用浏览器默的右键菜单弹出,
利用contextmenu
事件阻止浏览器原生的菜单栏出现,然后绑定我们的显示菜单栏的自定义事件。在vue3中使用修饰符即可实现@contextmenu.prevent="setSelectedTag(item,$event)"
-
第一种右键弹出菜单我们自己写,不借助插件,稍微麻烦
我们先来看看tabs组件模板里面的内容
<template>
<ul v-show="contextMenuVisible" class="right-menu" :style="{left: menuLeft, top: menuTop}" >
<li>
<a href="javascript:;" @click="refresh">刷新页面</a>
</li>
<li>
<a v-if="tagsList.length > 1" href="javascript:;" @click="closeTag">关闭当前</a>
</li>
<li>
<a v-if="tagsList.length > 1" href="javascript:;" @click="closeOtherTag">关闭其他</a>
</li>
<li>
<a v-if="tagsList.length > 1" href="javascript:;" @click="closeAllTag">关闭所有</a>
</li>
</ul>
<div v-if="showTags" class="tags">
<ul ref="scrollContainer" @wheel.prevent="ulWheel">
<li
v-for="(item, index) in tagsList"
:key="index"
class="tags-li"
:class="{ active: isActive(item.path) }"
@contextmenu.prevent="setSelectedTag(item,$event)"
>
<router-link
:ref="setTagRef"
:key="item.path + item.title"
:to="item.path"
class="tags-li-title"
>{{ item.title }}</router-link>
<span v-show="isShowClose(item.path)" class="tags-li-icon" @click="closeTags(index)">
<i class="el-icon-close"></i>
</span>
</li>
</ul>
</div>
</template>
1.模板中整个ul就是右键菜单的内容了、contextMenuVisible控制右键菜单显示隐藏。:style
动态设置菜单显示的位置,因为我们要让他跟随每个tag附近显示
2.从模板中可以看到我们遍历了tagsList,给每个tag都禁用了默认的右键弹出菜单,并且绑定到了自定义事件setSelectedTag(item,$event)
中
接下来我们再看看js里面的关键部分:
data() {
return {
tagsList: [],
selectedTag: undefined, // 右键选中的tag
tagRefs: [],
contextMenuVisible: false, // 是否显示菜单
menuLeft: '', // 右键菜单距离浏览器左边的距离
menuTop: '' // 右键菜单距离浏览器上边的距离
}
},
setSelectedTag(tag,event) {
const e = event || window.event
const target = e.target
this.menuLeft = e.layerX + 20 + 'px' // 菜单出现的位置距离左侧的距离
this.menuTop = e.layerY + 20 + 'px' // 菜单出现的位置距离顶部的距离
this.contextMenuVisible = true // 显示菜单
this.selectedTag = tag// 当前右击的菜单信息
}
再看CSS部分
.right-menu{
width: 80px;
text-align: center;
position: absolute;
z-index: 100000;
border: 1px solid #ccc;
background: #f2f2f2;
border-radius: 5px;
padding: 5px;
font-size: 14px;
}
所以可以看到我们自己实现这个右键弹出菜单很繁琐是吧,主要是告诉大家如何不用插件去实现的思路而已。好了我们开始用我们的插件吧。
-
第二种使用ContextMenu component for Vue 3插件实现右键弹出菜单
[文档地址](https://vuejsexamples.com/contextmenu-component-for-vue-3/)
1.运行 $ npm i -S v-contextmenu@next 或者 yarn add v-contextmenu@next
安装完以后我们来直接上代码吧
tags.vue 完整代码
<template>
<v-contextmenu ref="contextmenu">
<v-contextmenu-item @click="refreshSelectedTag"
>刷新页面</v-contextmenu-item
>
<v-contextmenu-item v-if="tagsList.length > 1" @click="closeCurrent"
>关闭当前</v-contextmenu-item
>
<v-contextmenu-item v-if="tagsList.length > 1" @click="closeOther"
>关闭其他</v-contextmenu-item
>
<v-contextmenu-item v-if="tagsList.length > 1" @click="closeAll"
>关闭所有</v-contextmenu-item
>
</v-contextmenu>
<div v-if="showTags" class="tags">
<ul ref="scrollContainer" @wheel.prevent="ulWheel">
<li
v-for="(item, index) in tagsList"
:key="index"
v-contextmenu:contextmenu
class="tags-li"
:class="{ active: isActive(item.path) }"
@contextmenu.prevent="setSelectedTag(item)"
>
<router-link
:ref="setTagRef"
:key="item.path + item.title"
:to="item.path"
class="tags-li-title"
>
{{ item.title }}
</router-link>
<span
v-show="isShowClose(item.path)"
class="tags-li-icon"
@click="closeTags(index)"
>
<i class="el-icon-close"></i>
</span>
</li>
</ul>
</div>
</template>
<script>
import { directive, Contextmenu, ContextmenuItem } from 'v-contextmenu'
import 'v-contextmenu/dist/themes/default.css'
import bus from 'lib@/utils/bus'
import { mapActions, mapGetters } from 'vuex'
const noTagRouteList = ['notFound', 'noAuthority', 'systemError']
const tagAndTagSpacing = 4 // tag标签移动的间距
const defaultPage = '/home' // 默认展示的页面,tags中至少有一个默认的tag
export default {
directives: {
contextmenu: directive,
},
components: {
[Contextmenu.name]: Contextmenu,
[ContextmenuItem.name]: ContextmenuItem,
},
data() {
return {
tagsList: [],
selectedTag: undefined, // 右键选中的tag
tagRefs: [],
}
},
computed: {
showTags() {
return this.tagsList.length > 0
},
...mapGetters('tags', ['getTagsList']),
scrollWrapper() {
// tag滚动容器
return this.$refs.scrollContainer
},
},
watch: {
$route(newValue) {
this.setTags(newValue)
this.moveToCurrentTag()
},
},
created() {
this.setTags(this.$route)
},
mounted() {
bus.$on('closeCurrentTag', () => {
this.closeCurrent()
})
if (Array.isArray(this.getTagsList) && this.getTagsList.length > 0) {
this.tagsList = this.getTagsList
}
},
beforeUpdate() {
this.tagRefs = []
},
updated() {
console.log(this.tagRefs)
},
methods: {
...mapActions('tags', ['updatedTags']),
isActive(path) {
return path === this.$route.fullPath
},
isShowClose(path) {
return path !== defaultPage
},
// 关闭单个标签
closeTags(index) {
const delItem = this.tagsList.splice(index, 1)[0]
const item = this.tagsList[index]
? this.tagsList[index]
: this.tagsList[index - 1]
if (item) {
delItem.path === this.$route.fullPath && this.$router.push(item.path)
} else {
this.$router.push(defaultPage)
}
bus.$emit('tags', this.tagsList)
},
closeCurrent() {
const index = this.tagsList.findIndex(
(el) => el.path === this.selectedTag.path
)
if (index > -1) {
this.closeTags(index)
}
},
// 关闭全部标签
closeAll() {
this.tagsList = []
bus.$emit('tags', this.tagsList)
this.$router.push(defaultPage)
},
// 关闭其他标签
closeOther() {
const curItem = this.tagsList.filter((item) => {
return item.path === this.$route.fullPath || item.path === defaultPage
})
this.tagsList = curItem
bus.$emit('tags', this.tagsList)
},
// 设置标签
setTags(route) {
if (noTagRouteList.findIndex((item) => item === route.name) > -1) return
//返回数组满足条件指定存在的路由
const isExist = this.tagsList.some((item) => {
return item.path === route.fullPath
})
// 获取二层子路由组件名称作为 keep-alive 需要缓存组件
let name = route.matched[1]
? route.matched[1].components.default.name
: route.name
if (!isExist) {
this.tagsList.push({
title: route.meta.title,
path: route.fullPath,
name: name,
})
bus.$emit('tags', this.tagsList)
}
},
// 刷新当前页面
refreshSelectedTag() {
this.$nextTick(() => {
this.$router.replace({
path: '/redirect' + this.selectedTag.path,
})
})
this.updatedTags(this.tagsList)
},
handleTags(command) {
command === 'other' ? this.closeOther() : this.closeAll()
},
setSelectedTag(tag) {
this.selectedTag = tag
},
// 用于实现tags出现滚动条的时候左右滑动
ulWheel(e) {
const eventDelta = e.wheelDelta || -e.deltaY * 40
const $scrollWrapper = this.scrollWrapper
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
},
/**计算滚动条位置,动态滚动对应位置 */
calculateTagsScroll(currentTag) {
const $container = this.scrollWrapper
const $containerWidth = $container.offsetWidth
const $scrollWrapper = this.scrollWrapper
const tagArr = this.tagRefs
let firstTag = null
let lastTag = null
if (tagArr.length > 0) {
firstTag = tagArr[0]
lastTag = tagArr[tagArr.length - 1]
}
// 如果当前tag是数组中第一个,滚动到最左边
if (firstTag === currentTag) {
$scrollWrapper.scrollLeft = 0
} else if (lastTag === currentTag) {
// 如果当前tag是数组中最后一个,则滚动到最右边
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
} else {
const currentIndex = tagArr.findIndex((item) => item === currentTag)
const prevTag = tagArr[currentIndex - 1]
const nextTag = tagArr[currentIndex + 1]
const afterNextTagOffsetLeft =
nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
const beforePrevTagOffsetLeft =
prevTag.$el.offsetLeft - tagAndTagSpacing
if (
afterNextTagOffsetLeft >
$scrollWrapper.scrollLeft + $containerWidth
) {
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
}
}
},
// 定位tag元素位置,用于出现横向滚筒条的时候tag滚动条自动滚动位置
moveToCurrentTag() {
this.$nextTick(() => {
for (const tag of this.tagRefs) {
if (tag.to === this.$route.path) {
this.calculateTagsScroll(tag)
break
}
}
})
},
// 获取tag元素
setTagRef(el) {
if (el) {
this.tagRefs.push(el)
}
},
},
}
</script>
<style lang="less">
.tags {
position: relative;
width: 100%;
background: #fff;
border-bottom: 1px solid #d8dce5;
}
.tags ul {
box-sizing: border-box;
width: 100%;
height: 100%;
display: flex;
overflow-x: auto;
height: 36px;
}
.tags ul::-webkit-scrollbar-track-piece {
background-color: transparent;
width: 2px;
}
.tags-li {
display: flex;
margin-left: 8px;
border-radius: 3px;
font-size: 12px;
cursor: pointer;
height: 23px;
line-height: 23px;
border: 1px solid #e9eaec;
background: #fff;
padding: 2px 5px 2px 12px;
vertical-align: middle;
color: #666;
-webkit-transition: all 0.3s ease-in;
-moz-transition: all 0.3s ease-in;
transition: all 0.3s ease-in;
}
.tags-li:not(.active):hover {
background: #f8f8f8;
}
.tags-li.active {
border: 1px solid #409eff;
background-color: #409eff;
.tags-li-icon {
color: #fff;
}
}
.tags-li-title {
float: left;
max-width: 80px;
min-width: 28px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-right: 5px;
color: #666;
}
.tags-li.active .tags-li-title {
color: #fff;
}
.v-contextmenu-item {
padding: 10px 16px;
font-size: 12px;
&--hover {
background-color: #eee;
color: #333;
}
}
</style>
用于vuex 里面tags.js标签存储内容(用于右键菜单刷新页面的时候重新加载tags标签)
export default {
namespaced: true,
state: {
tagsList: [],
},
getters: {
getTagsList: (state) => {
return state.tagsList
},
},
mutations: {
SET_TAGS_LIST(state, list) {
state.tagsList = list
},
CLEAR_TAGS_LIST(state) {
state.tagsList = []
},
},
actions: {
/* 赋值tags */
updatedTags({ commit }, list = []) {
// 去掉添加的重定向路由
const arr = list.filter((o) => {
return o.path.indexOf('/redirect/') < 0
})
commit('SET_TAGS_LIST', arr)
},
/* 清除tabs信息 */
clearTags({ commit }) {
commit('CLEAR_TAGS_LIST')
},
},
}
我们来看看ulWheel跟moveToCurrentTag可能不太好理解。函数主要是是为了做什么解决什么问题。看图
ulWheel函数是为了tags太多出现滚动条的时候,鼠标滚轮可以控制滚动条左右移动
moveToCurrentTag就是切换的时候每次重新计算,计算滚动条位置,动态滚动对应位置
以上就实现页面多tab,右键菜单关闭、刷新等功能所有内容了、虽然简单但是踩了不少坑跟改了不少bug