封装一个稍微复杂的组件-弹出窗口组件

2,456 阅读5分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

TIP 👉 老当益壮,宁移白首之心;穷且益坚,不坠青云之志。——王勃《滕王阁序》

前言

主要的目的将其中一些共同的业务场景封装为组件,避免去编写重复的代码,将精力放在更加核心的部分,因此就需要将这些重复的代码抽取出来,封装成公共的组件,提高开发效率,于是产生了这个组件,API不算多,欢迎大家指出问题。

一、弹出窗口组件(只PC端可用)

1. Popup 组件

Popup 属性

1. value
  • 是否显示弹框
  • 值为布尔类型
  • 默认值:false
2. showClose
  • 是否显示关闭按钮
  • 值为布尔类型
  • 默认值为true
3. title
  • 弹出窗口标题
  • 值为字符串
  • Popup 默认为'提示',Alert 和Confirm 默认为空
4. showClose
  • 是否显示右上角关闭按钮
  • 值为布尔类型
  • Popup 默认为true,Alert 和Confirm 默认为false
5. message
  • 信息内容
  • 值为字符串

如果有默认插槽则该配置无效

6. html
  • html内容
  • 值为字符串

如果有默认插槽则该配置无效 如果message有值则该配置无效

7. content
  • 弹框内容组件
  • 值为Vue组件

如果有默认插槽则该配置无效 如果message或者html有值则该配置无效

8. contentProps
  • 弹窗内容组件的props
  • 值为Object类型,属性名为组件的props的属性

content 有值时该配置才有效

9. contentEvents
  • 弹窗内容组件的事件
  • 值为Object类型,属性名为组件事件名,属性值为事件回调方法

content 有值时该配置才有效

10. contentWrapperStyle
  • 内容包裹器样式
  • 值为Object类型,属性名为样式
<BasePopup v-model="isShow" message="保存成功!"
  :contentWrapperStyle="{ fontWeight: 'bold' }"></BasePopup>
11. width
  • 弹出窗口宽度
  • 值为数值类型
  • 默认值:500,单位为像素
12. height
  • 弹出窗口宽度
  • 值为数值类型
  • 默认值:300,单位为像素
13. className
  • 弹框自定义样式名
  • 值为字符串
14. scroll
  • 内容区域是否需要滚动条(scroll为false时如果内容溢出会自动显示滚动条)
  • 值为布尔类型
  • 默认值:false
15. cover
  • 遮罩层覆盖范围
  • 值为字符串
  • 默认值:“full”
    • full 覆盖整个屏幕
    • tab 覆盖tab页功能区域,左侧菜单栏、顶部区域、tab页签选择区域不会被覆盖

插槽

1. default
  • 默认插槽为弹框内容
2. bottom
  • 弹框底部插槽,一般用于放操作按钮

脚本调用特有属性

1.store
  • Vuex 的 store 对象,当弹窗中需要使用 Vuex 的全局数据时设置

当使用 vm.$popup() 或者 vm.$popupAlert() 或者 vm.$popupConfirm() 时会默认设置为当前 Vue 实例的 $store

2.router
  • Vue Router 的 router 对象,当弹窗中需要使用路由时设置

当使用 vm.$popup() 或者 vm.$popupAlert() 或者 vm.$popupConfirm() 时会默认设置为当前 Vue 实例的 $router

Alert 组件特有属性

1.btnText
  • 按钮文字
  • 值为字符串
  • 默认值:'确定'

Alert 组件特有事件

1.ok
  • 点击按钮后触发的事件
  • 无参数

Confirm 组件特有属性

1.okBtnText
  • “确定”按钮文字
  • 值为字符串
  • 默认值:'确定'
2.cancelBtnText
  • “取消”按钮文字
  • 值为字符串
  • 默认值:'取消'

Confirm 组件特有事件

1.ok
  • 点击“确定”按钮后触发的事件
  • 无参数
2.cancel
  • 点击“取消”按钮后触发的事件
  • 无参数

二、弹出窗口示例

1. Popup 示例

简单示例

<template>
  <div>
    <BaseButton @click="doOpen()">简单弹窗</BaseButton>

    <!-- 简单弹框 -->
    <BasePopup v-model="isShow" title="提示信息" message="已修改成功!"></BasePopup>
  </div>
</template>
<script>
import BaseButton from '@/components/base/button/index.vue'
import BasePopup from '@/components/pc/popup/Popup.vue'

export default {
  name: 'PcPopupSimpleDemo',
  components: {
    BaseButton,
    BasePopup
  },
  data () {
    return {
      isShow: false
    }
  },
  methods: {
    doOpen () {
      this.isShow = true
    }
  }
}
</script>

自定义内容示例

