소스 검색

[Fix SEC-20260913] 子应用加载超时错误态与重试入口

lizuo 1 주 전
부모
커밋
2eb75d33b8

+ 54 - 0
src/components/vent/micro/MicroLoadError.vue

@@ -0,0 +1,54 @@
+<template>
+  <div class="micro-load-error">
+    <ExclamationCircleOutlined class="micro-load-error__icon" />
+    <div class="micro-load-error__text">{{ text }}</div>
+    <a-button type="primary" @click="onRetry">重试</a-button>
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
+
+  /**
+   * 子应用加载失败错误态(SEC-20260913):
+   * 微应用容器等待超时后展示,替代原先的"静默放行",并提供重试入口(emit retry 由容器重新挂载)。
+   * 四个微应用容器(ventModal/ventModal2D/needAir/ventDoc)共用,保持同构。
+   */
+  export default defineComponent({
+    name: 'MicroLoadError',
+    components: { ExclamationCircleOutlined },
+    props: {
+      text: { type: String, default: '子应用加载失败' },
+    },
+    emits: ['retry'],
+    setup(_props, { emit }) {
+      const onRetry = () => emit('retry');
+      return { onRetry };
+    },
+  });
+</script>
+<style lang="less" scoped>
+  .micro-load-error {
+    position: absolute;
+    top: 0;
+    left: 0;
+    z-index: 10;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    width: 100%;
+    height: 100%;
+    gap: 12px;
+
+    &__icon {
+      font-size: 40px;
+      color: #ff4d4f;
+    }
+
+    &__text {
+      color: #ff4d4f;
+      font-size: 14px;
+    }
+  }
+</style>

+ 33 - 6
src/components/vent/micro/needAir.vue

@@ -2,32 +2,59 @@
   <div style="position: absolute; width: 100%; height: 100%">
     <a-spin class="loading-box" size="large" :spinning="loading" tip="正在加载,请稍等。。。" />
   </div>
+  <MicroLoadError v-if="loadFailed" @retry="retryMount" />
   <div id="micro-need-air"></div>
 </template>
 <script lang="ts">
   import { onMounted, onBeforeUnmount, defineComponent, ref, unref } from 'vue';
   import { unmountMicroApps, mountMicroApp } from '/@/qiankun';
   import { resetMicroContentWH } from '/@/utils/domUtils';
+  import MicroLoadError from './MicroLoadError.vue';
+  import { useErrorLogStoreWithOut } from '/@/store/modules/errorLog';
+  import { ErrorTypeEnum } from '/@/enums/exceptionEnum';
   export default defineComponent({
     name: 'NeedAir',
+    components: { MicroLoadError },
     setup() {
       const loading = ref(true);
-      onMounted(() => {
+      // [SEC-20260913] 加载失败错误态:超时不再静默放行
+      const loadFailed = ref(false);
+      const startMount = () => {
         // [fix-subapp-loading] try/finally 保证就绪等待始终启动(同 ventModal)
         try {
           mountMicroApp('micro-need-air');
         } finally {
-          resetMicroContentWH('micro-need-air', () => {
-            loading.value = false;
-          });
+          resetMicroContentWH(
+            'micro-need-air',
+            () => {
+              loading.value = false;
+            },
+            () => {
+              loadFailed.value = true;
+              // 失败埋点:复用既有 errorLog store
+              useErrorLogStoreWithOut().addErrorLogInfo({
+                type: ErrorTypeEnum.RESOURCE,
+                file: 'components/vent/micro/needAir.vue',
+                name: 'MicroApp Load Timeout',
+                message: '子应用 micro-need-air 加载超时',
+                detail: 'resetMicroContentWH 等待容器内容超时',
+              });
+            }
+          );
         }
-      });
+      };
+      const retryMount = () => {
+        loadFailed.value = false;
+        loading.value = true;
+        startMount();
+      };
+      onMounted(startMount);
 
       onBeforeUnmount(async () => {
         await unmountMicroApps(['micro-need-air']);
       });
 
-      return { loading };
+      return { loading, loadFailed, retryMount };
     },
   });
 </script>

+ 33 - 6
src/components/vent/micro/ventDoc.vue

@@ -2,34 +2,61 @@
   <div style="position: absolute; width: 100%; height: 100%">
     <a-spin class="loading-box" size="large" :spinning="loading" tip="正在加载,请稍等。。。" />
   </div>
