优化项
全屏/退出全屏
<template>
<teleport :to="getContainer()">
<ProConfigProvider>
<div class="draggable-modal" :class="{ fullscreen: fullscreenModel }">
<Modal>
<template #closeIcon>
<slot name="closeIcon">
<Space class="ant-modal-operate" @click.stop>
<FullscreenOutlined v-if="!fullscreenModel" @click="fullscreenModel = true" />
<FullscreenExitOutlined v-else @click="restore" />
</Space>
</slot>
</template>
</Modal>
</div>
</ProConfigProvider>
</teleport>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { modalProps } from 'ant-design-vue/es/modal/Modal';
import { FullscreenOutlined, FullscreenExitOutlined } from '@ant-design/icons-vue';
import { Modal, Space } from 'ant-design-vue';
const props = defineProps({
...modalProps(),
fullscreen: {
type: Boolean,
default: false,
},
getContainer: {
type: Function,
default: () => document.body,
},
});
const fullscreenModel = ref(props.fullscreen);
const modalWrapRef = ref<HTMLDivElement>();
const centerModal = async () => {
await nextTick();
const modalEl = modalWrapRef.value?.querySelector<HTMLDivElement>('.ant-modal');
if (modalEl && modalEl.getBoundingClientRect().left < 1) {
modalEl.style.left = `${(document.documentElement.clientWidth - modalEl.offsetWidth) / 2}px`;
}
};
const restore = async () => {
fullscreenModel.value = false;
centerModal();
};
</script>
<style lang="less">
.draggable-modal {
&.fullscreen {
.ant-modal {
inset: 0 !important;
width: 100% !important;
max-width: 100vw !important;
height: 100% !important;
}
.ant-modal-content {
width: 100% !important;
height: 100% !important;
}
}
}
</style>
提供标题、内容、底部操作、关闭图标的插槽
<template>
<teleport :to="getContainer()">
<ProConfigProvider>
<div>
<Modal>
<template #title>
<slot name="title">{{ $attrs.title || '标题' }}</slot>
</template>
<template #closeIcon>
<slot name="closeIcon">
<Space class="ant-modal-operate" @click.stop>
<CloseOutlined @click="closeModal" />
</Space>
</slot>
</template>
<slot>内容</slot>
<template v-if="$slots.footer" #footer>
<slot name="footer"></slot>
</template>
</Modal>
</div>
</ProConfigProvider>
</teleport>
</template>
<script lang="ts" setup>
const fullscreenModel = ref(props.fullscreen);
const modalWrapRef = ref<HTMLDivElement>();
const centerModal = async () => {
await nextTick();
const modalEl = modalWrapRef.value?.querySelector<HTMLDivElement>('.ant-modal');
if (modalEl && modalEl.getBoundingClientRect().left < 1) {
modalEl.style.left = `${(document.documentElement.clientWidth - modalEl.offsetWidth) / 2}px`;
}
};
const restore = async () => {
fullscreenModel.value = false;
centerModal();
};
</script>
可拖拽
<script lang="ts" setup>
import { useVModel } from '@vueuse/core';
let inited = false;
const visibleModel = useVModel(props, 'open');
const registerDragTitle = (dragEl: HTMLDivElement, handleEl: HTMLDivElement) => {
handleEl.style.cursor = 'move';
handleEl.onmousedown = throttle((e: MouseEvent) => {
document.body.style.userSelect = 'none';
const disX = e.clientX - dragEl.getBoundingClientRect().left;
const disY = e.clientY - dragEl.getBoundingClientRect().top;
const mousemove = (event: MouseEvent) => {
let iL = event.clientX - disX;
let iT = event.clientY - disY;
const maxL = document.documentElement.clientWidth - dragEl.offsetWidth;
const maxT = document.documentElement.clientHeight - dragEl.offsetHeight;
iL <= 0 && (iL = 0);
iT <= 0 && (iT = 0);
iL >= maxL && (iL = maxL);
iT >= maxT && (iT = maxT);
dragEl.style.left = `${iL}px`;
dragEl.style.top = `${iT}px`;
};
const mouseup = () => {
document.removeEventListener('mousemove', mousemove);
document.removeEventListener('mouseup', mouseup);
document.body.style.userSelect = 'auto';
};
document.addEventListener('mousemove', mousemove);
document.addEventListener('mouseup', mouseup);
}, 20);
};
const initDrag = async () => {
await nextTick();
const modalWrapRefEl = modalWrapRef.value!;
const modalWrapEl = modalWrapRefEl.querySelector<HTMLDivElement>('.ant-modal-wrap');
const modalEl = modalWrapRefEl.querySelector<HTMLDivElement>('.ant-modal');
if (modalWrapEl && modalEl) {
centerModal();
const headerEl = modalEl.querySelector<HTMLDivElement>('.ant-modal-header');
headerEl && registerDragTitle(modalEl, headerEl);
}
inited = true;
};
watch(visibleModel, async (val) => {
if ((val && Object.is(inited, false)) || props.destroyOnClose) {
initDrag();
}
});
</script>
简易实现
- 透传props时,过滤掉双向绑定属性(
open
、onUpdate:open
)
<template>
<teleport :to="getContainer()">
<ProConfigProvider>
<div ref="modalWrapRef" class="draggable-modal">
<Modal
v-bind="omit(props, ['open', 'onCancel', 'onOk', 'onUpdate:open'])"
v-model:open="visibleModel"
:mask-closable="false"
:get-container="() => modalWrapRef"
:width="innerWidth || width"
@ok="emit('ok')"
@cancel="emit('cancel')"
>
<template #title>
<slot name="title">{{ $attrs.title || '标题' }}</slot>
</template>
<slot></slot>
</Modal>
</div>
</ProConfigProvider>
</teleport>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { modalProps } from 'ant-design-vue/es/modal/Modal';
import { useVModel } from '@vueuse/core';
import { omit } from 'lodash-es';
import { Modal } from 'ant-design-vue';
const props = defineProps({
...modalProps(),
fullscreen: {
type: Boolean,
default: false,
},
getContainer: {
type: Function,
default: () => document.body,
},
});
const emit = defineEmits(['update:open', 'update:fullscreen', 'ok', 'cancel']);
const route = useRoute();
const visibleModel = useVModel(props, 'open');
const innerWidth = ref('');
const modalWrapRef = ref<HTMLDivElement>();
const closeModal = () => {
visibleModel.value = false;
emit('cancel');
};
watch(() => route.fullPath, closeModal);
</script>
useModal组件方式
<template>
<a-button type="primary" @click="handleOpenUseModal">useModal组件方式</a-button>
<UseModalComp></UseModalComp>
</template>
<script setup lang="tsx">
import { useModal } from '@/hooks/useModal';
const [UseModalComp] = useModal();
const handleOpenUseModal = () => {
UseModalComp.show({
title: '我是UseModalComp',
content: '嘿嘿嘿',
});
};
</script>
hook纯函数式
<template>
<a-button type="primary" @click="handleOpenHookModal">hook纯函数式</a-button>
</template>
<script setup lang="tsx">
import { useModal } from '@/hooks/useModal';
const [fnModal] = useModal();
const handleOpenHookModal = () => {
fnModal.show({
title: '我是hook纯函数式模态框',
content: 'hello',
});
};
</script>
useModal
import { createVNode, ref, render, getCurrentInstance, nextTick } from 'vue';
import { MyModal, type MyModalInstance } from './modal';
import type { App, ComponentInternalInstance, FunctionalComponent } from 'vue';
import type { HookModalProps } from './types';
let _app: App;
export const useModal = () => {
let _modalInstance: ComponentInternalInstance;
const modalRef = ref<MyModalInstance>();
const appContext = _app?._context || getCurrentInstance()?.appContext;
const isAppChild = ref(false);
const getModalInstance = async () => {
await nextTick();
if (isAppChild.value && modalRef.value) {
return modalRef.value;
}
if (_modalInstance) {
return _modalInstance;
}
const container = document.createElement('div');
const vnode = createVNode(MyModal);
vnode.appContext = appContext;
render(vnode, container);
_modalInstance = vnode.component!;
_modalInstance.props.closeModal = hide;
return _modalInstance;
};
const setProps = async (_props) => {
const instance = await getModalInstance();
if (Object.is(instance, modalRef.value)) {
console.log('useModal组件方式');
instance?.setProps?.(_props);
} else {
console.log('hook纯函数式');
instance?.exposed?.setProps?.(_props);
}
};
const hide = () => {
setProps({ open: false });
};
const show = async (props: HookModalProps) => {
setProps({
...props,
closeModal: hide,
open: true,
});
await nextTick();
};
interface ModalRenderComp<T> extends FunctionalComponent<T> {
show: typeof show;
hide: typeof hide;
setProps: typeof setProps;
}
const ModalRender: ModalRenderComp<HookModalProps> = (props, { attrs, slots }) => {
isAppChild.value = true;
return <MyModal ref={modalRef} {...{ ...attrs, ...props }} v-slots={slots} />;
};
ModalRender.show = show;
ModalRender.hide = hide;
ModalRender.setProps = setProps;
return [ModalRender, modalRef] as const;
};
export type ModalInstance = ReturnType<typeof useModal>;
export const installUseModal = (app: App) => {
_app = app;
};
modal
import { defineComponent, watch, ref, computed, unref } from 'vue';
import { omit } from 'lodash-es';
import type { HookModalProps } from './types';
import { isFunction } from '@/utils/is';
import { DraggableModal } from '@/components/core/draggable-modal';
export type MyModalInstance = InstanceType<typeof MyModal>;
export const MyModal = defineComponent({
props: {
content: {
type: [String, Function] as PropType<string | JSX.Element | (() => JSX.Element)>,
},
closeModal: Function,
open: Boolean,
},
setup(props, { attrs, expose }) {
const confirmLoading = ref<boolean>(false);
const propsRef = ref({ ...attrs, ...props });
const getProps = computed(() => {
return { ...attrs, ...props, ...unref(propsRef) };
});
const bindValues = computed(() => {
const _props = unref(getProps);
return {
...omit(_props, ['onCancel', 'onOk', 'closeModal', 'content']),
open: _props.open,
confirmLoading: confirmLoading.value,
onCancel: handleCancel,
onOk: handleConfirm,
};
});
const setVisible = (open: boolean) => {
propsRef.value.open = open;
};
const setProps = (props: HookModalProps) => {
propsRef.value = {
...unref(getProps),
...props,
};
};
watch(
() => propsRef.value.open,
(val) => {
Object.is(val, false) && props.closeModal?.();
},
);
const handleConfirm = async (e: MouseEvent) => {
confirmLoading.value = true;
try {
await unref(getProps)?.onOk?.(e);
setVisible(false);
} catch (error) {
return Promise.reject(error);
} finally {
confirmLoading.value = false;
}
};
const handleCancel = async (e: MouseEvent) => {
await unref(getProps)?.onCancel?.(e);
setVisible(false);
};
expose({
setProps,
});
return () => {
const _props = unref(getProps);
const { content } = _props;
const Content = isFunction(content) ? content() : content;
return <DraggableModal {...unref(bindValues)}>{Content}</DraggableModal>;
};
},
});