From 966c7b4ef0134c12cfd4c1a3ed6c21a062d52a23 Mon Sep 17 00:00:00 2001 From: kuaifan Date: Thu, 16 Jul 2026 06:26:39 +0000 Subject: [PATCH] =?UTF-8?q?feat(microapp):=20iframe=20=E5=90=8C=E6=BA=90?= =?UTF-8?q?=20HTTP=205xx=20=E6=A3=80=E6=B5=8B=E4=B8=8E=E5=86=85=E8=81=94?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E5=B1=82=EF=BC=8C=E9=94=99=E8=AF=AF=E6=80=81?= =?UTF-8?q?=E5=BC=BA=E5=88=B6=E6=98=BE=E7=A4=BA=E8=83=B6=E5=9B=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - iframe.vue 读 responseStatus 检测同源 5xx(宁可漏判不误判),与原生 error 统一走 load-error - index.vue 新增可读的内联错误层(重试/关闭),覆盖深色下被反色成一条线的错误页 - 检测到错误时强制显示胶囊,保证关闭/更多入口 --- language/original-web.txt | 1 + .../assets/js/components/MicroApps/iframe.vue | 30 ++++++- .../assets/js/components/MicroApps/index.vue | 78 ++++++++++++++++++- .../assets/js/components/MicroApps/modal.vue | 7 +- 4 files changed, 110 insertions(+), 6 deletions(-) diff --git a/language/original-web.txt b/language/original-web.txt index 7d4e8e7d6..fd92d1d89 100644 --- a/language/original-web.txt +++ b/language/original-web.txt @@ -2531,3 +2531,4 @@ AI任务分析 共享文件 我共享的 共享给我的 +服务器返回错误(HTTP (*)) diff --git a/resources/assets/js/components/MicroApps/iframe.vue b/resources/assets/js/components/MicroApps/iframe.vue index 7e8abb303..9430acce0 100644 --- a/resources/assets/js/components/MicroApps/iframe.vue +++ b/resources/assets/js/components/MicroApps/iframe.vue @@ -142,14 +142,36 @@ export default { } }) } + + // HTTP 5xx 错误页浏览器视为加载成功、不触发原生 error,load 后主动检测 + this.detectHttpError() }, - // 处理 iframe 加载错误 - handleError(e) { - this.$emit('error', { + // 读同源导航状态码判断加载失败。只认 5xx(服务端/网关故障,必是错误页,不误判); + // 排除 4xx(soft-404、登录页等状态码错但页面可用);跨域读不到则放弃。 + detectHttpError() { + let nav + try { + nav = this.$refs.iframe.contentWindow.performance.getEntriesByType('navigation')[0] + } catch (e) { + return // 跨域,无法检测 + } + if (nav && nav.responseStatus >= 500) { + this.$emit('load-error', { + detail: { + name: this.name, + status: nav.responseStatus, + } + }) + } + }, + + // 处理 iframe 原生加载错误(无 HTTP 状态码,status 记 0),与 5xx 统一走 load-error + handleError() { + this.$emit('load-error', { detail: { name: this.name, - error: e, + status: 0, } }) }, diff --git a/resources/assets/js/components/MicroApps/index.vue b/resources/assets/js/components/MicroApps/index.vue index fcb7586eb..d1f4d825b 100644 --- a/resources/assets/js/components/MicroApps/index.vue +++ b/resources/assets/js/components/MicroApps/index.vue @@ -8,18 +8,20 @@ :size="1200" :options="app" :windowType="windowType" + :forceCapsuleVisible="loadErrors[app.name] != null" :beforeClose="onBeforeClose" @on-capsule-more="onCapsuleMore" @on-popout-window="onPopoutWindow" @on-confirm-close="closeMicroApp"> + @load-error="onLoadError"/> + + + +
+
+
{{ $L('应用加载失败') }}
+
{{ $L('服务器返回错误(HTTP (*))', loadErrors[app.name]) }}
+
+ + +
+
+
+
@@ -80,6 +96,46 @@ } } +// 错误层:浅色卡片,深色模式由全局反色引擎自动转深 +.micro-app-error { + position: absolute; + // z-index 0:靠 DOM 顺序盖住 iframe,低层级让胶囊/cmask 自然在其之上 + z-index: 0; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + background-color: #ffffff; + + .micro-app-error-box { + max-width: 360px; + text-align: center; + } + + .micro-app-error-title { + font-size: 16px; + font-weight: 600; + color: #17233d; + } + + .micro-app-error-desc { + margin-top: 8px; + font-size: 13px; + color: #808695; + } + + .micro-app-error-actions { + margin-top: 20px; + display: flex; + gap: 12px; + justify-content: center; + } +} + .micro-app-assist { width: 0; height: 0; @@ -125,6 +181,8 @@ export default { backupConfigs: {}, loadings: [], closings: [], + loadErrors: {}, // name -> status(5xx 状态码,或原生加载错误记 0) + reloadNonce: {}, // name -> 重试计数,用于强制重挂 iframe } }, @@ -217,6 +275,21 @@ export default { this.loadings = this.loadings.filter(item => item !== name); }, + onLoadError(e) { + this.$set(this.loadErrors, e.detail.name, e.detail.status); + }, + + // 重试:清错误态、重新加载(递增 nonce 触发 iframe 重挂) + retryMicroApp(name) { + this.$delete(this.loadErrors, name); + this.loadings.push(name); + this.$set(this.reloadNonce, name, (this.reloadNonce[name] || 0) + 1); + }, + + microFrameKey(name) { + return `iframe-${name}-${this.reloadNonce[name] || 0}`; + }, + /** * 应用数据 * @param name @@ -466,6 +539,7 @@ export default { // 更新微应用 if (app.url != config.url || !app.keep_alive) { this.unmountMicroApp(app) + this.$delete(this.loadErrors, app.name) this.loadings.push(app.name) } Object.assign(app, config) @@ -480,6 +554,7 @@ export default { config.postMessage = () => {} config.onBeforeClose = () => true this.$store.commit('microApps/push', config) + this.$delete(this.loadErrors, config.name) this.loadings.push(config.name) requestAnimationFrame(_ => { config.isOpen = true @@ -604,6 +679,7 @@ export default { } this.closeAppState(app) + this.$delete(this.loadErrors, name) if (destroy === true) { this.unmountMicroApp(app) } diff --git a/resources/assets/js/components/MicroApps/modal.vue b/resources/assets/js/components/MicroApps/modal.vue index 489abb826..c2c33500a 100644 --- a/resources/assets/js/components/MicroApps/modal.vue +++ b/resources/assets/js/components/MicroApps/modal.vue @@ -76,6 +76,11 @@ export default { type: String, default: 'embed', }, + // 强制显示胶囊(错误态等),覆盖 capsule.visible 的隐藏配置 + forceCapsuleVisible: { + type: Boolean, + default: false + }, beforeClose: Function }, data() { @@ -131,7 +136,7 @@ export default { } const {capsule} = this.options if ($A.isJson(capsule)) { - if (!this.getCapsuleVisible(capsule.visible)) { + if (!this.forceCapsuleVisible && !this.getCapsuleVisible(capsule.visible)) { styleObject.display = 'none'; } if (typeof capsule.top === 'number') {