vue 中 el-dialog 二次封装

PS:首先声明一点,本篇文章封装的是 element-plus 的对话框,别搞错了兄弟们,plus跟ui的对话框有些细节是不一样的。

1. 功能介绍

(1)对element-plus的el-dialog进行二次封装,实现自定义对话框宽高、定位、底部按钮,

(2)新增功能包括拉伸、全屏,

(3)对原本API也带入了封装,包括遮罩层、关闭图标、打开关闭事件回调、按钮事件等。

        附:我将所有用到的API写在了最下面(第6条)

2. 结果展示

el-dialog二次封装效果

3. 功能拆解

(1)首先对头部和尾部进行封装,实现头尾部的文本自定义,右下角增加用于拉伸的透明dom,头部增加全屏按钮图标,支持头部双击全屏;

        里面的<svgIcon></svgIcon>标签是我封装用来展示svg图标的组件,你们嫌麻烦可以直接换成小图片或者组件库里的图标都行

<template #header="{ titleId, titleClass }">
      <div class="my-header" @dblclick="screenfnc">
          <h4 :id="titleId" :class="titleClass">{{ title }}</h4>
          <svgIcon
            :name="screen ? 'bg-fullscreen' : 'fullscreen'"
            width="20px"
            v-show="fullscreen"
            @click="screenfnc"
            style="cursor: pointer"
            class="svgIcon"></svgIcon>
      </div>
</template>

<template #footer>
      <span class="dialog-footer">
          <el-button @click="leftBtnClick" v-show="leftBtn">{{ leftBtn }}</el-button>
          <el-button type="primary" @click="rightBtnClick" v-show="rightBtn"> {{ rightBtn }} </el-button>
      </span>
      <!-- 定位在右下角的用于拉伸的透明小块 -->
      <div class="stretch" @mousedown="mousedown"></div>
</template>

(2)全屏方法:通过控制el-dialog的类名进行样式修改,达到全屏的效果

// 全屏
const screenfnc = () => {
  if (screen.value) {
    // 恢复原来位置
    (document.getElementsByClassName("el-dialog")[0] as HTMLElement).style.height = height.value;
    width.value = screenWidth;
    (document.getElementsByClassName("el-dialog")[0] as HTMLElement).style.left = left;
    (document.getElementsByClassName("el-dialog")[0] as HTMLElement).style.top = top;
    (document.getElementsByClassName("el-dialog__body")[0] as HTMLElement).style.maxHeight = height.value
      ? `calc(${height.value} - ${bodyHeight})`
      : "";
  } else {
    // 全屏
    screenWidth = width.value;
    (document.getElementsByClassName("el-dialog")[0] as HTMLElement).style.height = "100%";
    width.value = "100%";
    (document.getElementsByClassName("el-dialog")[0] as HTMLElement).style.left = "0";
    (document.getElementsByClassName("el-dialog")[0] as HTMLElement).style.top = "0";
    (document.getElementsByClassName("el-dialog__body")[0] as HTMLElement).style.maxHeight = `calc(100% - ${bodyHeight})`;
  }
  screen.value = !screen.value;
};

 (3)拉伸方法:当鼠标按下预先设置在右下角的透明dom上时,通过监听鼠标的拖拽、抬起事件,实时计算鼠标位置,修改对话框的宽高;

        这里我设置了边界值高150长231,防止样式紊乱,具体的值根据你的需求修改。

// 拉伸
const mousedown = (e: any) => {
  if (!stretch) return;
  // 当前宽度/高度 = 点击时宽度/高度 + 移动的距离(移动时距离 - 点击时距离)
  // 点击时距离
  let startX = e.pageX;
  let startY = e.pageY;
  // 点击时宽度/高度
  let initWidth = width.value;
  let initHeight = height.value;
  // mousemove的回调
  const moveFunc = (el: any) => {
    // 移动的距离
    let x = el.pageX - startX;
    let y = el.pageY - startY;
    // 当前宽度/高度(边界值x: 231,y: 150)
    // el-dialog自带的拖拽不改变offsetTop和offsetLeft,需要使用getBoundingClientRect()获取
    if (el.pageY - document.getElementsByClassName("el-dialog")[0].getBoundingClientRect().top >= 150) {
      (document.getElementsByClassName("el-dialog")[0] as HTMLElement).style.height = height.value = `calc(${initHeight} + ${y}px)`;
    } else {
      (document.getElementsByClassName("el-dialog")[0] as HTMLElement).style.height = height.value = "calc(150px)";
    }
    if (el.pageX - document.getElementsByClassName("el-dialog")[0].getBoundingClientRect().left >= 231) {
      width.value = `calc(${initWidth} + ${x}px)`;
    } else {
      width.value = "calc(231px)";
    }
    (document.getElementsByClassName("el-dialog__body")[0] as HTMLElement).style.maxHeight = `calc(${height.value} - ${bodyHeight})`;
  };
  document.addEventListener("mousemove", moveFunc);
  document.addEventListener("mouseup", (_eu: any) => {
    document.removeEventListener("mousemove", moveFunc);
  });
};

