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

yizhihongxing

下面是详细的讲解。

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

相关文章

  • asp封装为DLL风火轮

    首先,需要了解ASP和DLL的概念,ASP是一种服务器端脚本语言,而DLL是一种动态链接库,可以存储可重用代码和数据,提高代码的复用性。 将ASP封装为DLL主要有以下步骤: 创建一个类库项目,例如使用Visual Studio创建C# Class Library项目。 编写类库的代码逻辑,例如编写一个实现风火轮加载效果的类,使用C# OO编程语言实现,可以…

    other 2023年6月25日
    00
  • 使用 Django 进行测试驱动开发

    使用 Django 进行测试驱动开发攻略 测试驱动开发(Test-Driven Development,TDD)是一种软件开发方法论,其中测试在开发过程中起到了至关重要的作用。在使用 Django 进行测试驱动开发时,我们可以按照以下步骤进行: 步骤一:编写测试用例 首先,我们需要编写测试用例来定义我们希望代码实现的功能。在 Django 中,我们可以使用内…

    other 2023年7月27日
    00
  • 详解Linux LVM逻辑卷配置过程(创建,增加,减少,删除,卸载)

    详解Linux LVM逻辑卷配置过程 什么是LVM? LVM是Linux Logical Volume Manager(逻辑卷管理器)的缩写。它是一种为Linux操作系统提供磁盘管理的方法,它允许用户将多个硬盘分区或整个硬盘组合成一个逻辑卷,从而为用户提供更加灵活的磁盘空间管理方式。 LVM的组成部分 LVM主要由三个部分组成: 物理卷(PV):即硬盘上的分…

    other 2023年6月27日
    00
  • VS2012开启右键菜单创建单元测试选项(Create Unit Tests) 的方法

    下面是详细讲解“VS2012开启右键菜单创建单元测试选项(Create Unit Tests) 的方法”的完整攻略。 1. 打开VS2012,创建项目 首先,打开VS2012,创建一个C#项目,比如一个控制台应用程序。 2. 安装“Microsoft Unit Test Adapter” 在VS2012中,安装并启用“Microsoft Unit Test …

    other 2023年6月27日
    00
  • 这些不常见的域名后缀 你怎么看?

    这些不常见的域名后缀 你怎么看? 简介 在互联网发展的过程中,域名后缀(也称为顶级域名)起到了标识和分类网站的作用。除了常见的域名后缀如.com、.net和.org之外,还存在一些不常见的域名后缀。这些不常见的域名后缀可能提供了更多的选择和个性化的机会,但同时也可能带来一些挑战和风险。 优点 1. 个性化 不常见的域名后缀可以帮助网站在众多网站中脱颖而出,展…

    other 2023年8月5日
    00
  • Win10 1607发布非安全累积更新KB4541329(附补丁+更新介绍)

    Win10 1607发布非安全累积更新KB4541329攻略 1. 更新介绍 Win10 1607发布非安全累积更新KB4541329是针对Windows 10版本1607的一个重要更新。该更新主要解决了一些已知的问题和改进了系统的稳定性和性能。以下是该更新的主要内容: 修复了一个导致系统在某些情况下出现蓝屏错误的问题。 优化了系统的性能,提高了系统的响应速…

    other 2023年8月3日
    00
  • Win10怎么自定义设置文件资源管理器打开位置?

    当我们在 Windows 10 中打开文件资源管理器时,默认情况下会打开“快速访问”页面,也就是最近访问和收藏的文件和文件夹列表。然而,我们可能希望直接打开某个特定的文件夹,比如我们项目常用的文件夹,这时就需要对文件资源管理器的默认打开位置进行自定义设置。以下是详细的攻略: 1. 打开文件资源管理器 首先,我们需要在 Windows 10 中打开文件资源管理…

    other 2023年6月27日
    00
  • Android10开发者常见问题(小结)

    Android 10开发者常见问题小结 在Android10中,我们面临着一些与之前版本不同的问题和挑战。本文将对Android 10开发者常见问题进行总结,并提供解决这些问题的方案。 1. 访问设备ID被禁止 在Android10中,应用无法访问设备IMEI号或序列号。如果您需要访问这些识别设备的唯一信息,您可以在设备上使用Android ID来代替。 以…

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