<template>
  <div>
    <BaseButton @click="doOpen()">自定义内容</BaseButton>

    <!-- 自定义内容的弹框 -->
    <BasePopup v-model="isShow" title="提示信息" :width="300" :height="280">
      <div class="info-content">
        <p><Icon name="cry" class="info-icon"></Icon></p>
        <p>修改失败了</p>
      </div>
    </BasePopup>
  </div>
</template>
<script>
import BaseButton from '@/components/base/button/index.vue'
import BasePopup from '@/components/pc/popup/Popup.vue'

export default {
  name: 'PcPopupDemo',
  components: {
    BaseButton,
    BasePopup
  },
  data () {
    return {
      isShow: false
    }
  },
  methods: {
    doOpen () {
      this.isShow = true
    }
  }
}
</script>
<style lang="scss" scoped px2rem="false">
.info-content{
  padding: 50px 24px;
  font-size: 16px;
  text-align: center;
  .info-icon{
    font-size: 84px;
    @include primary-font-color();
    margin-bottom: 10px;
  }
}
</style>

带滚动条示例

<template>
  <div>
    <BaseButton @click="doOpen()">带滚动条</BaseButton>

    <!-- 带滚动条的自定义内容的弹框 -->
    <BasePopup v-model="isShow" title="银行列表" :width="260" :height="320" :scroll="true">
      <div class="info-content">
        <ul>
          <li>中国建设银行</li>
          <li>中国工商银行</li>
          <li>中国农业银行</li>
          <li>中国交通银行</li>
          <li>中国银行</li>
          <li>中国招商银行</li>
          <li>邮政储蓄银行</li>
        </ul>
      </div>
    </BasePopup>
  </div>
</template>
<script>
import BaseButton from '@/components/base/button/index.vue'
import BasePopup from '@/components/pc/popup/Popup.vue'

export default {
  name: 'PcPopupScrollDemo',
  components: {
    BaseButton,
    BasePopup
  },
  data () {
    return {
      isShow: false
    }
  },
  methods: {
    doOpen () {
      this.isShow = true
    }
  }
}
</script>
<style lang="scss" scoped px2rem="false">
.info-content{
  padding: 24px;
  font-size: 16px;
  line-height: 50px;
  ul {
    list-style: none;
  }
}
</style>

动态指定组件作为内容示例

<template>
  <div>
    <BaseButton @click="doOpen()">动态指定组件作为内容</BaseButton>

    <!-- 动态指定组件作为内容的弹框 -->
    <BasePopup v-model="isShow" title="用户信息" :width="300" :height="200" :scroll="true"
               :content="contentComponent" :contentProps="contentProps"
               :contentEvents="contentEvents" :contentWrapperStyle="contentWrapperStyle"></BasePopup>
  </div>
</template>
<script>
import BaseButton from '@/components/base/button/index.vue'
import BasePopup from '@/components/pc/popup/Popup.vue'
import Content from './Content.vue'

export default {
  name: 'PcPopupDynamicDemo',
  components: {
    BaseButton,
    BasePopup
  },
  data () {
    return {
      isShow: false,
      contentComponent: Content,
      contentProps: {
        name: 'Tom',
        phone: 18399998888
      },
      contentEvents: {
        click () {
          console.log('点击了用户信息')
        }
      },
      contentWrapperStyle: {
        padding: '30px 20px 30px 50px',
        textAlign: 'left'
      }
    }
  },
  methods: {
    doOpen () {
      this.isShow = true
    }
  }
}
</script>

2. Alert 示例

简单带一个按钮示例

<template>
  <div>
    <BaseButton @click="doOpen()">带一个按钮</BaseButton>

    <!-- 带一个按钮的警告弹框 -->
    <BaseAlert v-model="isShow" message="保存成功!" btnText="继续" @ok="doOK"></BaseAlert>
  </div>
</template>
<script>
import BaseButton from '@/components/base/button/index.vue'
import BaseAlert from '@/components/pc/popup/Alert.vue'

export default {
  name: 'PcAlertDemo',
  components: {
    BaseButton,
    BaseAlert
  },
  data () {
    return {
      isShow: false
    }
  },
  methods: {
    doOpen () {
      this.isShow = true
    },
    doOK () {
      console.log('触发ok事件')
    }
  }
}
</script>

带一个按钮(有标题、可关闭)示例

<template>
  <div>
    <BaseButton @click="doOpen()">带一个按钮(有标题、可关闭)</BaseButton>

    <!-- 带一个按钮(有标题、可关闭)的警告弹框 -->
    <BaseAlert v-model="isShow" message="保存成功!" title="提示" :showClose="true" btnText="确定" @ok="doOK"></BaseAlert>
  </div>
</template>
<script>
import BaseButton from '@/components/base/button/index.vue'
import BaseAlert from '@/components/pc/popup/Alert.vue'

export default {
  name: 'PcAlertHeaderDemo',
  components: {
    BaseButton,
    BaseAlert
  },
  data () {
    return {
      isShow: false
    }
  },
  methods: {
    doOpen () {
      this.isShow = true
    },
    doOK () {
      console.log('触发ok事件')
    }
  }
}
</script>

