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 errorload
this.detectHttpError()
},
// iframe
handleError(e) {
this.$emit('error', {
// 5xx/
// 4xxsoft-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 -> status5xx 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') {