index.vue 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. <template>
  2. <div v-if="isBody" class="hc-context-menu-hide">
  3. <Teleport :disabled="!isBody" to="#app">
  4. <div :id="uuid" v-click-outside="onClickOutside" :class="ui" class="hc-context-menu-box">
  5. <template v-for="item in menus">
  6. <div class="hc-context-menu-item" @click.stop="optionClicked(item)">
  7. <slot v-if="item.isSlot" :item="item" :name='item.key'/>
  8. <template v-else>
  9. <HcIcon v-if="item.icon" :name="item.icon" class="menu-item-icon"/>
  10. <span class="menu-item-name">{{ item.label }}</span>
  11. </template>
  12. </div>
  13. </template>
  14. </div>
  15. </Teleport>
  16. </div>
  17. </template>
  18. <script setup>
  19. import {ref, useSlots, watch, nextTick, onMounted, onBeforeUnmount} from "vue";
  20. import {ClickOutside as vClickOutside} from 'element-plus'
  21. import {getRandom, deepClone} from "vue-utils-plus"
  22. const props = defineProps({
  23. ui: {
  24. type: String,
  25. default: ''
  26. },
  27. datas: {
  28. type: Array,
  29. default: () => ([])
  30. }
  31. })
  32. //初始变量
  33. const uuid = getRandom()
  34. const menus = ref(props.datas)
  35. const isBody = ref(false)
  36. //监听表头
  37. watch(() => [
  38. props.datas
  39. ], ([datas]) => {
  40. setIsSlots(datas)
  41. })
  42. //加载完成
  43. nextTick(() => {
  44. //页面渲染完成后,再让 vue3 的 Teleport,把弹出框挂载到外部节点上。
  45. isBody.value = true
  46. setIsSlots(props.datas)
  47. })
  48. //渲染完成
  49. onMounted(() => {
  50. document.body.addEventListener('keyup', onEscKeyRelease);
  51. })
  52. //判断<slot>是否有传值
  53. const slots = useSlots()
  54. const setIsSlots = (datas) => {
  55. let arr = deepClone(datas)
  56. for (let i = 0; i < arr.length; i++) {
  57. arr[i].isSlot = !!slots[arr[i].key]
  58. }
  59. menus.value = arr
  60. }
  61. //显示菜单
  62. const showMenu = (event) => {
  63. let menu = document.getElementById(uuid);
  64. if (!menu) return;
  65. //取宽高
  66. menu.style.visibility = 'hidden';
  67. menu.style.display = 'block';
  68. let menuWidth = menu.offsetWidth;
  69. let menuHeight = menu.offsetHeight;
  70. menu.removeAttribute('style');
  71. //宽
  72. if (menuWidth + event.pageX >= window.innerWidth) {
  73. menu.style.left = event.pageX - menuWidth + 2 + 'px';
  74. } else {
  75. menu.style.left = event.pageX - 2 + 'px';
  76. }
  77. //高
  78. if (menuHeight + event.pageY >= window.innerHeight) {
  79. menu.style.top = event.pageY - menuHeight + 2 + 'px';
  80. } else {
  81. menu.style.top = event.pageY - 2 + 'px';
  82. }
  83. menu.classList.add('active');
  84. }
  85. //事件
  86. const emit = defineEmits(['closed', 'item-click'])
  87. const hideContextMenu = () => {
  88. const element = document.getElementById(uuid);
  89. if (element) {
  90. element.classList.remove('active');
  91. emit('closed')
  92. }
  93. }
  94. const onClickOutside = () => {
  95. hideContextMenu()
  96. }
  97. //菜单被点击
  98. const optionClicked = (item) => {
  99. hideContextMenu()
  100. emit('item-click', item)
  101. }
  102. const onEscKeyRelease = (event) => {
  103. if (event.keyCode === 27) {
  104. hideContextMenu()
  105. }
  106. }
  107. //卸载之前
  108. onBeforeUnmount(() => {
  109. document.removeEventListener('keyup', onEscKeyRelease);
  110. })
  111. // 暴露出去
  112. defineExpose({
  113. showMenu
  114. })
  115. </script>
  116. <style lang="scss" scoped>
  117. .hc-context-menu-box {
  118. position: fixed;
  119. background-color: #fff;
  120. border-radius: 4px;
  121. width: max-content;
  122. display: none;
  123. left: 0;
  124. top: 0;
  125. box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
  126. z-index: 999999;
  127. &.active {
  128. display: block;
  129. }
  130. .hc-context-menu-item {
  131. align-items: center;
  132. padding: 5px 15px;
  133. display: flex;
  134. cursor: pointer;
  135. position: relative;
  136. height: 40px;
  137. font-size: 16px;
  138. color: #333639;
  139. .menu-item-icon {
  140. margin-right: 6px;
  141. }
  142. &:hover {
  143. background-color: #f3f3f5;
  144. }
  145. &:first-of-type {
  146. margin-top: 4px;
  147. }
  148. &:last-of-type {
  149. margin-bottom: 4px;
  150. }
  151. }
  152. }
  153. </style>
  154. <style lang="scss">
  155. .hc-context-menu-box .hc-context-menu-item .menu-item-icon {
  156. margin-right: 6px;
  157. }
  158. </style>