当前位置: 首页 > news >正文

开发适合el-dialog的拉伸拖拽自定义指令和适配自定义的图片查看组件

目录

一、应用场景

二、开发流程

1.自定义指令

2.功能原理

3.难点

三、详细开发

四、总结


一、应用场景

我之前有开发过一个图片查看的组件,这个组件可在单页面打开,也可以在弹窗里打开,但是弹窗因为是比较固定,所以有一些局限性,只能拖拽,不能改变弹窗大小,于是有了开发【可以拖拽改变大小的弹窗】组件

原先的图片查看组件的博客地址:仿照elemenet-image的预览开发图片切换和放大缩小等功能_vue3 <el-image> 下方一行缩略图 可左右-CSDN博客

上方的组件实现效果如此:

  1. 目标:这次我需要实现的是满足上方的图片查看的功能(去掉底部的轮播图,弹窗不太需要),还需要满足弹窗的拖拽边框可以改变弹窗大小,并且弹窗的顶部可以被拖拽着移动位置。
  2. 实现方案:因为我是使用的是el-dialog,所以本身弹窗就可以拖拽,只是不能被手动改变大小,查找了一些解决方案后,于是借助一些思想,实现了一个自定义指令,期间踩了一些坑。
  3. 实现功能:
  • 拖动弹窗:通过拖动弹窗的头部 (.el-dialog__header),可以在页面上自由移动弹窗的位置。鼠标按下头部并拖动时,会实时更新弹窗的位置。但是不能移出左侧和上侧视图范围,这部分也在下面的调整大小里有限制。
  • 双击全屏/还原:双击弹窗的头部,可以在全屏和恢复到之前的大小和位置之间切换。全屏状态下,弹窗覆盖整个视口,头部不可拖动。再次双击会恢复到初始的大小和位置。

  • 调整大小

    • 右下角调整:通过右下角的一个小区域 (se-resize),可以拖动调整弹窗的宽度和高度,同时保持最小宽度和高度限制。
    • 左右侧调整:通过左右两侧的小区域 (w-resize),可以水平调整弹窗的宽度。同样,宽度不能小于设定的最小值。
    • 下侧调整:通过下边的小区域 (n-resize),可以垂直调整弹窗的高度,高度不能小于设定的最小值

4.实现效果:


二、开发流程

这里对创建自定义指令做一些简单介绍

1.自定义指令

首先需要了解以下知识:

Vue 3 的自定义指令提供了一些生命周期钩子,用于在指令应用到元素的不同阶段执行特定的操作:

  • beforeMount:指令绑定到元素并插入父节点之前调用。
  • mounted:指令绑定到元素后调用。
  • beforeUpdate:指令所在组件的 VNode 更新之前调用。
  • updated:指令所在组件的 VNode 更新之后调用。
  • beforeUnmount:指令所在组件销毁之前调用。
  • unmounted:指令绑定的元素移出 DOM 之后调用。

简单的示例:
 

// 在 main.js 中注册全局自定义指令
import { createApp } from 'vue';
import App from './App.vue';

const app = createApp(App);

app.directive('focus', {
  mounted(el) {
    el.focus();
  }
});

app.mount('#app');
 

使用:
<template> <input v-focus /> </template>

我在开发过程中反复测试,得出我们在updated进行绑定就行,这样才能保证弹窗的创建和绑定。

如果需要实现一个可以拖拽改变弹窗大小的指令,那么首先建立一个文件夹,如下:

这里先不讨论dialog.js的具体内容,先创建如下的内容:

// src\directive\index.js
import drag from './dialog'
export default function (app) {app.directive('dialogDrag', drag)
}// src\directive\dialog.jsexport const dialogDrag = (el, binding, vnode, oldVnode) => {//这里是补充逻辑的地方
}export default {updated(el, binding, vnode, prevVnode) {dialogDrag(el, binding, vnode, prevVnode)}
}

2.功能原理

