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>
相关推荐
ggdpzhk10 分钟前
VUE:基于MVVN的前端js框架
前端·javascript·vue.js
小曲曲1 小时前
接口上传视频和oss直传视频到阿里云组件
javascript·阿里云·音视频
学不会•2 小时前
css数据不固定情况下,循环加不同背景颜色
前端·javascript·html
EasyNTS3 小时前
H.264/H.265播放器EasyPlayer.js视频流媒体播放器关于websocket1006的异常断连
javascript·h.265·h.264
活宝小娜5 小时前
vue不刷新浏览器更新页面的方法
前端·javascript·vue.js
程序视点5 小时前
【Vue3新工具】Pinia.js:提升开发效率,更轻量、更高效的状态管理方案!
前端·javascript·vue.js·typescript·vue·ecmascript
coldriversnow5 小时前
在Vue中,vue document.onkeydown 无效
前端·javascript·vue.js
我开心就好o5 小时前
uniapp点左上角返回键, 重复来回跳转的问题 解决方案
前端·javascript·uni-app
刚刚好ā6 小时前
js作用域超全介绍--全局作用域、局部作用、块级作用域
前端·javascript·vue.js·vue
yqcoder7 小时前
reactflow 中 useNodesState 模块作用
开发语言·前端·javascript