分享一个vue3封装ant-design-vue对话框的思路

42 阅读3分钟

基础用法

<template>
  <a-space>
    <a-button @click="open()">打开对话框</a-button>
  </a-space>
</template>

<script lang="ts" setup>
import { useModal } from './use-modal'
import type BaseModal from './base.modal.vue'
import { h } from 'vue'

const { open } = useModal<InstanceType<typeof BaseModal>>(
    () => import('./list-edit.modal.vue'),
    {
        contentPorps: {},
        modalProps: {}
        // ...
    }
)
</script>

接受不同类型的内容

<template>
  <a-space>
    <a-button @click="open1">打开对话框:组件</a-button>
    <a-button @click="open2">打开对话框:字符串</a-button>
    <a-button @click="open3">打开对话框:VNnode</a-button>
    <a-button @click="open4">打开对话框:异步组件</a-button>
  </a-space>
</template>

<script lang="ts" setup>
import { useModal } from './use-modal'
import BaseModal from './base.modal.vue'
import { h } from 'vue'

const { open: open1 } = useModal(BaseModal)
const { open: open2 } = useModal('string')
const { open: open3 } = useModal(h('div', null, 'vnode'))
const { open: open4 } = useModal(() => import('./base.modal.vue'))
</script>

实战用例

demo.vue

<template>
  <a-button @click="open({ data: { name: '1', number:1 }, mode: 'view' })">
    查看
  </a-button>
  <a-button @click="open({ data: { name: '2', number:2 }, mode: 'edit' })">
    编辑
  </a-button>
</template>

<script lang="ts" setup>
import { useModal } from './use-modal'
import type ListEditModal from './list-edit.modal.vue'

const { open } = useModal<InstanceType<typeof ListEditModal>>(
  () => import('./list-edit.modal.vue'),
  {
    // 配置AModal组件的属性
    modalProps: {
      width: 400,
      // 关闭时是否销毁内容组件 默认为true
      destroyOnClose: true,
      onOk() {
        console.log('点击了ok')
      },
    },

    // 配置content组件的属性
    contentProps: {
      gsId: '9527',
      onSuccess(val: any) {
        console.log(val)
      },
    },

    // 监听conentProps或者modalProps的变化
    propsChange(conentProps, modalProps) {
      modalProps.title = { view: '查看', edit: '编辑' }[conentProps.mode!]
    },

    // 或者使用自定义监听对象
    watcher: {
      source: conentProps => () => conentProps.mode,
      callback: (conentProps, modalProps) => {
        modalProps.title = { view: '查看', edit: '编辑' }[conentProps.mode!]
      },
    },
  }
)
</script>

内容

list-edit.modal.vue

<template>
  <a-spin :spinning="loading">
    <a-form
      ref="formRef"
      :model="formState"
      name="basic"
      :label-col="{ span: 6 }"
      :wrapper-col="{ span: 18 }"
      style="padding-top: 24px"
    >
      <a-form-item
        label="姓名"
        name="username"
        :rules="[{ required: true, message: 'Please input your username!' }]"
      >
        <a-input
          v-model:value="formState.username"
          :disabled="mode === 'view'"
        />
      </a-form-item>

      <a-form-item
        label="数字"
        name="number"
        :rules="[{ required: true, message: 'Please input your number!' }]"
      >
        <a-input-number
          v-model:value="formState.number"
          :disabled="mode === 'view'"
        />
      </a-form-item>
    </a-form>
  </a-spin>
</template>

<script lang="ts" setup>
import type { FormInstance } from 'ant-design-vue'
import { reactive, ref } from 'vue'

// 支持接受传入contentProps属性,任意值,可通过open方法动态传入
const props = defineProps<{ data: any; gsId: string; mode: 'view' | 'edit' }>()

// 内置双向绑定属性
const visible = defineModel<boolean>('visible')
const loading = defineModel<boolean>('loading')

// 支持发送事件,如success事件由contentProps配置中的onSuccess接收
const emits = defineEmits<{ success: [value?: any] }>()

const formRef = ref<FormInstance>()

const formState = reactive({
  username: props.row.name,
  number: props.row.number,
})

// 初始化一些加载
console.log('modal content init.')

// 定义onOk、onCancel并暴露出去,用于接受来自Modal的onOk、onCancel事件
const onOk = async () => {
  await formRef.value?.validate()
  loading.value = true
  setTimeout(() => {
    emits('success', formState)
    visible.value = false
    loading.value = false
  }, 2000)
}

const onCancel = () => {}

defineExpose({
  onOk,
  onCancel,
})
</script>

源码

use-modal.ts

import type { ModalProps } from 'ant-design-vue'
import { Modal } from 'ant-design-vue'
import {
  h,
  ref,
  reactive,
  getCurrentInstance,
  createVNode,
  render,
  type Component,
  useTemplateRef,
  type ExtractPropTypes,
  watch,
  type Reactive,
  type WatchSource,
  type WatchEffect,
  type WatchOptions,
  onUnmounted,
  onDeactivated,
  type Ref,
  shallowRef,
  defineAsyncComponent,
  isVNode,
  type VNode,
  type AsyncComponentLoader,
} from 'vue'
import { isFunction, isString } from 'lodash-es'