3. Confirm 示例

带确认、取消按钮示例

<template>
  <div>
    <BaseButton @click="doOpen()">带确认、取消按钮</BaseButton>

    <!-- 带确定和取消按钮的确认弹框 -->
    <BaseConfirm v-model="isShow" message="确定删除订单?" @ok="doOK" @cancel="doCancel"></BaseConfirm>
  </div>
</template>
<script>
import BaseButton from '@/components/base/button/index.vue'
import BaseConfirm from '@/components/pc/popup/Confirm.vue'

export default {
  name: 'PcConfirmDemo',
  components: {
    BaseButton,
    BaseConfirm
  },
  data () {
    return {
      isShow: false
    }
  },
  methods: {
    doOpen () {
      this.isShow = true
    },
    doOK () {
      console.log('触发ok事件')
    },
    doCancel () {
      console.log('触发cancel事件')
    }
  }
}
</script>

带确认、取消按钮(有标题、可关闭)示例

<template>
  <div>
    <BaseButton @click="doOpen()">带确认、取消按钮(有标题、可关闭)</BaseButton>

    <!-- 带确定和取消按钮(有标题、可关闭)的确认弹框 -->
    <BaseConfirm v-model="isShow" message="确定删除订单?" title="提示" :showClose="true" @ok="doOK" @cancel="doCancel"></BaseConfirm>
  </div>
</template>
<script>
import BaseButton from '@/components/base/button/index.vue'
import BaseConfirm from '@/components/pc/popup/Confirm.vue'

export default {
  name: 'PcConfirmHeaderDemo',
  components: {
    BaseButton,
    BaseConfirm
  },
  data () {
    return {
      isShow: false
    }
  },
  methods: {
    doOpen () {
      this.isShow = true
    },
    doOK () {
      console.log('触发ok事件')
    },
    doCancel () {
      console.log('触发cancel事件')
    }
  }
}
</script>

三、脚本方式调用示例

1. Popup 示例

简单弹窗示例

<template>
  <div>
    <BaseButton @click="doOpen()">简单弹窗</BaseButton>
  </div>
</template>
<script>
import BaseButton from '@/components/base/button/index.vue'
import { popup } from '@/components/pc/popup/index.js'

export default {
  name: 'PcJsPopupSimpleDemo',
  components: {
    BaseButton
  },
  methods: {
    doOpen () {
      popup('保存成功!')
    }
  }
}
</script>

设置弹窗参数示例

<template>
  <div>
    <BaseButton @click="doOpen()">设置弹窗参数</BaseButton>
  </div>
</template>
<script>
import BaseButton from '@/components/base/button/index.vue'
import { popup } from '@/components/pc/popup/index.js'

export default {
  name: 'PcJsPopupDemo',
  components: {
    BaseButton
  },
  methods: {
    doOpen () {
      popup({
        title: '提示信息',
        message: '保存成功!',
        width: 250,
        height: 200
      })
    }
  }
}
</script>

html作为内容示例

<template>
  <div>
    <BaseButton @click="doOpen()">html作为内容</BaseButton>
  </div>
</template>
<script>
import BaseButton from '@/components/base/button/index.vue'
import { popup } from '@/components/pc/popup/index.js'

export default {
  name: 'PcJsHtmlPopupDemo',
  components: {
    BaseButton
  },
  methods: {
    doOpen () {
      popup({ html: `<div class="message">
                      <p style="color: red;font-size: 30px;margin-bottom: 20px">导入失败!</p>
                      <p>第1行:身份证号不能为空!</p>
                      <p>第3行:手机号码不能为空!</p>
                    </div>` })
    }
  }
}
</script>

2. Alert 示例

简单带一个按钮示例

<template>
  <div>
    <BaseButton @click="doOpen()">带一个按钮</BaseButton>
  </div>
</template>
<script>
import BaseButton from '@/components/base/button/index.vue'
import { popupAlert } from '@/components/pc/popup/index.js'

export default {
  name: 'PcJsAlertDemo',
  components: {
    BaseButton
  },
  methods: {
    doOpen () {
      popupAlert({
        message: '确定要删除?',
        btnText: '确认',
        onOK () {
          console.log('触发ok事件')
        }
      })
    }
  }
}
</script>

带一个按钮(有标题、可关闭)示例

<template>
  <div>
    <BaseButton @click="doOpen()">带一个按钮(有标题、可关闭)</BaseButton>
  </div>
</template>
<script>
import BaseButton from '@/components/base/button/index.vue'
import { popupAlert } from '@/components/pc/popup/index.js'

export default {
  name: 'PcJsAlertHeaderDemo',
  components: {
    BaseButton
  },
  methods: {
    doOpen () {
      popupAlert({
        title: '提示',
        showClose: true,
        message: '确定要删除?',
        btnText: '确认',
        onOK () {
          console.log('触发ok事件')
        }
      })
    }
  }
}
</script>

