基于uview-pro的u-dropdown扩展自己的dropdown组件
uview-pro的u-dropdown只能是菜单,且只能向下展开,当前组件采用它的核心逻辑,去除多余逻辑,兼容上/下展开,以及自定义展示的内容,不再局限于菜单形式
1import type { ExtractPropTypes, PropType } from 'vue'; 2import { baseProps } from 'uview-pro/components/common/props'; 3 4/** 5 * u-dropdown 下拉菜单 Props 6 * @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景 7 */ 8export const DropdownProps = { 9 ...baseProps, 10 /** 点击遮罩是否关闭菜单 */ 11 closeOnClickMask: { type: Boolean, default: true }, 12 /** 过渡时间 */ 13 duration: { type: [Number, String] as PropType<number | string>, default: 300 }, 14 /** 下拉出来的内容部分的圆角值 */ 15 borderRadius: { type: [Number, String] as PropType<number | string>, default: 20 }, 16 /** 展开方向 down/up */ 17 direction: { type: String as PropType<'down' | 'up'>, default: 'up' }, 18 /** 弹出层最大高度 */ 19 maxHeight: { type: String as PropType<`${number}rpx` | `${number}vh`>, default: '80vh' }, 20 /** 弹出层最小高度 */ 21 minHeight: { type: String as PropType<`${number}rpx` | `${number}vh`>, default: '0rpx' }, 22 /** 是否隐藏关闭按钮 */ 23 hiddenClose: { type: Boolean, default: false }, 24 /** 弹出层标题 */ 25 title: { type: String, default: '' } 26}; 27 28export type DropdownProps = ExtractPropTypes<typeof DropdownProps>; 29 30
1<template> 2 <view class="u-dropdown" :style="$u.toStyle(styles, customStyle)" :class="customClass"> 3 <view class="u-dropdown__menu"> 4 <slot></slot> 5 </view> 6 <view 7 class="u-dropdown__content" 8 :style="[ 9 contentStyle, 10 { 11 transition: [`opacity ${Number(duration) / 1000}s linear`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.s.md), 12 [currentDirection === 'down' ? 'top' : 'bottom']: menuHeight + 'px', 13 height: contentHeight + 'px' 14 } 15 ]" 16 @tap="maskClick" 17 @touchmove.stop.prevent> 18 <view @tap.stop.prevent class="u-dropdown__content__popup" :style="[popupStyle]"> 19 <slot name="close" v-if="!hiddenClose"> 20 <view class="u-dropdown__content__popup__close" @click="close"> 21 <u-icon name="close" size="48" custom-prefix="custom-icon" /> 22 </view> 23 </slot> 24 25 <slot name="header"> 26 <view class="u-dropdown__content__popup__header" v-if="title"> {{ title }} </view> 27 </slot> 28 29 <view class="u-dropdown__content__popup__body"> 30 <scroll-view scroll-y class="u-dropdown__content__popup__scroll-view"> 31 <slot name="content"></slot> 32 </scroll-view> 33 </view> 34 <view class="u-dropdown__content__popup__footer"> 35 <slot name="footer"></slot> 36 </view> 37 </view> 38 <view class="u-dropdown__content__mask"></view> 39 </view> 40 </view> 41</template> 42 43<script lang="ts"> 44 export default { 45 name: 'hj-dropdown', 46 options: { 47 addGlobalClass: true, 48 // #ifndef MP-TOUTIAO 49 virtualHost: true, 50 // #endif 51 styleIsolation: 'shared' 52 } 53 }; 54</script> 55 56<script setup lang="ts"> 57 import { ref, computed, onMounted, getCurrentInstance, watch, type CSSProperties } from 'vue'; 58 import { $u } from 'uview-pro'; 59 import { DropdownProps } from './types'; 60 61 /** 62 * dropdown 下拉菜单 63 * @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景 64 * @tutorial https://uviewpro.cn/zh/components/dropdown.html 65 * @property {Boolean} close-on-click-mask 点击遮罩是否关闭菜单(默认true) 66 * @property {String | Number} duration 选项卡展开和收起的过渡时间,单位ms(默认300) 67 * @property {String | Number} border-radius 菜单展开内容下方的圆角值,单位任意(默认20) 68 * @property {String} direction 展开方向 down/up(默认up) 69 * @property {String} max-height 弹出层最大高度(默认80vh) 70 * @property {String} min-height 弹出层最小高度 71 * @property {Boolean} hidden-close 是否隐藏关闭按钮(默认false) 72 * @property {String} title 弹出层标题 73 * @property {Boolean} show 是否显示下拉菜单(默认false) 74 * @event {Function} open 下拉菜单被打开时触发 75 * @event {Function} close 下拉菜单被关闭时触发 76 * @example <hj-dropdown></hj-dropdown> 77 */ 78 79 const props = defineProps(DropdownProps); 80 const emit = defineEmits(['open', 'close']); 81 82 // 展开状态 83 const active = ref(false); 84 // 外层内容样式 85 const contentStyle = ref<CSSProperties>({ 86 zIndex: -1, 87 opacity: 0 88 }); 89 // 下拉内容高度 90 const contentHeight = ref<number>(0); 91 // 菜单实际高度 92 const menuHeight = ref<number>(0); 93 // 当前展开方向 94 const currentDirection = ref<'down' | 'up'>(props.direction); 95 // 子组件引用 96 const instance = getCurrentInstance(); 97 98 const vShow = defineModel('show', { 99 type: Boolean, 100 default: false 101 }); 102 103 watch(vShow, val => { 104 if (val === active.value) return; 105 if (val) { 106 open(); 107 } else { 108 close(); 109 } 110 }); 111 112 // 监听方向变化 113 watch( 114 () => props.direction, 115 val => { 116 currentDirection.value = val; 117 } 118 ); 119 120 // 兼容头条样式 121 const styles = computed<CSSProperties>(() => { 122 const style: CSSProperties = {}; 123 // #ifdef MP-TOUTIAO 124 style.width = '100vw'; 125 // #endif 126 return style; 127 }); 128 129 // 下拉出来部分的样式 130 const popupStyle = computed<CSSProperties>(() => { 131 const style: CSSProperties = {}; 132 const isDown = currentDirection.value === 'down'; 133 const hiddenTransformLate = isDown ? '-100%' : '100%'; 134 135 style.maxHeight = props.maxHeight; 136 style.minHeight = props.minHeight; 137 style.transform = `translateY(${active.value ? 0 : hiddenTransformLate})`; 138 style[isDown ? 'top' : 'bottom'] = 0; 139 // 进行Y轴位移,展开状态时,恢复原位。收起状态时,往上位移100%(或下),进行隐藏 140 style.transitionDuration = [`${Number(props.duration) / 1000}s`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.s.md); 141 142 if (isDown) { 143 style.borderRadius = [`0 0 ${$u.addUnit(props.borderRadius)} ${$u.addUnit(props.borderRadius)}`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.u.md); 144 } else { 145 style.borderRadius = [`${$u.addUnit(props.borderRadius)} ${$u.addUnit(props.borderRadius)} 0 0`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.u.md); 146 } 147 return style; 148 }); 149 150 // 生命周期 151 onMounted(() => { 152 getContentHeight(); 153 }); 154 155 /** 156 * 打开下拉菜单 157 * @param direction 展开方向 'down' | 'up' 158 */ 159 function open(direction?: 'down' | 'up') { 160 currentDirection.value = direction || props.direction; 161 162 // 重新计算高度,因为方向可能改变 163 getContentHeight(); 164 165 // 设置展开状态 166 active.value = true; 167 168 // 展开时,设置下拉内容的样式 169 contentStyle.value = { 170 zIndex: 11, 171 opacity: 1 172 }; 173 vShow.value = true; 174 emit('open'); 175 } 176 177 /** 178 * 关闭下拉菜单 179 */ 180 function close() { 181 // 下拉内容的样式进行调整,不透明度设置为0 182 active.value = false; 183 contentStyle.value = { 184 ...contentStyle.value, 185 opacity: 0 186 }; 187 188 // 等待过渡动画结束后隐藏 z-index 189 vShow.value = false; 190 setTimeout(() => { 191 contentStyle.value = { 192 zIndex: -1, 193 opacity: 0 194 }; 195 emit('close'); 196 }, Number(props.duration)); 197 } 198 199 /** 200 * 点击遮罩 201 */ 202 function maskClick() { 203 if (!props.closeOnClickMask) return; 204 close(); 205 } 206 207 /** 208 * 获取下拉菜单内容的高度 209 * @description 210 * dropdown组件是相对定位的,下拉内容必须给定高度, 211 * 才能让遮罩占满菜单以下直到屏幕底部的高度。 212 */ 213 function getContentHeight() { 214 const windowHeight = $u.sys().windowHeight; 215 216 $u.getRect('.u-dropdown__menu', instance).then((res: any) => { 217 // 获取菜单实际高度 218 menuHeight.value = res.height; 219 220 /** 221 * 尺寸计算说明: 222 * 在H5端,uniapp获取尺寸存在已知问题: 223 * 元素尺寸的top值为导航栏底部到元素的上边沿的距离 224 * 但元素的bottom值却是导航栏顶部到元素底部的距离 225 * 为避免页面滚动,此处取菜单栏的bottom值进行计算 226 */ 227 if (currentDirection.value === 'up') { 228 contentHeight.value = res.top; 229 } else { 230 contentHeight.value = windowHeight - res.bottom; 231 } 232 }); 233 } 234 235 // 暴露方法 236 defineExpose({ 237 close, 238 open 239 }); 240</script> 241 242<style scoped lang="scss"> 243 @import 'uview-pro/libs/css/style.components'; 244 245 .u-dropdown { 246 flex: 1; 247 width: 100%; 248 position: relative; 249 background-color: #fff; 250 251 &__content { 252 position: absolute; 253 z-index: 8; 254 width: 100%; 255 left: 0; 256 overflow: hidden; 257 258 &__mask { 259 position: absolute; 260 z-index: 9; 261 background: rgba(0, 0, 0, 0.3); 262 width: 100%; 263 left: 0; 264 top: 0; 265 bottom: 0; 266 } 267 268 &__popup { 269 position: absolute; 270 width: 100%; 271 z-index: 10; 272 transition: all 0.3s; 273 transform: translate3D(0, -100%, 0); 274 overflow: hidden; 275 background-color: var(--gray-2); 276 277 &__close { 278 width: 40rpx; 279 height: 40rpx; 280 display: flex; 281 align-items: center; 282 justify-content: center; 283 position: absolute; 284 right: 24rpx; 285 top: 30rpx; 286 z-index: 9; 287 } 288 289 &__header { 290 display: flex; 291 color: var(--title-1); 292 font-size: var(--ft-32); 293 font-weight: 500; 294 line-height: 44rpx; 295 padding: 30rpx 24rpx; 296 } 297 298 &__body { 299 flex: 1; 300 overflow: hidden; 301 display: flex; 302 } 303 304 &__scroll-view { 305 flex: 1; 306 } 307 308 &__footer { 309 display: flex; 310 } 311 } 312 } 313 } 314</style> 315 316
