基础用法
<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'),
{
modalProps: {
width: 400,
destroyOnClose: true,
onOk() {
console.log('点击了ok')
},
},
contentProps: {
gsId: '9527',
onSuccess(val: any) {
console.log(val)
},
},
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
// 内置双向绑定属性
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'
type ContentProps<P> = Partial<ExtractPropTypes<P>>
interface UseModalConfig<P extends Component> {
modalProps?: ModalProps
contentProps?: ContentProps<P>
defaultVisible?: boolean
propsChange?: (
contentProps: Reactive<ContentProps<P>>,
modalProps: Reactive<ModalProps>
) => void
watcher?: {
source: (
contentProps: Reactive<ContentProps<P>>,
modalProps: Reactive<ModalProps>
) => WatchSource | WatchSource[] | WatchEffect | object
callback: (
contentProps: Reactive<ContentProps<P>>,
modalProps: Reactive<ModalProps>
) => void
options?: WatchOptions
}
}
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 }
): {
visible: Ref<boolean>
loading: Ref<boolean>
open: (props: ContentProps<P>) => void
modalProps: Reactive<ModalProps>
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
}
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,
}
}