3. Confirm 示例

带确认、取消按钮示例

<template>
  <div>
    <BaseButton @click="doOpen()">带确认、取消按钮</BaseButton>
  </div>
</template>
<script>
import BaseButton from '@/components/base/button/index.vue'
import { popupConfirm } from '@/components/pc/popup/index.js'

export default {
  name: 'PcJsConfirmDemo',
  components: {
    BaseButton
  },
  methods: {
    doOpen () {
      popupConfirm({
        message: '是否继续?',
        okBtnText: '继续',
        cancelBtnText: '不了',
        onOK: () => {
          console.log('触发ok事件')
        },
        onCancel: () => {
          console.log('触发cancel事件')
        }
      })
    }
  }
}
</script>

带确认、取消按钮(有标题、可关闭)示例

<template>
  <div>
    <BaseButton @click="doOpen()">带确认、取消按钮(有标题、可关闭)</BaseButton>
  </div>
</template>
<script>
import BaseButton from '@/components/base/button/index.vue'
import { popupConfirm } from '@/components/pc/popup/index.js'

export default {
  name: 'PcJsConfirmHeaderDemo',
  components: {
    BaseButton
  },
  methods: {
    doOpen () {
      popupConfirm({
        title: '提示',
        showClose: true,
        message: '是否继续?',
        okBtnText: '继续',
        cancelBtnText: '不了',
        onOK: () => {
          console.log('触发ok事件')
        },
        onCancel: () => {
          console.log('触发cancel事件')
        }
      })
    }
  }
}
</script>

等等

实现

实现一个popup.vue

<!-- 弹出窗口组件 -->
<template>
  <div class="popup" :class="{'full-screen': fullScreen, 'fold-menu': foldMenu, 'show-tabs': isShowTabs,
                              'cover-full': cover==='full', 'cover-tab': cover==='tab'}">
    <transition name="mask">
      <div ref="popupMask" class="popup-mask" v-show="visible"></div>
    </transition>
    <transition name="pop">
      <div class="popup-box" v-show="visible" :class="className" :style="popupStyle" ref="popBox">
        <div class="close" v-show="showClose" @click="doClose"><Icon name="close" class="close-icon"></Icon></div>
        <div class="popup-pane" :class="noHeader? 'no-header' : ''">
          <div class="title" v-show="title">{{title}}</div>
          <div ref="contentWrap" class="content-wrap" :style="{overflow: needScroll ? 'auto' : 'hidden', ...contentWrapperStyle}">
            <slot>
              <div v-if="message" :class="needScroll ? 'scroll-message' : 'center-message'">{{message}}</div>
              <div v-else-if="html" class="html-message" v-html="html"></div>
              <component v-else-if="content" :is="content" v-bind="contentProps" v-on="contentEvents"></component>
            </slot>
          </div>
          <div class="bottom">
            <slot name="bottom"></slot>
          </div>
        </div>
      </div>
    </transition>
  </div>
</template>
<script>
/* eslint-disable no-undef */
const isShowTabs = APP_SHOW_TABS // 是否显示功能切换tab页签

