vue中通过自定义指令实现一个可拖拽,缩放的弹窗

效果

功能描述

  • 按住头部可拖拽
  • 鼠标放到边框,可缩放
  • 多层重叠
  • 丰富的插槽,易于扩展

示例

指令代码

js 复制代码
export const dragDialog = {
    inserted: function (el, { value, minWidth = 400, minHeight = 200 }) {
        // 让弹窗居中
        let dialogHeight = el.clientHeight ?? 0
        let dialogWidth = el.clientWidth ?? 0

        // 获取可视区域的宽高
        let windowWidth = document.documentElement.clientWidth ?? 0
        let windowHeight = document.documentElement.clientHeight ?? 0

        // 弹窗的可移动范围
        let leftMax = windowWidth - dialogWidth
        let topMax = windowHeight - dialogHeight

        //还需要判断是否传入了top,left值
        let { top, center } = value

        let left = (windowWidth - dialogWidth) / 2

        if (!center) {
            // 没有设置center
            if (top.includes('%') || top.includes('px')) {
                el.style.top = top
            } else {
                el.style.top = top + 'px'
            }

            el.style.left = left + 'px'
        } else {
            el.style.top = (windowHeight - dialogHeight) / 2 + 'px'
            el.style.left = (windowWidth - dialogWidth) / 2 + 'px'
        }

        const el_header = el.querySelector('.kl-dailog-header')
        // 只有点击头部才能拖拽
        if (!el_header) return
        let headerHeight = el_header.clientHeight - 0
        // 缩放相关
        el.onmousemove = function (e) {
            if(!e) return
            // 判断当前鼠标是否处于可以拖拽的边缘,不包含头部
            if (e.clientX > el.offsetLeft + el.clientWidth - 10 || el.offsetLeft + 10 > e.clientX) {
                el.style.cursor = 'w-resize'
            } else if (
                el.scrollTop + e.clientY >
                el.offsetTop + el.clientHeight - 10 - headerHeight
            ) {
                el.style.cursor = 's-resize'
            } else {
                el.style.cursor = 'default'
            }

            el.onmousedown = (e) => {
                if(!e) return
                // 获取头部的宽高以及到可视区域的距离
                const el_header_rect = el_header.getBoundingClientRect()

                if (!el_header_rect) return
                let offsetTopHeader = el_header_rect.top - 0

                // 判断当前元素是否是可拖拽的头部元素
                if (headerHeight > e.pageY - offsetTopHeader) {
                    // 是头部,拖拽相关
                    // 获取到鼠标与被拖拽节点的相对位置
                    let disx = e.pageX - el.offsetLeft
                    let disy = e.pageY - el.offsetTop

                    // 获取弹窗的宽高
                    let width = el.clientWidth ?? 0
                    let height = el.clientHeight ?? 0

                    // 设置其他弹窗的z-index 100
                    let maxZIndex = 100
                    document.querySelectorAll('.kl-dialog-container').forEach((item) => {
                        let zIndex = item.style.zIndex
                        zIndex = zIndex ? zIndex - 0 : 100
                        if (zIndex > maxZIndex) {
                            maxZIndex = zIndex
                        }
                    })
                    el.style.zIndex = maxZIndex + 1

                    document.onmousemove = function (e) {
                        const el_rect = el.getBoundingClientRect()

                        if (!el_rect) return

                        // 获取弹窗到可视区域的距离
                        let offsetTopEl = el_rect.top - 0
                        let offsetLeftEl = el_rect.left - 0

                        let left = e.pageX - disx
                        let top = e.pageY - disy

                        // 对弹窗的位置进行限制
                        if (offsetTopEl < 0 || top < 0) {
                            top = 0
                        }

                        if (offsetLeftEl < 0 || left < 0) {
                            left = 0
                        }

                        if (offsetTopEl + height > windowHeight || top > topMax) {
                            top = windowHeight - height
                        }

                        if (offsetLeftEl + width > windowWidth || left > leftMax) {
                            left = windowWidth - width
                        }

                        // 重新设置被拖拽节点的位置
                        el.style.left = left + 'px'
                        el.style.top = top + 'px'
                    }
                    document.onmouseup = function () {
                        document.onmousemove = document.onmouseup = null
                    }
                } else {
                    const clientX = e.clientX // 鼠标点击时的X坐标
                    const clientY = e.clientY // 鼠标点击时的Y坐标
                    let elW = el.clientWidth // 当前元素的宽度
                    let elH = el.clientHeight // 当前元素的高度
                    let EloffsetLeft = el.offsetLeft // 元素距离左边的距离
                    let EloffsetTop = el.offsetTop // 元素距离顶部的距离
                    el.style.userSelect = 'none'
                    let ELscrollTop = el.scrollTop // 元素滚动条距离顶部的距离
                    // 不是头部,缩放相关
                    document.onmousemove = function (e) {
                        e.preventDefault() // 移动时禁用默认事件
                        //左侧鼠标拖拽位置
                        if (clientX > EloffsetLeft && clientX < EloffsetLeft + 10) {
                            //往左拖拽
                            if (clientX > e.clientX) {
                                el.style.width = elW + (clientX - e.clientX) * 2 + 'px'
                                el.style.left = EloffsetLeft - (clientX - e.clientX) + 'px'
                            }
                            //往右拖拽
                            if (clientX < e.clientX) {
                                if (el.clientWidth < minWidth) {
                                } else {
                                    el.style.width = elW - (e.clientX - clientX) * 2 + 'px'
                                    el.style.left = EloffsetLeft + (e.clientX - clientX) + 'px'
                                }
                            }
                        }

                        //右侧鼠标拖拽位置
                        if (clientX > EloffsetLeft + elW - 10 && clientX < EloffsetLeft + elW) {
                            //往左拖拽
                            if (clientX > e.clientX) {
                                if (el.clientWidth < minWidth) {
                                } else {
                                    el.style.width = elW - (clientX - e.clientX) * 2 + 'px'
                                    el.style.left = EloffsetLeft + (clientX - e.clientX) + 'px'
                                }
                            }

                            //往右拖拽
                            if (clientX < e.clientX) {
                                el.style.width = elW + (e.clientX - clientX) * 2 + 'px'
                                el.style.left = EloffsetLeft + (clientX - e.clientX) + 'px'
                            }
                        }
                        //底部鼠标拖拽位置
                        if (
                            ELscrollTop + clientY > EloffsetTop + elH - 20 &&
                            ELscrollTop + clientY < EloffsetTop + elH
                        ) {
                            //往上拖拽
                            if (clientY > e.clientY) {
                                if (el.clientHeight < minHeight) {
                                } else {
                                    el.style.height = elH - (clientY - e.clientY) * 2 + 'px'
                                    el.style.top = EloffsetTop + (clientY - e.clientY) + 'px'
                                }
                            }
                            //往下拖拽
                            if (clientY < e.clientY) {
                                el.style.height = elH + (e.clientY - clientY) * 2 + 'px'
                                el.style.top = EloffsetTop + (clientY - e.clientY) + 'px'
                            }
                        }
                    }
                    //拉伸结束
                    document.onmouseup = function (e) {
                        document.onmousemove = null
                        document.onmouseup = null
                    }
                }
            }
        }
    },
    // 指令销毁
    unbind(el) {},
}

