效果图
一、创建目录tagView文件里index.vue文件
1.html部分
<div class="tag-view">
<el-tabs
v-model="tabActive"
type="card"
ref="tabsRef"
class="tag-view-content user-none"
:class="{ ['tag-view-content-' + layout.tabsBarStyle]: true }"
@tab-change="handleTabChange"
@tab-remove="handleTabsDelete"
>
<el-tab-pane
v-for="item in tabsViewList"
:key="item.fullPath"
:name="item.fullPath"
:closable="!item.meta.noClosable"
>
<template #label>
<div
@contextmenu.prevent="openMenu($event, item.fullPath)"
style="display: flex; align-items: center; justify-content: center"
>
<el-icon v-if="layout.isTagsviewIcon">
<SvgIcon :name="item.meta.icon" />
</el-icon>
<span>{{ item.meta.title }}</span>
</div>
</template>
</el-tab-pane>
</el-tabs>
//右侧更多菜单
<TagMenu
:list="openMenuList"
:tagViewLenght="tabsViewList.length"
@handleClick="handleOption"
/>
//右键下拉菜单
<Contextmenu
:list="openMenuList"
:tagViewLenght="tabsViewList.length"
:="contextmenuParmas"
v-model:visible="visible"
@handleClick="handleOption"
/>
</div>
1.1 TagMenu部分
<template>
<el-dropdown
placement="bottom-end"
popper-class="tagview-more-dropdown"
@visible-change="handleVisibleChange"
@command="handleCommand"
>
<span
class="tagview-contents-more"
:class="{ 'tagview-contents-more-active': active }"
>
<span class="tagview-contents-more-icon">
<i class="box box-t"></i>
<i class="box box-b"></i>
</span>
</span>
<template #dropdown>
<el-dropdown-menu>
<template v-for="(item, index) in list" :key="item.name">
<el-dropdown-item
:command="index"
:disabled="index === 1 && tagViewLenght === 1"
>
<SvgIcon :showDefault="false" size="16px" :name="item.icon" />
{{ item.name }}
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts">
import { ref } from 'vue'
withDefaults(
defineProps<{
list: { icon: string; name: string }[]
tagViewLenght: number
}>(),
{},
)
const $emit = defineEmits(['handleClick'])
const active = ref(false)
const handleVisibleChange = (type: boolean) => (active.value = type) //菜单下拉框事件
const handleCommand = (type: boolean) => $emit('handleClick', type, false) //点击某一项执行的方法
</script>
1.2 Contextmenu部分
<template>
<ul
v-if="visible"
class="contextmenu el-dropdown-menu"
:style="{ left: left + 'px', top: top + 'px' }"
>
<li
class="el-dropdown-menu__item user-none"
v-for="(item, index) in list"
@click="$emit('handleClick', index, true)"
:key="item.icon"
v-show="index === 1 && tagViewLenght === 1 ? false : true"
>
<SvgIcon :showDefault="false" size="16" :name="item.icon" />
{{ item.name }}
</li>
</ul>
</template>
<script setup lang="ts">
import { watch } from 'vue'
const $props = withDefaults(
defineProps<{
list: { icon: string; name: string }[]
tagViewLenght: number
visible: boolean
left: number
top: number
}>(),
{
visible: false,
},
)
const $emit = defineEmits(['update:visible', 'handleClick'])
watch(
() => $props.visible,
(newValue) => {
if (newValue) {
document.body.addEventListener('click', closeMenu)
} else {
document.body.removeEventListener('click', closeMenu)
}
},
)
//关闭右键事情Tabs
const closeMenu = () => $emit('update:visible', false)
2.js部分
import { ref, computed, watch, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { storeToRefs } from 'pinia'
import { useSortable } from '@/utils/sortablejs'
import useSettingStore from '@/store/modules/setting/index'
import TagMenu from './tagMenu.vue'
import Contextmenu from './contextmenu.vue'
import useTabsStore from '~/store/modules/tabs/index'
import useUserStore from '~/store/modules/user/index'
const Router = useRouter()
const Route = useRoute()
const SettingStore = useSettingStore()
const TabsStore = useTabsStore()
const UserStore = useUserStore()
const { refsh, layout } = storeToRefs(SettingStore) // 需要pinia 转换为响应式数据
const { tabsViewList } = storeToRefs(TabsStore)
const tabsRef = ref(null) //tabs实例对象
const sortableInstance = ref(null) //拖拽的实例对象
// 存储x轴y轴
const contextmenuParmas = ref({
left: 0,
top: 0,
})
const currentPath = ref('') // 当前存起路径
const visible = ref(false) //鼠标右键Tabs菜单
const tabActive = ref('')
//双击右键事件Tabs
const openMenu = ({ x, y }: MouseEvent, fullPath: string) => {
contextmenuParmas.value = {
left: x, // X轴
top: y, // Y轴
}
visible.value = true //打开菜单
currentPath.value = fullPath // 路径存起
}
// 初始化tabs导航栏标签
const initTabs = () => {
// 菜单栏 item.meta?.isAffix 是否固定的标签栏(多数情况默认是首页后续根据isAffix来配置)
UserStore.flatMenuListGet.forEach((item) => {
if (item.meta?.isAffix) {
TabsStore.addTabsView({
fullPath: item.path,
meta: item.meta,
name: item.name,
})
}
})
}
initTabs() //初始化执行Tabs
onMounted(() => {
tabsViewDrop() //初始化执行拖拽功能
})
// 监听路由完整路径
watch(
() => Route.fullPath,
() => {
// 如果tabHidden为true不执行
if (Route.meta.tabHidden) return
let { meta, name, fullPath } = Route
tabActive.value = fullPath //路径赋值
TabsStore.addTabsView({
fullPath,
meta,
name,
})
},
{
immediate: true,
},
)
// 跳转方法
const handleTabChange = (fullPath: string) => Router.push(fullPath)
// 移除事件
const handleTabsDelete = (fullPath: string) => {
TabsStore.removeTabsView(
fullPath,
Route.fullPath === fullPath,
handleTabChange,
)
}
//所有方法
const handleOption = (index: number, isHas: boolean) => {
let fullPath = isHas ? currentPath.value : Route.fullPath //当前路由参数
switch (index) {
case 0:
handleTabChange(fullPath)
refsh.value = !refsh.value //刷新事件
break
case 1:
TabsStore.closeOtherTabsView(fullPath)
handleTabChange(fullPath) //跳转到对应的路由
break
case 2:
TabsStore.closeTabsViewOnSide(fullPath, 'left')
handleTabChange(fullPath) //跳转到对应的路由
break
case 3:
TabsStore.closeTabsViewOnSide(fullPath, 'right')
handleTabChange(fullPath) //跳转到对应的路由
break
case 4:
TabsStore.closeAllTabsView() //关闭全部
handleTabChange('/home')
break
}
}
//双击右键值数据
const openMenuList = ref<{ icon: string; name: string }[]>([
{
icon: 'Refresh',
name: '刷新',
},
{
icon: 'Close',
name: '关闭其他',
},
{
icon: 'Back',
name: '关闭左侧',
},
{
icon: 'Right',
name: '关闭右侧',
},
{
icon: 'Close',
name: '关闭全部',
},
])
二、创建pinia仓库
index.ts文件
import { defineStore } from 'pinia'
import type { TagsViewType, TabsStore } from './type'
const useTabsStore = defineStore('useTabsStore', {
state(): TabsStore {
return {
tabsViewList: [], //tabsView数据存储
}
},
actions: {
// 添加导航标签方法
addTabsView(tabItem: TagsViewType) {
let row = this.tabsViewList.find((v) => v.fullPath === tabItem.fullPath)
if (!row) this.tabsViewList.push(tabItem)
},
// 删除导航标签方法
removeTabsView(
fullPath: string,
isCurrent: boolean,
callback: (val: string) => void,
) {
//如果不符合直接就删除
if (isCurrent) {
this.tabsViewList.forEach((item, index) => {
if (item.fullPath === fullPath) {
let navIndex =
this.tabsViewList[index + 1] || this.tabsViewList[index - 1]
if (navIndex) callback(navIndex.fullPath) //跳转到对应的路由
}
})
}
this.tabsViewList = this.tabsViewList.filter(
(v) => v.fullPath !== fullPath,
) //删除面包屑每一项
},
// 关闭其他
closeOtherTabsView(fullPath: string) {
// noClosable是true和当前路由留下
this.tabsViewList = this.tabsViewList.filter((item) => {
return item.meta.noClosable || item.fullPath === fullPath
})
},
// 关闭左侧Or右侧
closeTabsViewOnSide(fullPath: string, type: 'left' | 'right') {
// 找到当前index
let currentIndex = this.tabsViewList.findIndex(
(item) => item.fullPath === fullPath,
)
// 判断一下必须存在才会执行
if (currentIndex !== -1) {
let range =
type === 'left'
? [0, currentIndex]
: [currentIndex + 1, this.tabsViewList.length]
// 是左侧还是右侧 item.meta.noClosable固定留下的
this.tabsViewList = this.tabsViewList.filter((item, index) => {
return index < range[0] || index >= range[1] || item.meta.noClosable
})
}
},
//关闭全部
closeAllTabsView() {
this.tabsViewList = this.tabsViewList.filter((item) => {
return item.meta.noClosable
})
},
//从新设置tabsViewList
setTabsViewList(list: TagsViewType[]) {
this.tabsViewList = list
},
// 设置标签栏标题
setTabsViewTitle(fullPath: string, title: string) {
this.tabsViewList.forEach((item) => {
if (item.fullPath === fullPath) item.meta.title = title
})
},
},
getters: {},
})
export default useTabsStore