有一个vue的右键菜单的需求,先上网查了一下是否有插件,比如下面这个

1分钟Vue实现右键菜单

https://www.jb51.net/article/226761.htm

一顿操作之后,页面白屏,控制台报错,后来分析,大概应该是不适用vue3?

vue-contextmenu

关于这个插件在网上找了很多用法,都以失败告终。

还是自己动手造轮胎吧,正好也没做过这种东西。

先上效果图:

(仿windows桌面右键菜单,当然,没做快捷键功能)

还有个夜间主题:

思路:

内容大致分为两部分:

1、菜单列表

(1)数组数据,展示菜单项

(2)坐标控制显示

(3)显示开关

(4)子菜单

(5)定制主题

(6)下级菜单展示位置 处理

2、菜单项

(1)显示图标,文字,是否存在下级菜单(箭头)

(2)点击或禁用

(3)点击函数

(4)点击菜单项,关闭整个菜单后,执行对应函数

。。。。。。。。

代码如下:

RightMenu.vue

定义一个组件入口,规范并处理入参。

<template><div class="full" v-show="modelValue.status" style="position: fixed;top:0;left:0;user-select: none;" @contextmenu.prevent=""><div class="full" @click="handle_click" @contextmenu.prevent.stop="handle_click"></div><RightMenuList :setting="childInfo" :data="data" :theme="theme" :item-size="itemSize"></RightMenuList></div>
</template>
<script>
import RightMenuList from "@/view/rightmenu/RightMenuList";export default {name: "RightMenu",components: {RightMenuList},props: {data: Array,//菜单数据modelValue: Object,//设置入口theme: {//主题type: String,default: 'light',},},data() {return {itemSize: {width: 220,height: 30,},childInfo: {status: false,x: 0,y: 0,},}},watch: {modelValue(n) {if (n.status) {this.calculatePosition();}},},methods: {/*** 计算菜单生成位置*/calculatePosition() {let x = 0;let y = 0;let screen = this.getScreen();let childHeight = this.data.length * this.itemSize.height;if (screen.width - this.modelValue.x <= this.itemSize.width) {x = screen.width - this.itemSize.width;} else {x = this.modelValue.x;}if (screen.height - this.modelValue.y <= childHeight) {y = screen.height - childHeight-30;} else {y = this.modelValue.y;}this.childInfo = {status: true,x: x,y: y,}},/*** 获取窗口大小*/getScreen() {return {width: document.body.clientWidth,height: document.body.clientHeight,}},/*** 统一关闭菜单入口*/close() {this.childInfo = {status: false,x: 0,y: 0,}this.$event.$emit("RightMenuListClose");this.$emit('update:modelValue', {status: false, x: 0, y: 0});},/*** 单击空白地方,左右键通用*/handle_click(event) {this.close();setTimeout(() => {document.elementFromPoint(event.clientX, event.clientY).dispatchEvent(event);}, 10);},}}
</script>
<style scoped>
</style>

RightMenuList.vue

将主菜单列表,子菜单列表 抽象出来,作为一个菜单列表组件,该组件只负责根据指定坐标进行显示列表,隐藏。

<template><div  :class="'right_menu right_menu_'+theme" :style="{width:itemSize.width+'px',top:setting.y+'px',left:setting.x+'px'}" v-show="setting.status" ><template v-for="(item,index) in data" :key="'a'+index"><RightMenuItem  :data="item" :theme="theme" :top="setting.y+index*itemSize.height" :left="setting.x" :item-size="itemSize"></RightMenuItem><div v-if="item.outline" :class="'right_menu_outline right_menu_outline_'+theme"></div></template></div>
</template><script>
import RightMenuItem from "@/view/rightmenu/RightMenuItem";
export default {name: "RightMenuList",components: {RightMenuItem},props:{data:Array,theme:String,setting:Object,itemSize:Object,},mounted() {/*** 统一关闭入口*/this.$event.$on("RightMenuListClose",()=>{if(this.$parent.closeChild)this.$parent.closeChild();});},methods:{close() {this.$parent.close();},},
}
</script><style scoped>
.right_menu{box-shadow: 1px 1px 8px 2px rgba(0, 0, 0, 0.3);position: fixed;padding: 4px 2px;
}
.right_menu_light{background: #f3f3f3;
}
.right_menu_dark{border: 1px solid #bbbbbb;background: #282828;
}
.right_menu_outline{width: 90%;height: 1px;margin:3px 0 3px 5%;
}
.right_menu_outline_light{background: #aaaaaa;
}
.right_menu_outline_dark{background: #bbbbbb;
}</style>

RightMenuItem.vue

将菜单项抽象为一个组件,主要负责展示图片文字,点击事件,是否禁用等功能,

如果该菜单项下存在子菜单项,则要负责计算子菜单显示的坐标,也需要控制子菜单的显示和隐藏

<template><button ref="item" v-if="data.child&&data.child.length>0":class="`empty_button right_item right_item_${theme} ${!isEnable()?'right_item_enable_'+theme:''}`"@mouseenter="handle_enter"@mouseleave="handle_leave":style="{height:itemSize.height+'px' }"><RightMenuItemIcon :icon="data.icon" :theme="theme"></RightMenuItemIcon>{{ data.name }}<b-icon v-if="theme==='light'" class="right_item_arrow" local="arrow_thick_right" style="color: #3b3b3b;"></b-icon><b-icon v-else class="right_item_arrow" local="arrow_thick_right" style="color: #adadad;"></b-icon><RightMenuList v-if="data.child&&data.child.length>0"   :setting="childInfo" :data="data.child" :theme="theme" :item-size="itemSize"></RightMenuList></button><button v-else:class="`empty_button right_item right_item_${theme}  ${!isEnable()?'right_item_enable_'+theme:''}`"@click="handle_click":style="{height:itemSize.height+'px'}"><RightMenuItemIcon :icon="data.icon" :theme="theme"></RightMenuItemIcon>{{ data.name }}</button>
</template><script>
import RightMenuItemIcon from "@/view/rightmenu/RightMenuItemIcon";export default {name: "RightMenuItem",components: {RightMenuItemIcon},beforeCreate() {this.$options.components.RightMenuList = require('@/view/rightmenu/RightMenuList').default},props: {data: Object,theme: String,itemSize:Object,top:Number,left:Number,},data() {return {childPosition: "",childInfo: {status: false,x: 0,y: 0,},cancelTimer: null,}},methods: {/*** 鼠标进入菜单项时,计算子菜单展示的位置*/handle_enter() {let x = 0;let y = 0;let screen = this.getScreen();let item = this.$refs.item;let itemX = this.left;//当前菜单项的x坐标let itemY = this.top;//当前菜单项的y坐标let childHeight = this.data.child.length * item.clientHeight;//计算坐标xif ((screen.width - itemX - item.clientWidth) > item.clientWidth) {x = itemX + item.clientWidth;this.childPosition = "right";} else if (itemX > item.clientWidth) {x = itemX - item.clientWidth;}if (this.childPosition === "") this.childPosition = "left";//计算坐标yif ((screen.height - itemY) > childHeight) {y = itemY+10;} else if (screen.height > childHeight) {y = screen.height - childHeight-20;}this.noCloseChild();this.childInfo = {status: true,x: x,y: y,}},/*** 鼠标离开时,判断从哪个方向离开* @param e*/handle_leave() {this.noCloseChild();this.cancelTimer = setTimeout(() => {this.closeChild();}, 100);},/*** 获取窗口大小*/getScreen() {return {width: document.body.clientWidth,height: document.body.clientHeight,}},isEnable(){return this.data.enable!==false;},/*** 处理点击事件,先关闭按钮,在处理点击事件*/handle_click() {if(!this.isEnable())return;this.close();setTimeout(() => {if(this.data.click)this.data.click();}, 10);},/*** 通知整个菜单关闭*/close() {this.$parent.close();},/*** 关闭子菜单*/closeChild() {this.childInfo = {status: false,x: 0,y: 0,}this.childPosition = "";},/*** 取消关闭子菜单*/noCloseChild() {clearTimeout(this.cancelTimer);this.cancelTimer = null;},}
}
</script><style scoped>
.right_item{display: block;width: 100%;text-align: left;padding-left: 5px;font-size: 15px;white-space: nowrap;text-overflow:ellipsis;overflow: hidden;
}.right_item_light {font-size: 15px;
}.right_item_light:hover {background-color: #ffffff;
}.right_item_dark {color: #e2e2e2;font-size: 13px;
}.right_item_dark:hover {background-color: #444444;
}
.right_item_enable_light{color: #b6b6b6;
}
.right_item_enable_dark{color: #797979;
}.right_item_arrow {width: 25px;height: 25px;float: right;
}
</style>

RightMenuItemIcon.vue

这里将菜单项的展示图标单独抽象出来,为的是兼容多模式展示。可以自行定义。如base64编码,http地址,图片文件,svg代码,空白,还有根据不同主题显示不同类型的图标等等。

<template><img class="right_item_icon right_item_icon_blank" v-if="!icon||!icon.type" ><img class="right_item_icon" v-else-if="icon.type==='url'" :src="icon.value" ><b-icon class="right_item_icon" v-else-if="theme==='light'&& icon.type==='name'" :local="icon.value" style="color: black;"></b-icon><b-icon class="right_item_icon" v-else-if="theme==='dark'&& icon.type==='name'" :local="icon.value" style="color: white;"></b-icon><b-icon class="right_item_icon" v-else-if="theme==='light'&& icon.type==='type'" :type="icon.value" style="color: black;"></b-icon><b-icon class="right_item_icon" v-else-if="theme==='dark'&& icon.type==='type'" :type="icon.value" style="color: white;"></b-icon><img class="right_item_icon right_item_icon_blank" v-else >
</template><script>
export default {name: "RightMenuItemIcon",props:{icon:Object,theme: String,},
}
</script><style scoped>
.right_item_icon{width: 18px;height: 18px;margin-top: -3px;
}
.right_item_icon_blank{opacity: 0;
}</style>

* b-icon是自定义的一个svg处理组件,可以删除,修改。

一共四个文件,可以直接删去最后这个文件,不使用。

测试用例:

<template><div><div style="height: 100px;background: #1ba3bf;"></div><div class="full"  @contextmenu.prevent="showRightMenu" ></div><RightMenu v-model="menuSetting" :data="data" theme="light"></RightMenu></div>
</template><script>
import RightMenu from "@/view/rightmenu/RightMenu";export default {name: "RightMenuTestPane",components: {RightMenu},data(){return{menuSetting:{status:false,x:0,y:0,},data:[{name:'查看(V)',click:()=>{alert("查看(V)");}},{name:'排序方式(O)',click:()=>{alert("排序方式(O)");},},{name:'刷新(E)',outline:true,click:()=>{alert("刷新(E)");}},{name:'粘贴(P)',enable:false,click:()=>{alert("刷新(E)");}},{name:'粘贴快捷方式(S)',enable:false,outline:true,click:()=>{alert("刷新(E)");}},{name:'新建(W)',outline:true,child:[{name:'文件夹(F)',icon:{type:'url',value:require("@/assets/file/dir.png"),},},{name:'快捷方式(S)',icon:{type:'url',value:require("@/assets/rightmenu/shortcut.png"),},outline:true,},{name:'Microsoft Word 文档',icon:{type:'url',value:'https://docs.idqqimg.com/tim/docs/docs-design-resources/pc/png@2x/file_web_doc_64@2x-77242f419d.png',},},{name:'Microsoft PowerPrint 演示文稿',icon:{type:'url',value:'data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48bGluZWFyR3JhZGllbnQgaWQ9ImEiIHgxPSIwJSIgeDI9IjEwMCUiIHkxPSIwJSIgeTI9IjEwMCUiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2Y1ODQ2YSIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2U2NWUyZSIvPjwvbGluZWFyR3JhZGllbnQ+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48cmVjdCBmaWxsPSJ1cmwoI2EpIiBoZWlnaHQ9IjI0IiByeD0iMiIgd2lkdGg9IjI0Ii8+PGcgZmlsbD0iI2ZmZiI+PHBhdGggZD0iTTExIDYuMDE5VjEyaDYuOTgxYTYuNSA2LjUgMCAxMS02Ljk4LTUuOTgyeiIvPjxwYXRoIGQ9Ik0xMyA1LjAxOWE2LjUwNCA2LjUwNCAwIDAxNS44MjYgNC45OEwxMyAxMHoiIG9wYWNpdHk9Ii42Ii8+PC9nPjwvZz48L3N2Zz4=',},},{name:'文本文档',icon:{type:'url',value:'data:image/svg+xml;base64,PHN2ZyAgc3R5bGU9Im92ZXJmbG93OiBoaWRkZW47IiB2aWV3Qm94PSIwIDAgMTAyNCAxMDI0IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgcC1pZD0iMjIzMSI+PHBhdGggZD0iTTcyNi42MjQgNjRMODY0IDIwMS4zNzZWOTYwSDE2MFY2NGg1NjYuNjI0eiBtLTI0LjI1NiAzMkgxOTJ2ODMyaDY0MFYyMjkuOTJoLTY1LjZhNjQgNjQgMCAwIDEtNjQtNjRMNzAyLjM2OCA5NnoiIGZpbGw9IiM2NDZFN0YiIHAtaWQ9IjIyMzIiPjwvcGF0aD48cGF0aCBkPSJNMzUyIDM4NHYtNjRoMzIwdjY0aC0xMjh2MzIxLjc2aC02NFYzODRoLTEyOHoiIGZpbGw9IiMxNkIyQkMiIHAtaWQ9IjIyMzMiPjwvcGF0aD48L3N2Zz4=',},},{name:'Microsoft Excel 工作表',icon:{type:'url',value:'https://pub.idqqimg.com/pc/misc/files/20200904/2eb030216d9362bbc6c0df045857b718.png',},},],},{name:'显示设置(D)',icon:{type:'url',value:require("@/assets/rightmenu/viewsetting.png"),},click:()=>{alert("显示设置(D)");}},{name:'个性化(R)',icon:{type:'url',value:require("@/assets/rightmenu/individuation.png"),},click:()=>{alert("个性化(R)");}}],}},mounted() {},methods:{showRightMenu(e){this.menuSetting={status:true,x:e.clientX,y:e.clientY,}},}
}
</script><style scoped></style>

API

入参

使用方式(属性名) 解释 类型
v-model 显示状态,坐标 Object
:data 菜单数据 Array
theme 主题名 String

v-model  菜单设置

参数名 解释 类型
status 显示状态 Boolean
x 横坐标 Number
y 竖坐标 Number

:data    数组类型,数组项内容如下

参数名 解释 类型
name 菜单名称 String
icon type   图标类型 String
value   值 String
click 点击事件 function
outline

该菜单项下面是否显示分割线,默认true

Boolean
enable 是否可点击,默认true Boolean
child 子菜单数据数组 Array

theme  主题

枚举 解释
light 亮色主题
dark 暗色主题

自定义主题,可以在代码中仿照已有的两个主题样式 新增自定义css样式即可。

遇到问题请提问

动手做一个 vue 右键菜单相关推荐

  1. vue 右键菜单插件 简单、可扩展、样式自定义的右键菜单

    今天分享的不是技术,今天给大家分享个插件,针对现有的vue右键菜单插件,大多数都是需要使用插件本身自定义的标签,很多地方不方便,可扩展性也很低,所以我决定写了一款自定义指令调用右键菜单(vuerigh ...

  2. 开关面板如何自己印字_如何自己动手做一个智能开关

    现在的智能家居这么火,对于想自己动手的小伙伴们来说,都想自己去做一些家里使用 的智设备.现在的中国不缺卖唱卖惨的,缺的是能动手创造一些能实际使用的而不是哗众取宠的人,天天喊着要反击外国技术封锁.那么我 ...

  3. iOS动手做一个直播app开发(代码篇)

    iOS动手做一个直播app开发(代码篇) ###开篇 好久没写简书,因为好奇的我跑去学习直播了,今天就分享一下我的感慨. 目前为止直播还是比较热点的技术的,简书,git上有几篇阅读量和含金量都不错的文 ...

  4. 直播网站源码直播平台软件开发iOS动手做一个直播(原理篇)

    直播网站源码直播平台软件开发iOS动手做一个直播(原理篇) 上篇文章主要给出了代码,但是并没有详细说明直播相关的知识,这篇文章就说一下直播的相关理论知识.附上直播代码篇地址. ###推流 腾讯直播平台 ...

  5. 做自己的PHP语法解释器,PHP语言之自己动手做一个SQL解释器

    本文主要向大家介绍了PHP语言之自己动手做一个SQL解释器,通过具体的内容向大家展示,希望对大家学习php语言有所帮助. 这是从别的地方看到的,俺还不会写这么无聊的东西 class DB_text { ...

  6. 自己动手做一个小爱同学温湿度传感器(成本八块左右)

    自己动手做一个小爱同学温湿度传感器 1.开发环境简介 2.开发思路 3.程序编写 (1)将点灯科技库文件和DHT11模块库文件导入Arduino的libraries文件夹. (2)下载点灯科技APP, ...

  7. 动手做一个简单的智能小车

    动手做一个简单的智能小车 来到CNDN一年了,看到了许多大佬的杰出作品.也该写点什么来回馈给大家了前不久接触了单片机,想提前进行实践一下所以有想法做一个实体出来,想来想去难的怕自己搞不定,但是还好找到 ...

  8. arduino智能浇花系统_解放双手!自己动手做一个简易智能浇花系统

    原标题:解放双手!自己动手做一个简易智能浇花系统 面对疫情,宅在家的我们可以以各种方式为战"疫"一线的医护工作者.紧急研究病毒的科研人员.口罩厂日夜工作的人们......加油打气. ...

  9. 动手做一个自组网的网络 - 操作系统内核

    动手做一个自组网的网络 - 操作系统内核 动手做一个自组网的网络 - 项目介绍 动手做一个自组网的网络 - 硬件开发板 动手做一个自组网的网络 - 操作系统内核 动手做一个自组网的网络 - 网络协议栈 ...

最新文章

  1. 【CVPR 2022】只用一张图+相机走位,AI就能脑补周围环境
  2. 北京的CCIE考试面试变成中文了
  3. JSP 中的Cookie
  4. 在Python中,如何确定对象是否可迭代?
  5. BFS Sicily 1215: 脱离地牢
  6. checksum linux 命令_linux常用命令总结
  7. Linux sed替换内容中有空格解决办法
  8. [HIHO1323]回文字符串(区间dp)
  9. lua实现多继承-方式1
  10. (五)JS基础知识二(通过图理解原型和原型链)【三座大山之一,必考!!!】
  11. Angular自定义structural指令的实例化过程以及set方法的调用
  12. Java实现qq截图工具
  13. AcWing 4801 选数(二维费用背包的建立)
  14. emacs在windows下打开报错原因
  15. 李开复:无人驾驶必须一步到位,没有所谓的人机协同
  16. Python爬虫从入门到精通:(14)验证码识别_Python涛哥
  17. 23种设计模式——适配器模式
  18. python手机连点器代码_【触动精灵】手机万能连点器 Lua 源码
  19. 看懂555定时器,有哪些应用?
  20. 修改DarkNet的weights文件以编辑模型版本号

热门文章

  1. 如何把antlr4融合到编译器项目中使用
  2. html如何绘制弧形,用CSS画弧形
  3. c语言输出换行字符,float_printf格式换行_c语言printf里如何换行
  4. Python百度ai识别图片表格
  5. SUSE系统修改静态IP
  6. enclosing的意思_enclosing class是什么意思
  7. 双目立体视觉技术的内容
  8. 《小迪安全》第14天 SQL注入:注入类型及提交注入
  9. 2021 最受欢迎的九大顶级 Java 框架
  10. webrtc直播服务 licode docker 外网安装