vue3+elementPlus+ts实现js直接调用弹窗

179 阅读3分钟

前言

在开发过程中经常遇到需要打开各种弹窗功能,就比如删除提示框或者密码输入框等其他简单的弹窗。但是在vue引入弹窗需要再做很多工作如引入弹窗组件,控制弹窗显示隐藏等。这里提供了直接用js调用弹窗的方法,如引用ElementPlus的ElMessageBox组件一样,直接调用打开。

image.png

具体代码参考

/src/components/simple-dialog/index.ts

import { createApp, type App, type Ref } from 'vue'
import SimpleDialog from '~/components/simple-dialog/index.vue'

export interface SimpleDialogInstance extends App<Element> {
  showBtnLoading: () => void
  hideBtnLoading: () => void
  destroy: () => void
}

interface SimpleDialogItf {
  title?: string
  width?: string | number
  warnIconVisible?: boolean
  tips?: string
  description?: string
  onClose?: () => void
  onClosed?: () => void
  onCancel?: () => void
  onConfirm?: () => any
  // 确定事件:开发者自定义设置按钮加载loading和弹窗销毁
  onCustomConfirm?: (instance: SimpleDialogInstance) => void 
  contentRender?: () => any // 自定义弹窗content
  footerRender?: () => any // 自定义弹窗footer
}
const simpleDialogInstance = ({
  title,
  width,
  tips,
  warnIconVisible,
  description,
  onClose,
  onClosed,
  onCancel,
  onConfirm,
  onCustomConfirm,
  contentRender,
  footerRender
}: SimpleDialogItf) => {
  const wrapNode = document.createElement('div')
  const createAppOption = {
    modelValue: true,
    title,
    width,
    warnIconVisible,
    tips,
    description,
    contentRender,
    footerRender,
    onClose: () => {
      onClose?.()
    },
    onClosed: () => {
      onClosed?.()
      instance.unmount()
      document.body.removeChild(wrapNode)
    },
    onCancel: () => {
      onCancel?.()
      instance.destroy()
    },
    onConfirm: async () => {
      if (onConfirm) {
        instance.showBtnLoading()
        const flag = await onConfirm()
        instance.hideBtnLoading()
        // 如果返回false(Promise.resolve(false)),则不销毁弹窗
        if (flag !== false) {
          instance.destroy()
        }
      } else if (onCustomConfirm) {
        onCustomConfirm(instance)
      }
    },
    onInit: (dialogRef: Ref<any>, btnLoading: boolean) => {
      instance.config.globalProperties.dialogRef = dialogRef
      instance.config.globalProperties.btnLoading = btnLoading
    }
  }
  const instance = createApp(SimpleDialog, createAppOption) as SimpleDialogInstance
  document.body.appendChild(wrapNode)
  instance.mount(wrapNode)

  instance.showBtnLoading = () => {
    instance.config.globalProperties.btnLoading && (instance.config.globalProperties.btnLoading.value = true)
  }

  instance.hideBtnLoading = () => {
    instance.config.globalProperties.btnLoading && (instance.config.globalProperties.btnLoading.value = false)
  }

  instance.destroy = () => {
    instance.config.globalProperties.dialogRef && (instance.config.globalProperties.dialogRef.value.visible = false)
    setTimeout(() => {
      instance.unmount()
      // document.body.removeChild(wrapNode)
    }, 1500)
  }
  return instance
}

export default simpleDialogInstance

/src/components/simple-dialog/index.vue

<template>
  <el-dialog
    ref="simpleDialogRef"
    class="hn-simple-dialog"
    v-model="visible"
    :title="title"
    :width="width"
    :close-on-click-modal="closeOnClickModal"
    :close-on-press-escape="closeOnPressEscape"
    :draggable="draggable"
    :destroy-on-close="true"
    @close="emits('close')"
    @closed="emits('closed')"
  >
    <div class="question" v-if="tips">
      <svg-icon v-if="warnIconVisible" class="question-icon" name="warning" color="#1472FF" width="16" height="16"></svg-icon>
      <span class="question-txt">
        {{ tips }}
      </span>
    </div>
    <div class="description" v-if="description">{{ description }}</div>
    <ContentRender />
    <template #footer>
      <el-button v-if="!footerRender" @click="handleCancel">{{ cancelText }}</el-button>
      <el-button v-if="!footerRender" type="primary" @click="handleConfirm" :loading="btnLoading">{{ confirmText }}</el-button>
      <FooterRender />
    </template>
  </el-dialog>
</template>

<script lang="ts">
export default {
  name: 'simple-dialog'
}
</script>

<script lang="ts" setup>
import { computed, ref, h, onMounted } from 'vue'