/**
 * @description 内容组件的props
 */
type ContentProps<P> = Partial<ExtractPropTypes<P>>

/**
 * @description 配置项
 */
interface UseModalConfig<P extends Component> {
  /**
   * @description 传递给Modal的props
   */
  modalProps?: ModalProps
  /**
   * @description 传给内容组件的props
   */
  contentProps?: ContentProps<P>
  /**
   * @description 默认的打开状态
   */
  defaultVisible?: boolean
  /**
   * @description props变化时的回调
   * @param contentProps 内容组件的props
   * @param modalProps Modal的props
   */
  propsChange?: (
    contentProps: Reactive<ContentProps<P>>,
    modalProps: Reactive<ModalProps>
  ) => void
  /**
   * @description 自定义监听对象
   */
  watcher?: {
    source: (
      contentProps: Reactive<ContentProps<P>>,
      modalProps: Reactive<ModalProps>
    ) => WatchSource | WatchSource[] | WatchEffect | object
    callback: (
      contentProps: Reactive<ContentProps<P>>,
      modalProps: Reactive<ModalProps>
    ) => void
    options?: WatchOptions
  }
}

/**
 * @description 获取所有父组件的provides
 */
function getProvides(instance: any) {
  let provides = instance?.provides || {}
  if (instance.parent) {
    provides = { ...getProvides(instance.parent), ...provides }
  }
  return provides
}

export default function useModal<P extends Component>(
  content: Component | string | VNode | AsyncComponentLoader<P>,
  config: UseModalConfig<P> = { defaultVisible: false }
): {
  /**
   * @description 打开状态
   */
  visible: Ref<boolean>
  /**
   * @description 加载状态
   */
  loading: Ref<boolean>
  /**
   * @description 打开抽屉,可在参数中传入props
   */
  open: (props: ContentProps<P>) => void
  /**
   * @description Modal的props
   */
  modalProps: Reactive<ModalProps>
  /**
   * @description 内容组件的props
   */
  contentProps: Reactive<ContentProps<P>>
} {
  const instance = getCurrentInstance() as any
  const appContext = instance?.appContext
  const container = shallowRef<HTMLElement>()

  const provides = getProvides(instance)

  const visible = ref<boolean>(!!config.defaultVisible)
  const loading = ref(false)

  const modalProps = reactive<ModalProps>(config?.modalProps || {})
  const contentProps = reactive<ContentProps<P>>(config?.contentProps || {})

  const open = (props: ContentProps<P>) => {
    Object.assign(contentProps, props)
    visible.value = true
  }

  // 创建组件vnode
  const modalVNode = createVNode({
    setup() {
      const contentRef = useTemplateRef<any>('contentRef')

      const instance = getCurrentInstance() as any
      if (instance) {
        instance.provides = { ...provides, ...instance.provides }
      }

      return () =>
        h(
          Modal,
          {
            destroyOnClose: true,
            ...modalProps,
            onOk(e) {
              modalProps?.onOk?.(e)
              contentRef.value?.onOk?.(e)
            },
            onCancel(e) {
              modalProps?.onCancel?.(e)
              contentRef.value?.onCancel?.(e)
            },
            'onUpdate:open'(val: boolean) {
              visible.value = val
            },
            open: visible.value,
            confirmLoading: loading.value,
          },
          () =>
            createVNode({
              setup() {
                return () => {
                  if (isString(content)) {
                    return h('div', null, content)
                  } else if (isVNode(content)) {
                    return content
                  } else {
                    const modalContent = isFunction(content)
                      ? defineAsyncComponent(content as AsyncComponentLoader<P>)
                      : content
                    return h(modalContent, {
                      ...contentProps,
                      ref: 'contentRef',
                      visible: visible.value,
                      'onUpdate:visible'(val?: boolean) {
                        visible.value = !!val
                      },
                      loading: loading.value,
                      'onUpdate:loading'(val?: boolean) {
                        loading.value = !!val
                      },
                    })
                  }
                }
              },
            })
        )
    },
  })

  // 将组件渲染到当前应用上下文
  const setupRender = () => {
    if (container.value) return

    container.value = document.createElement('div')

    if (appContext) {
      modalVNode.appContext = appContext
      render(modalVNode, container.value)
    }
  }

  watch(
    visible,
    () => {
      if (visible.value) {
        setupRender()
      } else {
        loading.value = false
      }
    },
    { immediate: true }
  )

  onDeactivated(() => {
    visible.value = false
  })

  onUnmounted(() => {
    visible.value = false
    if (container.value) {
      render(null, container.value)
      container.value.remove()
    }
  })

  if (config.propsChange) {
    watch([contentProps, modalProps], () => {
      config.propsChange!(contentProps, modalProps)
    })
  }

  if (config.watcher) {
    watch(
      config.watcher.source(contentProps, modalProps),
      () => config.watcher?.callback(contentProps, modalProps),
      config.watcher.options
    )
  }

  return {
    visible,
    loading,
    open,
    modalProps,
    contentProps,
  }
}