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);

使用建议

  1. 基础用法

vue

复制

下载

<BaseDialog :visible.sync="visible" title="标题">
  内容
  <template #footer>
    <el-button @click="visible = false">关闭</el-button>
  </template>
</BaseDialog>
  1. 全屏模式

vue

复制

下载

<BaseDialog :visible.sync="visible" fullscreen title="全屏">
  内容
</BaseDialog>
  1. 自定义高度

vue

复制

下载

<BaseDialog 
  :visible.sync="visible" 
  :dialog-style="{ '--max-height': '80vh' }"
  :min-height="400"
>
  内容
</BaseDialog>

这个封装版本更加健壮、易用,并且修复了原代码中的多个问题。如果还需要其他功能,可以继续扩展!

posted @ 2026-08-16 10:52  7c89  阅读(4)  评论(0)    收藏  举报