为了实现我想要的功能,可以通过 JavaScript 操作 DOM 元素的样式和事件监听器,来实现拖拽拉伸移动等等,开发前,先进行三项功能的原理整理:

 1. 弹窗拖拽功能——通过拖动弹窗的标题栏来移动整个弹窗的位置

  • 通过 el.querySelector('.el-dialog__header') 获取弹窗的标题栏元素(.el-dialog__header)。
  • 设置标题栏的 cursormove,提示用户可以拖动该区域。
  • 通过 mousedown 事件监听用户按下鼠标的动作,计算并记录鼠标点击位置与弹窗左上角的偏移量。
  • 当鼠标移动时,通过 mousemove 事件更新弹窗的位置,使其跟随鼠标移动。在 mouseup 事件中移除 mousemovemouseup 事件监听,以终止拖拽操作。

2. 弹窗拉伸功能——通过拖动弹窗的边缘或角落来调整弹窗的尺寸。

  • 在弹窗的右下角(se-resize)、右边(w-resize)、左边(w-resize)、下边(n-resize)分别添加拉伸控制块,这些控制块是通过 document.createElement('div') 动态创建并插入到弹窗中。
  • 每个控制块绑定一个 mousedown 事件,用于监听用户的拉伸操作。根据用户鼠标移动的方向,计算弹窗的宽度或高度变化,并更新弹窗的 widthheight 样式属性。
  • 拉伸结束时,通过 mouseup 事件移除 mousemovemouseup 事件监听,停止尺寸调整操作。

3. 双击全屏与还原——通过双击弹窗的标题栏实现弹窗全屏和还原

  • 双击标题栏时(dblclick 事件),根据当前弹窗是否全屏状态(由 isFullScreen 标志控制)执行全屏或还原操作。
  • 全屏时,将弹窗的位置和尺寸调整为占满整个视窗(100VW, 100VH),并移除标题栏的拖拽功能。
  • 还原时,恢复弹窗到全屏前的尺寸和位置,并重新启用标题栏的拖拽功能。

3.难点

  1. 同步尺寸和位置:在拖拽或拉伸时,需要实时同步弹窗的位置和尺寸,这涉及到对鼠标移动的精确跟踪,并处理弹窗在不同浏览器窗口尺寸下的表现。
  2. 边界处理:在拖拽和拉伸时,防止弹窗超出窗口的可视区域,尤其是避免标题栏被拖出窗口顶部。
  3. 多方向拉伸的冲突处理:在实现多方向拉伸时,确保各方向的拉伸控制块不会互相冲突。例如,右下角的拉伸控制块涉及同时调整宽度和高度,需要正确处理与单方向拉伸控制块之间的优先级问题。

三、详细开发

注意!我先写踩坑的点,如下:

第一步,我们要找到我们需要在哪里使用,我的应用场景就是在弹窗的地方使用,所以我就想定义一个弹窗的指令,理想的情况是这样的:

  <el-dialogv-model="dialogVisible"v-dialogDrag  ///这里width="50%"top="0vh":z-index="2080":modal="false":close-on-click-modal="false"modal-class="dialog_class"><div class="image-view-container"><ImageView :url="dialogImageUrl" style="width: 100%" @changeImage="changeImage" /></div></el-dialog>

然后在指令里去写获取当前弹窗的DOM,比如这样:

export const dialogDrag = (el, binding, vnode, oldVnode) => {const dialogElement = el.querySelector('.el-dialog')// console.log(dialogElement) // 这里是 el-dialog 元素的 DOMif (!dialogElement) {return}
}

就会发现怎么也获取不到当前的dom。 

我一开始以为我是钩子时机不对,updated 钩子可能会在元素还未完全渲染时触发,这可能导致无法获取到子元素。所以为了确保 DOM 结构已经完全渲染,尝试使用 mounted 钩子,结果也一样,然后我尝试用我常用的方法:nextTick,也无法实现……于是第一步就卡在这里了。

问题就在于:el-dialog 组件可能还未完全渲染完成,无法正确获取到 DOM 元素。

当然可以通过添加一些调试信息,检查 el-dialog 是否确实存在,我在写的过程中,确实这样写无法实现。

出现这样的问题:

所以经过多次调试,我选择了这样的方式:

<div v-dialogDrag class="image-view"><el-dialogv-model="dialogVisible"width="50%"top="0vh":z-index="2080":modal="false":close-on-click-modal="false"modal-class="dialog_class"><div class="image-view-container"><ImageView :url="dialogImageUrl" style="width: 100%" @changeImage="changeImage" /></div></el-dialog></div>