(4)事件监听:默认的初始化操作都在open中完成

const open = () => {
  // 判断是否有遮罩层,有就不能穿透,没有就可以穿透
  (document.getElementsByClassName("modelClass")[0] as HTMLElement).style.pointerEvents = model ? "auto" : "none";
  // 初始化高度、位置、body最大高度
  (document.getElementsByClassName("el-dialog")[0] as HTMLElement).style.height = height.value;
  (document.getElementsByClassName("el-dialog")[0] as HTMLElement).style.left = left;
  (document.getElementsByClassName("el-dialog")[0] as HTMLElement).style.top = top;
  // 头部尾部是否显示
  (document.getElementsByClassName("el-dialog__header")[0] as HTMLElement).style.display = headerShow ? "" : "none";
  (document.getElementsByClassName("el-dialog__footer")[0] as HTMLElement).style.display = footerShow ? "" : "none";
  // body最大高度
  (document.getElementsByClassName("el-dialog__body")[0] as HTMLElement).style.maxHeight = `calc(${height.value} - ${bodyHeight})`;
  // 拉伸图标
  (document.getElementsByClassName("stretch")[0] as HTMLElement).style.cursor = stretch ? "nwse-resize" : "";
  emit("open", "刚打开时的回调");
};
// 左按钮事件
const leftBtnClick = () => {
  emit("leftBtn", "这是左按钮");
};
// 右按钮事件
const rightBtnClick = () => {
  emit("rightBtn", "这是右按钮");
};
const opened = () => {
  emit("opened", "打开动画结束时的回调");
};
const close = () => {
  dialogVisible.value = false;
  emit("update:modelValue", false);
  emit("close", "刚关闭时的回调");
};
const closed = () => {
  emit("closed", "关闭动画结束时的回调");
};
const openAutoFocus = () => {
  emit("openAutoFocus", "输入焦点聚焦在 Dialog 内容时的回调");
};
const closeAutoFocus = () => {
  emit("closeAutoFocus", "输入焦点从 Dialog 内容失焦时的回调");
};

4. 完整代码

<template>
  <div class="elDialog">
    <el-dialog
      v-model="dialogVisible"
      :width="width"
      :fullscreen="screen"
      :close-on-click-modal="closemodel"
      :close-on-press-escape="closeEsc"
      :show-close="showClose"
      :draggable="draggable"
      :modal="model"
      :z-index="zIndex"
      @open="open"
      @opened="opened"
      @close="close"
      @closed="closed"
      @open-auto-focus="openAutoFocus"
      @close-auto-focus="closeAutoFocus"
      modal-class="modelClass">
      <template #header="{ titleId, titleClass }">
        <div class="my-header" @dblclick="screenfnc">
          <h4 :id="titleId" :class="titleClass">{{ title }}</h4>
          <svgIcon
            :name="screen ? 'bg-fullscreen' : 'fullscreen'"
            width="20px"
            v-show="fullscreen"
            @click="screenfnc"
            style="cursor: pointer"
            class="svgIcon"></svgIcon>
        </div>
      </template>
      <slot></slot>
      <div class="stretch" @mousedown="mousedown" v-if="!footerShow"></div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="leftBtnClick" v-show="leftBtn">{{ leftBtn }}</el-button>
          <el-button type="primary" @click="rightBtnClick" v-show="rightBtn"> {{ rightBtn }} </el-button>
        </span>
        <!-- 定位在右下角的用于拉伸的透明小块 -->
        <div class="stretch" @mousedown="mousedown"></div>
      </template>
    </el-dialog>
  </div>
</template>

