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

需求分析

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

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

代码实现

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

<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

posted @ 2021-09-23 09:26  _zhiqiu  阅读(235)  评论(0编辑  收藏  举报