那么具体的指令的代码如下:

export const dialogDrag = (el, binding, vnode, oldVnode) => {const dialogElement = el.querySelector('.el-dialog')// console.log(dialogElement) // 这里是 el-dialog 元素的 DOMif (!dialogElement) {return}//弹框可拉伸最小宽高let minWidth = 400let minHeight = 400//初始非全屏let isFullScreen = false//当前宽高let nowWidth = 0let nowHight = 0//当前顶部高度let nowMarginTop = 0//获取弹框头部(这部分可双击全屏)const dialogHeaderEl = el.querySelector('.el-dialog__header')//弹窗const dragDom = el.querySelector('.el-dialog')//弹窗bodyconst dialogBodyEl = el.querySelector('.el-dialog__body')// 设置body的最小高宽dialogBodyEl.style.minWidth = minWidth - 5 + 'px'dialogBodyEl.style.minHeight = minHeight - 100 + 'px'dialogBodyEl.style.height = '100%'//给弹窗加上overflow auto;不然缩小时框内的标签可能超出dialog;dragDom.style.overflow = 'auto'//清除选择头部文字效果dialogHeaderEl.onselectstart = new Function('return false')//头部加上可拖动cursordialogHeaderEl.style.cursor = 'move'// 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);const sty = dragDom.currentStyle || window.getComputedStyle(dragDom, null)let moveDown = (e) => {// 鼠标按下,计算当前元素距离可视区的距离const disX = e.clientX - dialogHeaderEl.offsetLeftconst disY = e.clientY - dialogHeaderEl.offsetTop// 计算弹窗样式中的 --el-dialog-margin-top 值const dialogStyles = window.getComputedStyle(dragDom)const marginTopVh = parseFloat(dialogStyles.getPropertyValue('--el-dialog-margin-top'))// 计算初始弹窗顶部相对于可视区域顶部的偏移量const dialogMarginTopPx = window.innerHeight * (marginTopVh / 100)const initialTop = dialogMarginTopPx// 获取初始弹窗距离窗口左侧的距离const dialogMarginLeft = getComputedStyle(dragDom).marginLeftconst initialLeft = parseFloat(dialogMarginLeft)// 获取到的值带px 正则匹配替换let styL, styT// 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为pxif (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.onmousemove = function (e) {// 通过事件委托,计算移动的距离const l = e.clientX - disXconst t = e.clientY - disY// 计算弹窗的左边界,不能超过窗口的左侧const minLeft = -initialLeft// 控制弹窗的左边界const left = Math.max(minLeft, l + styL)// 移动当前元素dragDom.style.left = `${left}px`dragDom.style.top = `${Math.max(-initialTop, t + styT)}px` //确保了拖拽过程中弹窗头部不会超出窗口的顶部//将此时的位置传出去//binding.value({x:e.pageX,y:e.pageY})}document.onmouseup = function (e) {document.onmousemove = nulldocument.onmouseup = null}}dialogHeaderEl.onmousedown = moveDown//双击(头部)效果不想要可以注释dialogHeaderEl.ondblclick = (e) => {if (isFullScreen == false) {nowHight = dragDom.clientHeightnowWidth = dragDom.clientWidthnowMarginTop = dragDom.style.marginTopdragDom.style.left = 0dragDom.style.top = 0dragDom.style.height = '100VH'dragDom.style.width = '100VW'dragDom.style.marginTop = 0dragDom.style.marginBottom = 0isFullScreen = truedialogHeaderEl.style.cursor = 'initial'dialogHeaderEl.onmousedown = null} else {dragDom.style.height = 'auto'dragDom.style.width = nowWidth + 'px'dragDom.style.marginTop = nowMarginTopisFullScreen = falsedialogHeaderEl.style.cursor = 'move'dialogHeaderEl.onmousedown = moveDown}}//拉伸右下方let resizeEl = document.createElement('div')dragDom.appendChild(resizeEl)//在弹窗右下角加上一个10-10px的控制块resizeEl.style.cursor = 'se-resize'resizeEl.style.position = 'absolute'resizeEl.style.height = '10px'resizeEl.style.width = '10px'resizeEl.style.right = '0px'resizeEl.style.bottom = '0px'resizeEl.style.zIndex = '99'//鼠标拉伸弹窗resizeEl.onmousedown = (e) => {// 记录初始x位置let startX = e.clientX// 鼠标按下,计算当前元素距离可视区的距离let disX = e.clientX - resizeEl.offsetLeftlet disY = e.clientY - resizeEl.offsetTopdocument.onmousemove = function (e) {e.preventDefault() // 移动时禁用默认事件// 通过事件委托,计算移动的距离//这里 由于elementUI的dialog控制居中的,所以水平拉伸效果是双倍//比较最小宽高和现在的宽高的大小,取大值dragDom.style.width = `${Math.max(minWidth, e.clientX - disX + (e.clientX - startX))}px`dragDom.style.height = `${Math.max(minHeight, e.clientY - disY)}px`}//拉伸结束document.onmouseup = function (e) {document.onmousemove = nulldocument.onmouseup = null}}//拉伸右边let resizeElR = document.createElement('div')dragDom.appendChild(resizeElR)//在弹窗右下角加上一个10-10px的控制块resizeElR.style.cursor = 'w-resize'resizeElR.style.position = 'absolute'resizeElR.style.height = '100%'resizeElR.style.width = '10px'resizeElR.style.right = '0px'resizeElR.style.top = '0px'//鼠标拉伸弹窗resizeElR.onmousedown = (e) => {let elW = dragDom.clientWidthlet initialOffsetLeft = dragDom.offsetLeft// 记录初始x位置let startX = e.clientXdocument.onmousemove = function (e) {e.preventDefault() // 移动时禁用默认事件//右侧鼠标拖拽位置if (startX > initialOffsetLeft + elW - 20 && startX < initialOffsetLeft + elW) {//往左拖拽if (startX > e.clientX) {dragDom.style.width = `${Math.max(minWidth, elW - (startX - e.clientX) * 2)}px`}//往右拖拽if (startX < e.clientX) {dragDom.style.width = `${elW + (e.clientX - startX) * 2}px`}}}//拉伸结束document.onmouseup = function (e) {document.onmousemove = nulldocument.onmouseup = null}}//拉伸左边let resizeElL = document.createElement('div')dragDom.appendChild(resizeElL)//在弹窗右下角加上一个10-10px的控制块resizeElL.style.cursor = 'w-resize'resizeElL.style.position = 'absolute'resizeElL.style.height = '100%'resizeElL.style.width = '10px'resizeElL.style.left = '0px'resizeElL.style.top = '0px'//鼠标拉伸弹窗resizeElL.onmousedown = (e) => {let elW = dragDom.clientWidthlet initialOffsetLeft = dragDom.offsetLeft// 记录初始x位置let startX = e.clientXdocument.onmousemove = function (e) {e.preventDefault() // 移动时禁用默认事件//左侧鼠标拖拽位置if (startX > initialOffsetLeft && startX < initialOffsetLeft + 20) {//往左拖拽if (startX > e.clientX) {dragDom.style.width = `${elW + (startX - e.clientX) * 2}px`}//往右拖拽if (startX < e.clientX) {dragDom.style.width = `${Math.max(minWidth, elW - (e.clientX - startX) * 2)}px`}}}//拉伸结束document.onmouseup = function (e) {document.onmousemove = nulldocument.onmouseup = null}}// 拉伸下边let resizeElB = document.createElement('div')dragDom.appendChild(resizeElB)//在弹窗右下角加上一个10-10px的控制块resizeElB.style.cursor = 'n-resize'resizeElB.style.position = 'absolute'resizeElB.style.height = '10px'resizeElB.style.width = '100%'resizeElB.style.left = '0px'resizeElB.style.bottom = '0px'// 鼠标拉伸弹窗resizeElB.onmousedown = (e) => {// 记录初始鼠标位置和弹窗尺寸let startY = e.clientYlet elH = dragDom.clientHeightdocument.onmousemove = function (e) {e.preventDefault() // 移动时禁用默认事件dragDom.style.height = `${Math.max(minHeight, elH + (e.clientY - startY) * 2)}px`}// 拉伸结束document.onmouseup = function (e) {document.onmousemove = nulldocument.onmouseup = null}}
}export default {updated(el, binding, vnode, prevVnode) {dialogDrag(el, binding, vnode, prevVnode)}
}

 mousemovemouseupmousedown 是 JavaScript 中用于处理鼠标交互的事件,分别对应鼠标的移动、按下和松开操作,所以上述代码的实现也是注意借助这几个事件来实现的。当然加一些防抖,效果会更好。


