cwg-dropdown.vue 5.7 KB

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