<template>
<div class="drawer" v-show="isVisible"
:class="[
props.overlayClass,
{ 'drawer-active': isActive }
]"
@click.self="handleClickOverlay"
>
<div class="drawer-content"
:class="[
'drawer-content-'+props.position,
{ 'content-active': isActive }
]"
:style="{
...contentStyle,
...props.style
}"
>
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch} from 'vue';
interface PropType {
show: boolean,
position: 'top' | 'bottom' | 'left' | 'right',
round: boolean | number,
height: number | string,
overlayClass: string,
closeOnClickOverlay: boolean,
style: object
}
const props = withDefaults(defineProps<PropType>(), {
show: false,
position: 'bottom',
round: true,
height: '60vh',
overlayClass: 'drawer-overlay',
closeOnClickOverlay: true,
style: {}
});
const emit = defineEmits(['update:show', 'close']);
// 控制整个抽屉的显示隐藏
const isVisible = ref(false);
// 控制动画状态
const isActive = ref(false);
// 监听 show 的变化
watch(() => props.show, (newVal) => {
if (newVal) {
// 显示抽屉
isVisible.value = true;
// 等待 DOM 更新后添加动画类
requestAnimationFrame(() => {
isActive.value = true;
});
} else {
// 先移除动画类
isActive.value = false;
// 等待动画结束后隐藏抽屉
let timer: any = setTimeout(() => {
isVisible.value = false;
clearTimeout(timer);
timer = null;
}, 300); // 这个时间需要和 CSS 动画时间一致
}
}, { immediate: true });
// 圆角
const contentRound = computed(() => {
const stringRound = props.round ? (typeof props.round === 'number' ? props.round + 'px' : '16px') : '0';
let styleString = '';
switch(props.position){
case 'top':
styleString = `0 0 ${stringRound} ${stringRound}`;
break;
case 'bottom':
styleString = `${stringRound} ${stringRound} 0 0`;
break;
case 'right':
styleString = `${stringRound} 0 0 ${stringRound}`;
break;
case 'left':
styleString = `0 ${stringRound} ${stringRound} 0`;
break;
}
return styleString;
});
// 高度
const contentHeight = computed(() => {
return props.height ? (typeof props.height === 'number' ? props.height + 'px' : props.height) : '60vh';
});
// 内容区域的样式
const contentStyle = computed(() => {
const style = {
height: contentHeight.value,
borderRadius: contentRound.value,
width: 'auto',
}
if (props.position === 'top') {
style.width = '100%';
}
if (props.position === 'bottom') {
style.width = '100%';
}
if (props.position === 'left') {
style.width = 'auto';
style.height = '100%';
}
if (props.position === 'right') {
style.width = 'auto';
style.height = '100%';
}
return style;
});
// 点击遮罩层
const handleClickOverlay = () => {
if (props.closeOnClickOverlay) {
emit('update:show', false);
emit('close');
}
}
</script>
<style lang="scss" scoped>
.drawer {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
&-content{
width: 100%;
// height: 60vh;
background-color: #fff;
position: absolute;
bottom: 0;
transition: transform 0.3s ease;
}
}
.drawer-overlay{
background-color: rgba(0, 0, 0, 0.5);
}
.drawer-content {
position: fixed;
background: #fff;
z-index: 1000;
transition: all 0.3s ease;
overflow: scroll;
&::-webkit-scrollbar{
display: none;
}
}
// 底部弹出
.drawer-content-bottom {
bottom: 0;
left: 0;
width: 100%;
transform: translateY(100%);
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.1);
&.content-active {
transform: translateY(0);
}
}
// 顶部弹出
.drawer-content-top {
top: 0;
left: 0;
width: 100%;
transform: translateY(-100%);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
&.content-active {
transform: translateY(0);
}
}
// 左侧弹出
.drawer-content-left {
top: 0;
left: 0;
height: 100%;
transform: translateX(-100%);
box-shadow: 4px 0 12px rgba(0, 0, 0, 0.1);
&.content-active {
transform: translateX(0);
}
}
// 右侧弹出
.drawer-content-right {
top: 0;
right: 0;
height: 100%;
transform: translateX(100%);
box-shadow: -4px 0 12px rgba(0, 0, 0, 0.1);
&.content-active {
transform: translateX(0);
}
}
// 中心弹出
.drawer-content-center {
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.5);
opacity: 0;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
&.content-active {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
}
}
</style>
01-01
1712

12-11
674

06-30
828

02-14
03-20