wd-drop-menu.vue 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. <template>
  2. <view :style="customStyle" :class="`wd-drop-menu ${customClass}`" @click.stop.prevent="noop" :id="dropMenuId">
  3. <wd-overlay
  4. :show="overlayVisible"
  5. :duration="duration"
  6. :z-index="12"
  7. :custom-style="modalStyle"
  8. @click="handleClickOverlay"
  9. @touchmove="noop"
  10. v-if="modal"
  11. />
  12. <!-- #ifdef MP-DINGTALK -->
  13. <view :id="dropMenuId">
  14. <!-- #endif -->
  15. <view class="wd-drop-menu__list">
  16. <view
  17. v-for="(child, index) in children"
  18. :key="index"
  19. @click="toggle(child)"
  20. :class="`wd-drop-menu__item ${child.disabled ? 'is-disabled' : ''} ${child.$.exposed!.getShowPop() ? 'is-active' : ''}`"
  21. >
  22. <view class="wd-drop-menu__item-title">
  23. <view class="wd-drop-menu__item-title-text">{{ getDisplayTitle(child) }}</view>
  24. <wd-icon :name="child.icon" :size="child.iconSize" custom-class="wd-drop-menu__arrow" />
  25. </view>
  26. </view>
  27. </view>
  28. <slot />
  29. <!-- #ifdef MP-DINGTALK -->
  30. </view>
  31. <!-- #endif -->
  32. </view>
  33. </template>
  34. <script lang="ts">
  35. export default {
  36. name: 'wd-drop-menu',
  37. options: {
  38. virtualHost: true,
  39. addGlobalClass: true,
  40. styleIsolation: 'shared'
  41. }
  42. }
  43. </script>
  44. <script lang="ts" setup>
  45. import { computed, getCurrentInstance, inject, onBeforeMount, ref, watch } from 'vue'
  46. import { closeOther } from '../common/clickoutside'
  47. import { type Queue, queueKey } from '../composables/useQueue'
  48. import { getRect, uuid } from '../common/util'
  49. import { useChildren } from '../composables/useChildren'
  50. import { DROP_MENU_KEY, dropMenuProps } from './types'
  51. import wdOverlay from '../wd-overlay/wd-overlay.vue'
  52. const props = defineProps(dropMenuProps)
  53. const queue = inject<Queue | null>(queueKey, null)
  54. const dropMenuId = ref<string>(`dropMenuId${uuid()}`)
  55. const offset = ref<number>(0)
  56. const windowHeight = ref<number>(0)
  57. const modalStyle = computed(() => {
  58. return props.direction === 'down'
  59. ? `top: calc(var(--window-top) + ${offset.value}px); bottom: 0;`
  60. : `top: 0; bottom: calc(var(--window-bottom) + ${offset.value}px)`
  61. })
  62. const { proxy } = getCurrentInstance() as any
  63. const { linkChildren, children } = useChildren(DROP_MENU_KEY)
  64. const showOverlay = computed(() => {
  65. return children.some((child) => child.$.exposed!.getShowPop())
  66. })
  67. const overlayVisible = ref(false)
  68. let overlayTimer: ReturnType<typeof setTimeout> | null
  69. // 延迟关闭遮罩层,避免闪烁
  70. // 小程序中,即使先 fold 再 closeOther 也会有闪烁,使用延迟关闭遮罩层处理
  71. watch(showOverlay, (newVal) => {
  72. if (overlayTimer) {
  73. clearTimeout(overlayTimer)
  74. }
  75. if (newVal) {
  76. overlayVisible.value = true
  77. } else {
  78. overlayTimer = setTimeout(() => {
  79. overlayVisible.value = false
  80. overlayTimer = null
  81. }, 16)
  82. }
  83. })
  84. linkChildren({ props, fold, offset })
  85. watch(
  86. () => props.direction,
  87. (newValue) => {
  88. if (!['up', 'down'].includes(newValue)) {
  89. // eslint-disable-next-line quotes
  90. console.error("[wot ui] warning(wd-drop-menu): direction must be 'up' or 'down'")
  91. }
  92. },
  93. { deep: true, immediate: true }
  94. )
  95. onBeforeMount(() => {
  96. windowHeight.value = uni.getSystemInfoSync().windowHeight
  97. })
  98. function noop() {}
  99. function getDisplayTitle(child: any) {
  100. const { title, modelValue, options, valueKey, labelKey } = child
  101. if (title) {
  102. return title
  103. }
  104. for (let i = 0, len = options.length; i < len; i++) {
  105. if (modelValue === options[i][valueKey]) {
  106. return options[i][labelKey]
  107. }
  108. }
  109. console.error('[wot-design] warning(wd-drop-menu-item): no value is matched in the options option.')
  110. }
  111. function toggle(child: any) {
  112. // 点击当前 menu, 关闭其他 menu
  113. if (child && !child.disabled) {
  114. if (queue && queue.closeOther) {
  115. queue.closeOther(child)
  116. } else {
  117. closeOther(child)
  118. }
  119. fold(child)
  120. }
  121. }
  122. /**
  123. * 控制菜单内容是否展开
  124. */
  125. function fold(child: any) {
  126. getRect(`#${dropMenuId.value}`, false, proxy).then((rect) => {
  127. if (!rect) return
  128. const { top, bottom } = rect
  129. if (props.direction === 'down') {
  130. offset.value = Number(bottom)
  131. } else {
  132. offset.value = windowHeight.value - Number(top)
  133. }
  134. child.$.exposed!.toggle()
  135. })
  136. }
  137. function handleClickOverlay() {
  138. if (props.closeOnClickModal) {
  139. // 关闭所有打开的菜单项
  140. children.forEach((child) => {
  141. child.$.exposed!.close()
  142. })
  143. }
  144. }
  145. </script>
  146. <style lang="scss" scoped>
  147. @import './index.scss';
  148. </style>