四、总结

说下难点,第一个就是生命周期的选择和指令使用的位置,一定套一个div。

其他难点就是,需要动态计算弹窗的位置与尺寸,因为弹窗的位置和尺寸是动态计算的,涉及到鼠标的实时位置和弹窗初始位置之间的关系。为了确保用户体验,处理窗口边界的限制也是一个难点,确保弹窗不会拖出可视区域(这里我的可视区域是左边和上面不能拖出,但是右边和下边可以),还有一个比较难的就是弹窗内的图片查看组件的样式适配,因为要对弹窗边框拖拽改变大小时,图片也要自适应的改变,所以这个样式方面就做了很多功夫,代码也贴上去了,仅供参考~

至于可以优化的点,应该就是拖拽边框的时候更丝滑和防抖吧,如果有其他建议,麻烦评论区指出~

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Java使用Apache POI向Word文档中填充数据
  • 深度学习基础--卷积网络
  • 智能语音交互:人工智能如何改变我们的沟通方式?
  • BIOS基础
  • vue3 那些可以让 Vue3 开发更加丝滑的小东西
  • 如何判断IP地址是否异常?
  • cell phone teardown 手机拆卸
  • React18快速入门
  • 浅谈proc目录
  • 跨境电商TikTok Shop指南:高效选品与营销技巧攻略
  • c#如何实现触发另外一个文本框的回车事件
  • 结构者设计模式
  • 深入掌握大模型精髓:《实战AI大模型》带你全面理解大模型开发!
  • leetcode 392. 判断子序列
  • 基于APISIX实现API网关案例分享
  • CSS实用技巧干货
  • download使用浅析
  • HTTP--网络协议分层,http历史(二)
  • Node.js 新计划:使用 V8 snapshot 将启动速度提升 8 倍
  • react-core-image-upload 一款轻量级图片上传裁剪插件
  • Spring Cloud中负载均衡器概览
  • Theano - 导数
  • vue从创建到完整的饿了么(11)组件的使用(svg图标及watch的简单使用)
  • vue自定义指令实现v-tap插件
  • 分布式事物理论与实践
  • 开源SQL-on-Hadoop系统一览
  • 马上搞懂 GeoJSON
  • 码农张的Bug人生 - 初来乍到
  • 如何合理的规划jvm性能调优
  • 入门级的git使用指北
  • 我的面试准备过程--容器(更新中)
  • 一个SAP顾问在美国的这些年
  • 专访Pony.ai 楼天城:自动驾驶已经走过了“从0到1”,“规模”是行业的分水岭| 自动驾驶这十年 ...
  • ​ssh-keyscan命令--Linux命令应用大词典729个命令解读
  • ​直流电和交流电有什么区别为什么这个时候又要变成直流电呢?交流转换到直流(整流器)直流变交流(逆变器)​
  • # 20155222 2016-2017-2 《Java程序设计》第5周学习总结
  • # dbt source dbt source freshness命令详解
  • # Python csv、xlsx、json、二进制(MP3) 文件读写基本使用
  • # 计算机视觉入门
  • #include<初见C语言之指针(5)>
  • #laravel 通过手动安装依赖PHPExcel#
  • #NOIP 2014#Day.2 T3 解方程
  • $LayoutParams cannot be cast to android.widget.RelativeLayout$LayoutParams
  • (C++哈希表01)
  • (delphi11最新学习资料) Object Pascal 学习笔记---第14章泛型第2节(泛型类的类构造函数)
  • (LLM) 很笨
  • (Python) SOAP Web Service (HTTP POST)
  • (二十一)devops持续集成开发——使用jenkins的Docker Pipeline插件完成docker项目的pipeline流水线发布
  • (附源码)php新闻发布平台 毕业设计 141646
  • (三)Pytorch快速搭建卷积神经网络模型实现手写数字识别(代码+详细注解)
  • (实战篇)如何缓存数据
  • (万字长文)Spring的核心知识尽揽其中
  • (五)Python 垃圾回收机制
  • (转)树状数组
  • .net Application的目录