基于uview-pro的u-dropdown扩展自己的dropdown组件
uview-pro的u-dropdown只能是菜单,且只能向下展开,当前组件采用它的核心逻辑,去除多余逻辑,兼容上/下展开,以及自定义展示的内容,不再局限于菜单形式
import type { ExtractPropTypes, PropType } from 'vue';
import { baseProps } from 'uview-pro/components/common/props';
/**
* u-dropdown 下拉菜单 Props
* @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景
*/
export const DropdownProps = {
...baseProps,
/** 点击遮罩是否关闭菜单 */
closeOnClickMask: { type: Boolean, default: true },
/** 过渡时间 */
duration: { type: [Number, String] as PropType<number | string>, default: 300 },
/** 下拉出来的内容部分的圆角值 */
borderRadius: { type: [Number, String] as PropType<number | string>, default: 20 },
/** 展开方向 down/up */
direction: { type: String as PropType<'down' | 'up'>, default: 'up' },
/** 弹出层最大高度 */
maxHeight: { type: String as PropType<`${number}rpx` | `${number}vh`>, default: '80vh' },
/** 弹出层最小高度 */
minHeight: { type: String as PropType<`${number}rpx` | `${number}vh`>, default: '0rpx' },
/** 是否隐藏关闭按钮 */
hiddenClose: { type: Boolean, default: false },
/** 弹出层标题 */
title: { type: String, default: '' }
};
export type DropdownProps = ExtractPropTypes<typeof DropdownProps>;
<template>
<view class="u-dropdown" :style="$u.toStyle(styles, customStyle)" :class="customClass">
<view class="u-dropdown__menu">
<slot></slot>
</view>
<view
class="u-dropdown__content"
:style="[
contentStyle,
{
transition: `opacity ${Number(duration) / 1000}s linear`,
[currentDirection === 'down' ? 'top' : 'bottom']: menuHeight + 'px',
height: contentHeight + 'px'
}
]"
@tap="maskClick"
@touchmove.stop.prevent>
<view @tap.stop.prevent class="u-dropdown__content__popup" :style="[popupStyle]">
<slot name="close" v-if="!hiddenClose">
<view class="u-dropdown__content__popup__close" @click="close">
<u-icon name="close" size="48" custom-prefix="custom-icon" />
</view>
</slot>
<slot name="header">
<view class="u-dropdown__content__popup__header" v-if="title"> {{ title }} </view>
</slot>
<view class="u-dropdown__content__popup__body">
<scroll-view scroll-y class="u-dropdown__content__popup__scroll-view">
<slot name="content"></slot>
</scroll-view>
</view>
<view class="u-dropdown__content__popup__footer">
<slot name="footer"></slot>
</view>
</view>
<view class="u-dropdown__content__mask"></view>
</view>
</view>
</template>
<script lang="ts">
export default {
name: 'hj-dropdown',
options: {
addGlobalClass: true,
// #ifndef MP-TOUTIAO
virtualHost: true,
// #endif
styleIsolation: 'shared'
}
};
</script>
<script setup lang="ts">
import { ref, computed, onMounted, getCurrentInstance, watch, type CSSProperties } from 'vue';
import { $u } from 'uview-pro';
import { DropdownProps } from './types';
/**
* dropdown 下拉菜单
* @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景
* @tutorial https://uviewpro.cn/zh/components/dropdown.html
* @property {Boolean} close-on-click-mask 点击遮罩是否关闭菜单(默认true)
* @property {String | Number} duration 选项卡展开和收起的过渡时间,单位ms(默认300)
* @property {String | Number} border-radius 菜单展开内容下方的圆角值,单位任意(默认20)
* @property {String} direction 展开方向 down/up(默认up)
* @property {String} max-height 弹出层最大高度(默认80vh)
* @property {String} min-height 弹出层最小高度
* @property {Boolean} hidden-close 是否隐藏关闭按钮(默认false)
* @property {String} title 弹出层标题
* @property {Boolean} show 是否显示下拉菜单(默认false)
* @event {Function} open 下拉菜单被打开时触发
* @event {Function} close 下拉菜单被关闭时触发
* @example <hj-dropdown></hj-dropdown>
*/
const props = defineProps(DropdownProps);
const emit = defineEmits(['open', 'close']);
// 展开状态
const active = ref(false);
// 外层内容样式
const contentStyle = ref<CSSProperties>({
zIndex: -1,
opacity: 0
});
// 下拉内容高度
const contentHeight = ref<number>(0);
// 菜单实际高度
const menuHeight = ref<number>(0);
// 当前展开方向
const currentDirection = ref<'down' | 'up'>(props.direction);
// 子组件引用
const instance = getCurrentInstance();
const vShow = defineModel('show', {
type: Boolean,
default: false
});
watch(vShow, val => {
if (val === active.value) return;
if (val) {
open();
} else {
close();
}
});
// 监听方向变化
watch(
() => props.direction,
val => {
currentDirection.value = val;
}
);
// 兼容头条样式
const styles = computed<CSSProperties>(() => {
const style: CSSProperties = {};
// #ifdef MP-TOUTIAO
style.width = '100vw';
// #endif
return style;
});
// 下拉出来部分的样式
const popupStyle = computed<CSSProperties>(() => {
const style: CSSProperties = {};
const isDown = currentDirection.value === 'down';
const hiddenTransformLate = isDown ? '-100%' : '100%';
style.maxHeight = props.maxHeight;
style.minHeight = props.minHeight;
style.transform = `translateY(${active.value ? 0 : hiddenTransformLate})`;
style[isDown ? 'top' : 'bottom'] = 0;
// 进行Y轴位移,展开状态时,恢复原位。收起状态时,往上位移100%(或下),进行隐藏
style.transitionDuration = `${Number(props.duration) / 1000}s`;
if (isDown) {
style.borderRadius = `0 0 ${$u.addUnit(props.borderRadius)} ${$u.addUnit(props.borderRadius)}`;
} else {
style.borderRadius = `${$u.addUnit(props.borderRadius)} ${$u.addUnit(props.borderRadius)} 0 0`;
}
return style;
});
// 生命周期
onMounted(() => {
getContentHeight();
});
/**
* 打开下拉菜单
* @param direction 展开方向 'down' | 'up'
*/
function open(direction?: 'down' | 'up') {
currentDirection.value = direction || props.direction;
// 重新计算高度,因为方向可能改变
getContentHeight();
// 设置展开状态
active.value = true;
// 展开时,设置下拉内容的样式
contentStyle.value = {
zIndex: 11,
opacity: 1
};
vShow.value = true;
emit('open');
}
/**
* 关闭下拉菜单
*/
function close() {
// 下拉内容的样式进行调整,不透明度设置为0
active.value = false;
contentStyle.value = {
...contentStyle.value,
opacity: 0
};
// 等待过渡动画结束后隐藏 z-index
vShow.value = false;
setTimeout(() => {
contentStyle.value = {
zIndex: -1,
opacity: 0
};
emit('close');
}, Number(props.duration));
}
/**
* 点击遮罩
*/
function maskClick() {
if (!props.closeOnClickMask) return;
close();
}
/**
* 获取下拉菜单内容的高度
* @description
* dropdown组件是相对定位的,下拉内容必须给定高度,
* 才能让遮罩占满菜单以下直到屏幕底部的高度。
*/
function getContentHeight() {
const windowHeight = $u.sys().windowHeight;
$u.getRect('.u-dropdown__menu', instance).then((res: any) => {
// 获取菜单实际高度
menuHeight.value = res.height;
/**
* 尺寸计算说明:
* 在H5端,uniapp获取尺寸存在已知问题:
* 元素尺寸的top值为导航栏底部到元素的上边沿的距离
* 但元素的bottom值却是导航栏顶部到元素底部的距离
* 为避免页面滚动,此处取菜单栏的bottom值进行计算
*/
if (currentDirection.value === 'up') {
contentHeight.value = res.top;
} else {
contentHeight.value = windowHeight - res.bottom;
}
});
}
// 暴露方法
defineExpose({
close,
open
});
</script>
<style scoped lang="scss">
@import 'uview-pro/libs/css/style.components';
.u-dropdown {
flex: 1;
width: 100%;
position: relative;
background-color: #fff;
&__content {
position: absolute;
z-index: 8;
width: 100%;
left: 0;
overflow: hidden;
&__mask {
position: absolute;
z-index: 9;
background: rgba(0, 0, 0, 0.3);
width: 100%;
left: 0;
top: 0;
bottom: 0;
}
&__popup {
position: absolute;
width: 100%;
z-index: 10;
transition: all 0.3s;
transform: translate3D(0, -100%, 0);
overflow: hidden;
background-color: var(--gray-2);
&__close {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
right: 24rpx;
top: 30rpx;
z-index: 9;
}
&__header {
display: flex;
color: var(--title-1);
font-size: var(--ft-32);
font-weight: 500;
line-height: 44rpx;
padding: 30rpx 24rpx;
}
&__body {
flex: 1;
overflow: hidden;
display: flex;
}
&__scroll-view {
flex: 1;
}
&__footer {
display: flex;
}
}
}
}
</style>