Vue项目中如何实现弹窗系统的封装

最终效果:

// 函数式调用弹窗
showModal(ModalComponent, {props参数,emit事件})

为什么要封装弹窗系统

1.弹窗调用更简便

封装前使用:弹窗组件要定义打开弹窗的方法,再暴露给父组件。在父组件中,需要通过获取组件实例,再调用打开方法才能打开弹窗

<!-- 弹窗子组件 -->
<script setup lang="ts">
// 控制弹窗开关
const isShowModal = ref(false)
// 打开弹窗的方法
const open = () => {
    isShowModal.value = true
}
// 向父组件暴露打开对话框方法
defineExpose({ openDialog })
</script>
​
<!-- 父组件 -->
<template>
    XXX
    <Modal ref="modalRef" />
</template>
​
<script setup lang="ts">
import Modal frmo './Modal.vue'
// 获取modal实例
const modalRef = ref<InstanceType<typeof Modal>>()                            
// 使用时需通过实例方法打开弹窗
modalRef.value?.open()
</script>

封装后使用:子组件仅需在关闭弹窗时触发close事件,父组件则是采用函数式调用

<!-- 弹窗子组件 -->
<script setup lang="ts">
const emit = defineEmits<{
  // 关闭弹窗
  close: []
}>()
​
//每当弹窗关闭时需要触发close事件,删除该弹窗节点
emit('close')
</script>
​
<!-- 父组件 -->
<script setup lang="ts">
// 引入showModal函数
import { showModal } from './utils/showModal'
​
// 使用showModal函数
showModal(component, {props参数,emit事件})
</script>

预备知识

h()

h函数的作用是用来创建虚拟DOM节点。

  • 介绍

    // 完整参数签名
    function h(
      type: string | Component,           // 可以是原生标签,也可以是组件
      props?: object | null,              // 要传递的参数
      children?: Children | Slot | Slots  // 子节点 
    ): VNode
    ​
    // 类型说明
    type Children = string | number | boolean | VNode | null | Children[]
    // 插槽的作用主要是当h()第一个参数type为Component时,用来指定虚拟DOM渲染的位置
    // 默认插槽--需要以插槽函数进行传递,即用箭头函数
    type Slot = () => Children
    // 具名插槽--以插槽函数的对象形式来传递
    type Slots = { [name: string]: Slot }
  • 示例

    创建原生标签

    // 该DOM节点的类名,样式,内容等均可以在props中指定
    // 下面例子中class: [foo, { bar }] 表示 class 属性是一个数组,数组中包含两个元素:foo、bar
    h('div', { class: [foo, { bar: true }], style: { color: 'red' }, innerHTML: 'hello'})
    ​
    // 事件监听器应以 onXxx 的形式书写
    h('div', { onClick: () => {} })
    ​
    // children 数组可以同时包含 vnode 和字符串, 没有 prop 时可以省略不写
    h('div', ['hello', h('span', 'hello')])

    创建组件

    import Foo from './Foo.vue'
    ​
    // 传递 prop, 此处是重点,可以传递props参数,也可以定义emit事件的回调
    h(Foo, {
      // 等价于 some-prop="hello"
      someProp: 'hello',
      // 等价于 @update="() => {}"
      onUpdate: () => {}
    })
    ​
    // 传递单个默认插槽
    h(Foo, () => 'default slot')
    ​
    // 传递具名插槽--必须使用对象形式,为每一个具名插槽指定DOM
    // 注意,需要使用 `null` 来避免
    // 插槽对象被当作是 prop
    h(MyComponent, null, {
      default: () => 'default slot',
      foo: () => h('div', 'foo'),
      bar: () => [h('span', 'one'), h('span', 'two')]
    })
  • 具体使用--如何将VNode应用到页面上

    声明渲染函数

    import { ref, h } from 'vue'
    ​
    export default {
      props: {
        /* ... */
      },
      setup(props) {
        const count = ref(1)
    ​
        // 返回渲染函数
        // 返回必须是一个函数而不是一个值!这能保证渲染函数实时更新  
        return () => h('div', props.msg + count.value)
      }
    }

    由于渲染函数优先级,如果在setup()钩子中返回渲染函数,Vue将会忽略组件的 <template> 部分,转而使用setup()钩子返回的渲染函数来生成组件的 DOM 结构

了解完h()之后,你需要知道:

  • h()的三个参数。

    第二个参数如何传递props,定义emit回调。第三个参数中插槽的使用,用来指定子元素渲染的位置

  • 返回的VNode节点如何使用

如何实现

公共utils

