Vue3可拖拽弹窗封装

主要文件:

Dialog.vue 弹窗组件,可以直接在vue中以<Dialog></Dialog>组件嵌入的形式使用,使用Teleport将组件挂载至外层body中(Teleport在不使用函数方式调起弹窗时有用,如果只使用Dialog.ts,则可以不要)

Dialog.ts函数式调用的封装,可以直接使用函数调起弹窗。弹窗函数式调起(动态创建)一般有(createApp+monted) 和 (render+h)两种方案。

区别

createApp和mount用于创建和挂载整个Vue应用,而render和h函数用于渲染单个组件。
createApp和mount
1.创建应用实例:使用createApp创建一个Vue应用实例
2.挂载到dom:使用mount方法将这个应用实例挂载到DOM中的一个元素上。
render+h
1.创建虚拟节点:使用h函数手动创建一个虚拟节点(VNode),这个虚拟节点代表你想要渲染的组件。
2.渲染到DOM:使用render方法将这个虚拟节点渲染到DOM中的一个元素上。


下文采用createApp+mounted方案

具体代码:

//Dialog.vue
<template>
    <Teleport to="body">
        <div class="popModel" :style="{ backgroundColor: bg }">
            <div class="popContent" ref="el" :style="{ left: x + 'px', top: y + 'px' }">
                <div class="head">
                    <span>{{ title }}</span>
                    <img src="@/assets/imgs/close.png" alt="关闭" @click="cancel()">
                </div>
                <div class="content">
                    <slot></slot>
                </div>
            </div>
        </div>
    </Teleport>
</template>
<script setup lang="ts">
import { ref, onMounted, nextTick } from 'vue';
import { useDraggable } from '@vueuse/core'  //使用vueuse的拖拽

const props = defineProps<{
    title: string,  //弹窗标题
    bg?: string,    //蒙层背景
    x?: number,    //弹窗初始left位置,未设置默认为窗口中间
    y?: number,   //弹窗初top始位置,未设置默认为窗口中间
    cancel: Function    //关闭弹窗的回调
}>()

const el = ref<HTMLElement | null>(null)

const { x, y, style } = useDraggable(el, {
    initialValue: { x: document.body.clientWidth / 2, y: document.body.clientHeight / 2 },
})

onMounted(() => {
    if (el.value) {
        x.value = props.x ?? x.value - el.value.offsetWidth / 2    //设置弹窗初始居中
        y.value = props.y ?? y.value - el.value.offsetHeight / 2 //设置弹窗初始居中
        setTimeout(() => {
            el.value.style.transition = 'unset'    //为了使得弹窗出现时有动画,而拖拽时取消动画
        });
    }
})

</script>

<style scoped>
.popModel {
    position: fixed;
    top: 0px;
    left: 0px;
    width: 100%;
    height: 100vh;
}

.popContent {
    background: linear-gradient(to right, rgba(21, 40, 65, 0.9) 60%, rgba(21, 40, 65, 0.6));
    position: fixed;
    border-radius: 5px;
    padding: 20px;
    box-sizing: border-box;
    transition: all .2s ease-in;

    >.head {
        display: flex;
        justify-content: space-between;
        align-items: center;
        user-select: none;

        >span {
            font-weight: bold;
            font-size: 18px;
            color: #21A6FF;
            background: linear-gradient(90deg, #3BE1F6 0%, #21A6FF 100%);
            -webkit-text-fill-color: transparent;
            background-clip: text;
        }

        >img {
            width: 14px;
            height: 14px;
            object-fit: cover;
            cursor: pointer;
        }
    }
}
</style>

//Dialog.ts
import { createApp, h, type VNode } from "vue";
import DialogVue from "./Dialog.vue";

//也可以直接创建一个函数,而不是在DialogVue对象上加方法
DialogVue.open = (props: any, content: VNode) => {
    const mountDom = document.createElement('div')
    document.body.appendChild(mountDom)

    return new Promise((resolve, reject) => {
        const dialogInstance = createApp(h(DialogVue, {
            ...props,
            cancel,
        }, content))    //content作为Dialog.vue中的slot

        dialogInstance.mount(mountDom)  //挂载

        /**
         * 关闭
         */
        function cancel() {
            //卸载实例
            dialogInstance.unmount()
            //卸载容器
            mountDom.remove()
            //执行回调
            resolve(null)
        }
    })
}

export default DialogVue

函数式调用示例

import Dialog from '@/components/Dialog.js';

//ChildVue为弹窗中嵌入的slot
Dialog.open({ title: '弹窗标题' }, h(ChildVue, props)).then(()=>{console.log('弹窗关闭了')})
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容