vue组件代码

html 复制代码
<template>
    <!-- 添加弹窗的动画 -->
    <!-- <transition name="kl-dialog"> -->
        <div class="kl-dialog" v-if="dialogVisible">
            <!-- 遮罩 -->
            <div class="kl-mask" :id="klMaskId" v-if="modal" @click="close"></div>
            <!-- 弹窗 -->
            <!-- 这儿需要mousedown来控制顺序 -->
            <div
                :id="klDialogId"
                :class="[
                    'kl-dialog-container',
                    'resize-container',
                    nobg ? 'kl-dialog-container-bg-no' : '',
                ]"
                v-dragDialog="{ top: top, center: center }"
                :style="{ width: width, top: top }"
                @mousedown="setZIndex"
            >
                <!-- 弹窗头部 -->
                <slot name="header">
                    <!-- 必须要有这个kl-dailog-header类才能拖拽 -->
                    <div class="kl-dailog-header cc">
                        <div class="kl-dailog-header-title">
                            {{ title }}
                        </div>
                        <div class="kl-dailog-header-close" @click="close">
                            <i class="el-icon-close kl-dailog-header-close-icon"></i>
                        </div>
                    </div>
                </slot>

                <!-- 弹窗中间内容 -->
                <slot> default </slot>

                <!-- 弹窗底部 -->
                <slot name="footer">
                    <div class="kl-dailog-footer">
                        <el-button @click="close">取消</el-button>
                        <el-button type="primary" @click="determine">确定</el-button>
                    </div>
                </slot>
            </div>
        </div>
    <!-- </transition> -->
</template>