作用:

  • 维护两个响应式变量componentListnextComponentListcomponentList用来储存正在渲染的弹窗,nextComponentList用来存储待渲染的弹窗,在当前所有弹窗关闭后即会渲染

  • 导出一个showModal(),用于打开弹窗

  • 导出一个showNextModal(),用于呈现想要当前所有弹窗关闭后,再依次渲染的弹窗

  • 导出一个clearComponentList(),用于清空两个弹窗列表

  • 导出一个asyncModal(),用于将弹窗包装成异步弹窗
// modal.ts

import { ref, watch, shallowRef, defineAsyncComponent } from 'vue'
import type { Component, AsyncComponentLoader } from 'vue'
import LoadingModal from './components/LoadingModal/index.vue'

interface ITempModalItem {
  component: Component // 这里应该是一个组件构造器或函数组件
  props: object // 传递给组件的props,类型可以根据需要定义为更具体的类型
}

interface IModalItem extends ITempModalItem {
  id: symbol // 组件编号,用来标识每个组件
}

// 存储当前渲染组件
export const componentList = ref<IModalItem[]>([])
// 存储待渲染组件
const nextComponentList = ref<ITempModalItem[]>([])

/**
 * 展示弹窗
 * @param component 弹窗组件
 * @param props props参数或者emit事件回调
 */
export const showModal = (component: Component, props: any) => {
  // 对传入的组件参数进行处理
  const tempComponent = shallowRef(component)
  // 为每个弹窗生成唯一标识
  const id = Symbol()
  // 添加一个关闭弹窗事件
  props['onClose'] = () => {
    // 通过id找到改组件在数组中的次序
    const index = componentList.value.findIndex((item) => item.id === id)
    if (index !== -1) {
      // 删除组件列表对应元素
      componentList.value.splice(index, 1)
    }
  }
  componentList.value.push({ component: tempComponent, props, id })
}

/**
 * 当前渲染的弹窗全部关闭时才进行渲染的弹窗
 * @param component 弹窗组件
 * @param props props参数或者emit事件回调
 */
export const showNextModal = (component: Component, props: any) => {
  // 对传入的组件参数进行处理
  const tempComponent = shallowRef(component)
  // 将新增弹窗加入待渲染列表nextComponentList
  nextComponentList.value.push({ component: tempComponent, props })
}

// 监视当前正在渲染的弹窗,若为空,则渲染待渲染弹窗
watch(
  () => componentList.value.length,
  () => {
    if (nextComponentList.value.length && componentList.value.length === 0) {
      // 待渲染弹窗不为空时,且当前正在渲染的弹窗列表为空,则将待渲染的弹窗列表添加到正在渲染的弹窗列表中
      const nextModal = nextComponentList.value.shift()
      if (nextModal) {
        showModal(nextModal.component, nextModal.props)
      }
    }
  }
)

/**
 * 页面切换时,清空所有弹窗
 */
export const clearComponentList = () => {
  componentList.value = []
  nextComponentList.value = []
}

/**
 * 将弹窗包装成异步弹窗
 * @param loader 渲染函数,形如 () => import('./content.vue')
 * @param loadingComponent 弹窗加载前loading状态展示的组件
 * @returns 异步弹窗
 */
export const asyncModal = (loader: AsyncComponentLoader, loadingComponent?: Component) => {
  const temComponent = defineAsyncComponent({
    // 加载函数
    loader,
    // 加载异步组件时使用的组件
    loadingComponent: loadingComponent || LoadingModal,
    // 展示加载组件前的延迟时间,默认为 200ms
    delay: 200
  })
  temComponent['preload'] = loader
  return temComponent
}

组件ModalContainer

作用:从utils中获取弹窗列表,调用h函数,返回渲染函数

// ModalContainer.vue

<script>
import { componentList } from '@/utils/modal'
import { h } from 'vue'
import ModalWrapper from '@/components/ModalContainer/components/ModalWrapper.vue'

export default {
  setup() {
    return () =>
      componentList.value.map((item) => {
        return h(ModalWrapper, { key: item.id }, () => h(item.component, item.props))
      })
  }
}
</script>

组件ModalWrapper

作用:该组件为每一个弹窗组件的外层包裹组件,主要作用是抽离公共样式

// ModalWrapper.vue
<!-- 本组件为每个弹窗外包组件,仅提供布局--水平垂直居中 -->
<template>
  <div id="modal-wrapper">
    <slot></slot>
  </div>
</template>
​
<style scoped>
#modal-wrapper {
  display: flex;
  justify-content: center; /* 水平居中 */
  align-items: center; /* 垂直居中 */
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  z-index: 10;
}
</style>

组件LoadingModal

作用:作为弹窗打开的默认加载组件

