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 内容失焦时的回调 |