下面是详细的讲解。
如何利用Vue3+Element Plus实现动态标签页及右键菜单
前言
在实际的项目中,动态标签页和右键菜单是常见的UI需求。本文将以Vue3和Element Plus为基础,演示如何快速实现动态标签页及右键菜单功能。
实现步骤
第一步:安装Element Plus
Element Plus是饿了么前端团队开源的一套基于Vue的组件库,提供了丰富的UI组件和方便易用的API。我们可以通过npm安装Element Plus:
npm install element-plus
在Vue3项目中,需要在main.js中引入Element Plus并注册:
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/lib/theme-chalk/index.css'
import App from './App.vue'
createApp(App).use(ElementPlus).mount('#app')
第二步:实现动态标签页
动态标签页主要包括以下几个部分:
- 标签页组件(TabBar):用于显示和操作标签页;
- 标签页列表(tabs):用于保存所有打开的标签页信息;
- 标签页路由(router):用于管理打开的标签页和路由的映射关系。
首先,我们可以在一个单独的TabBar.vue组件中实现标签页组件:
<template>
<el-tabs v-model:value="activeIndex" type="card" @tab-click="handleTabClick" @tab-remove="handleTabRemove">
<el-tab-pane v-for="(tab,index) in tabs" :key="tab.name" :label="tab.title" :name="tab.name">
<router-view v-if="index===activeIndex" />
</el-tab-pane>
</el-tabs>
</template>
<script>
import { reactive, watch } from 'vue'
export default {
setup(props,ctx){
const router = ctx.root.$router
const state=reactive({
tabs:[],
activeIndex:0
})
watch(router.route, (to, from) => {
const index = state.tabs.findIndex(tab => tab.path === to.path)
if (index === -1) {
state.tabs.push({
name: to.name || '',
path: to.path,
title: to.meta.title || '',
closable: true
})
state.activeIndex = state.tabs.length - 1
} else {
state.activeIndex = index
}
},{deep:true})
const handleTabClick=tab => {
state.activeIndex=state.tabs.indexOf(tab)
router.push(tab.path)
}
const handleTabRemove=(tab)=>{
const index=state.tabs.indexOf(tab)
state.tabs.splice(index,1)
state.activeIndex=Math.max(0,index-1)
router.push(state.tabs[state.activeIndex].path)
}
const setActiveIndex=(index)=>{
state.activeIndex=index
router.push(state.tabs[state.activeIndex].path)
}
return {
state,
handleTabClick,
handleTabRemove,
setActiveIndex
}
}
}
</script>
这里使用了Vue3中的reactive
、watch
等响应式API,实现了动态添加和删除标签页。具体实现步骤如下:
- 在setup方法中,通过
reactive
创建一个响应式对象state,用来保存当前所有打开的标签页和活动标签页的下标; - 监听路由变化,在路由变化时判断当前路由是否已经打开,如果没有则通过
state.tabs.push
添加一个新标签页,并切换到该标签页;如果已经打开,则切换到该标签页; - 处理标签页的点击和移除事件:在点击标签页时,通过
state.activeIndex
更新活动标签页下标,并切换路由;在移除标签页时,删除对应的标签页对象并切换至上一个标签页; - 最后将
state
、handleTabClick
、handleTabRemove
和setActiveIndex
暴露给外部使用。
需要注意的是,由于我们使用了Vue3的响应式API,所以需要在相关变量和对象上使用.value
访问其原始值(即非响应式状态),如:state.activeIndex.value
、state.tabs.value
等等。
在App.vue中使用标签页组件:
<template>
<div id="app">
<TabBar />
</div>
</template>
<script>
import TabBar from "./components/TabBar.vue";
export default {
components:{
TabBar
},
setup() {
return {}
}
};
</script>
第三步:实现右键菜单
右键菜单主要有两个部分:
- 右键菜单组件(ContextMenu):用于显示和操作右键菜单;
- 右键菜单管理器(ContextMenuManager):用于在需要显示右键菜单时,创建并显示上下文菜单组件,并在菜单关闭时销毁该组件。
首先,我们可以在一个单独的ContextMenu.vue组件中实现右键菜单组件:
<template>
<el-dropdown
:visible-arrow="false"
@command="handleCommand"
@visible-change="handleVisibleChange"
:disabled="disabled"
:popper-append-to-body="false"
:popper-class="popperClass"
:placement="placement"
>
<el-button
slot="trigger"
type="text"
:disabled="disabled"
:icon="icon"
:size="size"
:class="buttonClass"
>
{{ text }}
</el-button>
<el-dropdown-menu
slot="dropdown"
:class="menuClass"
:style="{ minWidth: menuMinWidth + 'px' }"
v-for="(item,index) in items"
:key="item.text||index"
>
<el-dropdown-item
v-if="item.divider"
class="el-dropdown-menu__item el-dropdown-menu__item--divided"
:style="{ ...item.style }"
/>
<el-dropdown-item
v-else
class="el-dropdown-menu__item"
v-bind="item"
:index="index"
:command="item.action||index"
/>
</el-dropdown-menu>
</el-dropdown>
</template>
<script>
export default {
props: {
text: {
type: String,
default: ''
},
icon: {
type: String,
default: ''
},
size: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
},
items: {
type: Array,
default: () => []
},
placement: {
type: String,
default: 'bottom-end'
},
popperClass: {
type: String,
default: ''
},
menuClass: {
type: String,
default: ''
},
menuMinWidth: {
type: Number,
default: 120
},
buttonClass: {
type: String,
default: ''
}
},
setup(props, ctx) {
const state = reactive({
visible: false,
evt: null
})
const handleVisibleChange = (visible) => {
if (visible) {
state.visible = true
state.evt = window.event
ctx.emit('open', state.evt)
} else {
state.visible = false
state.evt = null
ctx.emit('close', window.event)
}
}
const handleCommand = command => {
ctx.emit('command', parseInt(command))
}
const create = () => {
const com = ctx.render()
const body = document.body
body.appendChild(com.$el)
return com
}
const destroy = com => {
com.$el.remove()
com.$destroy()
}
//创建和销毁上下文菜单组件
onBeforeMount(() => {
const evtCb = e => {
e.preventDefault()
e.stopPropagation()
destroy(com)
state.visible = false
}
const com = create()
com.$on('close', evtCb)
com.$on('command', ctx.emit.bind(ctx, 'command'))
window.addEventListener('click', evtCb, { capture: true })
})
return {
state,
handleVisibleChange,
handleCommand
}
}
}
</script>
这里使用了Vue3中的reactive
等响应式API,实现了右键菜单的显示和操作。具体实现步骤如下:
- 在setup方法中,通过
reactive
创建一个响应式对象state,用来保存当前右键菜单的可见状态及右键事件对象; - 监听el-dropdown的
visible-change
事件,当右键菜单显示时,通过state.visible=true
保存当前可见状态和右键事件对象,并触发上层组件的open
事件,用于显示遮罩层或更改鼠标状态;当右键菜单关闭时,通过state.visible=false
清除可见状态和右键事件对象,并触发上层组件的close
事件; - 监听el-dropdown的
command
事件,用于触发上层组件的command
事件,返回所选中的命令值; - 使用Vue3的Hook函数
onBeforeMount
在组件挂载前创建和销毁上下文菜单组件,通过ctx.render()
创建组件实例,并使用document.body
将其挂载到页面上;监听指定事件(如鼠标左键按下),在事件回调函数中销毁组件实例,并设置state.visible=false
。
接下来,我们需要实现右键菜单管理器,用于在需要显示右键菜单时,创建并显示ContextMenu组件,并在菜单关闭时销毁该组件。以TabBar.vue组件为例,我们可以在el-tabs的@contextmenu
事件回调函数中调用ContextMenuManager创建右键菜单组件:
<template>
<el-tabs
ref="tabs"
v-model:value="activeIndex"
type="card"
@click="handleTabClick"
@tab-remove="handleTabRemove"
@contextmenu.native.prevent.self="handleContextMenu"
>
<el-tab-pane v-for="(tab, index) in tabs" :key="tab.name" :label="tab.title" :name="tab.name">
<router-view v-if="index === activeIndex" />
</el-tab-pane>
</el-tabs>
<ContextMenu
:items="contextMenuItems"
:text="'操作菜单'"
@command="handleCommand"
@open="handleContextMenuOpen"
@close="handleContextMenuClose"
:style="{top:contextMenuTop+'px',left:contextMenuLeft+'px'}"
v-if="contextMenuVisible.value"
/>
</template>
<script>
import { reactive, watch, computed, onMounted } from 'vue'
import ContextMenu from "./ContextMenu.vue";
import ContextMenuManager from "./ContextMenuManager";
export default {
components: {
ContextMenu
},
setup(props, ctx) {
const router = ctx.root.$router
const state = reactive({
tabs: [], //保存所有标签页信息
activeIndex: 0, //活动标签页下标
contextMenuVisible: false, //上下文菜单是否显示
contextMenuTop: 0, //上下文菜单的顶部位置
contextMenuLeft: 0 //上下文菜单的左侧位置
})
const isMacLike = typeof navigator !== 'undefined' && /mac/i.test(navigator.platform)
//定义上下文菜单项
const contextMenuItems = computed(() => {
return state.tabs.value.map(view => ({
text: view.title,
icon: 'el-icon-document',
action: view.name,
disabled: state.tabs.value.length === 1
}))
})
const setActiveIndex = (index) => {
state.activeIndex = index
router.push(state.tabs.value[state.activeIndex].path)
}
const handleTabClick = tab => {
state.activeIndex = state.tabs.value.indexOf(tab)
router.push(tab.path)
}
const handleTabRemove = (tab) => {
const index = state.tabs.value.indexOf(tab)
state.tabs.splice(index, 1)
state.activeIndex = Math.max(0, index - 1)
router.push(state.tabs.value[state.activeIndex].path)
}
//处理上下文菜单事件
const handleContextMenu = (evt) => {
evt.preventDefault()
evt.stopPropagation()
state.contextMenuVisible = true
state.contextMenuTop = evt.clientY
state.contextMenuLeft = isMacLike ? evt.clientX : evt.clientX - 20 // macOS
}
const handleContextMenuOpen = () => {
const mgr = new ContextMenuManager(() => state.contextMenuVisible, !isMacLike)
mgr.show()
}
const handleContextMenuClose = () => {
const mgr = new ContextMenuManager(() => state.contextMenuVisible, !isMacLike)
mgr.hide()
}
const handleCommand = (index) => {
setActiveIndex(index)
}
watch(router.route, (to, from) => {
const index = state.tabs.value.findIndex(tab => tab.path === to.path)
if (index === -1) {
state.tabs.push({
name: to.name || '',
path: to.path,
title: to.meta.title || '',
closable: true
})
state.activeIndex = state.tabs.value.length - 1
} else {
state.activeIndex = index
}
},{deep:true})
onMounted(() => {
const mgr = new ContextMenuManager(() => state.contextMenuVisible, !isMacLike)
mgr.attach(state.refs.tabs?.listRef ?? null)
})
return {
state,
handleTabClick,
handleTabRemove,
contextMenuItems,
handleContextMenu,
setActiveIndex,
handleCommand,
handleContextMenuOpen,
handleContextMenuClose
}
}
}
//右键菜单管理器
export class ContextMenuManager {
constructor(visibleFn, contextMenu) {
this.visibleFn = visibleFn
this.contextMenu = contextMenu
this.contextMenuElement = null
this.attachEnabled = true
}
attach(refEl) {
if (!refEl) { return }
this.refEl = refEl
this.eventCb = (e) => {
if (this.attachEnabled) {
if (e.type === 'mousedown' && e.button === 2) {
this.showMenu(e)
} else if (e.type === 'click' && e.button === 0) {
this.hideMenu()
}
}
}
this.refEl.addEventListener('mousedown', this.eventCb, true)
window.addEventListener('click', this.eventCb, true)
}
detach() {
if (this.refEl && this.eventCb) {
this.refEl.removeEventListener('mousedown', this.eventCb, true)
window.removeEventListener('click', this.eventCb, true)
}
}
showMenu(e) {
if (this.contextMenu && this.visibleFn()) {
e.preventDefault()
this.contextMenuElement = this.contextMenu.$mount().$el
this.contextMenuElement.style.left = e.clientX + 'px'
this.contextMenuElement.style.top = e.clientY + 'px'
this.contextMenuElement.classList.add('fade-in')
this.attachEnabled = false
}
}
hideMenu() {
if (this.contextMenuElement && !this.contextMenuElement.classList.contains('fade-out')) {
this.contextMenuElement.classList.remove('fade-in')
this.contextMenuElement.classList.add('fade-out')
setTimeout(() => {
this.contextMenuElement.remove()
this.contextMenuElement = null
this.attachEnabled = true
}, 200)
}
}
show() {
setTimeout(() => {
if (!this.attachEnabled && this.contextMenuElement) {
this.contextMenuElement.classList.remove('fade-out')
this.contextMenuElement.classList.add('fade-in')
}
}, 10)
}
hide() {
setTimeout(() => {
if (!this.attachEnabled && this.contextMenuElement) {
this.contextMenuElement.classList.remove('fade-in')
this.contextMenuElement.classList.add('fade-out')
}
}, 10)
}
}
</script>
这里主要实现了右键菜单管理器ContextMenuManager类,用于对右键事件进行管理,在需要显示右键菜单时创建并显示ContextMenu组件,在菜单关闭后销毁ContextMenu组件。具体实现步骤如下:
- 定义
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:如何利用Vue3+Element Plus实现动态标签页及右键菜单 - Python技术站