소스 검색

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>
ming4762 4 주 전
부모
커밋
afce9dc5c0

+ 1 - 2
docs/src/components/common-ui/vben-modal.md

@@ -60,7 +60,6 @@ Modal 内的内容一般业务中,会比较复杂,所以我们可以将 moda
 
 - `VbenModal` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenModal参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
 - 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenModal`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。
-- 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。
 - 如果弹窗的默认行为不符合你的预期,可以在`src\bootstrap.ts`中修改`setDefaultModalProps`的参数来设置默认的属性,如默认隐藏全屏按钮,修改默认ZIndex等。
 
 :::
@@ -84,7 +83,7 @@ const [Modal, modalApi] = useVbenModal({
 | --- | --- | --- | --- |
 | appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
 | connectedComponent | 连接另一个Modal组件 | `Component` | - |
-| destroyOnClose | 关闭时销毁`connectedComponent` | `boolean` | `false` |
+| destroyOnClose | 关闭时销毁 | `boolean` | `false` |
 | title | 标题 | `string\|slot` | - |
 | titleTooltip | 标题提示信息 | `string\|slot` | - |
 | description | 描述信息 | `string\|slot` | - |

+ 1 - 0
packages/@core/ui-kit/popup-ui/src/modal/modal-api.ts

@@ -44,6 +44,7 @@ export class ModalApi {
       confirmDisabled: false,
       confirmLoading: false,
       contentClass: '',
+      destroyOnClose: true,
       draggable: false,
       footer: true,
       footerClass: '',

+ 4 - 4
packages/@core/ui-kit/popup-ui/src/modal/modal.ts

@@ -60,6 +60,10 @@ export interface ModalProps {
    * 弹窗描述
    */
   description?: string;
+  /**
+   * 在关闭时销毁弹窗
+   */
+  destroyOnClose?: boolean;
   /**
    * 是否可拖拽
    * @default false
@@ -153,10 +157,6 @@ export interface ModalApiOptions extends ModalState {
    * 独立的弹窗组件
    */
   connectedComponent?: Component;
-  /**
-   * 在关闭时销毁弹窗。仅在使用 connectedComponent 时有效
-   */
-  destroyOnClose?: boolean;
   /**
    * 关闭前的回调,返回 false 可以阻止关闭
    * @returns

+ 21 - 2
packages/@core/ui-kit/popup-ui/src/modal/modal.vue

@@ -1,7 +1,7 @@
 <script lang="ts" setup>
 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 {
   useIsMobile,
@@ -34,6 +34,7 @@ interface Props extends ModalProps {
 
 const props = withDefaults(defineProps<Props>(), {
   appendToMain: false,
+  destroyOnClose: true,
   modalApi: undefined,
 });
 
@@ -67,6 +68,7 @@ const {
   confirmText,
   contentClass,
   description,
+  destroyOnClose,
   draggable,
   footer: showFooter,
   footerClass,
@@ -100,10 +102,15 @@ const { dragging, transform } = useModalDraggable(
   shouldDraggable,
 );
 
+const firstOpened = ref(false);
+const isClosed = ref(false);
+
 watch(
   () => state?.value?.isOpen,
   async (v) => {
     if (v) {
+      isClosed.value = false;
+      if (!firstOpened.value) firstOpened.value = true;
       await nextTick();
       if (!contentRef.value) return;
       const innerContentRef = contentRef.value.getContentRef();
@@ -113,6 +120,7 @@ watch(
       dialogRef.value.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
     }
   },
+  { immediate: true },
 );
 
 watch(
@@ -176,6 +184,15 @@ const getAppendTo = computed(() => {
     ? `#${ELEMENT_ID_MAIN_CONTENT}>div:not(.absolute)>div`
     : undefined;
 });
+
+const getForceMount = computed(() => {
+  return !unref(destroyOnClose);
+});
+
+function handleClosed() {
+  isClosed.value = true;
+  props.modalApi?.onClosed();
+}
 </script>
 <template>
   <Dialog
@@ -197,9 +214,11 @@ const getAppendTo = computed(() => {
               shouldFullscreen,
             'top-1/2 !-translate-y-1/2': centered && !shouldFullscreen,
             'duration-300': !dragging,
+            hidden: isClosed,
           },
         )
       "