interface Props {
  title?: string
  modelValue: boolean
  width?: string | number
  draggable?: boolean
  closeOnClickModal?: boolean
  closeOnPressEscape?: boolean
  warnIconVisible?: boolean
  tips?: string
  description?: string
  cancelText?: string
  confirmText?: string
  contentRender?: Function
  footerRender?: Function
}

const props = withDefaults(defineProps<Props>(), {
  title: '提示',
  width: 380,
  draggable: true,
  closeOnClickModal: false,
  closeOnPressEscape: true,
  warnIconVisible: true,
  tips: '',
  description: '',
  cancelText: '取消',
  confirmText: '确定'
})

const emits = defineEmits(['update:modelValue', 'close', 'closed', 'cancel', 'confirm', 'init'])

const simpleDialogRef = ref()
const visible = computed({
  get () {
    return props.modelValue
  },
  set (value) {
    emits('update:modelValue', value)
  }
})

type RenderType = 'contentRender' | 'footerRender'
const createRender = (type: RenderType) => {
  return {
    render: () => {
      const methods = props[type]
      return methods ? methods(h) : ''
    }
  }
}
const ContentRender = createRender('contentRender')
const FooterRender = createRender('footerRender')

const btnLoading = ref(false)
const handleCancel = () => {
  emits('cancel')
}
const handleConfirm = async () => {
  emits('confirm')
}
const visibleChange = (val: boolean) => {
  simpleDialogRef.value.visible = val
}

onMounted(() => {
  emits('init', simpleDialogRef, btnLoading)
})

defineExpose({
  btnLoading,
  visibleChange
})
</script>

<style lang="scss">
.hn-simple-dialog {
  border-radius: 8px;
  overflow: hidden;
}

.hn-simple-dialog .el-dialog__header {
  height: 48px;
  box-sizing: border-box;
  background: #f6f7f9;
  margin-right: 0;
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 20px;
  padding: 0 20px;
}

.hn-simple-dialog .el-dialog__title {
  @include boldFont;
  font-size: 16px;
  font-weight: 500;
  color: #222324;
  line-height: 22px;
}

.hn-simple-dialog .el-dialog__headerbtn {
  top: 0px;
  width: 48px;
  height: 48px;
}

.hn-simple-dialog .el-dialog__header .el-icon {
  width: 24px;
  height: 24px;
}

.hn-simple-dialog .el-dialog__header .el-icon svg {
  width: 24px;
  height: 24px;
}

.hn-simple-dialog .el-dialog__body {
  padding: 24px;
}
</style>

<style lang="scss" scoped>
.question {
  .question-icon {
    margin-right: 4px;
    vertical-align: middle;
  }
  .question-txt {
    font-size: 14px;
    color: #5d6064;
    line-height: 20px;
    vertical-align: middle;
  }
}
.description {
  margin-top: 4px;
  font-size: 12px;
  color: #8a8d95;
  line-height: 16px;
}
</style>

/src/views/order/index.vue

<template>
  <button @click="open1">打开弹窗</button>
  <button @click="open2">打开弹窗2</button>
</template>

<script lang="ts">
export default { name: 'order-list' }
</script>
<script lang="ts" setup>
import simpleDialogInstance from '~/components/simple-dialog'

// 调用方法1:
const open1 = () => {
  simpleDialogInstance({
    title: '删除店铺',
    tips: '确认删除该条店铺信息',
    description: '删除后无法恢复该店铺数据',
    onConfirm: async () => {
      // 项目封装的请求方法,这里需要自己修改
      const [res] = await errorCaptured(() => otherShopApi.otherShopDel(row.id))
      if (res && res.code === 200) {
        ElMessage.success('操作成功')
        search()
      } else {
        // 不关闭弹窗
        return Promise.resolve(false)
      }
    }
  })
}
// 调用方法2:
const open2 = () => {
  simpleDialogInstance({
    title: '删除店铺',
    contentRender: (h: any) => {
      return h('p', { style: { color: '#e00f1b' } }, '确认删除该条店铺信息?')
    },
    // 确定事件:开发者自定义设置按钮加载和弹窗销毁
    onCustomConfirm: async (instance: SimpleDialogInstance) => {
      // 显示按钮加载动画
      instance.showBtnLoading()
      // 项目封装的请求方法,这里需要自己修改
      const [res] = await errorCaptured(() => otherShopApi.otherShopDel(row.id))
      if (res && res.code === 200) {
        ElMessage.success('操作成功')
        search()
        // 销毁弹窗
        instance.destroy()
      } else {
        ElMessage.error(msg)
      }
      // 移除按钮加载动画
      instance.hideBtnLoading()
    }
  })
}
</script>