<!-- 本组件为弹窗加载默认组件,为菊花图动画 -->
<template>
  <div class="juhua-loading">
    <div class="jh-circle"></div>
    <div class="jh-circle2 jh-circle"></div>
    <div class="jh-circle3 jh-circle"></div>
    <div class="jh-circle4 jh-circle"></div>
    <div class="jh-circle5 jh-circle"></div>
    <div class="jh-circle6 jh-circle"></div>
    <div class="jh-circle7 jh-circle"></div>
    <div class="jh-circle8 jh-circle"></div>
    <div class="jh-circle9 jh-circle"></div>
    <div class="jh-circle10 jh-circle"></div>
    <div class="jh-circle11 jh-circle"></div>
    <div class="jh-circle12 jh-circle"></div>
  </div>
</template>

<style scoped>
.juhua-loading {
  position: relative;
  width: 40px;
  height: 40px;
}
.juhua-loading .jh-circle {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
}
.juhua-loading .jh-circle:before {
  content: '';
  display: block;
  margin: 0 auto;
  width: 15%;
  height: 15%;
  background-color: #333;
  border-radius: 100%;
  -webkit-animation: jh-circleFadeDelay 1.2s infinite ease-in-out both;
  animation: jh-circleFadeDelay 1.2s infinite ease-in-out both;
}
.juhua-loading .jh-circle2 {
  -webkit-transform: rotate(30deg);
  -ms-transform: rotate(30deg);
  transform: rotate(30deg);
}
.juhua-loading .jh-circle3 {
  -webkit-transform: rotate(60deg);
  -ms-transform: rotate(60deg);
  transform: rotate(60deg);
}
.juhua-loading .jh-circle4 {
  -webkit-transform: rotate(90deg);
  -ms-transform: rotate(90deg);
  transform: rotate(90deg);
}
.juhua-loading .jh-circle5 {
  -webkit-transform: rotate(120deg);
  -ms-transform: rotate(120deg);
  transform: rotate(120deg);
}
.juhua-loading .jh-circle6 {
  -webkit-transform: rotate(150deg);
  -ms-transform: rotate(150deg);
  transform: rotate(150deg);
}
.juhua-loading .jh-circle7 {
  -webkit-transform: rotate(180deg);
  -ms-transform: rotate(180deg);
  transform: rotate(180deg);
}
.juhua-loading .jh-circle8 {
  -webkit-transform: rotate(210deg);
  -ms-transform: rotate(210deg);
  transform: rotate(210deg);
}
.juhua-loading .jh-circle9 {
  -webkit-transform: rotate(240deg);
  -ms-transform: rotate(240deg);
  transform: rotate(240deg);
}
.juhua-loading .jh-circle10 {
  -webkit-transform: rotate(270deg);
  -ms-transform: rotate(270deg);
  transform: rotate(270deg);
}
.juhua-loading .jh-circle11 {
  -webkit-transform: rotate(300deg);
  -ms-transform: rotate(300deg);
  transform: rotate(300deg);
}
.juhua-loading .jh-circle12 {
  -webkit-transform: rotate(330deg);
  -ms-transform: rotate(330deg);
  transform: rotate(330deg);
}
.juhua-loading .jh-circle2:before {
  -webkit-animation-delay: -1.1s;
  animation-delay: -1.1s;
}
.juhua-loading .jh-circle3:before {
  -webkit-animation-delay: -1s;
  animation-delay: -1s;
}
.juhua-loading .jh-circle4:before {
  -webkit-animation-delay: -0.9s;
  animation-delay: -0.9s;
}
.juhua-loading .jh-circle5:before {
  -webkit-animation-delay: -0.8s;
  animation-delay: -0.8s;
}
.juhua-loading .jh-circle6:before {
  -webkit-animation-delay: -0.7s;
  animation-delay: -0.7s;
}
.juhua-loading .jh-circle7:before {
  -webkit-animation-delay: -0.6s;
  animation-delay: -0.6s;
}
.juhua-loading .jh-circle8:before {
  -webkit-animation-delay: -0.5s;
  animation-delay: -0.5s;
}
.juhua-loading .jh-circle9:before {
  -webkit-animation-delay: -0.4s;
  animation-delay: -0.4s;
}
.juhua-loading .jh-circle10:before {
  -webkit-animation-delay: -0.3s;
  animation-delay: -0.3s;
}
.juhua-loading .jh-circle11:before {
  -webkit-animation-delay: -0.2s;
  animation-delay: -0.2s;
}
.juhua-loading .jh-circle12:before {
  -webkit-animation-delay: -0.1s;
  animation-delay: -0.1s;
}
@-webkit-keyframes jh-circleFadeDelay {
  0%, 39%, 100% {
    opacity: 0;
  }
  40% {
    opacity: 1;
  }
}
@keyframes jh-circleFadeDelay {
  0%, 39%, 100% {
    opacity: 0;
  }
  40% {
    opacity: 1;
  }
}
</style>