<script setup lang="ts">
import { ref, watch } from "vue";
// 这个组件时封装用来展示全屏svg图标的,不是必须的,用img标签一样。
import svgIcon from "@/components/svgIcon.vue";
const emit = defineEmits();
const props = defineProps({
  // 对话框v-model
  modelValue: {
    type: Boolean,
    default: false,
  },
  // 标题
  title: {
    type: String,
    default: "名称",
  },
  // 宽度
  width: {
    type: String,
    default: "600px",
  },
  // 高度
  height: {
    type: String,
    default: "300px",
  },
  // 左定位
  left: {
    type: String,
    default: "350px",
  },
  // 上定位
  top: {
    type: String,
    default: "50px",
  },
  // 全屏
  fullscreen: {
    type: Boolean,
    default: false,
  },
  // 关闭图标
  showClose: {
    type: Boolean,
    default: false,
  },
  // 拖拽
  draggable: {
    type: Boolean,
    default: false,
  },
  // 遮罩层
  model: {
    type: Boolean,
    default: true,
  },
  // 是否点击遮罩层关闭对话框
  closemodel: {
    type: Boolean,
    default: false,
  },
  // 是否按Esc关闭对话框
  closeEsc: {
    type: Boolean,
    default: true,
  },
  // 层级
  zIndex: {
    type: Number,
    default: 2000,
  },
  // 左按钮
  leftBtn: {
    type: String,
    default: "取消",
  },
  // 右按钮
  rightBtn: {
    type: String,
    default: "确认",
  },
  // 头部显隐
  headerShow: {
    type: Boolean,
    default: true,
  },
  // 尾部显隐
  footerShow: {
    type: Boolean,
    default: true,
  },
  // 拉伸
  stretch: {
    type: Boolean,
    default: false,
  },
});
const { left, top, model, headerShow, footerShow, stretch } = props;
const width = ref(props.width);
const height = ref(props.height);
const screen = ref(false);
const dialogVisible = ref(props.modelValue);

watch(
  () => props.modelValue,
  (val) => {
    dialogVisible.value = val;
  },
  { immediate: true, deep: true }
);
const open = () => {
  // 判断是否有遮罩层,有就不能穿透,没有就可以穿透
  (document.getElementsByClassName("modelClass")[0] as HTMLElement).style.pointerEvents = model ? "auto" : "none";
  // 初始化高度、位置、body最大高度
  (document.getElementsByClassName("el-dialog")[0] as HTMLElement).style.height = height.value;
  (document.getElementsByClassName("el-dialog")[0] as HTMLElement).style.left = left;
  (document.getElementsByClassName("el-dialog")[0] as HTMLElement).style.top = top;
  // 头部尾部是否显示
  (document.getElementsByClassName("el-dialog__header")[0] as HTMLElement).style.display = headerShow ? "" : "none";
  (document.getElementsByClassName("el-dialog__footer")[0] as HTMLElement).style.display = footerShow ? "" : "none";
  // body最大高度
  (document.getElementsByClassName("el-dialog__body")[0] as HTMLElement).style.maxHeight = `calc(${height.value} - ${bodyHeight})`;
  // 拉伸图标
  (document.getElementsByClassName("stretch")[0] as HTMLElement).style.cursor = stretch ? "nwse-resize" : "";
  emit("open", "刚打开时的回调");
};
// 全屏时保存全屏前的宽度
let screenWidth: any;
//  根据头部尾部显隐分别判断body的最大高度
let bodyHeight = "140px";
if (!headerShow && !footerShow) {
  bodyHeight = "50px";
} else if (!headerShow || !footerShow) {
  bodyHeight = "95px";
}
// 全屏
const screenfnc = () => {
  if (screen.value) {
    // 恢复原来位置
    (document.getElementsByClassName("el-dialog")[0] as HTMLElement).style.height = height.value;
    width.value = screenWidth;
    (document.getElementsByClassName("el-dialog")[0] as HTMLElement).style.left = left;
    (document.getElementsByClassName("el-dialog")[0] as HTMLElement).style.top = top;
    (document.getElementsByClassName("el-dialog__body")[0] as HTMLElement).style.maxHeight = height.value
      ? `calc(${height.value} - ${bodyHeight})`
      : "";
  } else {
    // 全屏
    screenWidth = width.value;
    (document.getElementsByClassName("el-dialog")[0] as HTMLElement).style.height = "100%";
    width.value = "100%";
    (document.getElementsByClassName("el-dialog")[0] as HTMLElement).style.left = "0";
    (document.getElementsByClassName("el-dialog")[0] as HTMLElement).style.top = "0";
    (document.getElementsByClassName("el-dialog__body")[0] as HTMLElement).style.maxHeight = `calc(100% - ${bodyHeight})`;
  }
  screen.value = !screen.value;
};
// 拉伸
const mousedown = (e: any) => {
  if (!stretch) return;
  // 当前宽度/高度 = 点击时宽度/高度 + 移动的距离(移动时距离 - 点击时距离)
  // 点击时距离
  let startX = e.pageX;
  let startY = e.pageY;
  // 点击时宽度/高度
  let initWidth = width.value;
  let initHeight = height.value;
  // mousemove的回调
  const moveFunc = (el: any) => {
    // 移动的距离
    let x = el.pageX - startX;
    let y = el.pageY - startY;
    // 当前宽度/高度(边界值x: 231,y: 150)
    // el-dialog自带的拖拽不改变offsetTop和offsetLeft,需要使用getBoundingClientRect()获取
    if (el.pageY - document.getElementsByClassName("el-dialog")[0].getBoundingClientRect().top >= 150) {
      (document.getElementsByClassName("el-dialog")[0] as HTMLElement).style.height = height.value = `calc(${initHeight} + ${y}px)`;
    } else {
      (document.getElementsByClassName("el-dialog")[0] as HTMLElement).style.height = height.value = "calc(150px)";
    }
    if (el.pageX - document.getElementsByClassName("el-dialog")[0].getBoundingClientRect().left >= 231) {
      width.value = `calc(${initWidth} + ${x}px)`;
    } else {
      width.value = "calc(231px)";
    }
    (document.getElementsByClassName("el-dialog__body")[0] as HTMLElement).style.maxHeight = `calc(${height.value} - ${bodyHeight})`;
  };
  document.addEventListener("mousemove", moveFunc);
  document.addEventListener("mouseup", (_eu: any) => {
    document.removeEventListener("mousemove", moveFunc);
  });
};
// 左按钮事件
const leftBtnClick = () => {
  emit("leftBtn", "这是左按钮");
};
// 右按钮事件
const rightBtnClick = () => {
  emit("rightBtn", "这是右按钮");
};
const opened = () => {
  emit("opened", "打开动画结束时的回调");
};
const close = () => {
  dialogVisible.value = false;
  emit("update:modelValue", false);
  emit("close", "刚关闭时的回调");
};
const closed = () => {
  emit("closed", "关闭动画结束时的回调");
};
const openAutoFocus = () => {
  emit("openAutoFocus", "输入焦点聚焦在 Dialog 内容时的回调");
};
const closeAutoFocus = () => {
  emit("closeAutoFocus", "输入焦点从 Dialog 内容失焦时的回调");
};
</script>

