From b73ab76bfb80757e23400352a40e02f83b41e542 Mon Sep 17 00:00:00 2001 From: kuaifan Date: Wed, 6 Aug 2025 16:51:21 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E5=BE=AE=E5=BA=94?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../assets/js/components/MicroApps/iframe.vue | 9 ++ .../assets/js/components/MicroApps/index.vue | 128 ++++++++++++----- .../assets/js/components/MicroApps/modal.vue | 132 +++++++++++------- .../pages/manage/components/DialogWrapper.vue | 1 + resources/assets/js/store/actions.js | 1 + 5 files changed, 187 insertions(+), 84 deletions(-) diff --git a/resources/assets/js/components/MicroApps/iframe.vue b/resources/assets/js/components/MicroApps/iframe.vue index 2359e2dc1..1d18f9af7 100644 --- a/resources/assets/js/components/MicroApps/iframe.vue +++ b/resources/assets/js/components/MicroApps/iframe.vue @@ -196,10 +196,19 @@ export default { this.$store.commit('microApps/update', { name: this.name, data: { + postMessage: (message) => { + if (!this.$refs.iframe || !this.$refs.iframe.contentWindow) { + return + } + this.$refs.iframe.contentWindow.postMessage(message, '*') + }, onBeforeClose: () => { if (this.hearTbeatLastTime && Date.now() - this.hearTbeatLastTime > 5000) { return true // 超时,允许关闭 } + if (!this.$refs.iframe || !this.$refs.iframe.contentWindow) { + return true // iframe 不存在,允许关闭 + } return new Promise(resolve => { const message = { id: $A.randomString(16), diff --git a/resources/assets/js/components/MicroApps/index.vue b/resources/assets/js/components/MicroApps/index.vue index 157ecc022..0cb565514 100644 --- a/resources/assets/js/components/MicroApps/index.vue +++ b/resources/assets/js/components/MicroApps/index.vue @@ -3,16 +3,14 @@ + :options="app" + :beforeClose="onBeforeClose" + @on-capsule-more="onCapsuleMore" + @on-popout-window="onPopoutWindow" + @on-close="closeMicroApp"> -
+ + + + +
- +
.micro-app-loader { position: absolute; + z-index: 9999; top: 0; left: 0; right: 0; @@ -113,6 +116,9 @@ export default { return { assistShow: false, userSelectOptions: {value: [], config: {}}, + + loadings: [], + closings: [], } }, @@ -122,8 +128,8 @@ export default { // 初始化微应用 microApp.start({ - 'iframe': true, 'router-mode': 'state', + 'iframe': true, 'iframeSrc': window.location.origin + '/assets/empty.html', }) }, @@ -193,7 +199,7 @@ export default { // 加载结束 finish(name) { - this.$store.commit('microApps/update', {name, data: {isLoading: false}}) + this.loadings = this.loadings.filter(item => item !== name); }, /** @@ -359,9 +365,9 @@ export default { } // 更新微应用 - if (app.url != config.url) { + if (app.url != config.url || !app.keep_alive) { this.unmountMicroApp(app) - app.isLoading = true + this.loadings.push(app.name) } Object.assign(app, config) requestAnimationFrame(_ => { @@ -371,10 +377,11 @@ export default { }) } else { // 新建微应用 - config.isLoading = true config.isOpen = false + config.postMessage = () => {} config.onBeforeClose = () => true this.$store.commit('microApps/push', config) + this.loadings.push(config.name) requestAnimationFrame(_ => { config.isOpen = true config.lastOpenAt = Date.now() @@ -401,7 +408,7 @@ export default { appConfig.url = windowConfig.url; delete windowConfig.url; } - // + const path = `/single/apps/${appConfig.name}` const apps = (await $A.IDBArray("cacheMicroApps")).filter(item => item.name != appConfig.name); apps.length > 50 && apps.splice(0, 10) @@ -466,6 +473,19 @@ export default { } }, + /** + * 关闭微应用状态 + * @param {Object} app 微应用对象 + * @param app + */ + closeAppState(app) { + this.closings.push(app.name); + app.isOpen = false; + setTimeout(() => { + this.closings = this.closings.filter(item => item !== app.name); + }, 300); + }, + /** * 关闭微应用(关闭前执行beforeClose) * @param name @@ -483,28 +503,18 @@ export default { * @param name * @param destroy */ - closeMicroApp(name, destroy) { + closeMicroApp(name, destroy = false) { const app = this.microApps.find(item => item.name == name); if (!app) { return; } - app.isOpen = false - if (destroy) { + this.closeAppState(app) + if (destroy === true) { this.unmountMicroApp(app) } }, - /** - * 卸载所有微应用 - */ - unmountAllMicroApp() { - this.microApps.forEach(app => { - app.isOpen = false - this.unmountMicroApp(app) - }); - }, - /** * 卸载微应用 * @param app @@ -517,6 +527,16 @@ export default { microApp.unmountApp(app.name, {destroy: true}) }, + /** + * 卸载所有微应用 + */ + unmountAllMicroApp() { + this.microApps.forEach(app => { + this.closeAppState(app) + this.unmountMicroApp(app) + }); + }, + /** * 关闭之前判断 * @param name @@ -545,7 +565,7 @@ export default { return } - if (/^iframe/i.test(app.url_type)) { + if (this.isIframe(app.url_type)) { const before = app.onBeforeClose(); if (before && before.then) { before.then(() => { @@ -583,6 +603,30 @@ export default { }) }, + /** + * 点击更多操作 + * @param name + * @param action + */ + onCapsuleMore(name, action) { + if (action === 'restart') { + this.onRestartApp(name) + return + } + const app = this.microApps.find(item => item.name == name); + if (!app) { + return + } + if (this.isIframe(app.url_type)) { + app.postMessage({ + type: 'MICRO_APP_MENU_CLICK', + message: action + }); + return + } + microApp.forceSetData(name, {type: 'menuClick', message: action}) + }, + /** * 重启应用 * @param name @@ -595,7 +639,7 @@ export default { if (!app) { $A.modalError("应用不存在"); } - app.isLoading = true; + this.loadings.push(app.name) requestAnimationFrame(_ => { app.isOpen = true app.lastOpenAt = Date.now() @@ -618,13 +662,31 @@ export default { this.closeMicroApp(name, true) }, + /** + * 是否 iframe 类型 + * @param type + * @returns {boolean} + */ + isIframe(type) { + return /^iframe/i.test(type) + }, + /** * 是否渲染 iframe * @param app * @returns {boolean} */ shouldRenderIFrame(app) { - return app.url_type === 'iframe' && (app.isOpen || app.keep_alive) && app.url; + return app.url && this.isIframe(app.url_type) && (app.isOpen || app.keep_alive); + }, + + /** + * 是否渲染 micro + * @param app + * @returns {boolean} + */ + shouldRenderMicro(app) { + return app.url && !this.isIframe(app.url_type) && (app.isOpen || this.closings.includes(app.name)); }, /** diff --git a/resources/assets/js/components/MicroApps/modal.vue b/resources/assets/js/components/MicroApps/modal.vue index 8cdc1e55c..259d1ea7c 100644 --- a/resources/assets/js/components/MicroApps/modal.vue +++ b/resources/assets/js/components/MicroApps/modal.vue @@ -2,11 +2,12 @@
-
+
-
+
+
@@ -23,6 +24,7 @@
+
@@ -30,13 +32,14 @@
-
+
+ + -
+
@@ -67,7 +71,7 @@ export default { components: {ResizeLine}, directives: {TransferDom}, props: { - value: { + open: { type: Boolean, default: false }, @@ -79,55 +83,49 @@ export default { type: Number, default: 300 }, - background: { - default: null - }, - transparent: { - type: Boolean, - default: false - }, - autoDarkTheme: { - type: Boolean, - default: true - }, - keepAlive: { - type: Boolean, - default: true + options: { + type: Object, + default: () => ({}) }, beforeClose: Function }, data() { return { dynamicSize: 0, - zIndex: 1000 + zIndex: 1000, + capsuleMenuShow: false, } }, computed: { ...mapState(['windowIsMobileLayout']), shouldRenderInDom() { - return this.value || this.keepAlive; + return this.open || !!this.options.keep_alive; }, - className({value, autoDarkTheme, transparent, windowIsMobileLayout}) { + className() { return { 'micro-modal': true, - 'micro-modal-hidden': !value, - 'no-dark-content': !autoDarkTheme, - 'transparent-mode': transparent, - 'capsule-mode': windowIsMobileLayout, + 'micro-modal-hidden': !this.open, + 'transparent-mode': !!this.options.transparent, + 'capsule-mode': this.windowIsMobileLayout, } }, - transitions({transparent}) { - if (transparent) { + transitions() { + if (!!this.options.transparent) { return ['', ''] } return ['micro-modal-fade', 'micro-modal-slide'] }, + bodyClass() { + return { + 'no-dark-content': !this.options.auto_dark_theme, + } + }, bodyStyle() { const styleObject = {} - if ($A.isJson(this.background)) { - styleObject.background = this.background - } else if (this.background) { - styleObject.backgroundColor = this.background; + if ($A.isJson(this.options.background)) { + styleObject.background = this.options.background + } else if (this.options.background) { + styleObject.backgroundColor = this.options.background; } return styleObject; }, @@ -139,11 +137,24 @@ export default { return {width, zIndex} }, capsuleStyle({zIndex}) { - return {zIndex} + const styleObject = {zIndex} + const {capsule} = this.options + if ($A.isJson(capsule)) { + if (capsule.visible === false) { + styleObject.display = 'none'; + } + if (typeof capsule.top === 'number') { + styleObject.top = `${capsule.top}px`; + } + if (typeof capsule.right === 'number') { + styleObject.right = `${capsule.right}px`; + } + } + return styleObject }, }, watch: { - value: { + open: { handler(val) { if (val) { this.zIndex = typeof window.modalTransferIndex === 'number' ? window.modalTransferIndex++ : 1000; @@ -181,19 +192,31 @@ export default { }, onCapsuleMore(event) { - const list = [ - {label: '重启应用', value: 'restart'}, - {label: '关闭应用', value: 'close'}, - ]; + const list = []; + const {capsule} = this.options; + if ($A.isJson(capsule) && $A.isArray(capsule.more_menus)) { + capsule.more_menus.forEach(item => { + if (item.label && item.value) { + list.push(item); + } + }); + } + list.push(...[ + {label: this.$L('重启应用'), value: 'restart', divided: list.length > 0}, + {label: this.$L('关闭应用'), value: 'close'}, + ]) this.$store.commit('menu/operation', { event, list, size: 'large', + onVisibleChange: (visible) => { + this.capsuleMenuShow = visible; + }, onUpdate: (value) => { - if (value === 'restart') { - this.$emit('on-restart-app'); - } else if (value === 'close') { + if (value === 'close') { this.onClose(true); + } else { + this.$emit('on-capsule-more', this.options.name, value); } } }) @@ -203,7 +226,7 @@ export default { if (!this.beforeClose) { return this.handleClose(); } - const before = this.beforeClose(isClick); + const before = this.beforeClose(this.options.name, isClick); if (before && before.then) { before.then(() => { this.handleClose(); @@ -214,7 +237,7 @@ export default { }, handleClose() { - this.$emit('input', false) + this.$emit('on-close', this.options.name); } } } @@ -280,10 +303,20 @@ export default { background-color: var(--modal-mask-bg, rgba(0, 0, 0, .4)); } + &-capsule-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: transparent; + z-index: 1; + } + &-capsule { position: absolute; - top: 8px; - right: 8px; + top: 10px; + right: 10px; z-index: 2; transform: translateY(var(--status-bar-height, 0)); display: var(--modal-capsule-display, none); @@ -466,13 +499,10 @@ export default { body.dark-mode-reverse { .micro-modal { &:not(.transparent-mode) { - &:not(.no-dark-content) { - --modal-mask-bg: rgba(230, 230, 230, 0.6); - --modal-close-color: #323232; - } + --modal-mask-bg: rgba(230, 230, 230, 0.6); + --modal-close-color: #323232; - &.no-dark-content { - --modal-mask-bg: rgba(20, 20, 20, 0.6); + .no-dark-content { --modal-body-background-color: #000000; } } diff --git a/resources/assets/js/pages/manage/components/DialogWrapper.vue b/resources/assets/js/pages/manage/components/DialogWrapper.vue index 9aef51441..c658f9c04 100644 --- a/resources/assets/js/pages/manage/components/DialogWrapper.vue +++ b/resources/assets/js/pages/manage/components/DialogWrapper.vue @@ -2642,6 +2642,7 @@ export default { name: 'okr_details', url: 'apps/okr/#details', props: {type: 'details', id}, + keep_alive: false, transparent: true, }); }, diff --git a/resources/assets/js/store/actions.js b/resources/assets/js/store/actions.js index e7f7516c6..9d8276c19 100644 --- a/resources/assets/js/store/actions.js +++ b/resources/assets/js/store/actions.js @@ -4745,6 +4745,7 @@ export default { url: $A.mainUrl(data.url), url_type: data.url_type || 'inline', background: data.background || null, + capsule: $A.isJson(data.capsule) ? data.capsule : {}, transparent: typeof data.transparent == 'boolean' ? data.transparent : false, disable_scope_css: typeof data.disable_scope_css == 'boolean' ? data.disable_scope_css : false, auto_dark_theme: typeof data.auto_dark_theme == 'boolean' ? data.auto_dark_theme : true,