+  <MicroLoadError v-if="loadFailed" @retry="retryMount" />
   <div id="micro-vent-doc"></div>
 </template>
 <script lang="ts">
   import { ref, onMounted, onBeforeUnmount, defineComponent, unref } from 'vue';
   import { unmountMicroApps, mountMicroApp, activeApps } from '/@/qiankun';
   import { resetMicroContentWH } from '/@/utils/domUtils';
+  import MicroLoadError from './MicroLoadError.vue';
+  import { useErrorLogStoreWithOut } from '/@/store/modules/errorLog';
+  import { ErrorTypeEnum } from '/@/enums/exceptionEnum';
 
   export default defineComponent({
     name: 'VentDoc',
+    components: { MicroLoadError },
     setup() {
       const loading = ref(true);
-      onMounted(() => {
+      // [SEC-20260913] 加载失败错误态:超时不再静默放行
+      const loadFailed = ref(false);
+      const startMount = () => {
         // 判断子应用是否存在,不存在注册
         // [fix-subapp-loading] try/finally 保证就绪等待始终启动(同 ventModal)
         try {
           mountMicroApp('micro-vent-doc');
         } finally {
-          resetMicroContentWH('micro-vent-doc', () => {
-            loading.value = false;
-          });
+          resetMicroContentWH(
+            'micro-vent-doc',
+            () => {
+              loading.value = false;
+            },
+            () => {
+              loadFailed.value = true;
+              // 失败埋点:复用既有 errorLog store
+              useErrorLogStoreWithOut().addErrorLogInfo({
+                type: ErrorTypeEnum.RESOURCE,
+                file: 'components/vent/micro/ventDoc.vue',
+                name: 'MicroApp Load Timeout',
+                message: '子应用 micro-vent-doc 加载超时',
+                detail: 'resetMicroContentWH 等待容器内容超时',
+              });
+            }
+          );
         }
-      });
+      };
+      const retryMount = () => {
+        loadFailed.value = false;
+        loading.value = true;
+        startMount();
+      };
+      onMounted(startMount);
 
       onBeforeUnmount(async () => {
         await unmountMicroApps(['micro-vent-doc']);
       });
 
-      return { loading };
+      return { loading, loadFailed, retryMount };
     },
   });
 </script>

+ 33 - 6
src/components/vent/micro/ventModal.vue

@@ -2,35 +2,62 @@
   <div style="position: absolute; width: 100%; height: 100%" class="modal-bg">
     <a-spin class="loading-box" size="large" :spinning="loading" tip="正在加载,请稍等。。。" />
   </div>
+  <MicroLoadError v-if="loadFailed" @retry="retryMount" />
   <div id="micro-vent-3dModal"></div>
 </template>
 <script lang="ts">
   import { ref, onMounted, onBeforeUnmount, defineComponent } from 'vue';
   import { unmountMicroApps, mountMicroApp } from '/@/qiankun';
   import { resetMicroContentWH } from '/@/utils/domUtils';
+  import MicroLoadError from './MicroLoadError.vue';
+  import { useErrorLogStoreWithOut } from '/@/store/modules/errorLog';
+  import { ErrorTypeEnum } from '/@/enums/exceptionEnum';
 
   export default defineComponent({
     name: 'VentModel',
+    components: { MicroLoadError },
     setup() {
       const loading = ref(true);
-      onMounted(() => {
+      // [SEC-20260913] 加载失败错误态:超时不再静默放行
+      const loadFailed = ref(false);
+      const startMount = () => {
         // 判断子应用是否存在,不存在注册
         // [fix-subapp-loading] mountMicroApp 同步抛错会让后续 resetMicroContentWH 不再执行 → 遮罩永久卡死;
         // try/finally 保证就绪等待始终启动(配合 domUtils 的 60s 超时兜底)
         try {
           mountMicroApp('micro-vent-3dModal', true);
         } finally {
-          resetMicroContentWH('micro-vent-3dModal', () => {
-            loading.value = false;
-          });
+          resetMicroContentWH(
+            'micro-vent-3dModal',
+            () => {
+              loading.value = false;
+            },
+            () => {
+              loadFailed.value = true;
+              // 失败埋点:复用既有 errorLog store
+              useErrorLogStoreWithOut().addErrorLogInfo({
+                type: ErrorTypeEnum.RESOURCE,
+                file: 'components/vent/micro/ventModal.vue',
+                name: 'MicroApp Load Timeout',
+                message: '子应用 micro-vent-3dModal 加载超时',
+                detail: 'resetMicroContentWH 等待容器内容超时',
+              });
+            }
+          );
         }
-      });
+      };
+      const retryMount = () => {
+        loadFailed.value = false;
+        loading.value = true;
+        startMount();
+      };
+      onMounted(startMount);
 
       onBeforeUnmount(async () => {
         await unmountMicroApps(['micro-vent-3dModal']);
       });
 
-      return { loading };
+      return { loading, loadFailed, retryMount };
     },
   });
 </script>

