自定义右键菜单组件
宽固定 高由传入的数组右键菜单决定
利用display:none 决定右键菜单的显示问题

<template>
  <ul
    class="context-container"
    :style="{ top: clientY + 'px', left: clientX + 'px', display: show ? 'flex' : 'none' }"
    ref="contextRef"
  >
    <li
      v-for="(item, index) in listArr"
      @click="onChangeMenu(item, index)"
      :class="item[propsObj.disabled] ? 'disabled' : ''"
    >
      <SvgIcon :name="item[propsObj.icon] || item[propsObj.elIcon]" class="svg" />
      <span>{{ item[propsObj.label] }}</span>
    </li>
  </ul>
</template>

传入参数主要是以下

const props = defineProps({
  list: { //右键菜单内容数组
    type: Array,
    default: () => []
  },
  propsObj: {//数组里的项-对象的key对应的参数字段,可以自定义 默认如下(模仿elementplus 里的props)
    type: Object,
    default: () => {
      return {
        label: 'label', //文字字段
        value: 'value',//每一项唯一值
        disabled: 'disabled', //该项是否可点
        icon: 'icon', //该邮件菜单的icon 引入项目本地的svg 具体用法查svg 组件用法
        elIcon: 'elIcon' //如果是el-icon,有所区别
      }
    }
  },
  modelValue: {//控制显示的参数
    type: Boolean, 
    default: false,
    required: true
  },
  position: {//控制右键菜单位置的参数 右键点击的点 分别对应 event.clientX  event.clientY
    type: Object,
    default: () => {
      return {
        x: 0,
        y: 0
      }
    }
  }
})
const emits = defineEmits(['change', 'update:modelValue'])
//change事件 点击右键菜单项时触发的事件

其他js代码

const show = computed({
  get() {
    console.log('show', props.modelValue)
    return props.modelValue
  },
  set(newVal) {
    emits('update:modelValue', newVal)
  }
})
//list 转入做一下处理
const listArr = computed(() => {
  return props.list.map((item) => {
    if (item[props.propsObj.elIcon]) {
      item[props.propsObj.elIcon] = 'ele-' + item[props.propsObj.elIcon]
    }
    return { ...item }
  })
})

const contextRef = ref()
//右键菜单位置是css fixed固定的
//计算右键菜单的宽高 及视口 保证右键菜单不出视口
//这里由于这个组件没有二级分类右键,宽度是固定的,所有相对来说,没有高度考虑情况复杂
const clientX = computed(() => {
  const width = document.documentElement.clientWidth
  let eleWidth = 150
  let X = props.position.x
  return width - X < eleWidth ? width - eleWidth : X
})
//const clientY = computed(() => { 
 // let height = document.documentElement.clientHeight
  //let eleHeight = props.list.length * 24
  //let Y = props.position.y
 // return height - Y < eleHeight ? height - (height - Y) - eleHeight : Y
})
//注释部分没有考虑到如果右键菜单过高,右键菜单在鼠标点击位置的上方或下方仍然超出可视区域的情况
//原来的逻辑默认是右键菜单在鼠标点击位置的上方,如果此时右键菜单的高度大于鼠标点击位置距离可视区下面的高度,就会导致右键菜单部分被遮住;之前是直接将右键菜单放在鼠标上方,但是这里也会有问题:右键菜单的高度大于鼠标点击位置距离可视区上面的高度,右键菜单被遮住;所以这里增加了一些判断,如果有这种情况,就以右键菜单的高度的一半放在鼠标点击位置,居中
const clientY = computed(() => {
  let height = document.documentElement.clientHeight
  let length = props.isRefresh ? props.list.length + 1 : props.list.length
  let eleHeight = length * 25
  let Y = props.position.y
  if (eleHeight >= height) {
    if (eleHeight > height) {
      ElMessage.error('右键菜单高度超过可视区域高度,部分会被遮挡')
    }
    return 0
  } else {
    const getHeight = () => {
      let halfEleHeight = eleHeight / 2
      if (halfEleHeight < Y && halfEleHeight < height - Y) {
        return Y - halfEleHeight
      } else if (halfEleHeight > Y) {
        return 0
      } else if (halfEleHeight > height - Y) {
        return height - eleHeight
      }
    }
    return height - Y >= eleHeight
      ? Y
      : Y < eleHeight
      ? getHeight()
      : height - (height - Y) - eleHeight
  }
})

const onChangeMenu = (item, index) => {
   if (item[props.propsObj.disabled]) return //如果是不可点击的 不用传事件出去
  let obj = {
    ...item
  }
  if (obj[props.propsObj.elIcon]) {
    obj[props.propsObj.elIcon] = obj[props.propsObj.elIcon].split('-')[1]
  } //为了传出去的item 与 传进来的list里item 保持一致
  emits('change', obj, index)
  close()
}
//关闭右键菜单
const close = () => {
  emits('update:modelValue', false)
}
//阻止浏览器右键菜单默认事件
const preventDefault = (event) => {
  event.preventDefault()
  let clientX = event.clientX
  let clientY = event.clientY
  //一个页面可以出现多个右键组件 当A、B组件都引用了右键组件,A里面右键菜单已点击出现,又点击了B组件右键,此时A组件里的右键菜单应消失;(一个页面,只有一个右键菜单出现) 这里主要是利用事件的冒泡  最终会冒泡到最上面document ,当前最新的事件对象event判断下与传入的position数据如果不符,说明不是最新的 可以关闭了
  if (clientY != props.position.y || clientX != props.position.x) {
    close()
  }
}
onMounted(() => {
  console.log('ref', contextRef.value)
  document.addEventListener('click', close) //如果右键菜单出现时 点击了页面,右键菜单消失
  document.addEventListener('scroll', close) //页面滚动,右键菜单消失
  document.addEventListener('contextmenu', preventDefault) //阻止浏览器右键菜单默认事件
})
onBeforeMount(() => {
  document.removeEventListener('click', close)
  document.removeEventListener('scroll', close)
  document.removeEventListener('contextmenu', preventDefault)
})
defineExpose({
  contextRef
})
.context-container {
  display: flex;
  flex-direction: column;
  position: fixed; //固定定位
  opacity: 1;
  pointer-events: auto;
  width: 150px;
  border-radius: 2px;
  z-index: 2022; //层级设定高点,以免被覆盖
  background-color: #fff;
  > li {
    height: 28px;
    line-height: 28px;
    padding: 0 12px;
    font-size: 12px;
    color: #333333;
    cursor: pointer;
    &:first-child {
      border-bottom: 1px solid #fafafa;
    }
    &:hover {
      background-color: @bg;
    }
    &.disabled {
      cursor: not-allowed;
      color: #999999;
      &:hover {
        background-color: #fff;
      }
    }
    .svg {
      margin-right: 5px;
      color: #4e5769;
      font-size: 14px !important;
      margin-right: 8px;
      vertical-align: middle;
    }
  }
}

Logo

前往低代码交流专区

更多推荐