perf: improve destroyOnClose
for VbenModal
(#5935)
* perf: 优化Vben Modal destroyOnClose,解决destroyOnClose=false,Modal依旧会被销毁的问题 影响范围(重要):destroyOnClose默认为true,这会导致所有的modal都会默认渲染到body radix-vue Dialog组件默认会销毁挂载的组件,所以即使destroyOnClose=false,Modal依旧会被销毁的问题 对于一些大表单重复渲染导致卡顿,ApiComponent也会频繁的加载数据 * fix: modal closing animation --------- Co-authored-by: Netfan <netfan@foxmail.com>
This commit is contained in:
parent
b5700bd0b1
commit
afce9dc5c0
@ -60,7 +60,6 @@ Modal 内的内容一般业务中,会比较复杂,所以我们可以将 moda
|
|||||||
|
|
||||||
- `VbenModal` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenModal参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
|
- `VbenModal` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenModal参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
|
||||||
- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenModal`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。
|
- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenModal`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。
|
||||||
- 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。
|
|
||||||
- 如果弹窗的默认行为不符合你的预期,可以在`src\bootstrap.ts`中修改`setDefaultModalProps`的参数来设置默认的属性,如默认隐藏全屏按钮,修改默认ZIndex等。
|
- 如果弹窗的默认行为不符合你的预期,可以在`src\bootstrap.ts`中修改`setDefaultModalProps`的参数来设置默认的属性,如默认隐藏全屏按钮,修改默认ZIndex等。
|
||||||
|
|
||||||
:::
|
:::
|
||||||
@ -84,7 +83,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
|
| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
|
||||||
| connectedComponent | 连接另一个Modal组件 | `Component` | - |
|
| connectedComponent | 连接另一个Modal组件 | `Component` | - |
|
||||||
| destroyOnClose | 关闭时销毁`connectedComponent` | `boolean` | `false` |
|
| destroyOnClose | 关闭时销毁 | `boolean` | `false` |
|
||||||
| title | 标题 | `string\|slot` | - |
|
| title | 标题 | `string\|slot` | - |
|
||||||
| titleTooltip | 标题提示信息 | `string\|slot` | - |
|
| titleTooltip | 标题提示信息 | `string\|slot` | - |
|
||||||
| description | 描述信息 | `string\|slot` | - |
|
| description | 描述信息 | `string\|slot` | - |
|
||||||
|
@ -44,6 +44,7 @@ export class ModalApi {
|
|||||||
confirmDisabled: false,
|
confirmDisabled: false,
|
||||||
confirmLoading: false,
|
confirmLoading: false,
|
||||||
contentClass: '',
|
contentClass: '',
|
||||||
|
destroyOnClose: true,
|
||||||
draggable: false,
|
draggable: false,
|
||||||
footer: true,
|
footer: true,
|
||||||
footerClass: '',
|
footerClass: '',
|
||||||
|
@ -60,6 +60,10 @@ export interface ModalProps {
|
|||||||
* 弹窗描述
|
* 弹窗描述
|
||||||
*/
|
*/
|
||||||
description?: string;
|
description?: string;
|
||||||
|
/**
|
||||||
|
* 在关闭时销毁弹窗
|
||||||
|
*/
|
||||||
|
destroyOnClose?: boolean;
|
||||||
/**
|
/**
|
||||||
* 是否可拖拽
|
* 是否可拖拽
|
||||||
* @default false
|
* @default false
|
||||||
@ -153,10 +157,6 @@ export interface ModalApiOptions extends ModalState {
|
|||||||
* 独立的弹窗组件
|
* 独立的弹窗组件
|
||||||
*/
|
*/
|
||||||
connectedComponent?: Component;
|
connectedComponent?: Component;
|
||||||
/**
|
|
||||||
* 在关闭时销毁弹窗。仅在使用 connectedComponent 时有效
|
|
||||||
*/
|
|
||||||
destroyOnClose?: boolean;
|
|
||||||
/**
|
/**
|
||||||
* 关闭前的回调,返回 false 可以阻止关闭
|
* 关闭前的回调,返回 false 可以阻止关闭
|
||||||
* @returns
|
* @returns
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { ExtendedModalApi, ModalProps } from './modal';
|
import type { ExtendedModalApi, ModalProps } from './modal';
|
||||||
|
|
||||||
import { computed, nextTick, provide, ref, useId, watch } from 'vue';
|
import { computed, nextTick, provide, ref, unref, useId, watch } from 'vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useIsMobile,
|
useIsMobile,
|
||||||
@ -34,6 +34,7 @@ interface Props extends ModalProps {
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
appendToMain: false,
|
appendToMain: false,
|
||||||
|
destroyOnClose: true,
|
||||||
modalApi: undefined,
|
modalApi: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -67,6 +68,7 @@ const {
|
|||||||
confirmText,
|
confirmText,
|
||||||
contentClass,
|
contentClass,
|
||||||
description,
|
description,
|
||||||
|
destroyOnClose,
|
||||||
draggable,
|
draggable,
|
||||||
footer: showFooter,
|
footer: showFooter,
|
||||||
footerClass,
|
footerClass,
|
||||||
@ -100,10 +102,15 @@ const { dragging, transform } = useModalDraggable(
|
|||||||
shouldDraggable,
|
shouldDraggable,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const firstOpened = ref(false);
|
||||||
|
const isClosed = ref(false);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => state?.value?.isOpen,
|
() => state?.value?.isOpen,
|
||||||
async (v) => {
|
async (v) => {
|
||||||
if (v) {
|
if (v) {
|
||||||
|
isClosed.value = false;
|
||||||
|
if (!firstOpened.value) firstOpened.value = true;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
if (!contentRef.value) return;
|
if (!contentRef.value) return;
|
||||||
const innerContentRef = contentRef.value.getContentRef();
|
const innerContentRef = contentRef.value.getContentRef();
|
||||||
@ -113,6 +120,7 @@ watch(
|
|||||||
dialogRef.value.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
|
dialogRef.value.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@ -176,6 +184,15 @@ const getAppendTo = computed(() => {
|
|||||||
? `#${ELEMENT_ID_MAIN_CONTENT}>div:not(.absolute)>div`
|
? `#${ELEMENT_ID_MAIN_CONTENT}>div:not(.absolute)>div`
|
||||||
: undefined;
|
: undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getForceMount = computed(() => {
|
||||||
|
return !unref(destroyOnClose);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleClosed() {
|
||||||
|
isClosed.value = true;
|
||||||
|
props.modalApi?.onClosed();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<Dialog
|
<Dialog
|
||||||
@ -197,9 +214,11 @@ const getAppendTo = computed(() => {
|
|||||||
shouldFullscreen,
|
shouldFullscreen,
|
||||||
'top-1/2 !-translate-y-1/2': centered && !shouldFullscreen,
|
'top-1/2 !-translate-y-1/2': centered && !shouldFullscreen,
|
||||||
'duration-300': !dragging,
|
'duration-300': !dragging,
|
||||||
|
hidden: isClosed,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
:force-mount="getForceMount"
|
||||||
:modal="modal"
|
:modal="modal"
|
||||||
:open="state?.isOpen"
|
:open="state?.isOpen"
|
||||||
:show-close="closable"
|
:show-close="closable"
|
||||||
@ -207,7 +226,7 @@ const getAppendTo = computed(() => {
|
|||||||
:overlay-blur="overlayBlur"
|
:overlay-blur="overlayBlur"
|
||||||
close-class="top-3"
|
close-class="top-3"
|
||||||
@close-auto-focus="handleFocusOutside"
|
@close-auto-focus="handleFocusOutside"
|
||||||
@closed="() => modalApi?.onClosed()"
|
@closed="handleClosed"
|
||||||
:close-disabled="submitting"
|
:close-disabled="submitting"
|
||||||
@escape-key-down="escapeKeyDown"
|
@escape-key-down="escapeKeyDown"
|
||||||
@focus-outside="handleFocusOutside"
|
@focus-outside="handleFocusOutside"
|
||||||
|
@ -1,14 +1,6 @@
|
|||||||
import type { ExtendedModalApi, ModalApiOptions, ModalProps } from './modal';
|
import type { ExtendedModalApi, ModalApiOptions, ModalProps } from './modal';
|
||||||
|
|
||||||
import {
|
import { defineComponent, h, inject, nextTick, provide, reactive } from 'vue';
|
||||||
defineComponent,
|
|
||||||
h,
|
|
||||||
inject,
|
|
||||||
nextTick,
|
|
||||||
provide,
|
|
||||||
reactive,
|
|
||||||
ref,
|
|
||||||
} from 'vue';
|
|
||||||
|
|
||||||
import { useStore } from '@vben-core/shared/store';
|
import { useStore } from '@vben-core/shared/store';
|
||||||
|
|
||||||
@ -32,7 +24,6 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
|||||||
const { connectedComponent } = options;
|
const { connectedComponent } = options;
|
||||||
if (connectedComponent) {
|
if (connectedComponent) {
|
||||||
const extendedApi = reactive({});
|
const extendedApi = reactive({});
|
||||||
const isModalReady = ref(true);
|
|
||||||
const Modal = defineComponent(
|
const Modal = defineComponent(
|
||||||
(props: TParentModalProps, { attrs, slots }) => {
|
(props: TParentModalProps, { attrs, slots }) => {
|
||||||
provide(USER_MODAL_INJECT_KEY, {
|
provide(USER_MODAL_INJECT_KEY, {
|
||||||
@ -42,11 +33,6 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
|||||||
Object.setPrototypeOf(extendedApi, api);
|
Object.setPrototypeOf(extendedApi, api);
|
||||||
},
|
},
|
||||||
options,
|
options,
|
||||||
async reCreateModal() {
|
|
||||||
isModalReady.value = false;
|
|
||||||
await nextTick();
|
|
||||||
isModalReady.value = true;
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
checkProps(extendedApi as ExtendedModalApi, {
|
checkProps(extendedApi as ExtendedModalApi, {
|
||||||
...props,
|
...props,
|
||||||
@ -55,7 +41,7 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
|||||||
});
|
});
|
||||||
return () =>
|
return () =>
|
||||||
h(
|
h(
|
||||||
isModalReady.value ? connectedComponent : 'div',
|
connectedComponent,
|
||||||
{
|
{
|
||||||
...props,
|
...props,
|
||||||
...attrs,
|
...attrs,
|
||||||
@ -84,14 +70,6 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
|||||||
injectData.options?.onOpenChange?.(isOpen);
|
injectData.options?.onOpenChange?.(isOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onClosed = mergedOptions.onClosed;
|
|
||||||
|
|
||||||
mergedOptions.onClosed = () => {
|
|
||||||
onClosed?.();
|
|
||||||
if (mergedOptions.destroyOnClose) {
|
|
||||||
injectData.reCreateModal?.();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const api = new ModalApi(mergedOptions);
|
const api = new ModalApi(mergedOptions);
|
||||||
|
|
||||||
const extendedApi: ExtendedModalApi = api as never;
|
const extendedApi: ExtendedModalApi = api as never;
|
||||||
|
@ -16,15 +16,18 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
},
|
},
|
||||||
onOpenChange(isOpen) {
|
onOpenChange(isOpen) {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
handleUpdate(10);
|
handleUpdate();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleUpdate(len: number) {
|
function handleUpdate(len?: number) {
|
||||||
modalApi.setState({ confirmDisabled: true, loading: true });
|
modalApi.setState({ confirmDisabled: true, loading: true });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
list.value = Array.from({ length: len }, (_v, k) => k + 1);
|
list.value = Array.from(
|
||||||
|
{ length: len ?? Math.floor(Math.random() * 10) + 1 },
|
||||||
|
(_v, k) => k + 1,
|
||||||
|
);
|
||||||
modalApi.setState({ confirmDisabled: false, loading: false });
|
modalApi.setState({ confirmDisabled: false, loading: false });
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
@ -40,7 +43,7 @@ function handleUpdate(len: number) {
|
|||||||
{{ item }}
|
{{ item }}
|
||||||
</div>
|
</div>
|
||||||
<template #prepend-footer>
|
<template #prepend-footer>
|
||||||
<Button type="link" @click="handleUpdate(6)">点击更新数据</Button>
|
<Button type="link" @click="handleUpdate()">点击更新数据</Button>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
@ -24,7 +24,7 @@ const value = ref();
|
|||||||
title="基础弹窗示例"
|
title="基础弹窗示例"
|
||||||
title-tooltip="标题提示内容"
|
title-tooltip="标题提示内容"
|
||||||
>
|
>
|
||||||
此弹窗指定在内容区域打开
|
此弹窗指定在内容区域打开,并且在关闭之后弹窗内容不会被销毁
|
||||||
<Input v-model="value" placeholder="KeepAlive测试" />
|
<Input v-model:value="value" placeholder="KeepAlive测试" />
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
@ -198,7 +198,7 @@ async function openPrompt() {
|
|||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card class="w-[300px]" title="指定容器">
|
<Card class="w-[300px]" title="指定容器+关闭后不销毁">
|
||||||
<p>在内容区域打开弹窗的示例</p>
|
<p>在内容区域打开弹窗的示例</p>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<Button type="primary" @click="openInContentModal">打开弹窗</Button>
|
<Button type="primary" @click="openInContentModal">打开弹窗</Button>
|
||||||
|
Loading…
Reference in New Issue
Block a user