当前位置: 首页 > article >正文

在vue3中使用tsx结合render封装一个项目内通用的弹窗组件

场景: 在大屏项目中经常需要用到弹窗的需求,通常一个项目内弹窗的样式是一致的,如果不封装一个弹窗组件hook的话,每一个弹窗都需要单独封装为一个组件然后再放到项目layout的最外层,再通过store全局判断哪个弹窗显示,这样显然很麻烦,所以现在我封装一个通过tsx及render渲染弹窗组件的hooks,需要使用弹窗就调用hooks传入弹窗组件就行

思路: 首先封装一个弹窗基础框架组件(包括弹窗外观样式及关闭和底部操作等事件)提供插槽展示每个弹窗的内容部分,然后在需要用到一个弹窗时封装这个弹窗的内容组件,然后调用hooks传入这个内容组件就可以打开弹窗(在封装的hooks中会通过render将弹窗基础组件和内容组件合并渲染出来生成一个元素节点,再通过document.body.appendChild将生成的元素节点挂载到页面中以实现弹窗的展示效果)

代码步骤:

(1)封装弹窗组件框架

<template>
  <el-dialog class="models-default" v-bind="props" :model-value="visible">
    <slot></slot>
    <!-- 弹窗头部插槽 -->
    <template v-if="$slots.header" #header>
      <slot name="header"></slot>
    </template>
    <!-- 弹窗尾部插槽 -->
    <template v-if="isFooter" #footer>
      <!-- 自定义尾部插槽组件显示 -->
      <template v-if="$slots.footer">
        <slot name="footer"></slot>
      </template>
      <!-- 默认尾部按钮列显示 -->
      <template v-else>
        <el-button @click="visible = false">{{ props.cancelBtnText }}</el-button>
        <el-button v-for="(item, index) in footerButtons" :key="index" :icon="item.icon" :type="item.type"
          @click="() => btnClickHandle(item)">{{ item.name }}</el-button>
        <el-button type="primary" @click="confirmHandle">{{ props.confirmBtnText }}</el-button>
      </template>
    </template>
  </el-dialog>
</template><script lang="ts" setup>
import { ref, getCurrentInstance } from 'vue'
import { ElDialog, ElButton, type DialogProps, type ButtonProps } from 'element-plus'const emits = defineEmits(['update:modelValue', 'confirm', 'beforeClose'])
// 传入的弹窗props
const props = withDefaults(defineProps<BProps>(), {
  footerButtons: () => [], //底部按钮
  confirmBtnText: '确认',  //底部确认按钮文字
  cancelBtnText: '取消',   //底部取消按钮文字
  isFooter: true,   //是否展示底部按钮
  showClose: true,  //是否展示关闭图标
  title: '弹窗名称', //弹窗标题
})const visible = ref(true)const instance = getCurrentInstance()const closeModel = (value = false) => {
  visible.value = value
}const confirmHandle = () => {
  emits('confirm', instance?.proxy?.$refs[props.contentRef], closeModel)
}const btnClickHandle = (item: FooterButtons) => {
  // item.onClick && item?.onClick(instance?.proxy?.$refs[props.contentRef], closeModel)
  item.onClick?.(instance?.proxy?.$refs[props.contentRef], closeModel)
}
</script><script lang="ts">
// 约束底部按钮
export interface FooterButtons {
  icon?: string
  name?: string
  type?: ButtonProps['type']
  onClick?: (contentInstance: any, done: () => void) => void
}
// ts接口约束弹窗接收外部的props
interface BProps extends Partial<DialogProps> {
  footerButtons?: FooterButtons[]
  confirmBtnText?: string
  cancelBtnText?: string
  isFooter?: boolean
  contentRef: string
}
</script>
<style lang="scss">
 // 省略样式
</style>

(2)封装tsx渲染弹窗组件的hook(通过tsx将弹窗框架以及传入的弹窗内容组件结合并渲染到页面上)


import type { ComponentInternalInstance, Ref } from 'vue'
import { h, render, onUnmounted, ref } from 'vue'
import { type DialogProps } from 'element-plus'
import type { JSX } from 'vue/jsx-runtime'
// 弹窗框架组件
import Model, { type FooterButtons } from '@/components/models/index.vue'
import { ElConfigProvider } from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'type Content = Parameters<typeof h>[0] | string | JSX.Element
​
interface UseModelProps extends Partial<DialogProps> {
  isFooter?: boolean // 是否展示底部
  footerButtons?: Array<FooterButtons> // 底部按钮(除取消和保存按钮之外的按钮)
  onConfirm?: (contentInstance: any) => void
  onClosed?: () => void
  onOpened?: () => void
}// 弹窗插槽暴露
interface ElDialogSlots {
  header?: () => JSX.Element
  footer?: () => JSX.Element
}interface ContentPropsType {
  ref: string
  [key: string]: any
}interface Options {
  props: UseModelProps // model props
  slots?: ElDialogSlots // 弹窗插槽对象
  contentProps?: ContentPropsType // 弹窗主体插入逐渐props
}/**
 * 窗体模块hooks
 * @param {Object} content 设置弹窗主体组件对象
 * @param {Object} options 配置信息
 * @param {Object} options.props 窗体组件props,继承element-puls dialog组件的所有props
 * @param {Boolean} options.props.isFooter 是否显示底部内容
 * @param {Array} options.props.footerButtons 底部按钮添加
 * @param {Function} options.props.onConfirm 确认按钮回调
 * @param {Function} options.props.onClosed 弹窗关闭回调
 * @param {Function} options.props.onOpened 弹窗打开回调
 * @param {Function} options.slots 弹窗插槽对象
 * @param {Object} options.contentProps 弹窗主体插入组件props
 * @returns
 */
