上一篇中,我们实现了标签栏的前三部分功能,这篇将会讲解剩下的右键菜单部分。我们先来看一下右键菜单需要包括哪些功能:
那么,我们就开始一个个的去实现吧。
众所周知,当我们点击浏览器的刷新按钮的时候,会直接刷新整个系统,而且会有白屏,用户体验也不好。所以,如果想要刷新某个标签页的话,我们需要使用局部刷新。vue给我们提供了三种局部强制刷新的方式:
v-if
的绑定值key
赋一个跟上次不一样的值$forceUpdate()
方法这里我采用第一种方式。不过 ,由于我们已经使用了keep-alive来缓存路由,如果这里我们直接改变v-if,是不会触发刷新的效果的。那么怎么办呢?其实很简单,我们在刷新之前,先将当前路由移出缓存数组,然后再刷新。刷新完毕之后,再将当前路由添加回缓存数组。下面我们来看一下关键代码吧:
<template>
<router-view v-slot="{ Component }">
<keep-alive :include="cachedList">
<component :is="Component" v-if="reload" :key="$route.name" />
keep-alive>
router-view>
template>
<script lang="ts">
import { computed, defineComponent, provide, ref, nextTick } from 'vue'
import { useStore } from '@/store'
import { RouteRecordName, useRouter } from 'vue-router'
export default defineComponent({
name: 'Index',
setup() {
const store = useStore()
const router = useRouter()
const cachedList = computed(() => store.state.tagModule.cachedList)
const activeView = computed(() => store.state.tagModule.activeView)
const reload = ref(true)
const onReload = (routeName: string | RouteRecordName) => {
store.commit('tagModule/REMOVE_CACHED_VIEW', routeName)
reload.value = false
nextTick(() => {
if (activeView.value !== routeName) {
router.push({ name: routeName })
}
reload.value = true
store.commit('tagModule/ADD_CACHED_VIEW', routeName)
})
}
provide('reload', onReload)
return {
reload,
cachedList
}
}
})
script>
解释一下,在上面的代码中:
reload.value = false
开始刷新nextTick
等待DOM变化完成reload.value = true
刷新完成provide
将onReload
方法提供给子组件使用这样,我们的刷新方法就定义好了,我们只需要在子组件中inject('reload')(routeName)
就可以触发刷新了。
关闭单个标签页我们在前面已经实现过了,下面来看一下余下的几个是如何实现的:
import { Module } from 'vuex'
import { RouteRecord, RouteRecordName } from 'vue-router'
import { TagStateProps, RootStateProps, ViewListItemProps } from '../typings'
import router from '@/router'
import Utils from '@/utils'
const clearCache = (state: TagStateProps) => {
state.cachedList = state.viewList.map((view) => Utils.getBigName(view.name as string))
}
const tagModule: Module<TagStateProps, RootStateProps> = {
namespaced: true,
state: {
activeView: 'workbench',
viewList: [{ name: 'workbench', label: '工作台' }],
cachedList: []
},
mutations: {
// 关闭右侧标签页
REMOVE_RIGHT_VIEWS(state, routeName: string | RouteRecordName) {
// 获取该view的索引值
const viewIndex = state.viewList.findIndex((view) => view.name === routeName)
state.viewList = state.viewList.slice(0, viewIndex + 1)
// 如果目前存在的view中,没有已激活的页面,则将传进来的页面设置为目标页面
if (!state.viewList.some((view) => view.name === state.activeView)) {
state.activeView = routeName
router.push({ name: routeName })
}
// 清除路由缓存
clearCache(state)
},
// 关闭其他标签页
REMOVE_OTHER_VIEWS(state, routeName: string | RouteRecordName) {
// 获取该view
const view = state.viewList.find((view) => view.name === routeName)!
// 由于工作台永远都在第一位,所以直接将长度调整为1
state.viewList.length = 1
// 如果传进来的路由不是工作台,就将当前传进来的路由添加进去
if (routeName !== 'workbench') {
state.viewList.push(view)
}
// 如果目前存在的view中,没有已激活的页面,则将传进来的页面设置为目标页面
if (!state.viewList.some((view) => view.name === state.activeView)) {
state.activeView = routeName
router.push({ name: routeName })
}
// 清除路由缓存
clearCache(state)
},
// 关闭所有标签页
REMOVE_ALL_VIEWS(state) {
// 如果长度已经是1,不继续执行
if (state.viewList.length === 1) {
return
}
// 由于工作台永远都在第一位,所以直接将长度调整为1
state.viewList.length = 1
state.activeView = 'workbench'
router.push({ name: 'workbench' })
// 清除路由缓存
clearCache(state)
}
}
}
export default tagModule
到这里,右键菜单的所有功能都已经实现完毕了,现在,让我们来创建组件吧。
在写之前,我们需要先了解一个vue3的新内置组件:teleport(传送门)。也就是我们打游戏时经常说的tp技能,就是这个单词的缩写。顾名思义,这个组件可以将里面所包裹的内容,传送到其to属性中所指定的DOM节点之内。也就是说,无论我在哪引用这个组件,最终都会渲染在to属性指定的DOM节点之中。详细了解请看官方文档-teleport内置组件。
<template>
<teleport to="#contextmenu">
<ul v-show="show" ref="contextmenuWrapper" class="contextmenu-wrapper" :style="style">
<template v-for="item in menu" :key="item.label">
<li
v-if="item.disabled ? !item.disabled(routeName) : true"
@click="item.onClick(routeName)"
>
{{ item.label }}
li>
template>
ul>
teleport>
template>
<script lang="ts">
import { computed, defineComponent, PropType, watch, onMounted } from 'vue'
import { ContextMenuItemProps } from './typings'
import { CommonObject } from '@/typings'
export default defineComponent({
name: 'Contextmenu',
props: {
visable: {
type: Boolean,
required: true
},
menu: {
type: Array as PropType<ContextMenuItemProps[]>,
required: true
},
style: Object as PropType<CommonObject>,
routeName: {
type: String,
required: true
}
},
emits: ['update:visable'],
setup(props, context) {
const show = computed({
get: () => props.visable,
set: (value) => context.emit('update:visable', value)
})
const hideMenu = () => show.value && (show.value = false)
watch(show, (value) => {
// 左键点击任意位置,隐藏菜单
value && document.addEventListener('click', hideMenu)
!value && document.removeEventListener('click', hideMenu)
})
onMounted(() => {
let rootNode = document.getElementById('contextmenu')
if (!rootNode) {
rootNode = document.createElement('div')
rootNode.id = 'contextmenu'
document.body.appendChild(rootNode)
}
})
return {
show
}
}
})
script>
<style lang="scss">
/*** 里面的sass变量就不贴了 ***/
.contextmenu-wrapper {
width: fit-content;
position: fixed;
padding: $space--sm 0;
background-color: $color--white;
border: 1px solid $color--line;
border-radius: 5px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
z-index: 9999;
li {
cursor: pointer;
font-size: $font-size--sm;
padding: $space--sm $space--lg;
&:hover {
background-color: rgba($color: $color--info, $alpha: 0.1);
}
}
}
style>
将Contextmenu
组件全局注册之后,我们来到TagsView
组件中(为节省篇幅,只贴相关ts代码、html标签和属性):
<template>
<div class="tagsView" v-bind="$attrs">
<div
class="route-tag"
@contextmenu.prevent="onTagRightClick($event, view.name)"
>
<span class="tag-text">{{ view.label }}span>
<i
class="el-icon el-icon-close tag-close"
@contextmenu.stop.prevent
/>
div>
div>
<contextmenu
v-model:visable="contextmenu.show"
:menu="contextmenu.menu"
:route-name="contextmenu.routeName"
:style="contextmenu.style"
/>
template>
<script lang="ts">
/* import语句省略 */
export default defineComponent({
name: 'TagsView',
setup() {
const store = useStore()
const reload = inject<Reload>('reload')!
const contextmenu = reactive<{
show: boolean
style: { top: string; left: string }
routeName: RouteRecordName | string
menu: ContextMenuItemProps[]
}>({
show: false,
style: { top: '0', left: '0' },
routeName: '',
menu: [
{
label: '刷新',
onClick: (routeName: RouteRecordName) => reload(routeName)
},
{
label: '关闭',
disabled: (routeName) => routeName === 'workbench',
onClick: (routeName) => {
store.commit(Consts.MutationKey.REMOVE_VIEW, routeName)
}
},
{
label: '关闭右侧',
onClick: (routeName) => {
store.commit(Consts.MutationKey.REMOVE_RIGHT_VIEWS, routeName)
}
},
{
label: '关闭其他',
onClick: (routeName) => {
store.commit(Consts.MutationKey.REMOVE_OTHER_VIEWS, routeName)
}
},
{
label: '关闭所有',
onClick: () => {
store.commit(Consts.MutationKey.REMOVE_ALL_VIEWS)
}
}
]
})
const onTagRightClick = (event: MouseEvent, viewName: RouteRecordName) => {
contextmenu.style.top = event.clientY + 'px'
contextmenu.style.left = event.clientX + 'px'
contextmenu.routeName = viewName
contextmenu.show = true
}
return {
contextmenu,
onTagRightClick
}
}
})
script>
解释一下,这里的右键菜单并不是每个标签都匹配渲染了一个,而是所有标签共用一个右键菜单。因为一旦标签开的很多的情况下,渲染过多的右键菜单将不会是一件好事。
那么怎么实现共用呢?其实很简单,我们在一开始的时候就将右键菜单渲染好,并设置其v-show
为false
,也就是相当于给右键菜单的根节点加了个display: none
。之后,当我们右键单击标签时,获取鼠标单击的位置,并将菜单移动至那个坐标,同时切换当前右键菜单的routeName
属性,使其所有功能都为我鼠标单击的标签服务。最后,将v-show
调整为true
,菜单就成功显示了。
到这里,本项目的所有核心功能就都讲述完毕了。希望喜欢的朋友们可以在github中star一下,这对我很重要。谢谢大家,完结撒花了!