export default {
  name: 'Popup',
  props: {
    // 是否显示弹框
    value: {
      type: Boolean,
      default: false
    },
    // 标题
    title: String,
    // 是否显示关闭按钮
    showClose: {
      type: Boolean,
      default: true
    },
    // 信息内容
    message: String,
    // html字符串内容
    html: String,
    // 弹窗内容组件
    content: Object,
    // 弹窗内容组件的props
    contentProps: Object,
    // 弹窗内容组件的事件
    contentEvents: Object,
    // 内容包裹器样式
    contentWrapperStyle: Object,
    // 弹框宽度
    width: {
      type: Number,
      default: 300
    },
    // 弹框高度
    height: {
      type: Number,
      default: 220
    },
    // 样式名
    className: {
      type: String,
      default: ''
    },
    // 内容区域是否需要滚动条
    scroll: {
      type: Boolean,
      default: false
    },
    // 遮罩层覆盖范围(full: 覆盖整个页面, tab: 覆盖当前tab页功能区域)
    cover: {
      type: String,
      default: 'full'
    }
  },
  data () {
    return {
      // 是否显示
      visible: this.value,
      // 内容是否溢出
      contentOverflow: false,
      // 弹窗宽度
      boxWidth: this.width,
      // 弹窗高度
      boxHeight: this.height,
      // 是否显示功能切换tab页签
      isShowTabs: isShowTabs,
      // scrollEventId: 'popup' + new Date().getTime()
      // 定时器ID
      timeoutId: null
    }
  },
  computed: {
    // 是否不显示头部(标题和关闭按钮)
    noHeader () {
      return !this.title && !this.showClose
    },
    // 计算弹框宽高位置相关样式
    popupStyle () {
      let styleObj = {}
      let boxWidth = this.boxWidth
      let boxHeight = this.boxHeight
      styleObj.width = boxWidth + 'px'
      styleObj.height = boxHeight + 'px'
      styleObj.marginLeft = `-${boxWidth / 2}px`
      styleObj.marginTop = `-${boxHeight / 2}px`
      if (boxWidth < 150) {
        styleObj.display = 'none'
      }
      return styleObj
    },
    // 是否需要显示滚动条
    needScroll () {
      return this.scroll || this.contentOverflow
    },
    fullScreen () {
      if (this.$store && this.$store.getters && this.$store.getters.fullScreen) {
        return true
      }
      return false
    },
    foldMenu () {
      if (this.$store && this.$store.getters && this.$store.getters.foldMenu) {
        return true
      }
      return false
    }
  },
  watch: {
    value (val) {
      this.visible = val
    },
    visible (val) {
      if (val && !this.scroll) {
        setTimeout(() => {
          const contentWrap = this.$refs.contentWrap
          const content = this.$refs.contentWrap.firstChild
          const contentWrapHeight = contentWrap.getBoundingClientRect().height
          const contentHeight = content.getBoundingClientRect().height
          console.log('弹窗内容包裹元素高度=', contentWrapHeight, ',弹窗内容元素高度=', contentHeight)
          if (contentHeight > contentWrapHeight) {
            this.contentOverflow = true
          }
        }, 100)
      }
      /* if (val && this.scroll) {
        this.$nextTick(() => {
          this.$eventBus.$emit('init-scroll-' + this.scrollEventId)
        })
      } */
    },
    cover (val) {
      if (val === 'tab') {
        // 弹出只遮罩页签功能区域的弹窗时,触发事件控制横向滚动条
        setTimeout(() => {
          this.$eventBus.$emit('TAB_POPUP')
        }, 0)
      }
    },
    width (val) {
      this.initSize()
    },
    height (val) {
      this.initSize()
    },
    fullScreen (val) {
      if (this.cover === 'tab') {
        setTimeout(() => {
          this.initSize()
        }, 300)
      }
    },
    foldMenu (val) {
      if (this.cover === 'tab') {
        setTimeout(() => {
          this.initSize()
        }, 300)
      }
    }
  },
  mounted () {
    // 初始化弹窗大小,设置弹窗大小不超过浏览器窗口大小
    setTimeout(() => {
      this.initSize()
    }, 50)

    window.addEventListener('resize', this.initSize)
    // 当用户登录失效时由于要跳转到登录页,关闭弹出窗口
    this.$eventBus.$on('NO_AUTH_EVENT', this.doClose)
  },
  beforeDestroy () {
    if (this.timeoutId) {
      clearTimeout(this.timeoutId)
    }
    window.removeEventListener('resize', this.initSize)
    this.$eventBus.$off('NO_AUTH_EVENT', this.doClose)
    // 关闭弹出窗口时,如果是只遮罩页签功能区域的弹窗,触发事件控制横向滚动条
    if (this.cover === 'tab') {
      this.$eventBus.$emit('TAB_POPUP')
    }
  },
  methods: {
    // 初始化弹窗大小,设置弹窗大小不超过浏览器窗口大小
    initSize () {
      let maxWidth = window.innerWidth
      let maxHeight = window.innerHeight

      let coverElement = document.getElementById('tabsViewContent')
      if (!coverElement) {
        coverElement = document.getElementById('viewContent')
      }
      if (this.cover === 'tab' && coverElement) {
        if (coverElement.clientWidth < maxWidth) {
          maxWidth = coverElement.clientWidth
        }
        if (coverElement.clientHeight < maxHeight) {
          maxHeight = coverElement.clientHeight
        }

        let offsetLeft = coverElement.getBoundingClientRect().left
        let winWidth = window.innerWidth
        let mainViewWidth = winWidth - offsetLeft
        if (mainViewWidth < maxWidth) {
          maxWidth = mainViewWidth
        }
      }

      if (maxWidth && (maxWidth - 10) < this.width) {
        this.boxWidth = maxWidth - 10
      } else {
        this.boxWidth = this.width
      }
      if (maxHeight && (maxHeight - 10) < this.height) {
        this.boxHeight = maxHeight - 10
      } else {
        this.boxHeight = this.height
      }
    },
    doClose () {
      this.visible = false
      this.$emit('input', this.visible)
      this.$emit('close')
    },
    close (timeout) {
      if (typeof timeout === 'number' && timeout > 0) {
        this.timeoutId = setTimeout(() => {
          this.doClose()
        }, timeout)
      } else {
        this.doClose()
      }
    }
  }
}
</script>
<style lang="scss" scoped px2rem="false">
$box-border-radius: 3px;
$title-height: 54px;
$close-size: 12px;
$content-tb-padding: 18px;
$content-lr-padding: 24px;
.popup{
  z-index: 1000;
  .popup-mask{
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: rgba(0,0,0,.5);
    transition: all .3s ease;
    z-index: 1000;
  }
  .popup-box{
    position: fixed;
    width: 500px;
    height: 300px;
    top: 50%;
    left: 50%;
    margin-left: -250px;
    margin-top: -150px;
    background-color: #FFF;
    border-radius: $box-border-radius;
    transition: all .2s ease;
    z-index: 1000;
    .close{
      height: $title-height;
      width: $title-height;
      position: absolute;
      top: 0;
      right: 0;
      text-align: center;
      padding-top: ($title-height - $close-size) / 2;
      z-index: 1003;
      .close-icon{
        line-height: 1;
        font-size: $close-size;
        color: #666;
      }
    }
    .popup-pane{
      height: 100%;
      padding: 20px 0;
      .title{
        margin-top: -20px;
        height: $title-height;
        line-height: $title-height;
        border-bottom: solid 1px #E5E5E5;
        font-size: 16px;
        text-align: left;
        padding: 0 ($content-lr-padding * 2 + $close-size) 0 $content-lr-padding;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        color: #666;
        font-weight: bold;
      }
      .content-wrap{
        position: absolute;
        top: $title-height;
        bottom: 0;
        left: 0;
        right: 0;
        .center-message{
          position: absolute;
          width: 100%;
          top: 50%;
          transform: translateY(-50%);
          padding: $content-tb-padding $content-lr-padding;
          box-sizing: border-box;
          font-size: 16px;
          line-height: 1.5;
          text-align: center;
          word-break: break-all;
          white-space: pre-wrap;
        }
        .scroll-message {
          padding: $content-tb-padding $content-lr-padding;
          font-size: 16px;
          line-height: 1.5;
        }
        .html-message {
          font-size: 16px;
          line-height: 1.5;
          ::v-deep .message{
            padding: $content-tb-padding $content-lr-padding;
            text-align: center;
          }
        }
      }
      &.no-header .content-wrap {
        top: 0;
      }
      .scroll{
        overflow-y: auto;
      }
      .bottom{
        position: absolute;
        bottom: 0;
        left: 0;
        right: 0;
        text-align: center;
      }
    }
  }
  &.cover-tab {
    .popup-mask {
      left: $app-menu-width;
      top: $app-header-height;
      transition: all .2s ease;
      transition-delay: .1s;
    }
    .popup-box {
      transform: translate($app-menu-width / 2, ($app-header-height) / 2);
    }
    &.fold-menu {
      .popup-mask {
        left: $app-menu-fold-width;
      }
      .popup-box {
        transform: translate($app-menu-fold-width / 2, $app-header-height / 2);
      }
    }

    &.show-tabs {
      .popup-mask {
        top: $app-header-height + $app-tabs-height;
      }
      .popup-box {
        transform: translate($app-menu-width / 2, ($app-header-height + $app-tabs-height) / 2);
      }
      &.fold-menu {
        .popup-box {
          transform: translate($app-menu-fold-width / 2, ($app-header-height + $app-tabs-height) / 2);
        }
      }
      &.full-screen {
        .popup-mask {
          left: 0;
          top: $app-tabs-height;
          transition-delay: .1s;
        }
        .popup-box {
          transform: translate(0, $app-tabs-height / 2);
        }
      }
    }
  }
}

