mirror of
https://github.com/kuaifan/dootask.git
synced 2026-03-03 16:02:08 +00:00
refactor: 统一客户端窗口打开接口并支持标签页名称复用
- 合并 openChildWindow 和 openWebTabWindow 为统一的 openWindow 接口 - 新增 webTabNameMap 映射,支持按名称查找和复用已存在的标签页 - 标签页增加 name、titleFixed 元数据支持 - 窗口间转移标签时同步更新名称映射 - 重构前端 actions,统一使用 openWindow 方法,通过 mode 参数区分窗口/标签模式 - 更新所有调用点使用新的统一接口
This commit is contained in:
parent
fc74e0d952
commit
16d5ffd4f9
144
electron/electron.js
vendored
144
electron/electron.js
vendored
@ -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"
|
||||
})
|
||||
|
||||
|
||||
@ -608,7 +608,7 @@ export default {
|
||||
return false;
|
||||
}
|
||||
// 使用内置浏览器打开
|
||||
this.$store.dispatch("openWebTabWindow", url)
|
||||
this.$store.dispatch("openWindow", url)
|
||||
return true
|
||||
}
|
||||
this.$Electron.listener('browserWindowBlur', _ => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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/",
|
||||
|
||||
@ -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/",
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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/",
|
||||
|
||||
@ -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/",
|
||||
|
||||
56
resources/assets/js/store/actions.js
vendored
56
resources/assets/js/store/actions.js
vendored
@ -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,
|
||||
|
||||
2
resources/assets/js/utils/file.js
vendored
2
resources/assets/js/utils/file.js
vendored
@ -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/",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user