开发适合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的具体内容,先创建如下的内容:

javascript 复制代码
// src\directive\index.js
import drag from './dialog'
export default function (app) {
  app.directive('dialogDrag', drag)
}


// src\directive\dialog.js

export 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 事件监听,以终止拖拽操作。
  1. 弹窗拉伸功能------通过拖动弹窗的边缘或角落来调整弹窗的尺寸。
  • 在弹窗的右下角(se-resize)、右边(w-resize)、左边(w-resize)、下边(n-resize)分别添加拉伸控制块,这些控制块是通过 document.createElement('div') 动态创建并插入到弹窗中。
  • 每个控制块绑定一个 mousedown 事件,用于监听用户的拉伸操作。根据用户鼠标移动的方向,计算弹窗的宽度或高度变化,并更新弹窗的 widthheight 样式属性。
  • 拉伸结束时,通过 mouseup 事件移除 mousemovemouseup 事件监听,停止尺寸调整操作。
  1. **双击全屏与还原------**通过双击弹窗的标题栏实现弹窗全屏和还原
  • 双击标题栏时(dblclick 事件),根据当前弹窗是否全屏状态(由 isFullScreen 标志控制)执行全屏或还原操作。
  • 全屏时,将弹窗的位置和尺寸调整为占满整个视窗(100VW, 100VH),并移除标题栏的拖拽功能。
  • 还原时,恢复弹窗到全屏前的尺寸和位置,并重新启用标题栏的拖拽功能。

3.难点

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

三、详细开发

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

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

javascript 复制代码
  <el-dialog
      v-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,比如这样:

javascript 复制代码
export const dialogDrag = (el, binding, vnode, oldVnode) => {
  const dialogElement = el.querySelector('.el-dialog')
  // console.log(dialogElement) // 这里是 el-dialog 元素的 DOM
  if (!dialogElement) {
    return
  }
}

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

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

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

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

出现这样的问题:

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

复制代码
<div v-dialogDrag class="image-view">
    <el-dialog
      v-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>

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

javascript 复制代码
export const dialogDrag = (el, binding, vnode, oldVnode) => {
  const dialogElement = el.querySelector('.el-dialog')
  // console.log(dialogElement) // 这里是 el-dialog 元素的 DOM
  if (!dialogElement) {
    return
  }
  //弹框可拉伸最小宽高
  let minWidth = 400
  let minHeight = 400
  //初始非全屏
  let isFullScreen = false
  //当前宽高
  let nowWidth = 0
  let nowHight = 0
  //当前顶部高度
  let nowMarginTop = 0
  //获取弹框头部(这部分可双击全屏)
  const dialogHeaderEl = el.querySelector('.el-dialog__header')
  //弹窗
  const dragDom = el.querySelector('.el-dialog')
  //弹窗body
  const 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')
  //头部加上可拖动cursor
  dialogHeaderEl.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.offsetLeft
    const 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).marginLeft
    const initialLeft = parseFloat(dialogMarginLeft)

    // 获取到的值带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.onmousemove = function (e) {
      // 通过事件委托,计算移动的距离
      const l = e.clientX - disX
      const 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 = null
      document.onmouseup = null
    }
  }
  dialogHeaderEl.onmousedown = moveDown
  //双击(头部)效果不想要可以注释
  dialogHeaderEl.ondblclick = (e) => {
    if (isFullScreen == false) {
      nowHight = dragDom.clientHeight
      nowWidth = dragDom.clientWidth
      nowMarginTop = dragDom.style.marginTop
      dragDom.style.left = 0
      dragDom.style.top = 0
      dragDom.style.height = '100VH'
      dragDom.style.width = '100VW'
      dragDom.style.marginTop = 0
      dragDom.style.marginBottom = 0
      isFullScreen = true
      dialogHeaderEl.style.cursor = 'initial'
      dialogHeaderEl.onmousedown = null
    } else {
      dragDom.style.height = 'auto'
      dragDom.style.width = nowWidth + 'px'
      dragDom.style.marginTop = nowMarginTop
      isFullScreen = false
      dialogHeaderEl.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.offsetLeft
    let disY = e.clientY - resizeEl.offsetTop
    document.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 = null
      document.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.clientWidth
    let initialOffsetLeft = dragDom.offsetLeft
    // 记录初始x位置
    let startX = e.clientX
    document.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 = null
      document.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.clientWidth
    let initialOffsetLeft = dragDom.offsetLeft
    // 记录初始x位置
    let startX = e.clientX
    document.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 = null
      document.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.clientY
    let elH = dragDom.clientHeight

    document.onmousemove = function (e) {
      e.preventDefault() // 移动时禁用默认事件
      dragDom.style.height = `${Math.max(minHeight, elH + (e.clientY - startY) * 2)}px`
    }

    // 拉伸结束
    document.onmouseup = function (e) {
      document.onmousemove = null
      document.onmouseup = null
    }
  }
}

export default {
  updated(el, binding, vnode, prevVnode) {
    dialogDrag(el, binding, vnode, prevVnode)
  }
}

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


四、总结

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

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

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

相关推荐
腾讯TNTWeb前端团队3 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰7 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪7 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪7 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy8 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom8 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom9 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom9 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom9 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom9 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试