|
|
@@ -74,6 +74,13 @@ function isHttpUrl(url: string): boolean {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+/** 接口可能返回 number,与 IPC 载荷对齐为 string,供 appId 复用标签 */
|
|
|
+function normalizeInternalBrowserAppId(raw: unknown): string | undefined {
|
|
|
+ if (typeof raw === 'string' && raw.length > 0) return raw
|
|
|
+ if (typeof raw === 'number' && Number.isFinite(raw)) return String(raw)
|
|
|
+ return undefined
|
|
|
+}
|
|
|
+
|
|
|
const LAUNCHPAD_ICON_SUBDIR = 'launchpad-icons'
|
|
|
|
|
|
function getLaunchpadIconCacheDir(): string {
|
|
|
@@ -440,11 +447,15 @@ interface TabInfo {
|
|
|
url: string
|
|
|
title: string
|
|
|
view: WebContentsView
|
|
|
+ /** 应用中心 SSO 打开时绑定,用于同一应用复用同一标签 */
|
|
|
+ appId?: string
|
|
|
}
|
|
|
|
|
|
class ViewManager {
|
|
|
private window: BrowserWindow
|
|
|
private tabs: Map<string, TabInfo> = new Map()
|
|
|
+ /** app_id → 标签 id(仅登记带 appId 的标签) */
|
|
|
+ private appIdToTabId: Map<string, string> = new Map()
|
|
|
private activeTabId: string | null = null
|
|
|
private bounds: Electron.Rectangle = { x: 0, y: 78, width: 1024, height: 600 }
|
|
|
|
|
|
@@ -452,7 +463,44 @@ class ViewManager {
|
|
|
this.window = window
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 若提供 appId 且已有对应标签,则在该标签中加载 url;否则新建标签。
|
|
|
+ */
|
|
|
+ openOrNavigateTab(
|
|
|
+ url: string,
|
|
|
+ opts?: { appId?: string; activate?: boolean }
|
|
|
+ ): { id: string; reused: boolean } {
|
|
|
+ const activate = opts?.activate !== false
|
|
|
+ const appId = opts?.appId
|
|
|
+ if (appId) {
|
|
|
+ const existingId = this.appIdToTabId.get(appId)
|
|
|
+ if (existingId && this.tabs.has(existingId)) {
|
|
|
+ if (activate) {
|
|
|
+ this.switchTab(existingId)
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ this.tabs.get(existingId)!.view.webContents.loadURL(url)
|
|
|
+ } catch (e) {
|
|
|
+ console.error('Failed to load URL:', url, e)
|
|
|
+ }
|
|
|
+ return { id: existingId, reused: true }
|
|
|
+ }
|
|
|
+ if (existingId && !this.tabs.has(existingId)) {
|
|
|
+ this.appIdToTabId.delete(appId)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const id = this.createNewTab(url, activate, appId)
|
|
|
+ if (appId) {
|
|
|
+ this.appIdToTabId.set(appId, id)
|
|
|
+ }
|
|
|
+ return { id, reused: false }
|
|
|
+ }
|
|
|
+
|
|
|
createTab(url: string, active: boolean = true): string {
|
|
|
+ return this.createNewTab(url, active, undefined)
|
|
|
+ }
|
|
|
+
|
|
|
+ private createNewTab(url: string, active: boolean, appId: string | undefined): string {
|
|
|
const view = new WebContentsView({
|
|
|
webPreferences: {
|
|
|
partition: INTERNAL_BROWSER_PARTITION,
|
|
|
@@ -515,7 +563,7 @@ class ViewManager {
|
|
|
console.error('Failed to load URL:', url, e)
|
|
|
}
|
|
|
|
|
|
- this.tabs.set(id, { id, url, title: 'Loading...', view })
|
|
|
+ this.tabs.set(id, { id, url, title: 'Loading...', view, appId })
|
|
|
|
|
|
if (active) {
|
|
|
this.switchTab(id)
|
|
|
@@ -612,6 +660,7 @@ class ViewManager {
|
|
|
} catch (e) { /* already destroyed */ }
|
|
|
})
|
|
|
this.tabs.clear()
|
|
|
+ this.appIdToTabId.clear()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1060,13 +1109,23 @@ function createWindow(): void {
|
|
|
let browserWindow: BrowserWindow | null = null
|
|
|
let viewManager: ViewManager | null = null
|
|
|
/** 新建内置浏览器窗口时,首标签在渲染进程就绪后再同步,避免 tab-created 早于 listener */
|
|
|
-let pendingBrowserInitialUrl: string | null = null
|
|
|
+let pendingBrowserInitialUrl: { url: string; appId?: string } | null = null
|
|
|
+
|
|
|
+function sendBrowserTabOpenToRenderer(url: string, result: { id: string; reused: boolean }): void {
|
|
|
+ if (!browserWindow || browserWindow.isDestroyed()) return
|
|
|
+ if (result.reused) {
|
|
|
+ browserWindow.webContents.send('tab-activate', result.id)
|
|
|
+ } else {
|
|
|
+ browserWindow.webContents.send('tab-created', { id: result.id, url, title: 'Loading...' })
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
function flushPendingBrowserInitialTab(): void {
|
|
|
if (!pendingBrowserInitialUrl || !viewManager || !browserWindow || browserWindow.isDestroyed()) return
|
|
|
- const url = pendingBrowserInitialUrl
|
|
|
+ const { url, appId } = pendingBrowserInitialUrl
|
|
|
pendingBrowserInitialUrl = null
|
|
|
- viewManager.createTab(url, true)
|
|
|
+ const result = viewManager.openOrNavigateTab(url, { appId, activate: true })
|
|
|
+ sendBrowserTabOpenToRenderer(url, result)
|
|
|
}
|
|
|
|
|
|
function syncBrowserTabsToRenderer(): void {
|
|
|
@@ -1077,16 +1136,15 @@ function syncBrowserTabsToRenderer(): void {
|
|
|
}
|
|
|
|
|
|
// Function to create the custom browser window
|
|
|
-function createInternalBrowserWindow(targetUrl: string): void {
|
|
|
+function createInternalBrowserWindow(targetUrl: string, appId?: string): void {
|
|
|
if (browserWindow && !browserWindow.isDestroyed()) {
|
|
|
if (browserWindow.isMinimized()) browserWindow.restore()
|
|
|
browserWindow.show()
|
|
|
browserWindow.focus()
|
|
|
-
|
|
|
- // 如果窗口已存在,且管理器存在,则新建标签
|
|
|
+
|
|
|
if (viewManager) {
|
|
|
- const id = viewManager.createTab(targetUrl, true)
|
|
|
- browserWindow.webContents.send('tab-created', { id, url: targetUrl, title: 'Loading...' })
|
|
|
+ const result = viewManager.openOrNavigateTab(targetUrl, { appId, activate: true })
|
|
|
+ sendBrowserTabOpenToRenderer(targetUrl, result)
|
|
|
}
|
|
|
return
|
|
|
}
|
|
|
@@ -1115,7 +1173,7 @@ function createInternalBrowserWindow(targetUrl: string): void {
|
|
|
browserWindow.maximize()
|
|
|
|
|
|
viewManager = new ViewManager(browserWindow)
|
|
|
- pendingBrowserInitialUrl = targetUrl
|
|
|
+ pendingBrowserInitialUrl = { url: targetUrl, appId }
|
|
|
|
|
|
// Load the browser route with initial URL
|
|
|
// We use hash parameter to avoid Vite dev server 403 error with query params on root
|
|
|
@@ -1351,15 +1409,28 @@ app.whenReady().then(() => {
|
|
|
updateUnreadStatus()
|
|
|
})
|
|
|
|
|
|
- // 处理打开 URL 请求(用于通知消息的 SSO 跳转)
|
|
|
+ // 主窗口专用:应用中心 SSO 带 app_id 打开内置浏览器(同 app 复用标签)
|
|
|
+ ipcMain.on('internal-browser-open', (event, payload: unknown) => {
|
|
|
+ if (!mainWindow || mainWindow.isDestroyed()) return
|
|
|
+ if (event.sender.id !== mainWindow.webContents.id) return
|
|
|
+ const p = payload as { url?: unknown; appId?: unknown }
|
|
|
+ const url = typeof p.url === 'string' ? p.url : ''
|
|
|
+ if (!isHttpUrl(url)) return
|
|
|
+ const appId = normalizeInternalBrowserAppId(p.appId)
|
|
|
+ createInternalBrowserWindow(url, appId)
|
|
|
+ })
|
|
|
+
|
|
|
+ // 处理打开 URL 请求(用于通知消息的 SSO 跳转等)
|
|
|
ipcMain.on('open-url', (_, url: string) => {
|
|
|
- if (url && (url.startsWith('http://') || url.startsWith('https://'))) {
|
|
|
- // 使用内部浏览器窗口打开
|
|
|
- if (viewManager) {
|
|
|
- viewManager.createTab(url, true)
|
|
|
+ if (url && isHttpUrl(url)) {
|
|
|
+ if (viewManager && browserWindow && !browserWindow.isDestroyed()) {
|
|
|
+ const result = viewManager.openOrNavigateTab(url, { activate: true })
|
|
|
+ sendBrowserTabOpenToRenderer(url, result)
|
|
|
+ if (browserWindow.isMinimized()) browserWindow.restore()
|
|
|
+ browserWindow.show()
|
|
|
+ browserWindow.focus()
|
|
|
} else {
|
|
|
- // 如果浏览器窗口未初始化,使用系统默认浏览器
|
|
|
- shell.openExternal(url)
|
|
|
+ createInternalBrowserWindow(url)
|
|
|
}
|
|
|
}
|
|
|
})
|