封装一个Vue3的弹框组件

设计思路:
1、组件结构: 基础弹框包含header、body、footer三部分。
2、功能设计:支持自定义内容、可以拖拽、多种预设类型的(如提示框,确认框,警告框等等)。
3、实现方式:使用Teleport将弹框渲染到body标签下面,避免父组件的css有影响。
4、状态管理:通过响应式的ref控制弹框显隐状态。
5、交互设计:支持多种关闭的方式(按钮的关闭、点击遮罩层关闭、ESC按键关闭)。
第一步创建组件
<template>
    <!-- 弹框组件 -->
    <Teleport to="body">
        <Transition name="modal-fade">
            <div v-if="modelValue" class="modal-overlay" @click="handleOverlayClick">
                <div class="modal-container" :style="modalSyle" @mousedown="startDrag" @mousemove="onDrag"
                    @mouseup="stopDrag" @mouseleave="stopDrag">
                    <!-- 头部标题栏 -->
                    <div class="modal-header">
                        <!-- 左边的标题栏 -->
                        <slot name="header-title">
                            {{ title }}
                        </slot>
                        <!-- 右边的按钮栏 -->
                        <button class="modal-close" @click="close">X</button>
                    </div>
                    <!-- body内容区 -->
                    <div class="modal-body">
                        <slot>
                            {{ content }}
                        </slot>
                    </div>
                    <!-- 底部按钮区 -->
                    <div class="modal-footer">
                        <slot name="footer">
                            <button class="modal-btn cancel" v-if="showCancel" @click="cancel">{{ cancelBtnTitle
                                }}</button>
                            <button class="modal-btn confirm" @click="confirm">{{ confirmBtnTitle }}</button>
                        </slot>
                    </div>
                </div>
            </div>
        </Transition>
    </Teleport>
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref } from 'vue';
const props = defineProps({
    modelValue: Boolean, //v-modelValue绑定值
    title: { //标题
        type: String,
        default: '提示'
    },
    content: String, //中间的内容
    cancelBtnTitle: { //取消的按钮文字
        type: String,
        default: '取消'
    },
    confirmBtnTitle: { //确认的按钮文字
        type: String,
        default: '确认'
    },
    showCancel: { //是否显示取消按钮
        type: Boolean,
        default: true
    },
    width: { //弹框宽度
        type: [String, Number],
        default: '500px'
    },
    height: { //弹框高度
        type: [String, Number],
        default: 'auto'
    },
    closeOnClickOverlay: {
        type: Boolean,
        default: true
    },
    draggable: { //是否拖拽
        type: Boolean,
        default: true
    },
})
const emit = defineEmits(['update:modelValue', 'confirm', 'cancel'])
//处理拖拽逻辑
const isDragging = ref(false);
const dragOffset = ref({ x: 0, y: 0 });
const position = ref({ x: 0, y: 0 });
const modalSyle = computed(() => {
    return {
        width: typeof props.width === 'number' ? `${props.width}px` : props.width,
        height: typeof props.height === 'number' ? `${props.height}px` : props.height,
        transform: `translate(${position.value.x}px, ${position.value.y}px)`
    }
})
//开始拖拽
const startDrag = (event: any) => {
    if (!props.draggable) return;
    //只有点击标题栏时才可以拖动
    if (event.target.closest('.modal-header')) {
        isDragging.value = true;
        dragOffset.value = {
            x: event.clientX - position.value.x,
            y: event.clientY - position.value.y
        };
    }
}
const onDrag = (event: any) => {
    if (isDragging.value) {
        position.value = {
            x: event.clientX - dragOffset.value.x,
            y: event.clientY - dragOffset.value.y
        }
    };
}
//结束拖拽
const stopDrag = () => {
    isDragging.value = false;
}
//取消
const cancel = () => {
    emit('cancel');
    close();
}
//确认
const confirm = () => {
    emit('confirm');
    close();
}
//关闭
const close = () => {
    emit('update:modelValue', false);
}
const handleOverlayClick = (event: any) => {
    if (props.closeOnClickOverlay && event.target.classList.contains('modal-overlay')) {
        close();
    }
}
//判断按ESC键关闭弹框
const handleKeyDown = (event: any) => {
    if (event.key === 'Escape' && props.modelValue) {
        close();
    }
}
//监听按键
onMounted(() => {
    window.addEventListener('keydown', handleKeyDown);
})
//移除按键
onUnmounted(() => {
    window.removeEventListener('keydown', handleKeyDown);
})
</script>
<style lang="scss" scoped>
.modal-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.5);
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 1000;
}
.modal-container {
    background-color: #fff;
    border-radius: 4px;
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
    overflow: hidden;
    display: flex;
    flex-direction: column;
    max-width: 90%;
    max-height: 90%;
}
.modal-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 12px 16px;
    border-bottom: 1px solid #e8e8e8;
    .modal-close {
        border: none;
        background: transparent;
        font-size: 20px;
        cursor: pointer;
        outline: none;
    }
}
.modal-body {
    padding: 16px;
    overflow-y: auto;
    flex: 1;
}
.modal-footer {
    padding: 12px 16px;
    border-top: 1px solid #e8e8e8;
    text-align: right;
    .modal-btn {
        padding: 8px 15px;
        margin-left: 10px;
        border-radius: 4px;
        border: 1px solid #dcdfe6;
        background: #fff;
        cursor: pointer;
    }
    .confirm {
        background: #409eff;
        color: #fff;
        border-color: #409eff;
    }
}
.modal-fade-enter-active,
.modal-fade-leave-active {
    transition: opacity 0.3s;
}
.modal-fade-enter-from,
.modal-fade-leave-to {
    opacity: 0;
}
</style>
第二步创建全局弹框服务
//注册并使用弹框组件
import { createApp, h, ref } from "vue";
import Modal from '../components/Modal.vue';
export function createModal() {
    //全局实列存储
    const instances: any = [];
    //封装了弹框内容
    function modal(options: any = {}) {
        //创建div元素
        const container = document.createElement('div');
        document.body.appendChild(container);
        const visible = ref(true);
        const app = createApp({
            setup() {
                const onClose = () => {
                    visible.value = false;
                    setTimeout(() => {
                        app.unmount();
                        container.remove();
                        const index = instances.findIndex((instance: any) => instance === app)
                        if (index !== -1) {
                            instances.splice(index, 1)
                        }
                    }, 300); //给过渡动画留时间
                };
                return () => h(Modal, {
                    modelValue: visible.value,
                    'onUpdate:modelValue': (val: any) => {
                        visible.value = val;
                        if (!val) onClose();
                    },
                    onConfirm: () => {
                        if (options.onConfirm) options.onConfirm();
                        onClose();
                    },
                    onClancel: () => {
                        if (options.onClancel) options.onClancel();
                        onClose();
                    },
                    ...options
                }, options.slots || {})
            }
        });
        instances.push(app);
        app.mount(container);
        return {
            close: () => {
                visible.value = false;
            }
        }
    }
    //alert弹框
    modal.alert = (content: any, title: any = '提示', onConfirm: any) => {
        return modal({
            title,
            content,
            showCancel: false,
            onConfirm
        })
    };
    //confirm弹框
    modal.onfirm = (content: any, title: any = '确认', onConfirm: any, onClancel: any) => {
        return modal({
            title,
            content,
            onConfirm,
            onClancel
        })
    };
    //关闭所有的弹框
    modal.closeAll = () => {
        instances.forEach((instance: any) => {
            const vm = instance._instance.subTree.component.ctx;
            if (vm?.close) vm.close();
        })
    };
    return modal;
}
export default {
    install(app: any) {
        const modal = createModal();
        app.config.globalProperties.$modal = modal;
        app.provide('modal', modal)
    }
}
第三步在main里面注册全局插件
import { createApp } from 'vue'
import App from './App.vue'
import ModalPlugin from './util/modal'
const app = createApp(App);
app.use(ModalPlugin);
app.mount('#app');
第四步在页面中使用该组件
<template>
    <div class="container">
        <el-button type="primary" @click="visible = true">打开组件式弹框</el-button>
        <el-button type="primary" @click="openModal">打开普通弹框</el-button>
        <el-button type="primary" @click="showAlert">打开Alert弹框</el-button>
        <el-button type="primary" @click="showConfirm">打开Confirm弹框</el-button>
        <Modal v-model="visible" title="组件方式弹框">
            <p>这是一个组件方式打开的弹框</p>
            <template #footer>
                <el-button @click="visible = false">取消</el-button>
                <el-button type="primary" @click="visible = false">确认</el-button>
            </template>
        </Modal>
    </div>
</template>
<script setup lang="ts">
import Modal from '../components/Modal.vue';
import { inject, ref } from 'vue';
const visible = ref(false);
const modal: any = inject('modal');
const openModal = () => {
    modal({
        title: '自定义弹框',
        content: '这是一个打开的弹框',
        width: 600,
        onConfirm: () => {
            console.log('点击了确定')
        },
        onCancel: () => {
            console.log('点击了取消')
        }
    })
}
const showAlert = () => {
    modal.alert('这是提示框', '提示', () => {
        console.log('alert确认')
    })
}
const showConfirm = () => {
    modal.onfirm('确认要执行操作吗?', '确认操作', () => {
        console.log('confirm确认')
    }, () => {
        console.log('confirm取消')
    })
}
</script>
<style scoped lang="scss">
.container {
    text-align: center;
    margin-top: 20px;
    width: 100%;
}
</style>
本文来自博客园,作者:小珍珠在河里敲代码,转载请注明原文链接:https://www.cnblogs.com/Jansens520/p/18829997
 

 
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号