<script>
export default {
    name: 'klDialog',
    props: {
        // 去除主题背景色
        nobg: {
            type: Boolean,
            default: false,
        },
        // 控制显示隐藏
        dialogVisible: {
            type: Boolean,
            default: false,
        },
        // 是否显示遮罩
        modal: {
            type: Boolean,
            default: true,
        },
        // 头部标题
        title: {
            type: String,
            default: '',
        },
        // 弹窗宽
        width: {
            type: String,
            default: '30%',
        },
        // 距离顶部的距离
        top: {
            type: String,
            default: '20%',
        },
        center: {
            type: Boolean,
            default: false,
        },
    },
    data() {
        return {
            klMaskId: '',
            klDialogId: '',
        }
    },
    created() {
        this.init()
    },
    beforeDestroy() {
        // console.log('beforeDestroy');
    },

    watch: {
        dialogVisible(val) {
            if (val) {
                this.setZIndex()
            }
        },
    },

    methods: {
        // 确定
        determine() {
            this.$emit('determine')
        },
        // 关闭弹窗
        close() {
            this.$emit('close')
        },

        // 给每个弹窗添加一个id
        initId() {
            this.klMaskId = this.createId()
            this.klDialogId = this.createId()
        },

        // 将当前弹窗的z-index设置为最高
        async setZIndex() {
            let { klDialogId } = this
            await this.$nextTick()
            let els = document.querySelectorAll('.kl-dialog-container')

            let maxZIndex = 100
            els.forEach((item) => {
                let zIndex = item.style.zIndex
                zIndex = zIndex ? zIndex - 0 : 100
                if (zIndex > maxZIndex) {
                    maxZIndex = zIndex
                }
            })

            let el = document.querySelector('#' + klDialogId)
            if (el) {
                el.style.zIndex = maxZIndex + 1
            }
        },

        // 初始化
        init() {
            this.initId()
            // 设置当前的弹窗层级最高
            this.setZIndex()
        },
    },
}
</script>

<style lang="scss" scoped>
.kl-mask {
    position: fixed;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vw;
    background-color: rgba(0, 0, 0, 0.5);
    z-index: 100;
}
.kl-dialog-container {
    position: fixed;
    border-radius: 2px;
    box-shadow: 0 1px 3px rgb(0 0 0 / 30%);
    box-sizing: border-box;
    background-color: #fff;
    z-index: 100;
}
.kl-dialog-container-bg-no {
    box-shadow: none;
    background: none;
}
.kl-dialog-container-center {
    left: 50% !important;
    top: 50% !important;
    transform: translate(-50%, -50%) !important;
}

.kl-dailog-header {
    padding: 0 20px;
    height: 54px;
    line-height: 54px;
    position: relative;
    font-size: 18px;
    font-weight: 500;
    user-select: none;
}
.kl-dailog-header-close {
    position: absolute;
    top: 50%;
    right: 20px;
    transform: translateY(-50%);
    cursor: pointer;
}
.kl-dailog-header-close-icon {
    color: #aaa;
}
.kl-dailog-footer {
    padding: 0 20px;
    height: 70px;
    line-height: 70px;
    text-align: right;
}

.cc {
    cursor: move;
}

// 弹窗动画
.kl-dialog-enter-active,
.kl-dialog-leave-active {
    transition: all 0.3s;
}

.kl-dialog-enter,
.kl-dialog-leave-to {
    opacity: 0;
    transform: translate(300px,300px);
}
</style>
相关推荐
HEX9CF13 分钟前
【CTF Web】Pikachu xss之href输出 Writeup(GET请求+反射型XSS+javascript:伪协议绕过)
开发语言·前端·javascript·安全·网络安全·ecmascript·xss
积水成江43 分钟前
关于Generator,async 和 await的介绍
前端·javascript·vue.js
Z3r4y44 分钟前
【Web】portswigger 服务端原型污染 labs 全解
javascript·web安全·nodejs·原型链污染·wp·portswigger
人生の三重奏1 小时前
前端——js补充
开发语言·前端·javascript
计算机学姐1 小时前
基于SpringBoot+Vue的高校运动会管理系统
java·vue.js·spring boot·后端·mysql·intellij-idea·mybatis
Tandy12356_1 小时前
js逆向——webpack实战案例(一)
前端·javascript·安全·webpack
老华带你飞1 小时前
公寓管理系统|SprinBoot+vue夕阳红公寓管理系统(源码+数据库+文档)
java·前端·javascript·数据库·vue.js·spring boot·课程设计
qbbmnnnnnn2 小时前
【WebGis开发 - Cesium】如何确保Cesium场景加载完毕
前端·javascript·vue.js·gis·cesium·webgis·三维可视化开发
f8979070703 小时前
layui动态表格出现 横竖间隔线
前端·javascript·layui
杨荧3 小时前
【JAVA开源】基于Vue和SpringBoot的水果购物网站
java·开发语言·vue.js·spring boot·spring cloud·开源