ai 优化弹窗组件
封装element-ui dialog 组件
<template>
<el-dialog
v-bind="$attrs"
v-on="$listeners"
v-drag="$attrs.drag"
v-if="$attrs.visible"
:custom-class="customClass"
:append-to-body="appendToBody"
:close-on-click-modal="closeOnClickModal"
:close-on-press-escape="closeOnPressEscape"
:title="title"
:fullscreen='fullscreen'
:style="{ '--max-height': dialogMaxHeight() }"
ref="t1"
>
<slot name="custom"></slot>
<!-- 遍历子组件非作用域插槽,并对父组件暴露 -->
<template v-for="(index, name) in $slots" :slot="name">
<slot :name="name" v-bind="scopedData" />
</template>
<!-- 遍历子组件作用域插槽,并对父组件暴露 -->
<!-- v-slot:[name]="data" -->
<!-- <template v-for="(index, name) in $scopedSlots" >
<slot :name="name" v-bind="data" v-slot:[name]="data"></slot>
</template> -->
</el-dialog>
</template>
<script>
export default {
inheritAttrs: false,
name: "mydialog",
props: {
customClass: {
type: String,
default: "mydialog",
},
appendToBody: {
type: Boolean,
default: true,
},
fullscreen: {
type: Boolean,
default: false,
},
title: {
type: String,
default: "查看",
},
top: {
type: String,
default: "15vh",
},
closeOnClickModal: {
type: Boolean,
default: false,
},
closeOnPressEscape: {
type: Boolean,
default: false,
},
},
directives: {
drag: {
bind(el, binding, vnode, oldVnode) {
// console.log(binding.value);
if (binding.value === false) {
return;
}
const dialogHeaderEl = el.querySelector(".el-dialog__header");
const dragDom = el.querySelector(".el-dialog");
dialogHeaderEl.style.cssText += ";cursor:move;";
dragDom.style.cssText += ";top:0px;";
// 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
const sty = (() => {
if (window.document.currentStyle) {
return (dom, attr) => dom.currentStyle[attr];
} else {
return (dom, attr) => getComputedStyle(dom, false)[attr];
}
})();
dialogHeaderEl.onmousedown = (e) => {
// 鼠标按下,计算当前元素距离可视区的距离
const disX = e.clientX - dialogHeaderEl.offsetLeft;
const disY = e.clientY - dialogHeaderEl.offsetTop;
const screenWidth = document.body.clientWidth; // body当前宽度
const screenHeight = document.documentElement.clientHeight; // 可见区域高度(应为body高度,可某些环境下无法获取)
const dragDomWidth = dragDom.offsetWidth; // 对话框宽度
const dragDomheight = dragDom.offsetHeight; // 对话框高度
const minDragDomLeft = dragDom.offsetLeft;
const maxDragDomLeft =
screenWidth - dragDom.offsetLeft - dragDomWidth;
const minDragDomTop = dragDom.offsetTop;
const maxDragDomTop =
screenHeight - dragDom.offsetTop - dragDomheight;
// 获取到的值带px 正则匹配替换
let styL = sty(dragDom, "left");
let styT = sty(dragDom, "top");
// 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
if (styL.includes("%")) {
styL =
+document.body.clientWidth * (+styL.replace(/\%/g, "") / 100);
styT =
+document.body.clientHeight * (+styT.replace(/\%/g, "") / 100);
} else {
styL = +styL.replace(/\px/g, "");
styT = +styT.replace(/\px/g, "");
}
document.onmousemove = function (e) {
// 通过事件委托,计算移动的距离
let left = e.clientX - disX;
let top = e.clientY - disY;
// 边界处理
if (-left > minDragDomLeft) {
left = -minDragDomLeft;
} else if (left > maxDragDomLeft) {
left = maxDragDomLeft;
}
if (-top > minDragDomTop) {
top = -minDragDomTop;
} else if (top > maxDragDomTop) {
top = maxDragDomTop;
}
// 移动当前元素
dragDom.style.cssText += `;left:${left + styL}px;top:${
top + styT
}px;`;
};
document.onmouseup = function (e) {
document.onmousemove = null;
document.onmouseup = null;
};
};
},
},
},
mounted() {
console.log(this.$scopedSlots);
console.log( this.$refs.t1.$el.querySelector('.el-dialog'));
// this.$refs.t1.$el.querySelector('.el-dialog').style.setProperty("max-height",`calc(100vh - ${this.top} - 6vh)`);
},
methods:{
dialogMaxHeight(){
console.log(this.fullscreen);
if(this.fullscreen){
return '100vh'
}
return `calc(100vh - ${this.top} - 6vh)`
}
}
};
</script>
<style scoped lang="scss">
::v-deep .mydialog {
max-height: var(--max-height, 79vh);
display: flex;
flex-direction: column;
border: 1px solid;
overflow-y: auto;
word-break: break-all;
white-space: pre-line;
box-sizing: border-box;
.el-dialog__header {
border-bottom: 1px solid #efefef;
flex-shrink: 0;
}
.el-dialog__body {
flex: 1;
// min-height: 100px;
overflow-y: auto;
word-break: break-all;
white-space: pre-line;
box-sizing: border-box;
}
.el-dialog__footer {
border-top: 1px solid #efefef;
flex-shrink: 0;
padding: 16px;
}
}
</style>
使用
<myDialog :visible="true" custom-class1="t777" fullscreen>
<div slot="custom" class="dialog-custom" style="height:1800px">123</div>
<div slot="footer" class="dialog-footer">
<el-button>取 消</el-button>
<el-button type="primary">打开内层 Dialog</el-button>
</div>
</myDialog>
<myDialog :visible="true" custom-class1="t777">
<div slot="custom" class="dialog-custom" style="height:1000px">123</div>
45454
<div slot="footer" class="dialog-footer">
<el-button>取 消</el-button>
<el-button type="primary">打开内层 Dialog</el-button>
</div>
</myDialog>
请进行改进 代码优化 并测试
优化后的组件 BaseDialog.vue
vue
复制
下载
<template>
<el-dialog
v-bind="$attrs"
v-on="$listeners"
v-if="visible"
:visible.sync="visible"
:custom-class="computedCustomClass"
:append-to-body="appendToBody"
:close-on-click-modal="closeOnClickModal"
:close-on-press-escape="closeOnPressEscape"
:title="title"
:top="top"
:style="dialogStyle"
v-drag="drag"
ref="dialogRef"
@open="handleOpen"
@opened="handleOpened"
@close="handleClose"
@closed="handleClosed"
>
<!-- 默认插槽 -->
<slot></slot>
<!-- 自定义头部插槽 -->
<template slot="title" v-if="$slots.title">
<slot name="title"></slot>
</template>
<!-- 底部插槽 -->
<template slot="footer" v-if="$slots.footer">
<slot name="footer"></slot>
</template>
<!-- 透传所有插槽(包括作用域插槽) -->
<template v-for="(_, slotName) in $slots" :slot="slotName" v-if="!['default', 'title', 'footer'].includes(slotName)">
<slot :name="slotName" v-bind="scopedData"></slot>
</template>
</el-dialog>
</template>
<script>
export default {
name: 'BaseDialog',
inheritAttrs: false,
props: {
// 控制显示
visible: {
type: Boolean,
default: false,
},
// 标题
title: {
type: String,
default: '',
},
// 自定义类名
customClass: {
type: String,
default: 'base-dialog',
},
// 是否追加到 body
appendToBody: {
type: Boolean,
default: true,
},
// 全屏
fullscreen: {
type: Boolean,
default: false,
},
// 顶部距离
top: {
type: String,
default: '15vh',
},
// 是否启用拖拽
drag: {
type: Boolean,
default: true,
},
// 点击遮罩是否关闭
closeOnClickModal: {
type: Boolean,
default: false,
},
// ESC 键是否关闭
closeOnPressEscape: {
type: Boolean,
default: false,
},
// 最小高度
minHeight: {
type: [Number, String],
default: 300,
},
// 额外的 style
dialogStyle: {
type: Object,
default: () => ({}),
},
// 是否显示默认的滚动条
showScrollbar: {
type: Boolean,
default: true,
},
},
directives: {
drag: {
bind(el, binding) {
if (!binding.value) return;
const dialogHeaderEl = el.querySelector('.el-dialog__header');
const dragDom = el.querySelector('.el-dialog');
if (!dialogHeaderEl || !dragDom) return;
dialogHeaderEl.style.cssText += ';cursor:move;';
dragDom.style.cssText += ';top:0px;';
const getStyle = (dom, attr) => {
return window.getComputedStyle(dom, false)[attr];
};
dialogHeaderEl.onmousedown = (e) => {
// 排除点击关闭按钮
if (e.target.closest('.el-dialog__headerbtn')) return;
const disX = e.clientX - dialogHeaderEl.offsetLeft;
const disY = e.clientY - dialogHeaderEl.offsetTop;
const screenWidth = document.body.clientWidth;
const screenHeight = document.documentElement.clientHeight;
const dragDomWidth = dragDom.offsetWidth;
const dragDomHeight = dragDom.offsetHeight;
const minDragDomLeft = dragDom.offsetLeft;
const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth;
const minDragDomTop = dragDom.offsetTop;
const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomHeight;
let styL = getStyle(dragDom, 'left');
let styT = getStyle(dragDom, 'top');
// 处理百分比值
if (styL.includes('%')) {
styL = document.body.clientWidth * (parseFloat(styL) / 100);
styT = document.body.clientHeight * (parseFloat(styT) / 100);
} else {
styL = parseFloat(styL) || 0;
styT = parseFloat(styT) || 0;
}
const onMouseMove = (e) => {
let left = e.clientX - disX;
let top = e.clientY - disY;
// 边界限制
left = Math.max(-minDragDomLeft, Math.min(left, maxDragDomLeft));
top = Math.max(-minDragDomTop, Math.min(top, maxDragDomTop));
dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`;
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
};
},
unbind() {
// 清理事件
document.onmousemove = null;
document.onmouseup = null;
},
},
},
computed: {
computedCustomClass() {
const classes = [this.customClass];
if (this.fullscreen) {
classes.push('is-fullscreen');
}
if (this.showScrollbar) {
classes.push('has-scrollbar');
}
return classes.join(' ');
},
dialogMaxHeight() {
if (this.fullscreen) {
return '100vh';
}
return `calc(100vh - ${this.top} - 6vh)`;
},
computedDialogStyle() {
return {
...this.dialogStyle,
'--max-height': this.dialogMaxHeight,
'--min-height': typeof this.minHeight === 'number' ? `${this.minHeight}px` : this.minHeight,
};
},
scopedData() {
return {
// 可以传递一些数据给作用域插槽
dialogRef: this.$refs.dialogRef,
close: this.close,
};
},
},
watch: {
// 当 visible 变化时重新计算高度
visible(val) {
if (val) {
this.$nextTick(() => {
this.updateDialogHeight();
});
}
},
},
mounted() {
// 监听窗口变化
this.resizeHandler = this.debounce(this.updateDialogHeight, 300);
window.addEventListener('resize', this.resizeHandler);
// 初始化高度
if (this.visible) {
this.$nextTick(() => {
this.updateDialogHeight();
});
}
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeHandler);
},
methods: {
// 更新弹窗高度
updateDialogHeight() {
const dialogEl = this.$refs.dialogRef?.$el?.querySelector('.el-dialog');
if (!dialogEl) return;
// 如果是全屏模式,不需要额外处理
if (this.fullscreen) return;
dialogEl.style.setProperty('max-height', this.dialogMaxHeight);
dialogEl.style.setProperty('min-height', this.computedDialogStyle['--min-height']);
},
// 防抖函数
debounce(fn, delay = 300) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
},
// 关闭弹窗
close() {
this.$emit('update:visible', false);
this.$emit('close');
},
// 打开弹窗
open() {
this.$emit('update:visible', true);
this.$emit('open');
},
// 事件处理
handleOpen() {
this.$emit('open');
this.updateDialogHeight();
},
handleOpened() {
this.$emit('opened');
this.updateDialogHeight();
},
handleClose() {
this.$emit('close');
},
handleClosed() {
this.$emit('closed');
},
},
};
</script>
<style scoped lang="scss">
::v-deep .base-dialog {
max-height: var(--max-height, 79vh);
min-height: var(--min-height, 300px);
display: flex !important;
flex-direction: column !important;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
// 全屏样式
&.is-fullscreen {
max-height: 100vh !important;
min-height: 100vh !important;
border-radius: 0 !important;
margin: 0 !important;
width: 100% !important;
top: 0 !important;
.el-dialog__body {
flex: 1;
max-height: calc(100vh - 120px);
}
}
.el-dialog__header {
border-bottom: 1px solid #e8e8e8;
flex-shrink: 0;
padding: 16px 20px;
background-color: #fafafa;
.el-dialog__title {
font-size: 16px;
font-weight: 600;
color: #333;
}
.el-dialog__headerbtn {
top: 16px;
right: 20px;
.el-dialog__close {
color: #999;
transition: all 0.3s;
&:hover {
color: #409eff;
transform: rotate(90deg);
}
}
}
}
.el-dialog__body {
flex: 1;
overflow-y: auto;
padding: 20px;
word-break: break-all;
white-space: pre-line;
box-sizing: border-box;
background-color: #fff;
// 自定义滚动条(可选)
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background-color: #d0d0d0;
border-radius: 3px;
&:hover {
background-color: #b0b0b0;
}
}
&::-webkit-scrollbar-track {
background-color: transparent;
}
}
.el-dialog__footer {
border-top: 1px solid #e8e8e8;
flex-shrink: 0;
padding: 12px 20px;
background-color: #fafafa;
border-radius: 0 0 8px 8px;
text-align: right;
.el-button {
margin-left: 10px;
}
}
// 没有底部时隐藏 border
&:not(:has(.el-dialog__footer)) {
.el-dialog__body {
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
}
// 没有头部时隐藏 border
&:not(:has(.el-dialog__header)) {
.el-dialog__body {
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
}
}
// 遮罩层优化
::v-deep .v-modal {
background-color: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(2px);
}
</style>
使用示例
vue
复制
下载
<template>
<div>
<el-button @click="dialogVisible = true">打开弹窗</el-button>
<el-button @click="fullscreenVisible = true">打开全屏弹窗</el-button>
<!-- 基础使用 -->
<BaseDialog
:visible.sync="dialogVisible"
title="基础弹窗"
width="800px"
:drag="true"
@open="onOpen"
@close="onClose"
>
<div style="height: 800px; background: #f5f5f5; padding: 20px;">
<h3>内容区域</h3>
<p>这里可以放任何内容</p>
<p>滚动查看效果</p>
<p v-for="i in 20" :key="i">第 {{ i }} 行内容</p>
</div>
<template #footer>
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="dialogVisible = false">确 定</el-button>
</template>
</BaseDialog>
<!-- 全屏弹窗 -->
<BaseDialog
:visible.sync="fullscreenVisible"
title="全屏弹窗"
fullscreen
:drag="false"
:close-on-click-modal="false"
>
<div style="padding: 20px; min-height: 500px;">
<h2>全屏内容</h2>
<p>全屏模式会自动适应窗口大小</p>
</div>
<template #footer>
<el-button @click="fullscreenVisible = false">关闭</el-button>
</template>
</BaseDialog>
<!-- 嵌套弹窗 -->
<BaseDialog
:visible.sync="nestedVisible"
title="嵌套弹窗"
width="600px"
>
<div style="padding: 20px;">
<p>这是外层弹窗</p>
<el-button @click="innerVisible = true">打开内层弹窗</el-button>
</div>
<BaseDialog
:visible.sync="innerVisible"
title="内层弹窗"
width="400px"
:top="'20vh'"
>
<div style="padding: 20px;">
<p>这是内层弹窗内容</p>
<p>可以继续嵌套</p>
</div>
<template #footer>
<el-button @click="innerVisible = false">关闭</el-button>
</template>
</BaseDialog>
<template #footer>
<el-button @click="nestedVisible = false">关闭</el-button>
</template>
</BaseDialog>
</div>
</template>
<script>
import BaseDialog from '@/components/BaseDialog.vue';
export default {
components: {
BaseDialog,
},
data() {
return {
dialogVisible: false,
fullscreenVisible: false,
nestedVisible: false,
innerVisible: false,
};
},
methods: {
onOpen() {
console.log('弹窗打开了');
},
onClose() {
console.log('弹窗关闭了');
},
},
};
</script>
主要改进点
1. 修复插槽透传问题
javascript
复制
下载
// 正确透传所有插槽
<template v-for="(_, slotName) in $slots" :slot="slotName" v-if="!['default', 'title', 'footer'].includes(slotName)">
<slot :name="slotName" v-bind="scopedData"></slot>
</template>
2. 增加 .sync 支持
javascript
复制
下载
// 使用 .sync 修饰符
:visible.sync="dialogVisible"
3. 优化拖拽指令
- 修复边界计算
- 排除点击关闭按钮触发拖拽
- 使用
addEventListener替代onmousemove - 增加
unbind清理事件
4. 增强样式
- 支持全屏模式
- 自定义滚动条
- 更好的视觉反馈
- 兼容性更好
5. 新增功能
javascript
复制
下载
props: {
minHeight: [Number, String], // 最小高度
showScrollbar: Boolean, // 是否显示滚动条
drag: Boolean, // 是否可拖拽
dialogStyle: Object, // 自定义样式
}
6. 优化计算高度
javascript
复制
下载
computed: {
dialogMaxHeight() {
if (this.fullscreen) return '100vh';
return `calc(100vh - ${this.top} - 6vh)`;
}
}
7. 增加生命周期事件
javascript
复制
下载
@open="handleOpen"
@opened="handleOpened"
@close="handleClose"
@closed="handleClosed"
8. 防抖优化
javascript
复制
下载
this.resizeHandler = this.debounce(this.updateDialogHeight, 300);
window.addEventListener('resize', this.resizeHandler);
使用建议
- 基础用法:
vue
复制
下载
<BaseDialog :visible.sync="visible" title="标题">
内容
<template #footer>
<el-button @click="visible = false">关闭</el-button>
</template>
</BaseDialog>
- 全屏模式:
vue
复制
下载
<BaseDialog :visible.sync="visible" fullscreen title="全屏">
内容
</BaseDialog>
- 自定义高度:
vue
复制
下载
<BaseDialog
:visible.sync="visible"
:dialog-style="{ '--max-height': '80vh' }"
:min-height="400"
>
内容
</BaseDialog>
这个封装版本更加健壮、易用,并且修复了原代码中的多个问题。如果还需要其他功能,可以继续扩展!

浙公网安备 33010602011771号