.pop-enter-active, .pop-leave{
  transform: scale(1);
  opacity: 1;
}
.pop-enter, .pop-leave-active{
  transform: scale(.5);
  opacity: 0;
}
.mask-leave-active{
  opacity: 0;
}
</style>

alert.vue

<template>
  <BasePopup v-model="visible" ref="basePop" class="alert-popup" :message="message" :title="title"
             :showClose="showClose" :width="width" :height="height" :scroll="scroll" :cover="cover"
             :contentWrapperStyle="contentWrapperStyle"
             :content="content" :contentProps="contentProps" :contentEvents="contentEvents"
             @close="doClose">
    <div slot="bottom" class="buttons">
      <BaseButton @click="doOK" type="primary" :text="btnText"></BaseButton>
    </div>
  </BasePopup>
</template>
<script>
import BasePopup from './Popup.vue'
import BaseButton from '../../base/button/index.vue'
export default {
  name: 'BaseAlert',
  components: {
    BasePopup,
    BaseButton
  },
  props: {
    // 是否显示弹框
    value: {
      type: Boolean,
      default: false
    },
    // 标题
    title: String,
    // 是否显示关闭按钮
    showClose: {
      type: Boolean,
      default: false
    },
    // 消息内容
    message: String,
    // 弹窗内容组件
    content: Object,
    // 弹窗内容组件的props
    contentProps: Object,
    // 弹窗内容组件的事件
    contentEvents: Object,
    // 内容包裹器样式
    contentWrapperStyle: Object,
    // 弹框宽度
    width: {
      type: Number,
      default: 300
    },
    // 弹框高度
    height: {
      type: Number,
      default: 220
    },
    // 内容区域是否需要滚动条
    scroll: {
      type: Boolean,
      default: false
    },
    // 遮罩层覆盖范围(full: 覆盖整个页面, tab: 覆盖当前tab页功能区域)
    cover: {
      type: String,
      default: 'full'
    },
    // 按钮文字
    btnText: {
      type: String,
      default: '确定'
    }
  },
  data () {
    return {
      // 是否显示
      visible: this.value
    }
  },
  watch: {
    value (val) {
      this.visible = val
    }
  },
  methods: {
    doClose () {
      this.visible = false
      this.$emit('input', this.visible)
      this.$emit('close')
    },
    doOK () {
      this.visible = false
      this.$emit('input', this.visible)
      this.$emit('ok')
    }
  }
}
</script>
<style lang="scss" scoped px2rem="false">
$content-padding: 24px;
$bottom-height: 54px;
$bottom-height: 54px;
.alert-popup {
  .buttons{
    padding: (($bottom-height - 32px) / 2) $content-padding;
  }

  ::v-deep .popup-box {
    .popup-pane {
      .content-wrap {
        bottom: $bottom-height;
      }
      .bottom {
        border-top: solid 1px $border-color-light;
      }
    }
  }
}
</style>

