vue编写可推拽的弹窗

作者: 前端小黑 | 来源:发表于2020-07-13 15:18 被阅读0次

    1. 前言

    早期在深圳工作的时候编写过一个可推拽的vue弹窗组件,实现的代码和写法有些简陋和不规范。最近回武汉工作刚好也有了这个需求,所以重新修改了下代码。

    • ps:早期的那篇文章还有好些人转载,最过分的是一个直接抄了还说我的实现方式蠢。ε=(´ο`*))) 唉无语了都。

    2. 实现原理

    主要的实现原理还是获取鼠标在div中的位置,获取位置后设置divlefttop来达到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. vuedirectives

    通过vue自定义指令获取绑定的元素,在对DOM进行操作。关于更多vue自定义指令用法,移步自定义指令

    6-2. 相关属性(事件对象eventdom元素,window对象)

    1. event.clientXclientX事件属性返回当事件被触发时鼠标指针向对于浏览器可视区域的水平坐标。
    2. event.clientYclientY事件属性返回当事件被触发时鼠标指针向对于浏览器页面可视区域的垂直坐标。
    3. offsetLeft/offsetLeftTop属性:可以返回当前元素距离某个定位父辈元素左边与顶部的距离(虽然我的父级遮罩层有了定位,但是它的宽高都是与body保持一致的)。
    4. offsetWidth/offsetHeight: 返回任何一个元素宽/高度,包括边框和填充
    5. 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 代码解析

    1. 给弹窗绑定onmousedown事件,获取到鼠标在弹窗中的位置(以弹窗左上角为原点)。
    2. document绑定onmousemove事件,获取当前的鼠标位置,当前鼠标位置减去鼠标在弹窗的相当位置即可得到此时弹窗应该处于的位置。然后在通过style设置弹窗的位置。
    3. 鼠标松开解绑document的鼠标事件。

    注意点:

    1. 弹窗要一直在页面可视区移动,最大的移动距离就是可视区的宽高减去盒子本身的宽高
      window.innerHeight - vnode.offsetHeight / 2;
      `window.innerWidth - vnode.offsetWidth / 2;
    2. 只有弹窗标题才能拖拽,所以判断非标题部分直接return
    3. 浏览器窗口大小改变会影响弹窗的位置,监听改变浏览器窗口改变把弹窗居中。

    8. 使用

    8-1. 单独引用

    1. 下载dialogDrag.vuedialogDrag.vue
    2. 单独组件使用
    <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全局引入

    1. src目录下新建components目录,下载dialogDrag.vue到此目录下。
    2. components目录下新建index.js
    import VueDrag form ./vueDrag.vue
    export default function install(Vue) {
        Vue.component("app-drag", VueDrag);
    }
    
    1. main.js中加入代码
    import appComponents from "./components/index.js";
    Vue.use(appComponents);
    
    1. 页面中使用<app-drag></app-drag>

    结语

    目前我正在做的项目中对弹窗还有放大和缩小的需求,我参考layer-ui做了一个类似的组件,实现原理与当前这个组件保持一致。后续优化好了再发出来。

    相关文章

      网友评论

        本文标题:vue编写可推拽的弹窗

        本文链接:https://www.haomeiwen.com/subject/wqpacktx.html