一、研发拖拽弹窗的起因
最近在使用宝塔的时候,发现他的弹框是可以拖拽的,于是就像自己的平台弹框也增加这个功能。
二、分析
ant-design-vue的文档发现没有相关的兼容实现
三、解决方案
3.1、使用vue2.x带的directives来进行自定义指令代码如下:
javascript
directives: {
dialogDrag: {
/**
* 拖拽指令的绑定函数
* 用于给模态框添加拖拽功能
*
* @param {HTMLElement} el - 指令绑定的元素(a-modal容器)
* @param {Object} binding - 包含指令参数的对象
* @param {Object} vnode - Vue虚拟节点
* @param {Object} oldVnode - 旧的虚拟节点
*/
bind(el, binding, vnode, oldVnode) {
const value = binding.value
// 如果拖拽功能被禁用或处于全屏模式,则不执行拖拽初始化
if (value === false || vnode.context.innerFullscreen) return
// 获取拖拽内容头部元素(用于触发拖拽事件)
const dialogHeaderEl = el.querySelector('.ant-modal-header')
// 获取模态框主体元素(用于设置位置样式)
const dragDom = el.querySelector('.ant-modal')
// 保存原始光标样式,用于后续恢复
vnode.context.cursor = dialogHeaderEl.style.cursor
// 设置拖拽头部的光标为移动样式
dialogHeaderEl.style.cursor = 'move'
// 设置模态框为绝对定位,以便进行拖拽定位
dragDom.style.position = 'absolute'
// 重置margin-top,避免影响定位计算
dragDom.style.marginTop = 0
// 获取模态框宽度,处理百分比和像素值
let width = dragDom.style.width
if (width.includes('%')) {
// 如果是百分比,转换为像素值
width = +document.body.clientWidth * (+width.replace(/\%/g, '') / 100)
} else {
// 如果是像素值,提取数值部分
width = +width.replace(/\px/g, '')
}
// 获取模态框实际宽度,如果计算宽度大于实际宽度则使用实际宽度
let modalClientWidth = el.getElementsByClassName('ant-modal')[0].clientWidth
if(width > modalClientWidth && modalClientWidth > 0){
width = modalClientWidth;
}
// 使用window.innerWidth获取视口宽度
const viewportWidth = window.innerWidth;
// 计算居中位置
const leftPosition = ((viewportWidth - width) / 2);
// 确保left值不会为负数,防止对话框移出屏幕
dragDom.style.left = Math.max(0, leftPosition) + 'px';
// 调用模态框实例的onDrag方法,初始化拖拽事件监听
vnode.context.onDrag();
}
},
},
3.2、元素中绑定该自定义功能:
html
<a-modal
ref="modal"
:class="getClass(modalClass)"
:style="getStyle(modalStyle)"
:visible="visible"
v-bind="_attrs"
v-on="$listeners"
@ok="handleOk"
@cancel="handleCancel"
destroyOnClose
v-dialog-drag
></a-modal>
3.3、watch监听弹框是否显示
javascript
watch: {
visible() {
if (this.visible) {
this.innerFullscreen = this.fullscreen
if(!this.innerFullscreen){
this.$nextTick(()=>{
const dialogHeaderEl = document.querySelector('.ant-modal-header')
const dragDom = document.querySelector('.ant-modal')
this.cursor = dragDom.style.cursor
dialogHeaderEl.style.cursor = 'move'
dragDom.style.position = 'absolute'
dragDom.style.marginTop = 0
let width = dragDom.style.width
if (width.includes('%')) {
width = +document.body.clientWidth * (+width.replace(/\%/g, '') / 100)
} else {
width = +width.replace(/\px/g, '')
}
let modalClientWidth = document.getElementsByClassName('ant-modal')[0].clientWidth
if(width > modalClientWidth && modalClientWidth > 0){
width = modalClientWidth;
}
// 使用window.innerWidth获取视口宽度
const viewportWidth = window.innerWidth;
console.log(viewportWidth,width)
// 考虑到顶部和底部可能存在的系统栏,我们可以减去一个安全的预留值,比如20px
const leftPosition = ((viewportWidth - width) / 2);
console.log('leftPosition',leftPosition)
// 确保left值不会为负数,防止对话框移出屏幕
dragDom.style.left = Math.max(0, leftPosition) + 'px';
this.onDrag();
})
}
}
},
},
3.4、拖拽方法实现
javascript
onDrag(){
let that = this;
this.$nextTick(() => {
const dialogHeaderEl = document.querySelector('.ant-modal-header');
const dragDom = document.querySelector('.ant-modal');
const sty = dragDom.style || window.getComputedStyle(dragDom, null)
const userAgent = navigator.userAgent;
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent)) {
dialogHeaderEl.addEventListener('touchstart', onTouchStart);
function onTouchStart(e){
// 记录初始触摸位置
const target = e.target;
// 检查点击的是否为全屏按钮
if (target.classList.contains('ant-modal-close') && target.classList.contains('ant-modal-close-x')) {
return; // 如果是全屏按钮,则直接返回不做处理
}
e.preventDefault();
const touchStart = e.touches[0];
const disX = touchStart.clientX - dialogHeaderEl.offsetLeft;
const disY = touchStart.clientY - dialogHeaderEl.offsetTop;
// 获取到的值带px 正则匹配替换
let styL, styT;
// 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
if (sty.left.includes('%')) {
styL = +document.body.clientWidth * (+sty.left.replace(/\%/g, '') / 100);
styT = +document.body.clientHeight * (+sty.top.replace(/\%/g, '') / 100);
} else {
styL = +sty.left.replace(/\px/g, '');
styT = +sty.top.replace(/\px/g, '');
}
// 添加触摸移动事件监听
document.addEventListener('touchmove', onTouchMove, { passive: false });
function onTouchMove(e) {
// 计算移动的距离
const touchMove = e.touches[0];
const l = touchMove.clientX - disX;
const t = touchMove.clientY - disY;
let finallyL = l + styL;
let finallyT = t + styT;
// 检查并防止对话框超出屏幕边界
const viewportWidth = document.documentElement.clientWidth;
const viewportHeight = document.documentElement.clientHeight;
const dialogWidth = dragDom.offsetWidth;
const dialogHeight = dragDom.querySelector('.ant-modal-content').offsetHeight;
const safeMargin = that.getStatusBarHeightByUserAgent() + 20;
finallyL = Math.max(0, finallyL); // 防止向左超出
finallyL = Math.min(viewportWidth - dialogWidth, finallyL); // 防止向右超出
finallyT = Math.max(0, finallyT); // 防止向上超出
finallyT = Math.min(viewportHeight - dialogHeight, finallyT + safeMargin); // 防止向下超出
// 移动当前元素
dragDom.style.left = `${finallyL}px`;
dragDom.style.top = `${finallyT}px`;
}
// 添加触摸结束事件监听
document.addEventListener('touchend', onTouchEnd);
function onTouchEnd() {
// 移除事件监听器
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', onTouchEnd);
}
}
} else {
//pc端
dialogHeaderEl.addEventListener('mousedown', onMousedown);
function onMousedown(e){
e.preventDefault();
// 鼠标按下,计算当前元素距离可视区的距离 (鼠标点击位置距离可视窗口的距离)
const disX = e.clientX - dialogHeaderEl.offsetLeft
const disY = e.clientY - dialogHeaderEl.offsetTop
// 获取到的值带px 正则匹配替换
let styL, styT
// 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
if (sty.left.includes('%')) {
styL = +document.body.clientWidth * (+sty.left.replace(/\%/g, '') / 100)
styT = +document.body.clientHeight * (+sty.top.replace(/\%/g, '') / 100)
} else {
styL = +sty.left.replace(/\px/g, '')
styT = +sty.top.replace(/\px/g, '')
}
document.addEventListener('mousemove',onMouseMove)
function onMouseMove(e){
// 通过事件委托,计算移动的距离 (开始拖拽至结束拖拽的距离)
const l = e.clientX - disX;
const t = e.clientY - disY;
let finallyL = l + styL;
let finallyT = t + styT;
// 检查并防止对话框超出屏幕边界
const viewportWidth = document.documentElement.clientWidth;
const viewportHeight = document.documentElement.clientHeight;
const dialogWidth = dragDom.offsetWidth;
const dialogHeight =dragDom.querySelector('.ant-modal-content').offsetHeight;
finallyL = Math.max(0, finallyL); // 防止向左超出
finallyL = Math.min(viewportWidth - dialogWidth, finallyL); // 防止向右超出
finallyT = Math.max(0, finallyT); // 防止向上超出
finallyT = Math.min(viewportHeight - dialogHeight, finallyT); // 防止向下超出
// 移动当前元素
dragDom.style.left = `${finallyL}px`;
dragDom.style.top = `${finallyT}px`;
}
document.addEventListener('mouseup',onMouseup)
function onMouseup(){
document.removeEventListener('mousemove',onMouseMove);
document.removeEventListener('mouseup',onMouseup)
}
}
}
})
},