添加路由守卫

作用:弹窗列表仅在单个页面有效,而弹窗列表是全局变量,故当切换其他页面时,应该清空弹窗列表

// router.vue
​
// 添加全局前置守卫
router.afterEach(() => {
  clearComponentList()
})

添加弹窗组件出口

作用:使用渲染函数,设置弹窗出口

// 在App.vue中添加
<script setup lang="ts">
import ModalContainer from '@/components/ModalContainer/index.vue'
</script>

<template>
    <router-view />
    <ModalContainer />
</template>

如何使用

自定义弹窗子组件

<!-- 弹窗子组件 (./components/modal/content.vue) --> 
<template>
  <el-dialog v-model="dialogVisible" title="Demo" width="500" @closed="emit('close')">
    <span>{{ props.modalContent }}</span>
    <template #footer>
      <div class="dialog-footer">
        <el-button type="primary" @click="confirmEvent"> Confirm </el-button>
      </div>
    </template>
  </el-dialog>
</template>

<script lang="ts" setup>
import { ref } from 'vue'
// 控制弹窗显示
const dialogVisible = ref(true)

// 获取组件列表
const props = defineProps<{
  // 文案
  modalContent: string
}>()

// 自定义事件
const emit = defineEmits<{
  // close事件用来销毁弹窗,当弹窗要关闭时调用emit('close')即可
  close: []
  // 确认弹窗
  confirm: [text: string]
}>()

const confirmEvent = () => {
  //关闭弹窗
  dialogVisible.value = false
  // 确认回调
  emit('confirm', props.modalContent)
}
</script>
// 由于要封装成异步组件,所以需要再进行一步操作,再导出弹窗组件
<!-- 弹窗子组件 (./components/modal/index.ts) --> 
import { asyncModal } from '@/utils/modal'

const Modal = asyncModal(() => import('./content.vue'))

// 如果弹窗过大,需要预加载,则调用 Modal.preload,进行预加载
// Modal.preload
export default Modal

使用弹窗的父组件

<!-- 父组件 -->
<template>
  <button @click="openDialog">点击打开对话框</button>
</template>
<script setup lang="ts">
import Modal from './components/modal'
import AnotherModal from './components/AnotherModal'
import { showModal, showNextModal } from './utils/showModal'

const openDialog = () => {
  // 打开弹窗
  showModal(Modal, {
    // props参数
    modalContent: '我是二聪明',
    // emit事件: 在组件内定义的emit事件名前加个on,即可指定emit回调
    onConfirm: confirmDialogEvent
  })
  // 当前所有弹窗关闭后再的弹窗
  showNextModal(AnotherModal, {})
}

// emit事件回调
const confirmDialogEvent = (text: string) => {
  alert(`确认了内容为:${text} 的弹窗`)
}
</script>

  • 13
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue3是最新版本的Vue.js框架,它具有更强大的功能和性能优化,可以实现可拖动弹窗封装。 首先,我们可以利用Vue3的自定义指令来实现拖动效果。首先在弹窗组件添加一个容器元素,用来表示弹窗的位置和大小。然后,在该容器元素上添加一个自定义指令,比如"v-draggable"。在该自定义指令,我们可以通过监听鼠标事件来实现拖动功能。当鼠标按下时,记录当前鼠标的位置坐标,并监听鼠标移动事件,不断地更新弹窗容器的位置坐标。最后,在鼠标松开的时候,停止拖动。 接下来,我们可以为弹窗组件添加一些控制按钮,比如关闭按钮和最小化按钮。这些按钮可以通过Vue3的事件绑定来实现对应的功能。例如,点击关闭按钮后,可以通过触发一个"close"事件来关闭弹窗。 此外,我们还可以利用Vue3的响应式数据和计算属性来实现弹窗的位置和大小自动调整。通过监听窗口大小变化的事件,并实时更新弹窗容器的位置和大小,使之适应不同的显示设备和分辨率。 最后,为了防止拖动时超出边界的情况,我们可以在自定义指令添加一些边界限制的判断逻辑,以确保弹窗不会超出容器范围。 综上所述,通过Vue3的自定义指令、事件绑定、响应式数据和计算属性,我们可以很方便地封装一个可拖动的弹窗组件,并且可以根据需要添加其他功能和样式,以满足不同的需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值