弹窗与遮罩的实现

开发中经常会遇到弹窗显示的情况,在当前页面进行更多操作或提示而不离开本页面,这时就是弹窗大显身手的时候了,开发过程中使用多的还是组件库中已经封装好的弹窗,最近在开发的时候突然对弹窗有了兴趣,想要更加深入了解一下。

写弹窗肯定无法离开遮罩,咱们就先来实现一下遮罩。

遮罩的实现

遮罩我们肯定是想显示在最上层,所有需要将遮罩元素添加到最外层元素中,vue3 中我们可以使用 teleport 元素将元素添加到目标元素中,to 属性就是我们的目标元素,我们也可以在遮罩中添加一个新的 props 可以叫做 target 指向我们的目标元素。react 使用的是 Portals 整体使用也是跟teleport类似。

遮罩展示后,通常情况下都有滚动穿透的问题,这里我在遮罩展示时添加 lock 类,只是简单的一个 overflow: hidden !important; 阻止 body 元素的滚动,并且我添加了一个 touchmove 事件 @touchmove="$event.preventDefault()" 阻止手势滑动,但是我使用的方法过于简单,如果要求不高的话可以使用。

滚动穿透更详细的解决方案可以参考像 vueuse useScrollLock 方法或者 ant-design-mobile use-lock-scroll

<template>
    <teleport to="body">
        <div
            class="overlay-wrapper"
            :class="{ centered }"
            :style="{ ...maskStyled, display: isShowing ? (centered ? 'flex' : 'block') : 'none', opacity }"
            @touchmove="$event.preventDefault()"
            @click="onOverlayClick"
        >
            <slot />
        </div>
    </teleport>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue';
import type { CSSProperties } from 'vue';

const props = withDefaults(
    defineProps<{
        // 是否显示遮罩
        show: boolean;
        // 是否需要锁定页面滚动
        needLock: boolean;
        // 遮罩中的元素是否居中显示
        centered: boolean;
        // 是否要在点击遮罩时关闭遮罩
        closeOnClickOverlay: boolean;
        // 自定义遮罩样式
        maskStyled: CSSProperties;
    }>(),
    { show: false, needLock: true, closeOnClickOverlay: true, maskStyled: () => ({}) }
);

const emit = defineEmits(['close']);

// 设置遮罩的透明度
const opacity = ref(0);
const isShowing = ref(props.show);

// 监听遮罩的显示和隐藏
watch(
    () => props.show,
    (value) => {
        if (value) {
            if (props.needLock) {
                document.body.classList.add('lock');
            }

            isShowing.value = true;
            // 使用 setTimeout 是为了延迟更改 opacity,为了让动画能够正常显示,如果直接更改,由于 display: none 改变后会直接显示元素而不会触发动画
            setTimeout(() => {
                opacity.value = 1;
            });
        } else {
            if (props.needLock) {
                document.body.classList.remove('lock');
            }

            opacity.value = 0;
            // 消失动画结束后再将 isShowing 设置为 false,直接更改会立即消失
            setTimeout(() => {
                isShowing.value = false;
            }, 200);
        }
    }
);

const onOverlayClick = (e: MouseEvent) => {
    // 如果当前点击的元素是遮罩层元素,就要关闭遮罩
    if (props.closeOnClickOverlay && e.target === e.currentTarget) {
        emit('close', false);
    }
};
</script>

<style scoped lang="less">
.overlay-wrapper {
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.5);
    z-index: 99;
    position: fixed;
    top: 0;
    opacity: 0;
    transition: all 0.2s ease-in-out;
}

.centered {
    justify-content: center;
    align-items: center;
}
</style>

弹窗的实现

遮罩实现之后,我们的弹窗实现就简单了,只要在遮罩的基础上加上我们的弹出层即可。

弹窗中主要注意出现时机与动画

<template>
    <Overlay :show="show" :centered="mode === 'center'">
        <div class="drawer-wrapper" :class="[currentModeClass, showClass, className]">
            <slot />
        </div>
    </Overlay>
</template>

<script setup lang="ts">
const props = withDefaults(
    defineProps<{
        // 弹窗 className
        className?: string;
        show?: boolean;
        // 弹窗展开方向
        mode?: 'top' | 'bottom' | 'left' | 'right' | 'center';
        // 弹窗样式
    }>(),
    { show: false, mode: 'bottom', className: '' }
);

const currentModeClass = computed(() => {
    return { top: 'top', bottom: 'bottom', left: 'left', right: 'right', center: 'center' }[props.mode];
});

const showClass = ref<string>('');

watch(
    () => props.show,
    (value) => {
        if (value) {
            setTimeout(() => {
                showClass.value = `${currentModeClass.value}-active`;
            });
        } else {
            showClass.value = '';
        }
    }
);
</script>

<style scoped lang="less">
.drawer-wrapper {
    overflow: auto;
    background-color: #fff;
    color: #000;

    position: absolute;

    transition: all 0.2s ease-in-out;
}

.bottom {
    width: 100%;
    max-height: 60%;
    bottom: 0;
    transform: translateY(100%);
}
.bottom-active {
    transform: none;
}

.top {
    width: 100%;
    max-height: 60%;
    top: 0;
    transform: translateY(-100%);
}
.top-active {
    transform: none;
}

.left {
    height: 100%;
    max-width: 70%;
    left: 0;
    transform: translateX(-100%);
}
.left-active {
    transform: none;
}

.right {
    height: 100%;
    max-width: 70%;
    right: 0;
    transform: translateX(100%);
}
.right-active {
    transform: none;
}

.center {
    width: 80%;
    height: 60%;
    transform: scale(0);
}
.center-active {
    transform: scale(1);
}
</style>

更多

html 提供了一个 dialog:对话框元素 – HTML(超文本标记语言) | MDN (mozilla.org) 实际我们可以直接使用

原文链接:https://juejin.cn/post/7319297259644289058 作者:过期也候

(0)
上一篇 2024年1月3日 上午10:00
下一篇 2024年1月3日 上午10:10

相关推荐

发表回复

登录后才能评论