1分钟Vue实现右键菜单
一顿操作之后,页面白屏,控制台报错,后来分析,大概应该是不适用vue3?

vue-contextmenu
关于这个插件在网上找了很多用法,都以失败告终。
还是自己动手造轮胎吧,正好也没做过这种东西。


内容大致分为两部分:
1、菜单列表
(1)数组数据,展示菜单项
(2)坐标控制显示
(3)显示开关
(4)子菜单
(5)定制主题
(6)下级菜单展示位置 处理
2、菜单项
(1)显示图标,文字,是否存在下级菜单(箭头)
(2)点击或禁用
(3)点击函数
(4)点击菜单项,关闭整个菜单后,执行对应函数
。。。。。。。。
定义一个组件入口,规范并处理入参。
- <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>
将主菜单列表,子菜单列表 抽象出来,作为一个菜单列表组件,该组件只负责根据指定坐标进行显示列表,隐藏。
- <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>
将菜单项抽象为一个组件,主要负责展示图片文字,点击事件,是否禁用等功能,
如果该菜单项下存在子菜单项,则要负责计算子菜单显示的坐标,也需要控制子菜单的显示和隐藏
- <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;
- //计算坐标x
- if ((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";
- //计算坐标y
- if ((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>
这里将菜单项的展示图标单独抽象出来,为的是兼容多模式展示。可以自行定义。如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>
| 使用方式(属性名) | 解释 | 类型 |
| v-model | 显示状态,坐标 | Object |
| :data | 菜单数据 | Array |
| theme | 主题名 | String |
| 参数名 | 解释 | 类型 |
| status | 显示状态 | Boolean |
| x | 横坐标 | Number |
| y | 竖坐标 | Number |
| 参数名 | 解释 | 类型 |
| name | 菜单名称 | String |
| icon | type 图标类型 | String |
| value 值 | String | |
| click | 点击事件 | function |
| outline | 该菜单项下面是否显示分割线,默认true | Boolean |
| enable | 是否可点击,默认true | Boolean |
| child | 子菜单数据数组 | Array |
| 枚举 | 解释 |
| light | 亮色主题 |
| dark | 暗色主题 |
自定义主题,可以在代码中仿照已有的两个主题样式 新增自定义css样式即可。
遇到问题请提问