从零开始设计一个右键菜单组件

简介: 从零开始设计一个右键菜单组件

需求分析

首先要分析右键菜单需要实现什么功能

  • 点击鼠标右键弹出自定义的弹窗
  • 实现菜单项的点击
  • 自定义菜单项的样式
  • 自定义弹窗容器的样式

代码实现

需求搞定之后就是写代码了,下面是基础的代码框架

<template>
  <div class="yak-content-menu" @contextmenu="showContentMenuFn" @click="hideContextMenuFn">
    <slot></slot>
    <transition>
      <div
        v-show="visiable"
        class="yak-content-menu-wrap"
        :class="menuWrapClass"
        :style="{ left: menuPosition.left, top: menuPosition.top }"
      >
        <slot name="menu" :menuList="menus">
          <!-- 给于用户完整的菜单项控制权限 -->
          <span
            class="yak-content-menu-wrap-item"
            :class="menuItemClass"
            v-for="item in menus"
            :key="item.command"
            @click="menuClick(item)"
          >
            {{ item.text }}
          </span>
        </slot>
      </div>
    </transition>
  </div>
</template>
<script lang="ts">
import {
  ref,
  reactive,
  onMounted,
  onBeforeUnmount,
  defineComponent,
  PropType,
} from "vue";
interface MenuItem {
  command: string;
  text: string;
}
type Menus = Array<MenuItem>;
export default defineComponent({
  name: "YakContextmenu",
  props: {
    menus: {
      type: Array as PropType<Menus>,
      default: () => {
        return [];
      },
      required: true,
    }, // 菜单的数组
    menuWrapClass: String, // 菜单容器的自定义class
    menuItemClass: String, // 菜单项的自定义class
  },
  emits: ["menu-click"],
  setup(props, { emit }) {
    const wrapEl = ref();
    const visiable = ref(false);
    const menuPosition: any = reactive({
      left: 0,
      top: 0,
    });
    const showContextMenuFn = (ev: any) => {
      // 1:禁用默认的右键点击事件
      // 2:获取当前鼠标的位置
      // 3:控制弹窗的显示
    };
    const hideContextMenuFn = () => {
      // 隐藏菜单
    };
    const menuClick = (item: MenuItem) => {
      // 添加自定义事件 menu-click,方便组件使用
    };
    return {
      wrapEl,
      visiable,
      menuPosition,
      menuClick,
      showContentMenuFn,
      hideContextMenuFn,
    };
  },
});
</script>

实现 showContextMenuFn

有了上面的框架,现在让我们实现显示菜单的功能

const showContextMenuFn = (ev: any) => {
  // 禁用默认事件
  ev.preventDefault();
  // 获取自定义菜单的根元素的位置
  const rootPosition = wrapEl.value.getBoundingClientRect();
  // 用鼠标所在位置减去根元素的位置,就是弹窗元素相对于根元素的位置
  const x = ev.x - rootPosition.left;
  const y = ev.y - rootPosition.top;
  menuPosition.top = `${y}px`;
  menuPosition.left = `${x}px`;
  // 控制弹窗的显示
  visiable.value = true;
};

实现隐藏菜单和菜单点击

const hideContextMenuFn = () => {
  visiable.value = false; // 隐藏菜单
};
const menuClick = (item: MenuItem) => {
  emit("menu-click", item); // 添加自定义事件 menu-click
};

实现的效果

完整的代码

<template>
  <div class="yak-content-menu" @contextmenu="showContentMenuFn" @click="hideContextMenuFn">
    <slot></slot>
    <transition>
      <div
        v-show="visiable"
        class="yak-content-menu-wrap"
        :class="menuWrapClass"
        :style="{ left: menuPosition.left, top: menuPosition.top }"
      >
        <slot name="menu" :menuList="menus">
          <!-- 给于用户完整的菜单项控制权限 -->
          <span
            class="yak-content-menu-wrap-item"
            :class="menuItemClass"
            v-for="item in menus"
            :key="item.command"
            @click="menuClick(item)"
          >
            {{ item.text }}
          </span>
        </slot>
      </div>
    </transition>
  </div>