confirm.vue

<template>
  <BasePopup v-model="visible" ref="basePop" class="confirm-popup" :message="message" :title="title"
             :showClose="showClose" :width="width" :height="height" :scroll="scroll" :cover="cover"
             :contentWrapperStyle="contentWrapperStyle"
             :content="content" :contentProps="contentProps" :contentEvents="contentEvents"
             @close="doClose">
    <div slot="bottom" class="buttons">
      <BaseButton :class="'cancel-btn'" @click="doCancel" :text="cancelBtnText"></BaseButton>
      <BaseButton :class="'ok-btn'" @click="doOK" type="primary" :text="okBtnText"></BaseButton>
    </div>
  </BasePopup>
</template>
<script>
import BasePopup from './Popup.vue'
import BaseButton from '../../base/button/index.vue'
export default {
  name: 'BaseConfirm',
  components: {
    BasePopup,
    BaseButton
  },
  props: {
    // 是否显示弹框
    value: {
      type: Boolean,
      default: false
    },
    // 标题
    title: String,
    // 是否显示关闭按钮
    showClose: {
      type: Boolean,
      default: false
    },
    // 消息内容
    message: String,
    // 弹窗内容组件
    content: Object,
    // 弹窗内容组件的props
    contentProps: Object,
    // 弹窗内容组件的事件
    contentEvents: Object,
    // 内容包裹器样式
    contentWrapperStyle: Object,
    // 弹框宽度
    width: {
      type: Number,
      default: 300
    },
    // 弹框高度
    height: {
      type: Number,
      default: 220
    },
    // 内容区域是否需要滚动条
    scroll: {
      type: Boolean,
      default: false
    },
    // 遮罩层覆盖范围(full: 覆盖整个页面, tab: 覆盖当前tab页功能区域)
    cover: {
      type: String,
      default: 'full'
    },
    // 确定按钮文字
    okBtnText: {
      type: String,
      default: '确定'
    },
    // 取消按钮文字
    cancelBtnText: {
      type: String,
      default: '取消'
    }
  },
  data () {
    return {
      // 是否显示
      visible: this.value
    }
  },
  watch: {
    value (val) {
      this.visible = val
    }
  },
  methods: {
    doClose () {
      this.visible = false
      this.$emit('input', this.visible)
      this.$emit('close')
    },
    doOK () {
      this.visible = false
      this.$emit('input', this.visible)
      this.$emit('ok')
    },
    doCancel () {
      this.visible = false
      this.$emit('input', this.visible)
      this.$emit('cancel')
    }
  }
}
</script>
<style lang="scss" scoped px2rem="false">
$content-padding: 24px;
$bottom-height: 54px;
$bottom-height: 54px;

.confirm-popup {
  .buttons{
    padding: (($bottom-height - 32px) / 2) $content-padding;
    .cancel-btn {
      background-color: #FFF;
      border-color: $border-color-light;
    }
    ::v-deep .btn:not(:last-of-type) {
      margin-right: 10px;
    }
  }

  ::v-deep .popup-box {
    .popup-pane {
      .content-wrap {
        bottom: $bottom-height;
      }
      .bottom {
        border-top: solid 1px $border-color-light;
      }
    }
  }
}
</style>

popup/index.js

/**
 * 弹出窗口组件
 * @author qinglianshizhe
 */
import Vue from 'vue'
import Popup from './Popup.vue'
import Alert from './Alert.vue'
import Confirm from './Confirm.vue'

// Popup构造函数
const PopupConstructor = Vue.extend({
  extends: Popup
})

