0
点赞
收藏
分享

微信扫一扫

动手做一个 vue 右键菜单

_阿瑶 2022-10-13 阅读 178

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

一顿操作之后,页面白屏,控制台报错,后来分析,大概应该是不适用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;
} 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" @mouseenter="handle_enter" @mouseleave="handle_leave">
<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_'+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();
},
/**
* 由父节点控制当前菜单列表是否展示,关闭
*/

handle_enter(){
if(this.$parent.noCloseChild)this.$parent.noCloseChild();
},
handle_leave(){
if(this.$parent.closeChild)this.$parent.closeChild();
},
},
}
</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_light{
width: 90%;
height: 1px;
margin:3px 0 3px 5%;
background: #aaaaaa;
}
.right_menu_outline_dark{
width: 90%;
height: 1px;
margin:3px 0 3px 5%;
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;
      //计算坐标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;
      } else if (screen.height > childHeight) {
        y = screen.height - childHeight;
      }
      this.noCloseChild();
      this.childInfo = {
        status: true,
        x: x,
        y: y,
      }
    },
    /**
     * 鼠标离开时,判断从哪个方向离开
     * @param e
     */
    handle_leave(e) {
      let item = this.$refs.item;
      let itemX = this.left;//当前菜单项的x坐标
      if (this.childPosition === "right") {
        if (Math.abs(item.clientWidth + itemX - e.clientX) < 5) {
          return;
        }
      } else if (this.childPosition === "left") {
        if (Math.abs(itemX - e.clientX) < 5) {
          return;
        }
      }
      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 {
  font-size: 13px;
  color: #e2e2e2;
}

.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
icontype   图标类型String
value   值String
click点击事件function
outline

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

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

theme  主题

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

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

遇到问题请提问

举报

相关推荐

0 条评论