ai 优化弹窗组件
封装element-ui dialog 组件
<template><el-dialogv-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% 移动之后赋值为pxif (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-dialogv-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',},// 是否追加到 bodyappendToBody: {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,},// 额外的 styledialogStyle: {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>
这个封装版本更加健壮、易用,并且修复了原代码中的多个问题。如果还需要其他功能,可以继续扩展!