浏览代码

V1.1.6改善重复打开标签

liuq 3 周之前
父节点
当前提交
afcac7ac73
共有 3 个文件被更改,包括 109 次插入18 次删除
  1. 88 17
      src/main/index.ts
  2. 7 1
      src/renderer/src/BrowserWindow.tsx
  3. 14 0
      src/renderer/src/components/AppMappingList.tsx

+ 88 - 17
src/main/index.ts

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

+ 7 - 1
src/renderer/src/BrowserWindow.tsx

@@ -23,6 +23,10 @@ const BrowserWindow: React.FC = () => {
     setTabs((prev) => prev.map((t) => (t.id === id ? { ...t, ...updates } : t)))
   }, [])
 
+  const handleTabActivate = useCallback((_event: unknown, id: string) => {
+    setActiveTabId(id)
+  }, [])
+
   const handleTabsSync = useCallback(
     (_event: unknown, snapshots: Array<{ id: string; url: string; title: string }>) => {
       if (!snapshots?.length) return
@@ -65,6 +69,7 @@ const BrowserWindow: React.FC = () => {
 
     ipc.on('tab-created', handleTabCreated)
     ipc.on('tab-update', handleTabUpdate)
+    ipc.on('tab-activate', handleTabActivate)
     ipc.on('browser-tabs-sync', handleTabsSync)
     ipc.send('browser-action', { type: 'browser-ui-ready' })
 
@@ -74,10 +79,11 @@ const BrowserWindow: React.FC = () => {
     return () => {
       ipc.removeListener('tab-created', handleTabCreated)
       ipc.removeListener('tab-update', handleTabUpdate)
+      ipc.removeListener('tab-activate', handleTabActivate)
       ipc.removeListener('browser-tabs-sync', handleTabsSync)
       window.removeEventListener('resize', updateViewBounds)
     }
-  }, [handleTabCreated, handleTabUpdate, handleTabsSync, updateViewBounds])
+  }, [handleTabActivate, handleTabCreated, handleTabUpdate, handleTabsSync, updateViewBounds])
   
   // 当 tabs 或 activeTabId 变化导致布局变化时,调整 BrowserView 的 bounds
   useEffect(() => {

+ 14 - 0
src/renderer/src/components/AppMappingList.tsx

@@ -241,6 +241,13 @@ const AppMappingList: React.FC<AppMappingListProps> = ({ token, currentUserId })
           }
           if (openInSystemBrowser && window.electron?.ipcRenderer) {
             window.electron.ipcRenderer.send('open-url-external', normalized)
+          } else if (window.electron?.ipcRenderer) {
+            window.electron.ipcRenderer.send('internal-browser-open', {
+              url: normalized,
+              ...(mapping.app_id != null && String(mapping.app_id) !== ''
+                ? { appId: String(mapping.app_id) }
+                : {})
+            })
           } else {
             window.open(normalized, '_blank')
           }
@@ -294,6 +301,13 @@ const AppMappingList: React.FC<AppMappingListProps> = ({ token, currentUserId })
           }
           if (openInSystemBrowser && window.electron?.ipcRenderer) {
             window.electron.ipcRenderer.send('open-url-external', normalized)
+          } else if (window.electron?.ipcRenderer) {
+            window.electron.ipcRenderer.send('internal-browser-open', {
+              url: normalized,
+              ...(item.app_id != null && String(item.app_id) !== ''
+                ? { appId: String(item.app_id) }
+                : {})
+            })
           } else {
             window.open(normalized, '_blank')
           }