动手做一个 vue 右键菜单
有一个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 右键菜单相关推荐
- vue 右键菜单插件 简单、可扩展、样式自定义的右键菜单
今天分享的不是技术,今天给大家分享个插件,针对现有的vue右键菜单插件,大多数都是需要使用插件本身自定义的标签,很多地方不方便,可扩展性也很低,所以我决定写了一款自定义指令调用右键菜单(vuerigh ...
- 开关面板如何自己印字_如何自己动手做一个智能开关
现在的智能家居这么火,对于想自己动手的小伙伴们来说,都想自己去做一些家里使用 的智设备.现在的中国不缺卖唱卖惨的,缺的是能动手创造一些能实际使用的而不是哗众取宠的人,天天喊着要反击外国技术封锁.那么我 ...
- iOS动手做一个直播app开发(代码篇)
iOS动手做一个直播app开发(代码篇) ###开篇 好久没写简书,因为好奇的我跑去学习直播了,今天就分享一下我的感慨. 目前为止直播还是比较热点的技术的,简书,git上有几篇阅读量和含金量都不错的文 ...
- 直播网站源码直播平台软件开发iOS动手做一个直播(原理篇)
直播网站源码直播平台软件开发iOS动手做一个直播(原理篇) 上篇文章主要给出了代码,但是并没有详细说明直播相关的知识,这篇文章就说一下直播的相关理论知识.附上直播代码篇地址. ###推流 腾讯直播平台 ...
- 做自己的PHP语法解释器,PHP语言之自己动手做一个SQL解释器
本文主要向大家介绍了PHP语言之自己动手做一个SQL解释器,通过具体的内容向大家展示,希望对大家学习php语言有所帮助. 这是从别的地方看到的,俺还不会写这么无聊的东西 class DB_text { ...
- 自己动手做一个小爱同学温湿度传感器(成本八块左右)
自己动手做一个小爱同学温湿度传感器 1.开发环境简介 2.开发思路 3.程序编写 (1)将点灯科技库文件和DHT11模块库文件导入Arduino的libraries文件夹. (2)下载点灯科技APP, ...
- 动手做一个简单的智能小车
动手做一个简单的智能小车 来到CNDN一年了,看到了许多大佬的杰出作品.也该写点什么来回馈给大家了前不久接触了单片机,想提前进行实践一下所以有想法做一个实体出来,想来想去难的怕自己搞不定,但是还好找到 ...
- arduino智能浇花系统_解放双手!自己动手做一个简易智能浇花系统
原标题:解放双手!自己动手做一个简易智能浇花系统 面对疫情,宅在家的我们可以以各种方式为战"疫"一线的医护工作者.紧急研究病毒的科研人员.口罩厂日夜工作的人们......加油打气. ...
- 动手做一个自组网的网络 - 操作系统内核
动手做一个自组网的网络 - 操作系统内核 动手做一个自组网的网络 - 项目介绍 动手做一个自组网的网络 - 硬件开发板 动手做一个自组网的网络 - 操作系统内核 动手做一个自组网的网络 - 网络协议栈 ...
最新文章
- 【CVPR 2022】只用一张图+相机走位,AI就能脑补周围环境
- 北京的CCIE考试面试变成中文了
- JSP 中的Cookie
- 在Python中,如何确定对象是否可迭代?
- BFS Sicily 1215: 脱离地牢
- checksum linux 命令_linux常用命令总结
- Linux sed替换内容中有空格解决办法
- [HIHO1323]回文字符串(区间dp)
- lua实现多继承-方式1
- (五)JS基础知识二(通过图理解原型和原型链)【三座大山之一,必考!!!】
- Angular自定义structural指令的实例化过程以及set方法的调用
- Java实现qq截图工具
- AcWing 4801 选数(二维费用背包的建立)
- emacs在windows下打开报错原因
- 李开复:无人驾驶必须一步到位,没有所谓的人机协同
- Python爬虫从入门到精通:(14)验证码识别_Python涛哥
- 23种设计模式——适配器模式
- python手机连点器代码_【触动精灵】手机万能连点器 Lua 源码
- 看懂555定时器,有哪些应用?
- 修改DarkNet的weights文件以编辑模型版本号