refactor: 统一客户端窗口打开接口并支持标签页名称复用

- 合并 openChildWindow 和 openWebTabWindow 为统一的 openWindow 接口
  - 新增 webTabNameMap 映射,支持按名称查找和复用已存在的标签页
  - 标签页增加 name、titleFixed 元数据支持
  - 窗口间转移标签时同步更新名称映射
  - 重构前端 actions,统一使用 openWindow 方法,通过 mode 参数区分窗口/标签模式
  - 更新所有调用点使用新的统一接口
This commit is contained in:
kuaifan 2026-01-10 02:08:36 +00:00
parent fc74e0d952
commit 16d5ffd4f9
13 changed files with 172 additions and 62 deletions

144
electron/electron.js vendored
View File

@ -75,13 +75,16 @@ let mainWindow = null,
preloadWindow = null,
mediaWindow = null;
// 窗口数组和状态
// 独立子窗口管理
let childWindow = [];
// 多窗口 Tab 管理
// Map<windowId, {window, views: [{id, view}], activeTabId}>
// Map<windowId, {window, views: [{id, view, name, favicon}], activeTabId}>
let webTabWindows = new Map();
let webTabWindowIdCounter = 1;
// 标签名称到标签位置的映射,用于复用已存在的标签
// Map<name, {windowId, tabId}>
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"
})

View File

@ -608,7 +608,7 @@ export default {
return false;
}
// 使
this.$store.dispatch("openWebTabWindow", url)
this.$store.dispatch("openWindow", url)
return true
}
this.$Electron.listener('browserWindowBlur', _ => {

View File

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

View File

@ -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/",

View File

@ -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/",

View File

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

View File

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

View File

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

View File

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

View File

@ -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/",

View File

@ -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/",

View File

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

View File

@ -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/",