feat(microapp): iframe 同源 HTTP 5xx 检测与内联错误层,错误态强制显示胶囊

- iframe.vue 读 responseStatus 检测同源 5xx(宁可漏判不误判),与原生 error 统一走 load-error
- index.vue 新增可读的内联错误层(重试/关闭),覆盖深色下被反色成一条线的错误页
- 检测到错误时强制显示胶囊,保证关闭/更多入口
This commit is contained in:
kuaifan 2026-07-16 06:26:39 +00:00
parent f8eaab5a50
commit 966c7b4ef0
4 changed files with 110 additions and 6 deletions

View File

@ -2531,3 +2531,4 @@ AI任务分析
共享文件
我共享的
共享给我的
服务器返回错误(HTTP (*))

View File

@ -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,
}
})
},

View File

@ -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">
<MicroIFrame
v-if="shouldRenderIFrame(app)"
:key="microFrameKey(app.name)"
:name="app.name"
:url="app.url"
:data="appData(app.name)"
:immersive="app.immersive"
@mounted="mounted"
@error="error"/>
@load-error="onLoadError"/>
<micro-app
v-else-if="shouldRenderMicro(app)"
:name="app.name"
@ -36,6 +38,20 @@
<Loading/>
</div>
</transition>
<!--加载错误层(同源 5xx 或原生加载错误时展示)-->
<transition name="fade">
<div v-if="loadErrors[app.name] != null" class="micro-app-error">
<div class="micro-app-error-box">
<div class="micro-app-error-title">{{ $L('应用加载失败') }}</div>
<div v-if="loadErrors[app.name] > 0" class="micro-app-error-desc">{{ $L('服务器返回错误(HTTP (*))', loadErrors[app.name]) }}</div>
<div class="micro-app-error-actions">
<Button type="primary" @click="retryMicroApp(app.name)">{{ $L('重试') }}</Button>
<Button @click="closeMicroApp(app.name, true)">{{ $L('关闭') }}</Button>
</div>
</div>
</div>
</transition>
</MicroModal>
<!--选择用户-->
@ -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)
}

View File

@ -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') {