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

相关文章

  • elasticdump离线安装

    Elasticdump离线安装攻略 Elasticdump是一个用于将Elasticsearch数据导入和导出的工具。在某些情况下,我们可能需要在没有网络连接的情况下安装Elasticdump。本攻略将介绍如何在离线环境中安装Elasticdump。 步骤一:下载Elasticdump 首先,我们需要在有网络连接的环境中下载Elasticdump的安装包。我…

    other 2023年5月9日
    00
  • Win10累积更新补丁KB3211320下载地址 (32位+64位)

    Win10累积更新补丁KB3211320下载地址 (32位+64位)攻略 简介 Win10累积更新补丁KB3211320是微软发布的一个重要的安全补丁,用于修复系统中的漏洞和提升系统的稳定性。本攻略将详细介绍如何下载和安装该补丁。 步骤 打开浏览器,进入微软官方网站。 在搜索框中输入“KB3211320下载”。 在搜索结果中找到微软官方的下载页面,并点击进入…

    other 2023年7月28日
    00
  • js获取当前位置的地理坐标(经纬度)

    js获取当前位置的地理坐标(经纬度) 在现代的Web应用中,获取用户当前位置的地理坐标是十分普遍的需求。通过JavaScript API可以轻松地获取用户的经纬度信息,从而实现更加精准和个性化的服务。 获取地理位置 使用JavaScript API获取用户位置信息的主要接口是 Geolocation API,该API提供了三个主要的方法: getCurren…

    其他 2023年3月28日
    00
  • iconfont-阿里巴巴矢量图标库

    iconfont-阿里巴巴矢量图标库 简介 Iconfont是阿里巴巴矢量图标库,是一个集成矢量图标和图标管理的平台。Iconfont包含大量的开源图标库和自由上传的图标库,其中包括主流的字体图标库,用户可以不需要下载文件,直接通过链接、HTML代码、SVG等方式使用这些图标。 特点 矢量图标:图标可无限放大缩小而不失真。 多种格式:提供多种格式供使用,如字…

    其他 2023年3月29日
    00
  • A、B、C类IP地址的具体划分方法及同一个子网的判断方法

    A、B、C类IP地址的具体划分方法 IP地址是用于在互联网上唯一标识设备的一组数字。根据IP地址的前几位,可以将其分为A、B、C类。下面是每个类别的具体划分方法: A类IP地址:A类IP地址的第一个字节范围是1到126。A类地址的网络部分占用了8个比特位,而主机部分占用了24个比特位。这意味着A类地址可以支持最多的网络数量,每个网络可以容纳最多1677721…

    other 2023年7月29日
    00
  • 浅谈Linux文件目录介绍及文件颜色区别

    浅谈Linux文件目录介绍及文件颜色区别 Linux操作系统采用了树形结构来管理文件和目录,这一结构称为文件系统。Linux的文件系统被组织成一颗以根目录(/)为顶级节点的树。在Linux系统中,对文件和目录的访问和操作是通过在树形结构中导航、查找和选择所需文件和目录来完成的。本文将介绍Linux文件系统中常用的文件目录及其作用,并解释不同文件颜色代表的意义…

    other 2023年6月26日
    00
  • 服务端配置实现AJAX跨域请求

    要实现AJAX跨域请求,需要在服务端进行配置。以下是实现AJAX跨域请求的完整攻略: 步骤一:使用CORS(跨域资源共享) CORS(Cross-Origin Resource Sharing)是W3C标准,用于跨域请求资源。通过CORS的配置,服务端允许客户端跨域访问资源。在服务端的响应头中添加如下代码即可实现CORS: Access-Control-Al…

    other 2023年6月27日
    00
  • 详解Python函数作用域的LEGB顺序

    详解Python函数作用域的LEGB顺序 在Python中,函数作用域是指变量的可见性和访问性。Python使用LEGB规则来确定变量的作用域,即Local(局部)、Enclosing(嵌套)、Global(全局)和Built-in(内置)的顺序。下面将详细解释每个作用域的含义和查找顺序。 Local(局部)作用域 局部作用域是指在函数内部定义的变量。这些变…

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