// Alert构造函数
const AlertConstructor = Vue.extend({
  extends: Alert
})

// Confirm构造函数
const ConfirmConstructor = Vue.extend({
  extends: Confirm
})

// 获取tab页弹窗的父节点
function getTabPopupParentElement () {
  let tabContentElement = window.document.getElementById('tabsViewContent')
  if (tabContentElement) {
    let tabArray = tabContentElement.childNodes
    if (tabArray.length > 0) {
      return tabArray[0]
    }
  }
  return document.body
}

function initInstance (instance, options) {
  // 显示内容
  instance.message = typeof options === 'string' ? options : options.message
  // html内容
  instance.html = typeof options === 'object' ? options.html : null
  // 弹窗内容组件
  instance.content = typeof options === 'object' && options._isVue ? options : options.content
  // 弹窗内容组件参数
  instance.contentProps = typeof options.contentProps === 'object' ? options.contentProps : {}
  // 弹窗内容组件事件
  instance.contentEvents = typeof options.contentEvents === 'object' ? options.contentEvents : {}
  // 内容包裹器样式
  instance.contentWrapperStyle = options.contentWrapperStyle
  // 窗口宽度
  instance.width = typeof options.width === 'number' ? options.width : 300
  // 窗口高度
  instance.height = typeof options.height === 'number' ? options.height : 220
  // 自定义样式名
  instance.className = options.className || ''
  // 是否显示滚动条
  instance.scroll = options.scroll
  // 遮罩层覆盖范围(full: 覆盖整个页面, tab: 覆盖当前tab页功能区域,只显示tab页时可用)
  instance.cover = options.cover || 'full'
  // 父节点
  let parentElement = options.parentElement || (options.cover === 'tab' ? getTabPopupParentElement() : document.body)

  // 关闭时移除
  instance.$on('input', visible => {
    if (!visible) {
      setTimeout(() => {
        parentElement.removeChild(instance.$el)
        instance.$destroy()
      }, 2000)
      /* // 获取popBox元素,如果Popop组件从refs中获取,如果Alert或Confirm组件,先获取Popop,在从Popop组件refs中获取
        let popBox = instance.$refs.popBox || (instance.$refs.basePop && instance.$refs.basePop.$refs.popBox)
        popBox.addEventListener('transitionend', event => {
        // 动画完成后移除DOM节点
        // parentElement.removeChild(instance.$el)
        if (event.target.parentNode && event.target.parentNode.parentNode) {
          event.target.parentNode.parentNode.removeChild(event.target.parentNode)
        }
        // 销毁组件
        instance.$destroy()
      }) */
    }
  })
  // console.log('instance.$el=', instance.$el)
  // 将节点添加到文档
  parentElement.appendChild(instance.$el)

  instance.visible = true
  instance.closed = false
}

// 显示弹出窗口
export function popup (options = {}) {
  let instance = new PopupConstructor({
    el: document.createElement('div'),
    store: options.store || (this && this.$store),
    router: options.router || (this && this.$router)
  })
  initInstance(instance, options)
  // 弹窗标题
  instance.title = options.title || '提示'
  // 是否显示关闭按钮
  instance.showClose = typeof options.showClose === 'boolean' ? options.showClose : true
  return instance
}

// 显示提示框(底部有确定按钮)
export function popupAlert (options = {}) {
  let instance = new AlertConstructor({
    el: document.createElement('div'),
    store: options.store || (this && this.$store),
    router: options.router || (this && this.$router)
  })
  initInstance(instance, options)
  // 弹窗标题
  instance.title = options.title || ''
  // 是否显示关闭按钮
  instance.showClose = typeof options.showClose === 'boolean' ? options.showClose : false
  // 按钮文字
  instance.btnText = options.btnText || '确定'
  // 点击确定按钮时调用options.onOK方法
  instance.$on('ok', () => {
    options.onOK && options.onOK()
  })
  instance.$on('input', visible => {})
  return instance
}

export function popupConfirm (options = {}) {
  let instance = new ConfirmConstructor({
    el: document.createElement('div'),
    store: options.store || (this && this.$store),
    router: options.router || (this && this.$router)
  })
  initInstance(instance, options)
  // 弹窗标题
  instance.title = options.title || ''
  // 是否显示关闭按钮
  instance.showClose = typeof options.showClose === 'boolean' ? options.showClose : false
  // 确定按钮文字
  instance.okBtnText = options.okBtnText || '确定'
  // 取消按钮文字
  instance.cancelBtnText = options.cancelBtnText || '取消'
  // 点击确定按钮时调用options.onOK方法
  instance.$on('ok', visible => {
    options.onOK && options.onOK()
  })
  // 点击确定按钮时调用options.onOK方法
  instance.$on('cancel', visible => {
    options.onCancel && options.onCancel()
  })
  return instance
}

export default popup

感谢评论区大佬的点拨。

希望看完的朋友可以给个赞,鼓励一下