+ 33 - 6
src/components/vent/micro/ventModal2D.vue

@@ -2,32 +2,59 @@
   <div style="position: absolute; width: 100%; height: 100%" class="modal-bg">
     <a-spin class="loading-box" size="large" :spinning="loading" tip="正在加载,请稍等。。。" />
   </div>
+  <MicroLoadError v-if="loadFailed" @retry="retryMount" />
   <div id="micro-vent-2dModal"></div>
 </template>
 <script lang="ts">
   import { onMounted, onBeforeUnmount, defineComponent, ref, unref, nextTick } from 'vue';
   import { unmountMicroApps, mountMicroApp } from '/@/qiankun';
   import { resetMicroContentWH } from '/@/utils/domUtils';
+  import MicroLoadError from './MicroLoadError.vue';
+  import { useErrorLogStoreWithOut } from '/@/store/modules/errorLog';
+  import { ErrorTypeEnum } from '/@/enums/exceptionEnum';
   export default defineComponent({
     name: 'Vent2dModal',
+    components: { MicroLoadError },
     setup() {
       const loading = ref(true);
-      onMounted(() => {
+      // [SEC-20260913] 加载失败错误态:超时不再静默放行
+      const loadFailed = ref(false);
+      const startMount = () => {
         // [fix-subapp-loading] try/finally 保证就绪等待始终启动(同 ventModal)
         try {
           mountMicroApp('micro-vent-2dModal');
         } finally {
-          resetMicroContentWH('micro-vent-2dModal', () => {
-            loading.value = false;
-          });
+          resetMicroContentWH(
+            'micro-vent-2dModal',
+            () => {
+              loading.value = false;
+            },
+            () => {
+              loadFailed.value = true;
+              // 失败埋点:复用既有 errorLog store
+              useErrorLogStoreWithOut().addErrorLogInfo({
+                type: ErrorTypeEnum.RESOURCE,
+                file: 'components/vent/micro/ventModal2D.vue',
+                name: 'MicroApp Load Timeout',
+                message: '子应用 micro-vent-2dModal 加载超时',
+                detail: 'resetMicroContentWH 等待容器内容超时',
+              });
+            }
+          );
         }
-      });
+      };
+      const retryMount = () => {
+        loadFailed.value = false;
+        loading.value = true;
+        startMount();
+      };
+      onMounted(startMount);
 
       onBeforeUnmount(async () => {
         await unmountMicroApps(['micro-vent-2dModal']);
       });
 
-      return { loading };
+      return { loading, loadFailed, retryMount };
     },
   });
 </script>

+ 3 - 1
src/utils/domUtils.ts

@@ -171,10 +171,11 @@ export function useRafThrottle<T extends FunctionArgs>(fn: T): T {
   };
 }
 
-export function resetMicroContentWH(domId, callBack?: Function, maxWaitMs = 60000) {
+export function resetMicroContentWH(domId, callBack?: Function, onTimeout?: Function, maxWaitMs = 60000) {
   // [fix-subapp-loading] 增加超时兜底:原实现每秒无限轮询等待微应用容器出现内容,
   // 微应用挂载失败/被卸载时容器永远为空 → 加载遮罩永久转圈(整页"正在加载"卡死,见 docs/17 §8)。
   // 超时后强制放行(隐藏遮罩),把"整页永久卡死"降级为"页面可见但内容可能缺失"。
+  // [SEC-20260913] 增加 onTimeout 回调:超时不再静默放行,由容器进入错误态 UI 并提供重试。
   const start = Date.now();
   let done = false;
   const poll = () => {
@@ -188,6 +189,7 @@ export function resetMicroContentWH(domId, callBack?: Function, maxWaitMs = 6000
     } else if (Date.now() - start >= maxWaitMs) {
       done = true;
       console.error('[fix-subapp-loading] 微应用容器等待超时,强制放行:', domId);
+      if (onTimeout) onTimeout();
       if (callBack) callBack();
     } else {
       setTimeout(poll, 1000);