From 16d5ffd4f90a978a06ed51f8f4c731c4584a0415 Mon Sep 17 00:00:00 2001 From: kuaifan Date: Sat, 10 Jan 2026 02:08:36 +0000 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=BB=9F=E4=B8=80=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E7=AA=97=E5=8F=A3=E6=89=93=E5=BC=80=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E5=B9=B6=E6=94=AF=E6=8C=81=E6=A0=87=E7=AD=BE=E9=A1=B5?= =?UTF-8?q?=E5=90=8D=E7=A7=B0=E5=A4=8D=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 合并 openChildWindow 和 openWebTabWindow 为统一的 openWindow 接口 - 新增 webTabNameMap 映射,支持按名称查找和复用已存在的标签页 - 标签页增加 name、titleFixed 元数据支持 - 窗口间转移标签时同步更新名称映射 - 重构前端 actions,统一使用 openWindow 方法,通过 mode 参数区分窗口/标签模式 - 更新所有调用点使用新的统一接口 --- electron/electron.js | 144 ++++++++++++++---- resources/assets/js/App.vue | 2 +- .../assets/js/components/MicroApps/index.vue | 8 +- .../pages/manage/components/DialogWrapper.vue | 2 +- .../pages/manage/components/FileHistory.vue | 2 +- .../components/MeetingManager/index.vue | 3 +- .../js/pages/manage/components/ProjectLog.vue | 2 +- .../js/pages/manage/components/Report.vue | 4 +- .../manage/components/TaskContentHistory.vue | 2 +- .../js/pages/manage/components/TaskDetail.vue | 5 +- resources/assets/js/pages/manage/file.vue | 2 +- resources/assets/js/store/actions.js | 56 ++++--- resources/assets/js/utils/file.js | 2 +- 13 files changed, 172 insertions(+), 62 deletions(-) diff --git a/electron/electron.js b/electron/electron.js index d36b02961..3238277c1 100644 --- a/electron/electron.js +++ b/electron/electron.js @@ -75,13 +75,16 @@ let mainWindow = null, preloadWindow = null, mediaWindow = null; -// 窗口数组和状态 +// 独立子窗口管理 let childWindow = []; // 多窗口 Tab 管理 -// Map +// Map let webTabWindows = new Map(); let webTabWindowIdCounter = 1; +// 标签名称到标签位置的映射,用于复用已存在的标签 +// Map +let webTabNameMap = new Map(); // 窗口配置和状态 let mediaType = null, @@ -484,6 +487,7 @@ function createChildWindow(args) { if (!utils.isJson(args)) { args = {path: args, config: {}} } + args.path = args.path || args.url; const name = args.name || "auto_" + utils.randomString(6); const wind = childWindow.find(item => item.name == name); @@ -740,7 +744,7 @@ function createMediaWindow(args, type = 'image') { /** * 创建内置浏览器窗口(支持多窗口) - * @param args {url, windowId, position, afterId, ...} + * @param args {url, windowId, position, afterId, insertIndex, name, force, userAgent, title, titleFixed, webPreferences, ...} * @returns {number} 窗口ID */ function createWebTabWindow(args) { @@ -752,6 +756,34 @@ function createWebTabWindow(args) { args = {url: args} } + // 如果有 name,先查找是否已存在同名标签 + if (args.name) { + const existing = webTabNameMap.get(args.name); + if (existing) { + const existingWindowData = webTabWindows.get(existing.windowId); + if (existingWindowData && existingWindowData.window && !existingWindowData.window.isDestroyed()) { + const viewItem = existingWindowData.views.find(v => v.id === existing.tabId); + if (viewItem && viewItem.view && !viewItem.view.webContents.isDestroyed()) { + // 激活已存在的标签 + if (existingWindowData.window.isMinimized()) { + existingWindowData.window.restore(); + } + existingWindowData.window.focus(); + existingWindowData.window.show(); + activateWebTabInWindow(existing.windowId, existing.tabId); + + // force=true 时重新加载 + if (args.force === true && args.url) { + viewItem.view.webContents.loadURL(args.url).catch(_ => {}); + } + return existing.windowId; + } + } + // 标签已失效,清理映射 + webTabNameMap.delete(args.name); + } + } + // 确定目标窗口ID let windowId = args.windowId; let windowData = windowId ? webTabWindows.get(windowId) : null; @@ -805,18 +837,28 @@ function createWebTabWindow(args) { insertIndex = Math.max(0, Math.min(args.insertIndex, windowData.views.length)); } - // 插入到指定位置 + // 插入到指定位置,包含 name 信息 windowData.views.splice(insertIndex, 0, { id: browserView.webContents.id, - view: browserView + view: browserView, + name: args.name || null }); + // 如果有 name,注册到映射 + if (args.name) { + webTabNameMap.set(args.name, { + windowId: windowId, + tabId: browserView.webContents.id + }); + } + utils.onDispatchEvent(webTabWindow.webContents, { event: 'create', id: browserView.webContents.id, url: args.url, afterId: args.afterId, windowId: windowId, + title: args.title, }).then(_ => { }); activateWebTabInWindow(windowId, browserView.webContents.id); @@ -907,7 +949,11 @@ function createWebTabWindowInstance(windowId, position) { webTabWindow.on('closed', () => { const windowData = webTabWindows.get(windowId); if (windowData) { - windowData.views.forEach(({view}) => { + windowData.views.forEach(({view, name}) => { + // 清理 name 映射 + if (name) { + webTabNameMap.delete(name); + } try { view.webContents.close(); } catch (e) { @@ -990,10 +1036,24 @@ function createWebTabView(windowId, args) { height: (webTabWindow.getContentBounds().height || 800) - webTabHeight, }); - // 保存所属窗口ID + // 保存所属窗口ID和元数据 browserView.webTabWindowId = windowId; + browserView.tabName = args.name || null; + browserView.titleFixed = args.titleFixed || false; + + // 设置自定义 UserAgent + if (args.userAgent) { + const originalUA = browserView.webContents.getUserAgent(); + browserView.webContents.setUserAgent( + originalUA + " SubTaskWindow/" + process.platform + "/" + os.arch() + "/1.0 " + args.userAgent + ); + } browserView.webContents.on('destroyed', () => { + // 清理 name 映射 + if (browserView.tabName) { + webTabNameMap.delete(browserView.tabName); + } closeWebTabInWindow(windowId, browserView.webContents.id); }); browserView.webContents.setWindowOpenHandler(({url}) => { @@ -1004,7 +1064,11 @@ function createWebTabView(windowId, args) { } return {action: 'deny'}; }); - browserView.webContents.on('page-title-updated', (event, title) => { + browserView.webContents.on('page-title-updated', (_, title) => { + // titleFixed 时不更新标题 + if (browserView.titleFixed) { + return; + } // 使用动态窗口ID,支持标签在窗口间转移 const currentWindowId = browserView.webTabWindowId; const wd = webTabWindows.get(currentWindowId); @@ -1293,6 +1357,12 @@ function closeWebTabInWindow(windowId, id) { webTabWindow.hide(); } webTabWindow.contentView.removeChildView(item.view); + + // 清理 name 映射 + if (item.name) { + webTabNameMap.delete(item.name); + } + try { item.view.webContents.close(); } catch (e) { @@ -1335,6 +1405,7 @@ function detachWebTab(windowId, tabId, screenX, screenY) { const tabItem = sourceWindowData.views[tabIndex]; const view = tabItem.view; const favicon = tabItem.favicon || ''; + const tabName = tabItem.name || null; const sourceWindow = sourceWindowData.window; // 从源窗口移除视图 @@ -1368,8 +1439,9 @@ function detachWebTab(windowId, tabId, screenX, screenY) { const newWindowData = { window: newWindow, views: [{ - id: tabId, - view, + id: tabId, + name: tabName, + view, favicon }], activeTabId: tabId @@ -1379,6 +1451,14 @@ function detachWebTab(windowId, tabId, screenX, screenY) { // 更新视图所属窗口 view.webTabWindowId = newWindowId; + // 更新 name 映射中的 windowId + if (tabName) { + webTabNameMap.set(tabName, { + windowId: newWindowId, + tabId: tabId + }); + } + // 添加视图到新窗口 newWindow.contentView.addChildView(view); view.setBounds({ @@ -1448,6 +1528,7 @@ function attachWebTab(sourceWindowId, tabId, targetWindowId, insertIndex) { const tabItem = sourceWindowData.views[tabIndex]; const view = tabItem.view; const favicon = tabItem.favicon || ''; + const tabName = tabItem.name || null; const sourceWindow = sourceWindowData.window; const targetWindow = targetWindowData.window; @@ -1464,15 +1545,24 @@ function attachWebTab(sourceWindowId, tabId, targetWindowId, insertIndex) { // 更新视图所属窗口 view.webTabWindowId = targetWindowId; + // 更新 name 映射中的 windowId + if (tabName) { + webTabNameMap.set(tabName, { + windowId: targetWindowId, + tabId: tabId + }); + } + // 确定插入位置 const actualInsertIndex = typeof insertIndex === 'number' ? Math.max(0, Math.min(insertIndex, targetWindowData.views.length)) : targetWindowData.views.length; - // 添加到目标窗口 + // 添加到目标窗口,保留 name 信息 targetWindowData.views.splice(actualInsertIndex, 0, { - id: tabId, - view, + id: tabId, + name: tabName, + view, favicon }); targetWindow.contentView.addChildView(view); @@ -1688,15 +1778,6 @@ ipcMain.on('windowQuit', (event) => { app.quit(); }) -/** - * 创建路由窗口 - * @param args {path, ?} - */ -ipcMain.on('openChildWindow', (event, args) => { - createChildWindow(args) - event.returnValue = "ok" -}) - /** * 显示预加载窗口(用于调试) */ @@ -1747,11 +1828,22 @@ ipcMain.on('openMediaViewer', (event, args) => { }); /** - * 内置浏览器 - 打开创建 - * @param args {url, ?} + * 统一窗口打开接口 + * @param args {url, name, mode, force, config, userAgent, webPreferences, ...} + * - url: 要打开的地址 + * - name: 窗口/标签名称 + * - mode: 'tab' | 'window' + * - 'window': 独立窗口模式 + * - 'tab': 标签页模式(默认) */ -ipcMain.on('openWebTabWindow', (event, args) => { - createWebTabWindow(args) +ipcMain.on('openWindow', (event, args) => { + if (args.mode === 'window') { + // 独立窗口模式 + createChildWindow(args) + } else { + // 标签页模式 + createWebTabWindow(args) + } event.returnValue = "ok" }) diff --git a/resources/assets/js/App.vue b/resources/assets/js/App.vue index 831710a4b..d4282d337 100755 --- a/resources/assets/js/App.vue +++ b/resources/assets/js/App.vue @@ -608,7 +608,7 @@ export default { return false; } // 使用内置浏览器打开 - this.$store.dispatch("openWebTabWindow", url) + this.$store.dispatch("openWindow", url) return true } this.$Electron.listener('browserWindowBlur', _ => { diff --git a/resources/assets/js/components/MicroApps/index.vue b/resources/assets/js/components/MicroApps/index.vue index 52c390f47..b48ddeede 100644 --- a/resources/assets/js/components/MicroApps/index.vue +++ b/resources/assets/js/components/MicroApps/index.vue @@ -275,10 +275,10 @@ export default { params.path = params.url delete params.url } - this.$store.dispatch('openChildWindow', params); + this.$store.dispatch('openWindow', params); }, openTabWindow: (url) => { - this.$store.dispatch('openWebTabWindow', url); + this.$store.dispatch('openWindow', {path: url}); }, openAppPage: (params) => { if (!$A.isJson(params)) { @@ -481,7 +481,7 @@ export default { await $A.IDBSet("cacheMicroApps", $A.cloneJSON(apps)); if (this.$Electron) { - await this.$store.dispatch('openChildWindow', { + await this.$store.dispatch('openWindow', { name: `single-apps-${$A.randomString(6)}`, path: path, force: false, @@ -513,7 +513,7 @@ export default { */ async externalWindow(config) { if (this.$Electron) { - await this.$store.dispatch('openChildWindow', { + await this.$store.dispatch('openWindow', { name: `external-apps-${$A.randomString(6)}`, path: config.url, force: false, diff --git a/resources/assets/js/pages/manage/components/DialogWrapper.vue b/resources/assets/js/pages/manage/components/DialogWrapper.vue index 900e1c58d..9546a0698 100644 --- a/resources/assets/js/pages/manage/components/DialogWrapper.vue +++ b/resources/assets/js/pages/manage/components/DialogWrapper.vue @@ -3781,7 +3781,7 @@ export default { const path = `/single/file/msg/${data.id}`; const title = data.type === 'longtext' ? this.$L('消息详情') : (`${msg.name} (${$A.bytesToSize(msg.size)})`); if (this.$Electron) { - this.$store.dispatch('openChildWindow', { + this.$store.dispatch('openWindow', { name: `file-msg-${data.id}`, path: path, userAgent: "/hideenOfficeTitle/", diff --git a/resources/assets/js/pages/manage/components/FileHistory.vue b/resources/assets/js/pages/manage/components/FileHistory.vue index 35ca42a0b..051e9969e 100644 --- a/resources/assets/js/pages/manage/components/FileHistory.vue +++ b/resources/assets/js/pages/manage/components/FileHistory.vue @@ -181,7 +181,7 @@ export default { const title = $A.getFileName(this.file) + ` [${row.created_at}]`; const path = `/single/file/${this.fileId}?history_id=${row.id}&history_title=${title}`; if (this.$Electron) { - this.$store.dispatch('openChildWindow', { + this.$store.dispatch('openWindow', { name: `file-${this.fileId}-${row.id}`, path: path, userAgent: "/hideenOfficeTitle/", diff --git a/resources/assets/js/pages/manage/components/MeetingManager/index.vue b/resources/assets/js/pages/manage/components/MeetingManager/index.vue index da32b629e..407cca612 100644 --- a/resources/assets/js/pages/manage/components/MeetingManager/index.vue +++ b/resources/assets/js/pages/manage/components/MeetingManager/index.vue @@ -411,9 +411,10 @@ export default { video: this.addData.tracks.includes("video") ? 1 : 0, token: this.userToken, }); - this.$store.dispatch('openChildWindow', { + this.$store.dispatch('openWindow', { name: `meeting-window`, path: meetingPath, + mode: 'window', force: false, config }); diff --git a/resources/assets/js/pages/manage/components/ProjectLog.vue b/resources/assets/js/pages/manage/components/ProjectLog.vue index 617b107db..bb89d0669 100644 --- a/resources/assets/js/pages/manage/components/ProjectLog.vue +++ b/resources/assets/js/pages/manage/components/ProjectLog.vue @@ -219,7 +219,7 @@ export default { const path = `/${url}` if (this.$Electron) { e.preventDefault() - this.$store.dispatch('openChildWindow', { + this.$store.dispatch('openWindow', { name: `project-log-${id}`, path: path, force: false, diff --git a/resources/assets/js/pages/manage/components/Report.vue b/resources/assets/js/pages/manage/components/Report.vue index 15e4c994c..69ea9cfd6 100644 --- a/resources/assets/js/pages/manage/components/Report.vue +++ b/resources/assets/js/pages/manage/components/Report.vue @@ -111,7 +111,7 @@ export default { width: Math.min(window.screen.availWidth, 1440), height: Math.min(window.screen.availHeight, 900), } - this.$store.dispatch('openChildWindow', { + this.$store.dispatch('openWindow', { name: `report-detail-${row.id}`, path: `/single/report/detail/${row.id}`, force: false, @@ -134,7 +134,7 @@ export default { width: Math.min(window.screen.availWidth, 1440), height: Math.min(window.screen.availHeight, 900), } - this.$store.dispatch('openChildWindow', { + this.$store.dispatch('openWindow', { name: `report-edit-${id}`, path: `/single/report/edit/${id}`, force: false, diff --git a/resources/assets/js/pages/manage/components/TaskContentHistory.vue b/resources/assets/js/pages/manage/components/TaskContentHistory.vue index 1cd7d24b1..34a655347 100644 --- a/resources/assets/js/pages/manage/components/TaskContentHistory.vue +++ b/resources/assets/js/pages/manage/components/TaskContentHistory.vue @@ -169,7 +169,7 @@ export default { const title = (this.taskName || `ID: ${this.taskId}`) + ` [${row.created_at}]`; const path = `/single/task/content/${this.taskId}?history_id=${row.id}&history_title=${title}`; if (this.$Electron) { - this.$store.dispatch('openChildWindow', { + this.$store.dispatch('openWindow', { name: `task-content-${this.taskId}-${row.id}`, path: path, force: false, diff --git a/resources/assets/js/pages/manage/components/TaskDetail.vue b/resources/assets/js/pages/manage/components/TaskDetail.vue index f6510b984..1ca3b07ad 100755 --- a/resources/assets/js/pages/manage/components/TaskDetail.vue +++ b/resources/assets/js/pages/manage/components/TaskDetail.vue @@ -1905,9 +1905,10 @@ export default { config.minWidth = 800; config.minHeight = 600; } - this.$store.dispatch('openChildWindow', { + this.$store.dispatch('openWindow', { name: `task-${this.taskDetail.id}`, path: `/single/task/${this.taskDetail.id}?navActive=${this.navActive}`, + mode: 'window', force: false, config }); @@ -1967,7 +1968,7 @@ export default { } const path = `/single/file/task/${file.id}`; if (this.$Electron) { - this.$store.dispatch('openChildWindow', { + this.$store.dispatch('openWindow', { name: `file-task-${file.id}`, path: path, userAgent: "/hideenOfficeTitle/", diff --git a/resources/assets/js/pages/manage/file.vue b/resources/assets/js/pages/manage/file.vue index 64a1fc745..b5a4de6e1 100644 --- a/resources/assets/js/pages/manage/file.vue +++ b/resources/assets/js/pages/manage/file.vue @@ -1520,7 +1520,7 @@ export default { openFileSingle(item) { const path = `/single/file/${item.id}`; if (this.$Electron) { - this.$store.dispatch('openChildWindow', { + this.$store.dispatch('openWindow', { name: `file-${item.id}`, path: path, userAgent: "/hideenOfficeTitle/", diff --git a/resources/assets/js/store/actions.js b/resources/assets/js/store/actions.js index d4677e0e8..91de94ca8 100644 --- a/resources/assets/js/store/actions.js +++ b/resources/assets/js/store/actions.js @@ -1383,28 +1383,43 @@ export default { }, /** - * 打开子窗口(客户端) + * 打开窗口(客户端) * @param dispatch - * @param params + * @param params {path, name, mode, force, config, userAgent, webPreferences} + * - path: 要打开的地址(或直接传 URL 字符串) + * - name: 窗口/标签名称 + * - mode: 'tab' | 'window',默认 'tab' + * - force: 是否强制刷新 + * - config: 窗口配置(独立窗口模式有效) + * - userAgent: 自定义 UserAgent + * - webPreferences: 网页偏好设置 */ - async openChildWindow({dispatch}, params) { - params.path = await dispatch("userUrl", params.path) - $A.Electron.sendMessage('openChildWindow', params) - }, - - /** - * 打开新标签窗口(客户端) - * @param dispatch - * @param url - */ - async openWebTabWindow({dispatch}, url) { - const params = {url} - if ($A.getDomain(url) == $A.getDomain($A.mainUrl())) { - params.url = await dispatch("userUrl", url) - } else { - params.webPreferences = {contextIsolation: false} + async openWindow({dispatch}, params) { + // 兼容直接传入 URL 字符串的情况 + if (typeof params === 'string') { + params = { path: params } } - $A.Electron.sendMessage('openWebTabWindow', params) + + // 外站 URL 自动移除 preload 脚本(通过 contextIsolation: false) + const pathDomain = $A.getDomain(params.path) + const isExternal = pathDomain && pathDomain !== $A.getDomain($A.mainUrl()) + if (isExternal) { + params.webPreferences = Object.assign({contextIsolation: false}, params.webPreferences) + } else { + params.path = await dispatch("userUrl", params.path) + } + + $A.Electron.sendMessage('openWindow', { + url: params.path, + name: params.name, + mode: params.mode, + force: params.force, + config: params.config, + userAgent: params.userAgent, + title: params.config?.title, + titleFixed: params.config?.titleFixed, + webPreferences: params.webPreferences, + }) }, /** *****************************************************************************************/ @@ -3575,9 +3590,10 @@ export default { return } const dialogData = state.cacheDialogs.find(({id}) => id === dialogId) || {} - dispatch('openChildWindow', { + dispatch('openWindow', { name: `dialog-${dialogId}`, path: `/single/dialog/${dialogId}`, + mode: 'window', force: false, config: { title: dialogData.name, diff --git a/resources/assets/js/utils/file.js b/resources/assets/js/utils/file.js index 983e9a01e..49f7f65d5 100644 --- a/resources/assets/js/utils/file.js +++ b/resources/assets/js/utils/file.js @@ -65,7 +65,7 @@ export function openFileInClient(vm, item, options = {}) { }, options.windowConfig || {}); if (vm.$Electron) { - vm.$store.dispatch('openChildWindow', { + vm.$store.dispatch('openWindow', { name: windowName, path, userAgent: "/hideenOfficeTitle/",