From 4929d44ce79c4de613b4e5c8cbf2c9f7f00482d2 Mon Sep 17 00:00:00 2001 From: kuaifan Date: Sat, 10 Jan 2026 15:44:58 +0000 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E9=A1=B5=E5=8A=A0=E8=BD=BD=E7=8A=B6=E6=80=81=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E4=B8=8E=20URL=20=E5=8A=A0=E8=BD=BD=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 loadContentUrl 方法统一处理完整 URL 和相对路径的加载 - 优化标签页加载状态,忽略 SPA 路由切换(isSameDocument),避免频繁闪烁 - 添加定时检查器确保加载状态正确停止 - windowClose/windowDestroy 支持识别 tab 页面发送者,仅关闭对应标签 - 子窗口重启过程中不再意外销毁窗口 - 微应用打开标签页时传递标题信息 - isLocalHost 对空 URL 和相对路径返回 true --- electron/electron.js | 102 ++++++++++++------ electron/lib/utils.js | 17 +++ electron/render/tabs/assets/image/more.svg | 1 + electron/render/tabs/index.html | 5 +- .../assets/js/components/MicroApps/index.vue | 28 +++-- resources/assets/js/store/actions.js | 1 + 6 files changed, 111 insertions(+), 43 deletions(-) create mode 100644 electron/render/tabs/assets/image/more.svg diff --git a/electron/electron.js b/electron/electron.js index 3238277c1..7b837590b 100644 --- a/electron/electron.js +++ b/electron/electron.js @@ -625,10 +625,10 @@ function createChildWindow(args) { // 加载地址 const hash = `${args.hash || args.path}`; if (/^https?:/i.test(hash)) { - browser.loadURL(hash) - .then(_ => { }) - .catch(_ => { }) + // 完整 URL 直接加载 + browser.loadURL(hash).then(_ => { }).catch(_ => { }) } else if (isPreload) { + // preload 窗口尝试调用 __initializeApp,失败则 loadUrl browser .webContents .executeJavaScript(`if(typeof window.__initializeApp === 'function'){window.__initializeApp('${hash}')}else{throw new Error('no function')}`, true) @@ -636,6 +636,7 @@ function createChildWindow(args) { utils.loadUrl(browser, serverUrl, hash) }); } else { + // 相对路径使用 loadUrl utils.loadUrl(browser, serverUrl, hash) } @@ -774,7 +775,7 @@ function createWebTabWindow(args) { // force=true 时重新加载 if (args.force === true && args.url) { - viewItem.view.webContents.loadURL(args.url).catch(_ => {}); + utils.loadContentUrl(viewItem.view.webContents, serverUrl, args.url); } return existing.windowId; } @@ -1050,10 +1051,13 @@ function createWebTabView(windowId, args) { } browserView.webContents.on('destroyed', () => { - // 清理 name 映射 if (browserView.tabName) { webTabNameMap.delete(browserView.tabName); } + if (browserView._loadingChecker) { + clearInterval(browserView._loadingChecker); + browserView._loadingChecker = null; + } closeWebTabInWindow(windowId, browserView.webContents.id); }); browserView.webContents.setWindowOpenHandler(({url}) => { @@ -1134,27 +1138,46 @@ function createWebTabView(windowId, args) { favicon: base64Favicon || '' }).then(_ => { }); }); - browserView.webContents.on('did-start-loading', _ => { - // 使用动态窗口ID,支持标签在窗口间转移 - const currentWindowId = browserView.webTabWindowId; - const wd = webTabWindows.get(currentWindowId); - if (!wd || !wd.window) return; - utils.onDispatchEvent(wd.window.webContents, { - event: 'start-loading', - id: browserView.webContents.id, - }).then(_ => { }); + // 页面加载状态管理,忽略SPA路由切换(isSameDocument) + browserView._loadingActive = false; + browserView._loadingChecker = null; + const dispatchLoading = (event) => { + const wd = webTabWindows.get(browserView.webTabWindowId); + if (wd && wd.window) { + utils.onDispatchEvent(wd.window.webContents, { + event, + id: browserView.webContents.id, + }).then(_ => { }); + } + }; + const startLoading = () => { + if (browserView._loadingActive) return; + browserView._loadingActive = true; + dispatchLoading('start-loading'); + if (!browserView._loadingChecker) { + browserView._loadingChecker = setInterval(() => { + if (browserView.webContents.isDestroyed() || !browserView.webContents.isLoading()) { + stopLoading(); + } + }, 3000); + } + }; + const stopLoading = () => { + if (browserView._loadingChecker) { + clearInterval(browserView._loadingChecker); + browserView._loadingChecker = null; + } + if (!browserView._loadingActive) return; + browserView._loadingActive = false; + dispatchLoading('stop-loading'); + }; + browserView.webContents.on('did-start-navigation', (_, _url, _isInPlace, isMainFrame, _frameProcessId, _frameRoutingId, _navigationId, isSameDocument) => { + if (isMainFrame && !isSameDocument) { + startLoading(); + } }); browserView.webContents.on('did-stop-loading', _ => { - // 使用动态窗口ID,支持标签在窗口间转移 - const currentWindowId = browserView.webTabWindowId; - const wd = webTabWindows.get(currentWindowId); - if (!wd || !wd.window) return; - utils.onDispatchEvent(wd.window.webContents, { - event: 'stop-loading', - id: browserView.webContents.id, - }).then(_ => { }); - - // 加载完成暗黑模式下把窗口背景色改成白色,避免透明网站背景色穿透 + stopLoading(); if (nativeTheme.shouldUseDarkColors) { browserView.setBackgroundColor('#FFFFFF'); } @@ -1179,7 +1202,8 @@ function createWebTabView(windowId, args) { electronMenu.webContentsMenu(browserView.webContents, true); - browserView.webContents.loadURL(args.url).then(_ => { }).catch(_ => { }); + // 加载地址 + utils.loadContentUrl(browserView.webContents, serverUrl, args.url); browserView.setVisible(true); @@ -2128,20 +2152,36 @@ ipcMain.on('windowHidden', (event) => { }) /** - * 关闭窗口 + * 关闭窗口(或关闭 tab,如果发送者是 tab 中的页面) */ ipcMain.on('windowClose', (event) => { - const win = BrowserWindow.fromWebContents(event.sender); - win.close() + const tabId = event.sender.id; + const windowId = findWindowIdByTabId(tabId); + if (windowId !== null) { + // 发送者是 tab 中的页面,只关闭这个 tab + closeWebTabInWindow(windowId, tabId); + } else { + // 发送者是独立窗口,关闭整个窗口 + const win = BrowserWindow.fromWebContents(event.sender); + win?.close() + } event.returnValue = "ok" }) /** - * 销毁窗口 + * 销毁窗口(或销毁 tab,如果发送者是 tab 中的页面) */ ipcMain.on('windowDestroy', (event) => { - const win = BrowserWindow.fromWebContents(event.sender); - win.destroy() + const tabId = event.sender.id; + const windowId = findWindowIdByTabId(tabId); + if (windowId !== null) { + // 发送者是 tab 中的页面,只关闭这个 tab + closeWebTabInWindow(windowId, tabId); + } else { + // 发送者是独立窗口,销毁整个窗口 + const win = BrowserWindow.fromWebContents(event.sender); + win?.destroy() + } event.returnValue = "ok" }) diff --git a/electron/lib/utils.js b/electron/lib/utils.js index 854802fc7..67b7f9bdf 100644 --- a/electron/lib/utils.js +++ b/electron/lib/utils.js @@ -768,6 +768,23 @@ const utils = { } }, + /** + * 加载内容 URL(自动判断完整 URL 或相对路径) + * @param webContents - BrowserWindow 或 WebContents 对象 + * @param serverUrl - 服务器地址 + * @param url - 要加载的 URL(完整 URL 或相对路径) + */ + loadContentUrl(webContents, serverUrl, url) { + if (!url) return; + if (/^https?:/i.test(url)) { + // 完整 URL 直接加载 + webContents.loadURL(url).then(_ => { }).catch(_ => { }) + } else { + // 相对路径使用 loadUrl 处理 + utils.loadUrl(webContents, serverUrl, url) + } + }, + /** * 获取主题名称 * @returns {string|*} diff --git a/electron/render/tabs/assets/image/more.svg b/electron/render/tabs/assets/image/more.svg new file mode 100644 index 000000000..4086e0e8d --- /dev/null +++ b/electron/render/tabs/assets/image/more.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/electron/render/tabs/index.html b/electron/render/tabs/index.html index 6559771c7..1dfcb6e56 100644 --- a/electron/render/tabs/index.html +++ b/electron/render/tabs/index.html @@ -617,7 +617,10 @@ */ isLocalHost(url) { if (!url) { - return false + return true + } + if (!/^https?:\/\//i.test(url)) { + return true } try { const uri = new URL(url) diff --git a/resources/assets/js/components/MicroApps/index.vue b/resources/assets/js/components/MicroApps/index.vue index b48ddeede..10bfb178c 100644 --- a/resources/assets/js/components/MicroApps/index.vue +++ b/resources/assets/js/components/MicroApps/index.vue @@ -119,6 +119,7 @@ export default { data() { return { assistShow: false, + isRestarting: false, userSelectOptions: {value: [], config: {}}, backupConfigs: {}, @@ -160,8 +161,8 @@ export default { this.unmountAllMicroApp() }, assistShow(show) { - if (!show && $A.isSubElectron) { - // 如果是子 Electron 窗口,关闭窗口助理时销毁窗口 + if (!show && $A.isSubElectron && !this.isRestarting) { + // 如果是子 Electron 窗口,关闭窗口助理时销毁窗口(但是重启过程中不销毁) $A.Electron.sendMessage('windowDestroy'); } }, @@ -486,7 +487,7 @@ export default { path: path, force: false, config: Object.assign({ - title: ' ', + title: appConfig.title || ' ', parent: null, width: Math.min(window.screen.availWidth, 1440), height: Math.min(window.screen.availHeight, 900), @@ -518,7 +519,7 @@ export default { path: config.url, force: false, config: { - title: ' ', + title: config.title || ' ', parent: null, width: Math.min(window.screen.availWidth, 1440), height: Math.min(window.screen.availHeight, 900), @@ -706,15 +707,20 @@ export default { * @param name */ async onRestartApp(name) { - this.closeMicroApp(name, true) - await new Promise(resolve => setTimeout(resolve, 300)); + this.isRestarting = true + try { + this.closeMicroApp(name, true) + await new Promise(resolve => setTimeout(resolve, 300)); - const app = this.backupConfigs[name]; - if (!app) { - $A.modalError("应用不存在"); - return + const app = this.backupConfigs[name]; + if (!app) { + $A.modalError("应用不存在"); + return + } + await this.onOpen(app) + } finally { + this.isRestarting = false } - await this.onOpen(app) }, /** diff --git a/resources/assets/js/store/actions.js b/resources/assets/js/store/actions.js index d268113a6..4649a2da0 100644 --- a/resources/assets/js/store/actions.js +++ b/resources/assets/js/store/actions.js @@ -5120,6 +5120,7 @@ export default { const config = { id: microAppId, name: data.name, + title: data.label || data.title || data.name, url: $A.mainUrl(url), type: data.type || data.url_type, background: data.background || null,