记录--vue中封装一个右键菜单组件(复制粘贴即可使用)
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助
组件介绍
关于web端的右键功能常用的地方有表格的右键,或者tab标签的右键等,本文记录一下封装一个右键菜单组件的思路步骤代码。
程序员除了会用轮子,还要尝试去贴合自己公司业务场景造轮子。
组件效果图
我们先看一下右键组件的效果图
组件分析
1.封装组件第一步考虑dom结构
我们观察这个右键菜单,可以明白右键菜单就是一个ul
标签包裹着很多li
标签的弹出层组件,如下图:
每一行都是一个li,每一行中包含图标
和行按钮
名称文字,于是我们的dom结构
可以这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | < ul class="table-right-menu"> <!-- 每个li都是一行,循环菜单数据,菜单数据后面再设计 --> < li v-for="item in menulists" :key="item.btnName" @click.stop="fnHandler(item)" > < div class="table-right-menu-item-btn"> <!-- 图标和按钮名 --> < i class="el-icon-ele" /> < span >复制数据</ span > </ div > </ li > </ ul > |
2.dom结构搞清楚了,接下来就是考虑右键菜单组件接收的参数
如何考虑菜单组件接收哪些参数呢?
主要是想组件中会使用到哪些变量。如下:
- 右键菜单需要一个数组,数组中存放的是每个菜单项的数据(菜单项图标、菜单项按钮名字、当然还有一些其他的需要传递的参数,统一挂在一个变量身上,如params)
- 其次右键菜单组件的触发时机是拥挤点击右键的时候,那我们就得知道,用户右键点击的位置x、y的距离,所以这里还需要参数position中的x和y去记录距离视口的clientX和clientY值,因为右键菜单的位置就以这个作基准
- 同时,我们还需要知道用户点击的是哪个菜单项按钮,所以再加一个事件名参数进去
综上所述,我们可以设计右键点击时,要给右键菜单组件传递的参数信息如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | this.rightclickInfo = { position: { x: event.clientX, y: event.clientY, }, menulists: [ { fnName: "copy", // 事件名字,组件届时可this.$emit(fnName)抛出事件 params: xxx, // 参数,组件届时可this.$emit(fnName,params)抛出事件,并携带参数 icoName: "el-icon-document-copy", // 图标名 btnName: "复制数据", // 菜单项按钮名 // 这三项是发散,可往下看 // divided: true, // 是否禁用 // disabled: true, // 是否带分隔线 // children: [], // 是否有子菜单(递龟) }, { fnName: "look", params: { row, column, event }, icoName: "el-icon-view", btnName: "查看行数据", }, ], }; |
注意,上述参数代码示例中,多了三个参数divided、disabled、children,实际上,参数的设计要结合业务场景,我司的需求没有右键菜单禁用项,也不用有分割线,以及没有右键菜单的子菜单,所以封装组件就暂时没有加上这三个参数。
组件化、模块化的同时,主要高内聚,一个组件满足业务需求,精简为主,不可无节制的死命封装,否则就变成了
诗山代码
了,当然大家也可以仿照真正右键菜单去加功能,比如右键菜单可以绑定快捷键、改成递归形式等更多功能...
所以组件props中接收参数可以写成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | props: { // 接收右键点击的信息 rightclickInfo: { type: Object, default: () => { return { position: { // 右键点击的位置 x: null, y: null, }, menulists: [ { fnName: "", // 点击菜单项的事件名 params: {}, // 点击的参数 icoName: "", // 图标名 btnName: "", // 按钮名 }, ], }; }, }, }, |
3.实现右键打开菜单弹出层,左键点击一下菜单弹出层就关闭了
不难发现,只要一右键菜单就弹出,点一下菜单消失,这种不停的显示和消失,去不停的v-if
就不合适了,所以这里可以从v-show
的角度出发
- 一开始让菜单层隐藏
display:none
,而后再设置成dispaly:block
- 当右键点击时,右键点击的位置参数
position
的x和y
的值就会发生变化 - 我们可以
watch
监听这个变化,position的x、y
值变了,说明右键点击了 - 右键点击了,我们就可以让菜单弹出层出现
- 同时,需要监听鼠标点击事件,当点击的是右键或者中间滚轮键时,不去隐藏面板,点击的是左键时,才去隐藏面板
通过上述五点,我们即做到了显示隐藏菜单面板了
4.监听右键位置变化,显示菜单项代码
这一块的思路请看代码中注释即可,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | .table-right-menu { dispaly:none; // 初始为隐藏,监听更改显示 } watch: { // 监听右键点击时点击位置的变化,只要变化了,就弹出右键菜单供用户点击操作 "rightclickInfo.position"(val) { let x = val.x; // 获取x轴坐标 let y = val.y; // 获取y轴坐标 let innerWidth = window.innerWidth; // 获取页面可是区域宽度,即页面的宽度 let innerHeight = window.innerHeight; // 获取可视区域高度,即页面的高度 /** * 注意,这里要使用getElementsByClassName去选中对应dom,因为右键菜单组件可能被多处使用 * classIndex标识就是去找到对应的那个右键菜单组件的,需要加的 * */ let menu = document.getElementsByClassName("table-right-menu")[this.classIndex]; menu.style.display = "block"; // 由隐藏改为显示 let menuHeight = this.rightclickInfo.menulists.length * 30; // 菜单容器高 let menuWidth = 180; // 菜单容器宽 // 菜单的位置计算(边界留点间隙空间) menu.style.top = (y + menuHeight > innerHeight ? innerHeight - menuHeight : y) + "px"; menu.style.left = (x + menuWidth > innerWidth ? innerWidth - menuWidth : x) + "px"; // 因为菜单还要关闭,就绑定一个鼠标点击事件,通过e.button判断点击的是否是左键,左键关闭菜单 document.addEventListener("mouseup", this.hide, false); }, }, hide(e) { if (e.button === 0) { // 0是左键、1是滚轮按钮或中间按钮(若有)、2鼠标右键 let menu = document.querySelector(".table-right-menu"); menu.style.display = "none"; // 菜单关闭 document.removeEventListener("mouseup", this.hide); // 及时解绑监听事件 } }, |
事件绑定后别忘了解绑 document.removeEventListener("mouseup", this.hide);
5.知识点回顾e.button
e.button
,鼠标事件- 返回一个数字,表示触发鼠标事件的是按下了哪个按钮
- 值为只读,不可修改
具体返回数字值,表示鼠标事件发生时按下的鼠标按钮。
可能的值:
0:鼠标左键、 1:滚轮按钮或中间按钮(如果有)、 2:鼠标右键
IE8返回有一些不同:1:鼠标左键、 2:鼠标右键、 4:滚轮按钮或中间按钮(如果有)
注意:左手鼠标,返回值相反
6.组件中的事件要抛出去哦
item即为循环的菜单项,包含事件名、参数、图标名、按钮名
1 2 3 4 5 | fnHandler(item) { this.$emit(item.fnName, item.params); // 事件再传出去,即为: // this.$emit('事件名',事件参数) }, |
7.外界接收事件,正常@xxx='xxx'使用即可
如下:
1 2 3 4 5 6 7 8 | < my-right-menu :rightclickInfo="rightclickInfo" @copy="copy" @look="look" @edit="edit" @delete="deleteFn" @refresh="refresh" ></ my-right-menu > |
使用组件
搭配el-table使用
-
el-table中可以使用封装好的事件:
@row-contextmenu="xxx"
-
然后在xxx方法中去传递参数给右键菜单组件即可,如下简化代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | < el-table :data="tableData" @row-contextmenu="rightclick" > ... </ el-table > < my-right-menu :rightclickInfo="rightclickInfo" @copy="copy" ></ my-right-menu > rightclickInfo:{} // 饿了么UI封装好的右键菜单事件,可直接使用,有行数据,列数据,以及事件 rightclick(row, column, event) { this.rightclickInfo = { position: { x: event.clientX, y: event.clientY, }, menulists: [ { fnName: "copy", params: { row, column, event }, icoName: "el-icon-document-copy", btnName: "复制数据", }, ], }; event.preventDefault(); // 阻止默认的鼠标右击事件 }, |
event.preventDefault()
要加上,阻止默认的右键菜单事件
搭配普通dom使用
也同理,传参的时,需要阻止默认时间,如下:
1 2 3 4 5 6 | <!-- 右键菜单搭配普通的dom元素使用,普通的dom元素需要阻止默认右键事件,即.prevent --> < div class="normalDom" @contextmenu.prevent="onContextmenu">区域内右键</ div > onContextmen(){ // 定义参数传递给my-right-menu组件 } |
完整代码
复制粘贴即可使用哦
使用组件代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | < template > < div > < h5 >表格内右键</ h5 > < br /> <!-- 右键菜单搭配el-table使用 --> < el-table border :data="tableData" style="width: 100%" @row-contextmenu="rightclick" > < el-table-column prop="name" label="姓名"> </ el-table-column > < el-table-column prop="age" label="年龄"> </ el-table-column > < el-table-column prop="home" label="家乡"> </ el-table-column > < el-table-column prop="hobby" label="爱好"> </ el-table-column > </ el-table > < br /> < br /> < br /> <!-- 右键菜单搭配普通的dom元素使用,普通的dom元素需要阻止默认右键事件,即.prevent --> < div class="normalDom" @contextmenu.prevent="onContextmenu">区域内右键</ div > <!-- 右键菜单 --> < my-right-menu :class-index="0" :rightclickInfo="rightclickInfo" @copy="copy" @look="look" @edit="edit" @delete="deleteFn" @refresh="refresh" ></ my-right-menu > </ div > </ template > < script > export default { name: "myRightMenuName", data() { return { tableData: [ { id: "1", name: "孙悟空", age: 500, home: "花果山水帘洞", hobby: "桃子", }, { id: "2", name: "猪八戒", age: 88, home: "高老庄", hobby: "肉包子", }, { id: "3", name: "沙和尚", age: 500, home: "通天河", hobby: "游泳", }, { id: "4", name: "唐僧", age: 1000, home: "东土大唐", hobby: "吃斋念经", }, ], rightclickInfo: {}, }; }, methods: { // 饿了么UI封装好的右键菜单事件,可直接使用 rightclick(row, column, event) { this.rightclickInfo = { position: { x: event.clientX, y: event.clientY, }, menulists: [ { fnName: "copy", params: { row, column, event }, icoName: "el-icon-document-copy", btnName: "复制数据", // divided: true, // disabled: true, // children: [], }, { fnName: "look", params: { row, column, event }, icoName: "el-icon-view", btnName: "查看行数据", }, { fnName: "edit", params: { row, column, event }, icoName: "el-icon-edit", btnName: "编辑行数据", }, { fnName: "delete", params: { row, column, event }, icoName: "el-icon-delete", btnName: "删除行数据", }, { fnName: "refresh", params: { row, column, event }, icoName: "el-icon-refresh", btnName: "刷新页面", }, ], }; event.preventDefault(); // 阻止默认的鼠标右击事件 }, copy(params) { console.log( "copy", params.row ? params.row[params.column.property] : params ); }, look(params) { console.log("look", params.row ? JSON.stringify(params.row) : params); }, edit(params) { console.log("edit", params); }, deleteFn(params) { console.log("deleteFn", params.row ? params.row.id : params); }, refresh(params) { console.log("refresh 刷新页面啦"); }, // 普通dom右键 onContextmenu(e) { this.rightclickInfo = { position: { x: e.clientX, y: e.clientY, }, menulists: [ { fnName: "copy", params: "代码修仙", icoName: "el-icon-star-on", btnName: "代码修仙", }, { fnName: "look", params: "路漫漫", icoName: "el-icon-star-off", btnName: "路漫漫", }, ], }; }, }, }; </ script > < style > .normalDom { width: 240px; height: 240px; line-height: 240px; text-align: center; border: 6px dotted pink; font-family: "楷体", Courier, monospace; font-weight: 600; } </ style > |
封装组件代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | < template > < ul class="table-right-menu"> <!-- 循环菜单项,事件带参数抛出 --> < li v-for="item in rightclickInfo.menulists" :key="item.btnName" class="table-right-menu-item" @click.stop="fnHandler(item)" > < div class="table-right-menu-item-btn"> <!-- 图标和按钮名 --> < i :class="item.icoName" class="iii" /> < span >{{ item.btnName }}</ span > </ div > </ li > </ ul > </ template > < script > export default { name: "myRightMenu", props: { // 接收右键点击的信息 rightclickInfo: { type: Object, default: () => { return { position: { // 右键点击的位置 x: null, y: null, }, menulists: [ { fnName: "", // 点击菜单项的事件名 params: {}, // 点击的参数 icoName: "", // 图标名 btnName: "", // 按钮名 }, ], }; }, }, // 重要参数,用于标识是哪个右键菜单dom元素 classIndex: { type: Number, default: 0, }, }, watch: { // 监听右键点击时点击位置的变化,只要变化了,就弹出右键菜单供用户点击操作 "rightclickInfo.position"(val) { let x = val.x; // 获取x轴坐标 let y = val.y; // 获取y轴坐标 let innerWidth = window.innerWidth; // 获取页面可是区域宽度,即页面的宽度 let innerHeight = window.innerHeight; // 获取可视区域高度,即页面的高度 /** * 注意,这里要使用getElementsByClassName去选中对应dom,因为右键菜单组件可能被多处使用 * classIndex标识就是去找到对应的那个右键菜单组件的,需要加的 * */ let menu = document.getElementsByClassName("table-right-menu")[this.classIndex]; menu.style.display = "block"; let menuHeight = this.rightclickInfo.menulists.length * 30; // 菜单容器高 let menuWidth = 180; // 菜单容器宽 // 菜单的位置计算 menu.style.top = (y + menuHeight > innerHeight ? innerHeight - menuHeight : y) + "px"; menu.style.left = (x + menuWidth > innerWidth ? innerWidth - menuWidth : x) + "px"; // 因为菜单还要关闭,就绑定一个鼠标点击事件,通过e.button判断点击的是否是左键,左键关闭菜单 document.addEventListener("mouseup", this.hide, false); }, }, methods: { hide(e) { if (e.button === 0) { // 0是左键、1是滚轮按钮或中间按钮(若有)、2鼠标右键 let menu = document.getElementsByClassName("table-right-menu")[this.classIndex]; // 同样的精确查找 menu.style.display = "none"; // 菜单关闭 document.removeEventListener("mouseup", this.hide); // 及时解绑监听事件 } }, fnHandler(item) { this.$emit(item.fnName, item.params); // 事件再传出去,即为: // this.$emit('事件名',事件参数) }, }, }; </ script > < style lang='less' scoped> .table-right-menu { color: #333; background: #fff; border-radius: 4px; list-style-type: none; box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3); font-size: 12px; font-weight: 500; box-sizing: border-box; padding: 4px 0; // 固定定位,抬高层级,初始隐藏,右击时置为display:block显示 position: fixed; z-index: 3000; display: none; .table-right-menu-item { box-sizing: border-box; padding: 6px 12px; border-radius: 4px; transition: all 0.36s; cursor: pointer; .table-right-menu-item-btn { .iii { margin-right: 4px; } } } .table-right-menu-item:hover { background-color: #ebf5ff; color: #6bacf2; } } </ style > |
· Blazor Hybrid适配到HarmonyOS系统
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· 解决跨域问题的这6种方案,真香!
· 分享4款.NET开源、免费、实用的商城系统
· 一套基于 Material Design 规范实现的 Blazor 和 Razor 通用组件库