cwg-dropdown.vue 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. <template>
  2. <view>
  3. <view class="cwg-dropdown" :style="customStyle" @click="click">
  4. <slot></slot>
  5. </view>
  6. <view class="cwg-dropdown-menu">
  7. <view class="cwg-dropdown-menu-container" :style="[layout]" @click.stop>
  8. <slot name="menu">
  9. <view class="menu">
  10. <slot name="btn"></slot>
  11. <view class="menu-item" v-for="(item, idx) in menuList" :key="idx"
  12. @click="menuClick(item, idx)">
  13. <view>{{ item.label || item }}</view>
  14. </view>
  15. </view>
  16. </slot>
  17. </view>
  18. </view>
  19. <view class="cwg-dropdown-mask" :class="{ 'cwg-dropdown-mask-show': maskShow }" @touchmove.prevent="() => { }"
  20. @click="close" />
  21. </view>
  22. </template>
  23. <script setup>
  24. import { ref, reactive, nextTick, onMounted } from 'vue'
  25. import { queryElementRect } from '@/uni_modules/x-tools/tools/sugar.js'
  26. import { str2px, commonProps } from '@/uni_modules/x-tools/tools/com.js'
  27. // 合并 props
  28. const props = defineProps({
  29. ...commonProps,
  30. menuList: {
  31. type: Object,
  32. default: () => ['菜单1', '菜单2', '菜单3']
  33. },
  34. menuStyle: {
  35. type: Object,
  36. default: () => ({})
  37. },
  38. interspace: {
  39. type: [String, Number],
  40. default: '10rpx'
  41. }
  42. })
  43. const emit = defineEmits(['open', 'close', 'change', 'menuClick'])
  44. // 响应式数据
  45. const maskShow = ref(false)
  46. const windowInfo = reactive({
  47. width: 0,
  48. height: 0
  49. })
  50. const layout = reactive({})
  51. const innerInterspace = ref(0)
  52. // 获取系统信息
  53. const getSystemInfo = () => {
  54. return new Promise((resolve) => {
  55. if (windowInfo.width > 0) {
  56. resolve(windowInfo)
  57. } else {
  58. uni.getSystemInfo({
  59. success: (res) => {
  60. windowInfo.width = res.windowWidth
  61. windowInfo.height = res.windowHeight
  62. resolve(windowInfo)
  63. },
  64. fail: () => {
  65. setTimeout(() => getSystemInfo().then(resolve), 100)
  66. }
  67. })
  68. }
  69. })
  70. }
  71. // 点击触发器
  72. const click = async (e) => {
  73. e.stopPropagation()
  74. await getSystemInfo()
  75. const triggerRect = await queryElementRect('.cwg-dropdown')
  76. if (!triggerRect) return
  77. const tempStyle = {
  78. transform: 'scaleY(1)',
  79. visibility: 'hidden',
  80. top: '-9999px',
  81. left: '-9999px',
  82. transition: 'none'
  83. }
  84. Object.assign(layout, tempStyle)
  85. await nextTick()
  86. const menuRect = await queryElementRect('.cwg-dropdown-menu-container')
  87. if (!menuRect) {
  88. Object.keys(layout).forEach(key => delete layout[key])
  89. layout.transform = 'scaleY(0)'
  90. return
  91. }
  92. const { width: winWidth } = windowInfo
  93. const { left, right, bottom } = triggerRect
  94. const interspaceVal = innerInterspace.value
  95. const finalLayout = {
  96. transition: 'transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)',
  97. transform: 'scaleY(1)',
  98. top: `10px`
  99. }
  100. if (left + menuRect.width < winWidth) {
  101. finalLayout.left = `-110px`
  102. } else {
  103. const val = winWidth - right
  104. finalLayout.right = `${val > 0 ? val : 0}px`
  105. }
  106. Object.keys(layout).forEach(key => delete layout[key])
  107. Object.assign(layout, finalLayout)
  108. maskShow.value = true
  109. emit('open')
  110. emit('change', true)
  111. }
  112. const menuClick = (value, index) => {
  113. emit('menuClick', { value, index })
  114. close()
  115. }
  116. const close = () => {
  117. maskShow.value = false
  118. Object.keys(layout).forEach(key => delete layout[key])
  119. layout.transform = 'scaleY(0)'
  120. emit('close')
  121. emit('change', false)
  122. }
  123. onMounted(() => {
  124. getSystemInfo()
  125. innerInterspace.value = str2px(props.interspace)
  126. })
  127. </script>
  128. <style lang="scss" scoped>
  129. @import '@/uni.scss';
  130. .cwg-dropdown {
  131. width: fit-content;
  132. position: relative;
  133. overflow: hidden;
  134. }
  135. .cwg-dropdown-menu {
  136. position: relative;
  137. z-index: 1000;
  138. }
  139. .cwg-dropdown-mask {
  140. position: fixed;
  141. top: 0;
  142. left: 0;
  143. width: 100vw;
  144. height: 100vh;
  145. z-index: 999;
  146. transition: all 0.3s;
  147. opacity: 0;
  148. pointer-events: none;
  149. background-color: rgba(0, 0, 0, 0);
  150. }
  151. .cwg-dropdown-mask-show {
  152. opacity: 1;
  153. pointer-events: auto;
  154. }
  155. .cwg-dropdown-menu-container {
  156. position: absolute;
  157. transform-origin: top;
  158. transform: scaleY(0);
  159. .menu {
  160. position: relative;
  161. background: #FFFFFF;
  162. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  163. border-radius: px2rpx(4);
  164. overflow: hidden;
  165. .menu-item {
  166. min-width: px2rpx(150);
  167. display: flex;
  168. align-items: center;
  169. padding: px2rpx(10) px2rpx(16);
  170. font-size: px2rpx(14);
  171. line-height: px2rpx(30);
  172. color: #333;
  173. transition: background 0.2s;
  174. box-sizing: border-box;
  175. &:last-child {
  176. border-bottom: none;
  177. }
  178. text {
  179. flex: 1;
  180. line-height: 1.4;
  181. }
  182. &:hover {
  183. background-color: rgba(0, 0, 0, 0.05);
  184. }
  185. &:active {
  186. background-color: rgba(0, 0, 0, 0.05);
  187. }
  188. }
  189. }
  190. }
  191. </style>