• 动手做一个 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

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

    1. <template>
    2. <div class="full" v-show="modelValue.status" style="position: fixed;top:0;left:0;user-select: none;" @contextmenu.prevent="">
    3. <div class="full" @click="handle_click" @contextmenu.prevent.stop="handle_click">div>
    4. <RightMenuList :setting="childInfo" :data="data" :theme="theme" :item-size="itemSize">RightMenuList>
    5. div>
    6. template>
    7. <script>
    8. import RightMenuList from "@/view/rightmenu/RightMenuList";
    9. export default {
    10. name: "RightMenu",
    11. components: {RightMenuList},
    12. props: {
    13. data: Array,//菜单数据
    14. modelValue: Object,//设置入口
    15. theme: {//主题
    16. type: String,
    17. default: 'light',
    18. },
    19. },
    20. data() {
    21. return {
    22. itemSize: {
    23. width: 220,
    24. height: 30,
    25. },
    26. childInfo: {
    27. status: false,
    28. x: 0,
    29. y: 0,
    30. },
    31. }
    32. },
    33. watch: {
    34. modelValue(n) {
    35. if (n.status) {
    36. this.calculatePosition();
    37. }
    38. },
    39. },
    40. methods: {
    41. /**
    42. * 计算菜单生成位置
    43. */
    44. calculatePosition() {
    45. let x = 0;
    46. let y = 0;
    47. let screen = this.getScreen();
    48. let childHeight = this.data.length * this.itemSize.height;
    49. if (screen.width - this.modelValue.x <= this.itemSize.width) {
    50. x = screen.width - this.itemSize.width;
    51. } else {
    52. x = this.modelValue.x;
    53. }
    54. if (screen.height - this.modelValue.y <= childHeight) {
    55. y = screen.height - childHeight-30;
    56. } else {
    57. y = this.modelValue.y;
    58. }
    59. this.childInfo = {
    60. status: true,
    61. x: x,
    62. y: y,
    63. }
    64. },
    65. /**
    66. * 获取窗口大小
    67. */
    68. getScreen() {
    69. return {
    70. width: document.body.clientWidth,
    71. height: document.body.clientHeight,
    72. }
    73. },
    74. /**
    75. * 统一关闭菜单入口
    76. */
    77. close() {
    78. this.childInfo = {
    79. status: false,
    80. x: 0,
    81. y: 0,
    82. }
    83. this.$event.$emit("RightMenuListClose");
    84. this.$emit('update:modelValue', {status: false, x: 0, y: 0});
    85. },
    86. /**
    87. * 单击空白地方,左右键通用
    88. */
    89. handle_click(event) {
    90. this.close();
    91. setTimeout(() => {
    92. document.elementFromPoint(event.clientX, event.clientY).dispatchEvent(event);
    93. }, 10);
    94. },
    95. }
    96. }
    97. script>
    98. <style scoped>
    99. style>

    RightMenuList.vue

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

    1. <template>
    2. <div :class="'right_menu right_menu_'+theme" :style="{width:itemSize.width+'px',top:setting.y+'px',left:setting.x+'px'}" v-show="setting.status" >
    3. <template v-for="(item,index) in data" :key="'a'+index">
    4. <RightMenuItem :data="item" :theme="theme" :top="setting.y+index*itemSize.height" :left="setting.x" :item-size="itemSize">RightMenuItem>
    5. <div v-if="item.outline" :class="'right_menu_outline right_menu_outline_'+theme">div>
    6. template>
    7. div>
    8. template>
    9. <script>
    10. import RightMenuItem from "@/view/rightmenu/RightMenuItem";
    11. export default {
    12. name: "RightMenuList",
    13. components: {RightMenuItem},
    14. props:{
    15. data:Array,
    16. theme:String,
    17. setting:Object,
    18. itemSize:Object,
    19. },
    20. mounted() {
    21. /**
    22. * 统一关闭入口
    23. */
    24. this.$event.$on("RightMenuListClose",()=>{
    25. if(this.$parent.closeChild)this.$parent.closeChild();
    26. });
    27. },
    28. methods:{
    29. close() {
    30. this.$parent.close();
    31. },
    32. },
    33. }
    34. script>
    35. <style scoped>
    36. .right_menu{
    37. box-shadow: 1px 1px 8px 2px rgba(0, 0, 0, 0.3);
    38. position: fixed;
    39. padding: 4px 2px;
    40. }
    41. .right_menu_light{
    42. background: #f3f3f3;
    43. }
    44. .right_menu_dark{
    45. border: 1px solid #bbbbbb;
    46. background: #282828;
    47. }
    48. .right_menu_outline{
    49. width: 90%;
    50. height: 1px;
    51. margin:3px 0 3px 5%;
    52. }
    53. .right_menu_outline_light{
    54. background: #aaaaaa;
    55. }
    56. .right_menu_outline_dark{
    57. background: #bbbbbb;
    58. }
    59. style>

    RightMenuItem.vue

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

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

    1. <template>
    2. <button ref="item" v-if="data.child&&data.child.length>0"
    3. :class="`empty_button right_item right_item_${theme} ${!isEnable()?'right_item_enable_'+theme:''}`"
    4. @mouseenter="handle_enter"
    5. @mouseleave="handle_leave"
    6. :style="{height:itemSize.height+'px' }">
    7. <RightMenuItemIcon :icon="data.icon" :theme="theme">RightMenuItemIcon>
    8. {{ data.name }}
    9. <b-icon v-if="theme==='light'" class="right_item_arrow" local="arrow_thick_right" style="color: #3b3b3b;">b-icon>
    10. <b-icon v-else class="right_item_arrow" local="arrow_thick_right" style="color: #adadad;">b-icon>
    11. <RightMenuList v-if="data.child&&data.child.length>0" :setting="childInfo" :data="data.child" :theme="theme" :item-size="itemSize">RightMenuList>
    12. button>
    13. <button v-else
    14. :class="`empty_button right_item right_item_${theme} ${!isEnable()?'right_item_enable_'+theme:''}`"
    15. @click="handle_click"
    16. :style="{height:itemSize.height+'px'}">
    17. <RightMenuItemIcon :icon="data.icon" :theme="theme">RightMenuItemIcon>
    18. {{ data.name }}
    19. button>
    20. template>
    21. <script>
    22. import RightMenuItemIcon from "@/view/rightmenu/RightMenuItemIcon";
    23. export default {
    24. name: "RightMenuItem",
    25. components: {
    26. RightMenuItemIcon
    27. },
    28. beforeCreate() {
    29. this.$options.components.RightMenuList = require('@/view/rightmenu/RightMenuList').default
    30. },
    31. props: {
    32. data: Object,
    33. theme: String,
    34. itemSize:Object,
    35. top:Number,
    36. left:Number,
    37. },
    38. data() {
    39. return {
    40. childPosition: "",
    41. childInfo: {
    42. status: false,
    43. x: 0,
    44. y: 0,
    45. },
    46. cancelTimer: null,
    47. }
    48. },
    49. methods: {
    50. /**
    51. * 鼠标进入菜单项时,计算子菜单展示的位置
    52. */
    53. handle_enter() {
    54. let x = 0;
    55. let y = 0;
    56. let screen = this.getScreen();
    57. let item = this.$refs.item;
    58. let itemX = this.left;//当前菜单项的x坐标
    59. let itemY = this.top;//当前菜单项的y坐标
    60. let childHeight = this.data.child.length * item.clientHeight;
    61. //计算坐标x
    62. if ((screen.width - itemX - item.clientWidth) > item.clientWidth) {
    63. x = itemX + item.clientWidth;
    64. this.childPosition = "right";
    65. } else if (itemX > item.clientWidth) {
    66. x = itemX - item.clientWidth;
    67. }
    68. if (this.childPosition === "") this.childPosition = "left";
    69. //计算坐标y
    70. if ((screen.height - itemY) > childHeight) {
    71. y = itemY+10;
    72. } else if (screen.height > childHeight) {
    73. y = screen.height - childHeight-20;
    74. }
    75. this.noCloseChild();
    76. this.childInfo = {
    77. status: true,
    78. x: x,
    79. y: y,
    80. }
    81. },
    82. /**
    83. * 鼠标离开时,判断从哪个方向离开
    84. * @param e
    85. */
    86. handle_leave() {
    87. this.noCloseChild();
    88. this.cancelTimer = setTimeout(() => {
    89. this.closeChild();
    90. }, 100);
    91. },
    92. /**
    93. * 获取窗口大小
    94. */
    95. getScreen() {
    96. return {
    97. width: document.body.clientWidth,
    98. height: document.body.clientHeight,
    99. }
    100. },
    101. isEnable(){
    102. return this.data.enable!==false;
    103. },
    104. /**
    105. * 处理点击事件,先关闭按钮,在处理点击事件
    106. */
    107. handle_click() {
    108. if(!this.isEnable())return;
    109. this.close();
    110. setTimeout(() => {
    111. if(this.data.click)this.data.click();
    112. }, 10);
    113. },
    114. /**
    115. * 通知整个菜单关闭
    116. */
    117. close() {
    118. this.$parent.close();
    119. },
    120. /**
    121. * 关闭子菜单
    122. */
    123. closeChild() {
    124. this.childInfo = {
    125. status: false,
    126. x: 0,
    127. y: 0,
    128. }
    129. this.childPosition = "";
    130. },
    131. /**
    132. * 取消关闭子菜单
    133. */
    134. noCloseChild() {
    135. clearTimeout(this.cancelTimer);
    136. this.cancelTimer = null;
    137. },
    138. }
    139. }
    140. script>
    141. <style scoped>
    142. .right_item{
    143. display: block;
    144. width: 100%;
    145. text-align: left;
    146. padding-left: 5px;
    147. font-size: 15px;
    148. white-space: nowrap;
    149. text-overflow:ellipsis;
    150. overflow: hidden;
    151. }
    152. .right_item_light {
    153. font-size: 15px;
    154. }
    155. .right_item_light:hover {
    156. background-color: #ffffff;
    157. }
    158. .right_item_dark {
    159. color: #e2e2e2;
    160. font-size: 13px;
    161. }
    162. .right_item_dark:hover {
    163. background-color: #444444;
    164. }
    165. .right_item_enable_light{
    166. color: #b6b6b6;
    167. }
    168. .right_item_enable_dark{
    169. color: #797979;
    170. }
    171. .right_item_arrow {
    172. width: 25px;
    173. height: 25px;
    174. float: right;
    175. }
    176. style>

    RightMenuItemIcon.vue

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

    1. <template>
    2. <img class="right_item_icon right_item_icon_blank" v-if="!icon||!icon.type" >
    3. <img class="right_item_icon" v-else-if="icon.type==='url'" :src="icon.value" >
    4. <b-icon class="right_item_icon" v-else-if="theme==='light'&& icon.type==='name'" :local="icon.value" style="color: black;">b-icon>
    5. <b-icon class="right_item_icon" v-else-if="theme==='dark'&& icon.type==='name'" :local="icon.value" style="color: white;">b-icon>
    6. <b-icon class="right_item_icon" v-else-if="theme==='light'&& icon.type==='type'" :type="icon.value" style="color: black;">b-icon>
    7. <b-icon class="right_item_icon" v-else-if="theme==='dark'&& icon.type==='type'" :type="icon.value" style="color: white;">b-icon>
    8. <img class="right_item_icon right_item_icon_blank" v-else >
    9. template>
    10. <script>
    11. export default {
    12. name: "RightMenuItemIcon",
    13. props:{
    14. icon:Object,
    15. theme: String,
    16. },
    17. }
    18. script>
    19. <style scoped>
    20. .right_item_icon{
    21. width: 18px;
    22. height: 18px;
    23. margin-top: -3px;
    24. }
    25. .right_item_icon_blank{
    26. opacity: 0;
    27. }
    28. style>

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

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

    测试用例:

    1. <template>
    2. <div>
    3. <div style="height: 100px;background: #1ba3bf;">div>
    4. <div class="full" @contextmenu.prevent="showRightMenu" >
    5. div>
    6. <RightMenu v-model="menuSetting" :data="data" theme="light">RightMenu>
    7. div>
    8. template>
    9. <script>
    10. import RightMenu from "@/view/rightmenu/RightMenu";
    11. export default {
    12. name: "RightMenuTestPane",
    13. components: {RightMenu},
    14. data(){
    15. return{
    16. menuSetting:{
    17. status:false,
    18. x:0,
    19. y:0,
    20. },
    21. data:[{
    22. name:'查看(V)',
    23. click:()=>{
    24. alert("查看(V)");
    25. }
    26. },{
    27. name:'排序方式(O)',
    28. click:()=>{
    29. alert("排序方式(O)");
    30. },
    31. },{
    32. name:'刷新(E)',
    33. outline:true,
    34. click:()=>{
    35. alert("刷新(E)");
    36. }
    37. },{
    38. name:'粘贴(P)',
    39. enable:false,
    40. click:()=>{
    41. alert("刷新(E)");
    42. }
    43. },{
    44. name:'粘贴快捷方式(S)',
    45. enable:false,
    46. outline:true,
    47. click:()=>{
    48. alert("刷新(E)");
    49. }
    50. },{
    51. name:'新建(W)',
    52. outline:true,
    53. child:[{
    54. name:'文件夹(F)',
    55. icon:{
    56. type:'url',
    57. value:require("@/assets/file/dir.png"),
    58. },
    59. },{
    60. name:'快捷方式(S)',
    61. icon:{
    62. type:'url',
    63. value:require("@/assets/rightmenu/shortcut.png"),
    64. },
    65. outline:true,
    66. },{
    67. name:'Microsoft Word 文档',
    68. icon:{
    69. type:'url',
    70. value:'https://docs.idqqimg.com/tim/docs/docs-design-resources/pc/png@2x/file_web_doc_64@2x-77242f419d.png',
    71. },
    72. },{
    73. name:'Microsoft PowerPrint 演示文稿',
    74. icon:{
    75. type:'url',
    76. value:'data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48bGluZWFyR3JhZGllbnQgaWQ9ImEiIHgxPSIwJSIgeDI9IjEwMCUiIHkxPSIwJSIgeTI9IjEwMCUiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2Y1ODQ2YSIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2U2NWUyZSIvPjwvbGluZWFyR3JhZGllbnQ+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48cmVjdCBmaWxsPSJ1cmwoI2EpIiBoZWlnaHQ9IjI0IiByeD0iMiIgd2lkdGg9IjI0Ii8+PGcgZmlsbD0iI2ZmZiI+PHBhdGggZD0iTTExIDYuMDE5VjEyaDYuOTgxYTYuNSA2LjUgMCAxMS02Ljk4LTUuOTgyeiIvPjxwYXRoIGQ9Ik0xMyA1LjAxOWE2LjUwNCA2LjUwNCAwIDAxNS44MjYgNC45OEwxMyAxMHoiIG9wYWNpdHk9Ii42Ii8+PC9nPjwvZz48L3N2Zz4=',
    77. },
    78. },{
    79. name:'文本文档',
    80. icon:{
    81. type:'url',
    82. value:'data:image/svg+xml;base64,PHN2ZyAgc3R5bGU9Im92ZXJmbG93OiBoaWRkZW47IiB2aWV3Qm94PSIwIDAgMTAyNCAxMDI0IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgcC1pZD0iMjIzMSI+PHBhdGggZD0iTTcyNi42MjQgNjRMODY0IDIwMS4zNzZWOTYwSDE2MFY2NGg1NjYuNjI0eiBtLTI0LjI1NiAzMkgxOTJ2ODMyaDY0MFYyMjkuOTJoLTY1LjZhNjQgNjQgMCAwIDEtNjQtNjRMNzAyLjM2OCA5NnoiIGZpbGw9IiM2NDZFN0YiIHAtaWQ9IjIyMzIiPjwvcGF0aD48cGF0aCBkPSJNMzUyIDM4NHYtNjRoMzIwdjY0aC0xMjh2MzIxLjc2aC02NFYzODRoLTEyOHoiIGZpbGw9IiMxNkIyQkMiIHAtaWQ9IjIyMzMiPjwvcGF0aD48L3N2Zz4=',
    83. },
    84. },{
    85. name:'Microsoft Excel 工作表',
    86. icon:{
    87. type:'url',
    88. value:'https://pub.idqqimg.com/pc/misc/files/20200904/2eb030216d9362bbc6c0df045857b718.png',
    89. },
    90. },],
    91. },{
    92. name:'显示设置(D)',
    93. icon:{
    94. type:'url',
    95. value:require("@/assets/rightmenu/viewsetting.png"),
    96. },
    97. click:()=>{
    98. alert("显示设置(D)");
    99. }
    100. },{
    101. name:'个性化(R)',
    102. icon:{
    103. type:'url',
    104. value:require("@/assets/rightmenu/individuation.png"),
    105. },
    106. click:()=>{
    107. alert("个性化(R)");
    108. }
    109. }],
    110. }
    111. },
    112. mounted() {
    113. },
    114. methods:{
    115. showRightMenu(e){
    116. this.menuSetting={
    117. status:true,
    118. x:e.clientX,
    119. y:e.clientY,
    120. }
    121. },
    122. }
    123. }
    124. script>
    125. <style scoped>
    126. 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样式即可。

    遇到问题请提问

  • 相关阅读:
    【Java】Optional
    python笔记--列表、字典、元组和集合
    Linux-tmux工具
    五款最热低代码平台推荐!
    Linux的目录结构
    android ndk一些编译链接错误及解决办法
    操作系统实验5:信号量的实现与应用
    SSM框架真没那么难,这份阿里大佬的进阶实战笔记真给讲透了!
    第十一节:抽象类和接口【java】
    数据库笔记——SQL语言DQL语句
  • 原文地址:https://blog.csdn.net/qq_43319748/article/details/127288027