效果预览
技术方案
vue3 ( vite | TS | vueUse | AutoImport | pinia) + Element Plus + UnoCSS
技术要点
- 需开启 pinia 持久化
- 右键菜单组件借助了 Element Plus 的样式
代码实现
src/components/PageTabs.vue
<script setup lang="ts">
import { usePageTabsStore } from '@/stores/pageTabs'const PageTabsStore = usePageTabsStore()// 导入自定义的数据类型
import type { MenuProps, menu } from '@/types/menu'/** 父组件传参* @param menu_list 菜单列表*/
const { menu_list, homePath } = defineProps<MenuProps>()const route = useRoute()/*** 通过路径获取菜单项** @param menu_list 菜单列表* @param path 菜单路径* @returns 返回匹配的菜单项,如果没有找到则返回null*/
function getMenuByPath(menu_list: menu[], path: string) {let finalResult = nullfor (const item of menu_list) {if (item.path === path) {finalResult = itembreak} else if (item.children && item.children.length > 0) {let result: any = getMenuByPath(item.children, path)if (result) {finalResult = resultbreak}}}return finalResult
}watch(() => route.path,() => {let newMenu = getMenuByPath(menu_list, route.path)if (newMenu && newMenu.path) {PageTabsStore.current_pageTab = newMenu.pathPageTabsStore.addTab(newMenu)} else {PageTabsStore.current_pageTab = homePath}},{ immediate: true }
)// 获取全局路由
const router = useRouter()/*** 根据新的标签页名称更改当前路由** @param newTab 新的标签页名称*/
function tabChange(newTab: string) {router.push(newTab)
}/*** 移除指定的标签页** @param targetTab 要移除的标签页的路径*/
function tabRemove(targetTab: string) {if (targetTab === PageTabsStore.current_pageTab) {let targetIndex = 0for (const [index, tab] of PageTabsStore.pageTabs.entries()) {if (tab.path === targetTab) {targetIndex = indexbreak}}if (targetIndex === 0) {PageTabsStore.current_pageTab = homePath} else {PageTabsStore.current_pageTab = PageTabsStore.pageTabs[targetIndex - 1].path}router.push(PageTabsStore.current_pageTab)}PageTabsStore.delTab(targetTab)
}const menuInfo = reactive({x: 0,y: 0,menuItems: [{label: '关闭其他页签',onClick: (targetPath: string) => {PageTabsStore.closeOtherTabs(targetPath)router.push(targetPath)}},{label: '关闭全部页签',onClick: () => {PageTabsStore.closeAllTabs()router.push(homePath)}}],visible: false,targetPath: ''
})/*** 右键点击事件处理函数** @param event 鼠标事件对象* @param targetPath 目标路径*/
const onContextmenu = (event: MouseEvent, targetPath: string) => {event.preventDefault()menuInfo.x = event.clientXmenuInfo.y = event.clientYmenuInfo.visible = truemenuInfo.targetPath = targetPath
}
</script><template><el-tabs@tab-change="tabChange"@tab-remove="tabRemove"v-model="PageTabsStore.current_pageTab"class="demo-tabs"type="border-card"><el-tab-pane :name="homePath"><template #label><Icon icon="icon-park-outline:mind-mapping" /></template><slot></slot></el-tab-pane><el-tab-pane closable v-for="item in PageTabsStore.pageTabs" :key="item.name" :name="item.path"><template #label><!-- 仅页签名称上响应右键菜单 --><span @contextmenu.prevent="onContextmenu($event, item.path)">{{ item.name }}</span></template><slot></slot></el-tab-pane></el-tabs><!-- 右键菜单 --><Contextmenu :menuInfo="menuInfo" @closeMenu="menuInfo.visible = false" />
</template>
src/components/Contextmenu.vue
<template><transition name="el-zoom-in-top"><divv-show="visible"class="menu el-dropdown__popper el-popper is-light is-pure":style="{ top: y + 10 + 'px', left: x - 10 + 'px' }"data-popper-placement="bottom"><div class="el-popper__arrow" style="left: 10px"></div><ul class="el-dropdown-menu"><liclass="el-dropdown-menu__item"v-for="item in menuItems":key="item.label"@click="handleClick(item)">{{ item.label }}</li></ul></div></transition>
</template><script setup lang="ts">
interface MenuProps {menuInfo: {x: numbery: numbermenuItems: { label: string; onClick: () => void }[]visible: booleantargetPath: string}
}const props = defineProps<MenuProps>()
const { x, y, menuItems, visible, targetPath } = toRefs(props.menuInfo)const handleClick = (item: { label: string; onClick: (arg1: string) => void }) => {if (item.onClick) {item.onClick(targetPath.value)}
}const container = ref<HTMLElement | null>(null)const emit = defineEmits<{(e: 'closeMenu'): void
}>()/*** 页面其他位置点击时隐藏菜单** @param event 鼠标事件对象*/
function handleGlobalClick(event: MouseEvent) {if (!container.value?.contains(event.target as Node)) {emit('closeMenu')}
}onMounted(() => {window.addEventListener('click', handleGlobalClick)
})onUnmounted(() => {window.removeEventListener('click', handleGlobalClick)
})
</script><style scoped lang="scss">
.menu {position: fixed;z-index: 99999;.el-dropdown-menu__item {font-size: 12px !important;white-space: nowrap;}
}
</style>
src/stores/pageTabs.ts
import { ref } from 'vue'
import { defineStore } from 'pinia'
import type { menu } from '@/types/menu'
export const usePageTabsStore = defineStore('pageTabs', () => {const pageTabs = ref<menu[]>([])const current_pageTab = ref('')function addTab(newTab: menu) {if (pageTabs.value.some((tab) => tab.path === newTab.path)) returnpageTabs.value.push(newTab)}function delTab(targetPath: string) {pageTabs.value = pageTabs.value.filter((tab) => tab.path !== targetPath)}function closeOtherTabs(targetPath: string) {pageTabs.value = pageTabs.value.filter((tab) => tab.path === targetPath)}function closeAllTabs() {pageTabs.value = []current_pageTab.value = ''}return {pageTabs,current_pageTab,addTab,delTab,closeOtherTabs,closeAllTabs}
})
src/types/menu.ts
export interface menu {id: numbername: stringpath: stringmenuHide?: booleannickName?: stringicon?: stringindex?: stringchildren?: menu[]
}export interface MenuProps {menu_list: menu[]default_openeds?: string[]collapse?: booleanhomePath: string
}
页面使用
- 用 PageTabs 组件包裹页面内容
- 传入菜单数组 menu_list
- 传入主页 homePath (默认展示主页,且不会被关闭)
<PageTabs :menu_list="menu_list" homePath="/notes"><el-scrollbar height="calc(100vh - 236px)"><divv-if="showMindMap"class="flex justify-center items-center p-10"style="margin: auto"><Mindmapclass="dark:color-white!":active="activeLabel":data="mapData"@activeChange="activeChange"/></div><router-view v-else class="p-4 markdown_views mdViews max-w-full"></router-view></el-scrollbar></PageTabs>