如何利用Vue3+Element Plus实现动态标签页及右键菜单

下面是详细的讲解。

如何利用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')

第二步:实现动态标签页

动态标签页主要包括以下几个部分:

  1. 标签页组件(TabBar):用于显示和操作标签页;
  2. 标签页列表(tabs):用于保存所有打开的标签页信息;
  3. 标签页路由(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中的reactivewatch等响应式API,实现了动态添加和删除标签页。具体实现步骤如下:

  1. 在setup方法中,通过reactive创建一个响应式对象state,用来保存当前所有打开的标签页和活动标签页的下标;
  2. 监听路由变化,在路由变化时判断当前路由是否已经打开,如果没有则通过state.tabs.push添加一个新标签页,并切换到该标签页;如果已经打开,则切换到该标签页;
  3. 处理标签页的点击和移除事件:在点击标签页时,通过state.activeIndex更新活动标签页下标,并切换路由;在移除标签页时,删除对应的标签页对象并切换至上一个标签页;
  4. 最后将statehandleTabClickhandleTabRemovesetActiveIndex暴露给外部使用。

需要注意的是,由于我们使用了Vue3的响应式API,所以需要在相关变量和对象上使用.value访问其原始值(即非响应式状态),如:state.activeIndex.valuestate.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>

第三步:实现右键菜单

右键菜单主要有两个部分:

  1. 右键菜单组件(ContextMenu):用于显示和操作右键菜单;
  2. 右键菜单管理器(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,实现了右键菜单的显示和操作。具体实现步骤如下:

  1. 在setup方法中,通过reactive创建一个响应式对象state,用来保存当前右键菜单的可见状态及右键事件对象;
  2. 监听el-dropdown的visible-change事件,当右键菜单显示时,通过state.visible=true保存当前可见状态和右键事件对象,并触发上层组件的open事件,用于显示遮罩层或更改鼠标状态;当右键菜单关闭时,通过state.visible=false清除可见状态和右键事件对象,并触发上层组件的close事件;
  3. 监听el-dropdown的command事件,用于触发上层组件的command事件,返回所选中的命令值;
  4. 使用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组件。具体实现步骤如下:

  1. 定义

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:如何利用Vue3+Element Plus实现动态标签页及右键菜单 - Python技术站

(0)
上一篇 2023年6月27日
下一篇 2023年6月27日

相关文章

  • Win11 全新右键菜单获开发者支持,WinRAR 已完成适配:无须再忍受二级菜单

    Win11 全新右键菜单获开发者支持 Windows 11 在右键菜单方面进行了全面升级,增加了许多新的功能,如全局音量、Snip & Sketch 等。同时,微软还允许开发人员对右键菜单进行自定义,这意味着用户可以通过安装软件等方式获得更好的右键菜单体验。 步骤一:安装支持 Win11 右键菜单的软件 为了获得更好的右键菜单体验,用户需要安装支持 …

    other 2023年6月27日
    00
  • linux中ctrl+s的作用

    Linux中ctrl+s的作用 在Linux中,Ctrl+S不像其他组合键一样,其功能可能会让人迷惑。在本篇文章中,我们将解释Ctrl+S在Linux中的作用,它如何影响终端和如何恢复。 Ctrl+S的作用 当你按下 Ctrl+S 组合键时,它将请求终端停止所有输出。这是由于 Ctrl+S 与一个叫做特殊字符 XOFF 的控制字符相关联。当您按下该组合键时,…

    其他 2023年3月29日
    00
  • Python 3.5学习笔记(第一章)

    Python 3.5学习笔记(第一章) Python是一种易学的编程语言,强调简洁、易读和易维护的编码风格,适合初学者入门。本文将介绍Python 3.5的入门知识,让读者轻松掌握Python的基础知识。 安装Python 3.5 首先要了解Python 3.5的安装方法,可以在Python官网上(https://www.python.org/downloa…

    其他 2023年3月28日
    00
  • 91助手无法导入短信、通讯录、联系人等问题的解决方法

    下面是针对“91助手无法导入短信、通讯录、联系人等问题的解决方法”的完整攻略。 问题描述 在使用91助手备份和恢复手机数据的过程中,有些用户可能会遇到无法导入短信、通讯录、联系人等问题。这种情况一般比较头疼,因为我们经常需要这些信息来帮助我们联系朋友、同事或客户等。所以,解决这个问题非常必要。 解决方法 以下是解决这个问题的几个步骤: 步骤一:检查91助手版…

    other 2023年6月27日
    00
  • mysqlbinlogflashback5.6完全使用手册与原理

    mysqlbinlogflashback5.6完全使用手册与原理 简介 mysqlbinlogflashback 是一个基于 python 实现的用于回滚数据的命令行工具。在使用 mysql 数据库进行开发的过程中,由于不可避免地会出现误操作、数据错误等问题,需要进行数据回滚。mysqlbinlogflashback 能够根据 mysql 的 binlog …

    其他 2023年3月28日
    00
  • 如何创建 JavaScript 自定义事件

    下面是如何创建 JavaScript 自定义事件的完整攻略: 什么是 JavaScript 自定义事件 JavaScript 自定义事件是一种由开发者自行定义并触发的事件类型,可以在任何时候和地点触发,用于实现更加灵活的交互功能。 创建 JavaScript 自定义事件的步骤 1. 定义事件类型 首先我们需要定义一个事件类型,可以通过 new Event()…

    other 2023年6月25日
    00
  • js日期增加或减少一天

    以下是关于“JS日期增加或减少一天”的完整攻略,包括基本概念、解决方法、示例说明和注意事项。 基本概念 在JavaScript中,日期是一个内置对象,可以用于表示日期和时间。日期对象有许多方法,可以用于获取、设置和日期和时间。其中,增加或减少一天是常见的操作之一。 解决方法 以下是JS日期增加或减少一天的解决方法: 使用setDate()方法 使用getDa…

    other 2023年5月7日
    00
  • JavaWeb servlet实现下载与上传功能的方法详解

    JavaWeb servlet实现下载与上传功能的方法详解 本文将详细讲解如何使用JavaWeb servlet实现文件下载和上传功能,需要使用到servlet API和JavaIO的相关知识。 文件下载 实现概述 文件下载的实现是利用servlet向客户端提供文件资源,通常会涉及到以下步骤: 定义一个servlet来处理请求,并配置相关映射规则。 通过se…

    other 2023年6月26日
    00
合作推广
合作推广
分享本页
返回顶部