cwg-dropdown.vue 7.1 KB

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