export function useModel (content: Content, options?: Options) {
  // 弹窗组件实例
  const modelInstance: Ref<ComponentInternalInstance | null> = ref(null)
  // 弹窗的元素节点
  let fragment: Element | null = null// 关闭并卸载组件
  const closeAfter = () => {
    if (fragment) {
      render(null, fragment as unknown as Element) // 卸载组件
      fragment.textContent = '' // 清空文档片段
      fragment = null
    }
    modelInstance.value = null
  }
  // 关闭弹窗
  function close () {
    if (modelInstance.value) modelInstance.value.props.modelValue = false
  }
 // 核心代码
  function open () {
    // 打开弹窗前判断如果存在当前弹窗实例就销毁掉重新创建
    if (modelInstance.value) {
      close()
      closeAfter()
    }
    const { props = {}, slots = {}, contentProps = { ref: 'content' } } = options ?? {}
    // 创建元素节点
    fragment = document.createDocumentFragment() as unknown as Element
    // tsx组件内容(将弹窗框架组件和传入的内容组件整合)
    const vNode = (
      <ElConfigProvider locale={zhCn} size="small" zIndex={3000}>
        {/* 将传入的props弹窗配置信息都传入到弹窗框架组件中 */}
        <Model align-center {...props} modelValue={true} contentRef={contentProps.ref}>
          {{
            {/* 弹窗内容组件 */}
            default: () => <content {...contentProps}></content>,
            ...slots,
          }}
        </Model>
      </ElConfigProvider>
    )
    // render将tsx生成的组件vNode渲染到元素节点上
    render(vNode, fragment)
    // 弹窗实例
    modelInstance.value = vNode.component
    // 将弹窗的元素节点挂载到页面上
    document.body.appendChild(fragment)
  }onUnmounted(() => {
    close()
  })
  
  // 将外部需要用到的方法和变量暴露出去
  return {
    open, // 打开弹窗
    close, // 关闭弹窗
    closeAfter, // 销毁弹窗
    modelInstance, // 弹窗实例
  }
}

(3)在需要用弹窗组件的地方使用封装的hooks


// 弹窗hooks
import { useModel } from '@/hooks/useModel'
// 弹窗内容组件
import ShopDetail from '../detailDialog/ShopDetail.vue';const showDetail = () => {
  // 传入弹窗内容组件及弹窗组件需要用到的props参数
  const { open, closeAfter } = useModel(ShopDetail, {
    // 弹窗框架组件props
    props: {
      title: '店铺详情',
      width: '900px',
      isFooter: false,
      // 关闭弹窗回调
      onClosed: () => {
        closeAfter()
      }
    },
    // 弹窗内容组件props
    contentProps: {
      ref: 'detail',
      dataInfo: props.markerData
    },
  })
  // 打开弹窗
  open()
}

http://www.kler.cn/a/458029.html

相关文章:

  • Chapter4.2:Normalizing activations with layer normalization
  • 【Pytorch报错】AttributeError: cannot assign module before Module.__init__() call
  • MYsql--------ubantu中安装mysql
  • PostgreSQL 表达式
  • AfuseKt1.4.4 | 刮削视频播放器,支持阿里云盘和自动海报墙
  • CentOS7安装配置JDK保姆级教程(图文详解)
  • Docker的概述与安装
  • 算法基础一:冒泡排序
  • React引入Echart水球图
  • systemverilog语法:assertion sequence
  • node-js Express防盗链
  • Spring Boot 多数据源解决方案:dynamic-datasource-spring-boot-starter 的奥秘(下)
  • 1.flask介绍、入门、基本用法
  • Python-网络爬虫
  • Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin(自测问题解决!)
  • 【每日学点鸿蒙知识】页面反向传值、图片撑满问题、清除Web缓存、Refresh支持swiper、POP颜色没效果
  • 验证ETL程序产生数据的正确性以及确保数据质量的方法
  • 【畅购商城】详情页详情之商品详情
  • Windows下C++使用SQLite
  • 手机联系人 查询 添加操作
  • 【VulnOSv2靶场渗透】
  • Vue.js组件开发-使用Paho MQTT数据传输
  • 德州仪器 cookie _px3 分析
  • BOOST 库在信号处理领域的具体应用及发展前景
  • 基于Springboot的社区老人健康信息管理系统的设计与实现​
  • 如果你的网站是h5网站,如何将h5网站变成小程序-除开完整重做方法如何快速h5转小程序-h5网站转小程序的办法-优雅草央千澈