<style lang="scss" scoped>
.elDialog {
  -moz-user-select: none;
  -webkit-user-select: none;
  user-select: none;
}
.my-header {
  display: flex;
  justify-content: space-between;
  padding-right: 15px;
}
::v-deep(.el-dialog) {
  margin: 0 !important;
}
::v-deep(.el-dialog__header) {
  border-bottom: 1px dashed #19e8ca;
}
::v-deep(.el-dialog__body) {
  padding-top: 15px !important;
}
.svgIcon:hover {
  transform: scale(1.1);
}
.stretch {
  position: absolute;
  bottom: 0;
  right: 0;
  width: 10px;
  height: 10px;
}
</style>

5. 调用示例

        基本上把原组件库的事件监听也都带上了,属性没有全部带上,很多不常用的没有加上,有需要的话可以自行加上,可以按照代码里showClose的方式添加就可以,或者不明白的可以评论区留言。

       有不足的地方, 欢迎大佬们指正,享受鞭挞哈哈哈哈哈哈...

<Dialog
    v-model="dialogVisible"
    left="350px"
    top="100px"
    fullscreen
    showClose
    draggable
    :model="false"
    stretch
    leftBtn=""
    rightBtn="保存"
    @leftBtn="leftBtn"
    @rightBtn="rightBtn"
    @open="open"
    @opened="opened"
    @close="close"
    @closed="closed"
    @openAutoFocus="openAutoFocus"
    @closeAutoFocus="closeAutoFocus">
    <div v-for="(item, index) in list" :key="index">{{ item }}</div>
</Dialog>

6. API

属性

v-model绑定值title标题文本
width对话框宽度height对话框高度
left左定位距离top上定位距离
fullscreen是否开启全屏showClose是否展示关闭图标
draggable是否开启拖拽model是否展示遮罩层
closemodel是否点击遮罩层关闭对话框closeEsc是否按Esc关闭对话框
zIndex层级leftBtn左按钮文本(值为空时隐藏按钮)
rightBtn右按钮文本(值为空时隐藏按钮)

 

事件

leftBtn左按钮事件rightBtn右按钮事件
open刚打开时的回调opened打开动画结束时的回调
close刚关闭时的回调closed关闭动画结束时的回调
openAutoFocus输入焦点聚焦在 Dialog 内容时的回调closeAutoFocus输入焦点从 Dialog 内容失焦时的回调

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值