</template>
<script lang="ts">
import {
  ref,
  reactive,
  onMounted,
  onBeforeUnmount,
  defineComponent,
  PropType,
} from "vue";
interface MenuItem {
  command: string;
  text: string;
}
type Menus = Array<MenuItem>;
export default defineComponent({
  name: "YakContextmenu",
  props: {
    menus: {
      type: Array as PropType<Menus>,
      default: () => {
        return [];
      },
      required: true,
    }, // 菜单的数组
    menuWrapClass: String, // 菜单容器的自定义class
    menuItemClass: String, // 菜单项的自定义class
  },
  emits: ["menu-click"],
  setup(props, { emit }) {
    const wrapEl = ref();
    const visiable = ref(false);
    const menuPosition: any = reactive({
      left: 0,
      top: 0,
    });
    const showContextMenuFn = (ev: any) => {
      // 禁用默认事件
      ev.preventDefault();
      // 获取自定义菜单的根元素的位置
      const rootPosition = wrapEl.value.getBoundingClientRect();
      // 用鼠标所在位置减去根元素的位置,就是弹窗元素相对于根元素的位置
      const x = ev.x - rootPosition.left;
      const y = ev.y - rootPosition.top;
      menuPosition.top = `${y}px`;
      menuPosition.left = `${x}px`;
      // 控制弹窗的显示
      visiable.value = true;
    };
    const hideContextMenuFn = () => {
      visiable.value = false; // 隐藏菜单
    };
    const menuClick = (item: MenuItem) => {
      emit("menu-click", item); // 添加自定义事件 menu-click
    };
    return {
      wrapEl,
      visiable,
      menuPosition,
      menuClick,
      showContentMenuFn,
      hideContextMenuFn,
    };
  },
});
</script>
<style lang="scss">
.yak-content-menu {
  position: relative;
  &-wrap {
    display: inline-block;
    box-sizing: border-box;
    background-color: #fff;
    position: absolute;
    box-shadow: 0 1px 6px rgb(0 0 0 / 20%);
    border-color: 1px solid #eee;
    border-radius: 4px;
    padding: 5px 0;
    min-width: 160px;
    &-item {
      cursor: pointer;
      display: block;
      line-height: 28px;
      font-size: 14px;
      padding: 0 24px;
      text-align: left;
      &:hover {
        background-color: #eee;
      }
    }
  }
}

仓库地址 Github,如有需求,欢迎提交issue


相关文章
|
存储 安全 对象存储
oss访问控制(Access Control)
oss访问控制(Access Control)
1167 4
|
1月前
|
弹性计算 运维 负载均衡
阿里云轻量应用服务器产品介绍、收费标准以及搭建个人博客教程参考
本文为大家介绍阿里云轻量应用服务器的产品优势、应用场景、使用须知、地域与网络连通性、与云服务器ECS的区别以及使用轻量应用服务器搭建WordPress个人博客的图文教程,以供大家了解和使用轻量应用服务器。
|
10月前
|
存储 调度 数据安全/隐私保护
鸿蒙Flutter实战:13-鸿蒙应用打包上架流程
鸿蒙应用打包上架流程包括创建应用、打包签名和上传应用。首先,在AppGallery Connect中创建项目、APP ID和元服务。接着,使用Deveco进行手动签名,生成.p12和.csr文件,并在AppGallery Connect中上传CSR文件获取证书。最后,配置签名并打包生成.app文件,上传至应用市场。常见问题包括检查签名配置文件是否正确。参考资料:[应用/服务签名](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/ide-signing-V5)。
556 3
鸿蒙Flutter实战:13-鸿蒙应用打包上架流程
|
10月前
|
JavaScript 前端开发 API
前端框架对比:Vue.js与Angular的优劣分析与选择建议
【10月更文挑战第26天】前端技术的飞速发展让开发者在构建用户界面时有了更多选择。本文对比了Vue.js和Angular两大框架,介绍了它们的特点和优劣,并给出了在实际项目中如何选择的建议。Vue.js轻量级、易上手,适合小型项目;Angular结构化、功能强大,适合大型项目。
346 1
OOP的缺点有哪些
【7月更文挑战第17天】OOP的缺点有哪些
374 2
|
存储 传感器 网络协议
异步传输:概念、特点与应用
【8月更文挑战第24天】
913 0
|
机器学习/深度学习 存储 人工智能
文本情感识别分析系统Python+SVM分类算法+机器学习人工智能+计算机毕业设计
使用Python作为开发语言,基于文本数据集(一个积极的xls文本格式和一个消极的xls文本格式文件),使用Word2vec对文本进行处理。通过支持向量机SVM算法训练情绪分类模型。实现对文本消极情感和文本积极情感的识别。并基于Django框架开发网页平台实现对用户的可视化操作和数据存储。
291 0
文本情感识别分析系统Python+SVM分类算法+机器学习人工智能+计算机毕业设计
|
11月前
|
存储 Python
Python Logging 限制文件大小
Python Logging 限制文件大小
193 0
|
12月前
|
Python
Python量化炒股的获取数据函数— get_billboard_list()
Python量化炒股的获取数据函数— get_billboard_list()
182 0
|
SQL 监控 关系型数据库
深入解析MySQL死锁:原因、检测与解决方案
深入解析MySQL死锁:原因、检测与解决方案