+      :force-mount="getForceMount"
       :modal="modal"
       :open="state?.isOpen"
       :show-close="closable"
@@ -207,7 +226,7 @@ const getAppendTo = computed(() => {
       :overlay-blur="overlayBlur"
       close-class="top-3"
       @close-auto-focus="handleFocusOutside"
-      @closed="() => modalApi?.onClosed()"
+      @closed="handleClosed"
       :close-disabled="submitting"
       @escape-key-down="escapeKeyDown"
       @focus-outside="handleFocusOutside"

+ 2 - 24
packages/@core/ui-kit/popup-ui/src/modal/use-modal.ts

@@ -1,14 +1,6 @@
 import type { ExtendedModalApi, ModalApiOptions, ModalProps } from './modal';
 
-import {
-  defineComponent,
-  h,
-  inject,
-  nextTick,
-  provide,
-  reactive,
-  ref,
-} from 'vue';
+import { defineComponent, h, inject, nextTick, provide, reactive } from 'vue';
 
 import { useStore } from '@vben-core/shared/store';
 
@@ -32,7 +24,6 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
   const { connectedComponent } = options;
   if (connectedComponent) {
     const extendedApi = reactive({});
-    const isModalReady = ref(true);
     const Modal = defineComponent(
       (props: TParentModalProps, { attrs, slots }) => {
         provide(USER_MODAL_INJECT_KEY, {
@@ -42,11 +33,6 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
             Object.setPrototypeOf(extendedApi, api);
           },
           options,
-          async reCreateModal() {
-            isModalReady.value = false;
-            await nextTick();
-            isModalReady.value = true;
-          },
         });
         checkProps(extendedApi as ExtendedModalApi, {
           ...props,
@@ -55,7 +41,7 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
         });
         return () =>
           h(
-            isModalReady.value ? connectedComponent : 'div',
+            connectedComponent,
             {
               ...props,
               ...attrs,
@@ -84,14 +70,6 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
     injectData.options?.onOpenChange?.(isOpen);
   };
 
-  const onClosed = mergedOptions.onClosed;
-
-  mergedOptions.onClosed = () => {
-    onClosed?.();
-    if (mergedOptions.destroyOnClose) {
-      injectData.reCreateModal?.();
-    }
-  };
   const api = new ModalApi(mergedOptions);
 
   const extendedApi: ExtendedModalApi = api as never;

+ 7 - 4
playground/src/views/examples/modal/auto-height-demo.vue

@@ -16,15 +16,18 @@ const [Modal, modalApi] = useVbenModal({
   },
   onOpenChange(isOpen) {
     if (isOpen) {
-      handleUpdate(10);
+      handleUpdate();
     }
   },
 });
 
-function handleUpdate(len: number) {
+function handleUpdate(len?: number) {
   modalApi.setState({ confirmDisabled: true, loading: true });
   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 });
   }, 2000);
 }
@@ -40,7 +43,7 @@ function handleUpdate(len: number) {
       {{ item }}
     </div>
     <template #prepend-footer>
-      <Button type="link" @click="handleUpdate(6)">点击更新数据</Button>
+      <Button type="link" @click="handleUpdate()">点击更新数据</Button>
     </template>
   </Modal>
 </template>

+ 2 - 2
playground/src/views/examples/modal/in-content-demo.vue

@@ -24,7 +24,7 @@ const value = ref();
     title="基础弹窗示例"
     title-tooltip="标题提示内容"
   >
-    此弹窗指定在内容区域打开
-    <Input v-model="value" placeholder="KeepAlive测试" />
+    此弹窗指定在内容区域打开,并且在关闭之后弹窗内容不会被销毁
+    <Input v-model:value="value" placeholder="KeepAlive测试" />
   </Modal>
 </template>

+ 1 - 1
playground/src/views/examples/modal/index.vue

@@ -198,7 +198,7 @@ async function openPrompt() {
         </template>
       </Card>
 
-      <Card class="w-[300px]" title="指定容器">
+      <Card class="w-[300px]" title="指定容器+关闭后不销毁">
         <p>在内容区域打开弹窗的示例</p>
         <template #actions>
           <Button type="primary" @click="openInContentModal">打开弹窗</Button>