1. 前言
早期在深圳工作的时候编写过一个可推拽的vue弹窗组件,实现的代码和写法有些简陋和不规范。最近回武汉工作刚好也有了这个需求,所以重新修改了下代码。
- ps:早期的那篇文章还有好些人转载,最过分的是一个直接抄了还说我的实现方式蠢。ε=(´ο`*))) 唉无语了都。
2. 实现原理
主要的实现原理还是获取鼠标在div
中的位置,获取位置后设置div
的left
和top
来达到div
跟随鼠标移动的效果。因为写的是vue
,所以利用了vue
的自定义指令来操作dom
。
3. 搭建主体ui
3-1. 代码结构
- html代码
<template>
<transition name="el-fade-in-linear">
<div class="dialog_box" v-if="dialogVisible">
<div class="dialog_mask"></div>
<div
class="loading_wrap"
v-if="confirmLoading"
></div>
<div
:style="{ width: dialogWidth }"
class="normal_dialog"
v-drag
>
<div class="dialog_header">
<div class="header_title fl">{{ title }}</div>
<div class="header_button_box fr">
<i
class="el-icon-close"
@click="closeDialog"
/>
</div>
</div>
<div
:style="{ height: bodyHeight }"
class="dialog_body"
>
<!-- 弹窗中心区域 -->
<slot/>
</div>
<div
class="dialog_footer"
:style="{ 'textAlign': footerAlign }"
>
<el-button
plain
class="edit-dialog-btn"
size="small"
type="info"
@click="cancel"
>{{ cancelText }}
</el-button>
<el-button
v-if="dialogType === 'confirm'"
class="edit-dialog-btn"
size="small"
type="primary"
:loading="confirmLoading"
@click="confirm"
>{{ confirmText }}
</el-button>
</div>
</div>
</div>
</transition>
</template>
- less代码
.dialog_box {
position: fixed;
z-index: 99;
left: 0;
top: 0;
bottom: 0;
right: 0;
.dialog_mask {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
background-color: #000;
opacity: 0.5;
}
.loading_wrap {
position: absolute;
z-index: 999;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: transparent;
}
.normal_dialog {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #FFFFFF;
box-shadow: 0px 0px 0px 0px;
border-radius: 10px;
border: 1px solid #DDDDDD;
z-index: 1000;
box-sizing: border-box;
.dialog_header {
overflow: hidden;
height: 50px;
line-height: 55px;
padding: 0 15px;
font-size: 15px;
color: #000000;
border-bottom: 1px solid #EEEEEE;
cursor: move;
.header_button_box {
cursor: pointer;
i {
display: inline-block;
font-size: 18px;
color: #666666;
margin-left: 10px;
}
}
}
.dialog_body {
color: #606266;
font-size: 14px;
overflow: auto;
}
.dialog_footer {
width: 100%;
border-top: 1px solid #EEEEEE;
padding: 10px;
box-sizing: border-box;
.edit-dialog-btn {
width: 70px;
}
}
}
}
3-2. 设计要点
- 背景遮罩
我这里选择了使用了3个遮罩板,第一块是覆盖全屏幕的白色遮罩dialog_box
使用fixed定位,让弹窗的所有内容与浏览器之间不会出现留白。第2块就是灰色背景dialog_mask
,用来突显弹窗。最后一块是点击确定的遮罩窗loading_wrap
,来防止提交ajax
时,用户点击按钮或修改弹窗数据。 - 弹窗构成
这里的弹窗就包括标题
,内容
和底部
部分。内容部分通过插槽
插入内容。由于现在再项目中使用的是element ui
的组件库,所以底部按钮没有写原生的。如果有需要不使用框架组件的需求可以查看我的早期的样式设计。
陈小黑的blog-可推拽弹窗
4. 定义组件props
props | 类型 | 描述 |
---|---|---|
dialogShow | Boolean | 设置弹窗的显示隐藏 |
dialogWidth | String | 设置弹窗的整体宽度 |
title | String | 弹窗的标题 |
dialogType | String | 弹窗的类型 (confirm, info) |
footerAlign | String | 底部按钮的对齐方式 (left, center, right) |
confirmText | String | 确定按钮的文案 |
cancelText | String | 取消按钮的文案 |
confirmLoading | Boolean | 确定按钮的加载遮罩 |
5. 自定义事件实现按钮回调
点击头部的
x
按钮关闭弹窗和点击确认
,取消
按钮的$emit
回调事件
methods: {
// emit按钮的点击事件给父组件
cancel() {
this.$emit("cancel");
},
confirm() {
this.$emit("confirm");
},
// 关闭弹窗
closeDialog() {
this.dialogVisible = false;
}
},
6. 自定义指令drag
实现拖拽效果
6-1. vue
的directives
。
通过vue
自定义指令获取绑定的元素,在对DOM
进行操作。关于更多vue
自定义指令用法,移步自定义指令
6-2. 相关属性(事件对象event
,dom
元素,window
对象)
-
event.clientX
:clientX
事件属性返回当事件被触发时鼠标指针向对于浏览器可视区域的水平坐标。 -
event.clientY
:clientY
事件属性返回当事件被触发时鼠标指针向对于浏览器页面可视区域的垂直坐标。 -
offsetLeft/offsetLeftTop
属性:可以返回当前元素距离某个定位父辈元素左边与顶部的距离(虽然我的父级遮罩层有了定位,但是它的宽高都是与body
保持一致的)。 -
offsetWidth/offsetHeight
: 返回任何一个元素宽/高度,包括边框和填充 -
window.innerHeight/Width
: 获取当前页面可视区的宽高(包括滚动条)。
6-3. 相关事件
props | 描述 |
---|---|
onmusedown | 按下鼠标时触发 |
onmusemove | 按下鼠标过程中移动鼠标触发 |
onmuseup | 松开鼠标时触发 |
onresize | 页面可视化区域变化触发 |
6-4. 自定义指令代码
directives: {
drag: {
inserted(el, binding, vnode) {
vnode = vnode.elm;
el.onmousedown = ((event) => {
if (event.target.className !== "dialog_header") {
return;
}
//获取鼠标在盒子中的位置
let mouseX = event.clientX - vnode.offsetLeft;
let mouseY = event.clientY - vnode.offsetTop;
//绑定移动和停止函数
document.onmousemove = ((event) => {
let left, top;
//获取新的鼠标位置对应下的盒子应该在的位置
left = event.clientX - mouseX;
top = event.clientY - mouseY;
//获取div在页面中X轴的最小最大位置
let minX = vnode.offsetWidth / 2;
let maxX = window.innerWidth - (vnode.offsetWidth / 2);
if (left <= minX) {
left = minX;
} else if (left >= maxX) {
left = maxX;
}
//获取div在页面中Y轴的最大最小位置
let minY = vnode.offsetHeight / 2;
let maxY = window.innerHeight - (vnode.offsetHeight / 2);
if (top <= minY) {
top = minY;
} else if (top >= maxY) {
top = maxY;
}
//赋值移动
vnode.style.left = left + "px";
vnode.style.top = top + "px";
});
document.onmouseup = (() => {
document.onmousemove = document.onmouseup = null;
});
});
window.onresize = (() => {
vnode.style.left = "50%";
vnode.style.top = "50%";
});
}
}
}
7 代码解析
- 给弹窗绑定
onmousedown
事件,获取到鼠标在弹窗中的位置(以弹窗左上角为原点)。 -
document
绑定onmousemove
事件,获取当前的鼠标位置,当前鼠标位置减去鼠标在弹窗的相当位置即可得到此时弹窗应该处于的位置。然后在通过style
设置弹窗的位置。 - 鼠标松开解绑
document
的鼠标事件。
注意点:
- 弹窗要一直在页面可视区移动,最大的移动距离就是可视区的宽高减去盒子本身的宽高
window.innerHeight - vnode.offsetHeight / 2
;
`window.innerWidth - vnode.offsetWidth / 2; - 只有弹窗标题才能拖拽,所以判断非标题部分直接
return
。 - 浏览器窗口大小改变会影响弹窗的位置,监听改变浏览器窗口改变把弹窗居中。
8. 使用
8-1. 单独引用
- 下载
dialogDrag.vue
。dialogDrag.vue - 单独组件使用
<v-drag
:dialog-show.sync="dialogShow"
dialog-width="700px"
body-height="500px"
:confirm-loading="loading"
@confirm="dialogShow = false"
@cancel="dialogShow = false"
>
<div>test</div>
</v-drag>
import vDrag from "/dialogDrag";
export default {
name: "test",
components: {
vDrag,
},
data() {
return {
dialogShow: false,
loading: false
}
}
}
9. 脚手架v-cli全局引入
-
src
目录下新建components
目录,下载dialogDrag.vue
到此目录下。 -
components
目录下新建index.js
import VueDrag form ./vueDrag.vue
export default function install(Vue) {
Vue.component("app-drag", VueDrag);
}
-
main.js
中加入代码
import appComponents from "./components/index.js";
Vue.use(appComponents);
- 页面中使用
<app-drag></app-drag>
结语
目前我正在做的项目中对弹窗还有放大和缩小的需求,我参考layer-ui做了一个类似的组件,实现原理与当前这个组件保持一致。后续优化好了再发出来。