feat: add 'pop out tab as new window' functionality

- Add createWindow IPC for creating secondary windows
- Add PopoutPage for content-only rendering in new windows
- Add multi-window management to electron state
- Add '在新窗口中打开' context menu to tabs
- Fix: Use standard URL path instead of hash for React Router routing
This commit is contained in:
2026-03-21 23:42:48 +08:00
parent 43828a87f0
commit f160adbdb1
9 changed files with 198 additions and 1 deletions

View File

@@ -98,6 +98,65 @@ async function createWindow() {
}
}
async function createSecondaryWindow(tabData: { route: string; title: string }): Promise<number> {
const initialSymbolColor = nativeTheme.shouldUseDarkColors ? '#ffffff' : '#000000';
const win = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
autoHideMenuBar: true,
titleBarStyle: 'hidden',
titleBarOverlay: {
color: '#00000000',
symbolColor: initialSymbolColor,
height: 32,
},
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: false,
webviewTag: true,
preload: path.join(__dirname, 'preload.cjs'),
},
});
electronState.addWindow(win);
win.setMenu(null);
win.on('closed', () => {
electronState.removeWindow(win.id);
});
win.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith('http:') || url.startsWith('https:')) {
shell.openExternal(url);
return { action: 'deny' };
}
return { action: 'allow' };
});
const baseUrl = electronState.isDevelopment()
? 'http://localhost:5173'
: `http://localhost:${electronState.getServerPort()}`;
const fullUrl = `${baseUrl}${tabData.route}`;
log.info(`[PopOut] Loading secondary window with URL: ${fullUrl}`);
try {
await win.loadURL(fullUrl);
} catch (e) {
log.error('[PopOut] Failed to load URL:', e);
}
if (electronState.isDevelopment()) {
win.webContents.openDevTools();
}
return win.id;
}
ipcMain.handle('export-pdf', async (event, title, htmlContent) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (!win) return { success: false, error: 'No window found' };
@@ -421,6 +480,17 @@ ipcMain.handle('terminal-stop', async () => {
return await terminalService.stop();
});
ipcMain.handle('create-window', async (_event, tabData: { route: string; title: string }) => {
try {
log.info('[PopOut] Creating new window for:', tabData);
const windowId = await createSecondaryWindow(tabData);
return { success: true, windowId };
} catch (error: any) {
log.error('[PopOut] Failed to create window:', error);
return { success: false, error: error.message };
}
});
async function startServer() {
if (electronState.isDevelopment()) {
log.info('In dev mode, assuming external servers are running.');

View File

@@ -53,4 +53,5 @@ contextBridge.exposeInMainWorld('electronAPI', {
terminalStop: () => ipcRenderer.invoke('terminal-stop'),
terminalGetStatus: () => ipcRenderer.invoke('terminal-get-status'),
terminalGetPort: () => ipcRenderer.invoke('terminal-get-port'),
createWindow: (tabData: { route: string; title: string }) => ipcRenderer.invoke('create-window', tabData),
})

View File

@@ -13,6 +13,8 @@ class ElectronState {
isDev: false,
}
private windows = new Map<number, BrowserWindow>()
getMainWindow(): BrowserWindow | null {
return this.state.mainWindow
}
@@ -37,7 +39,24 @@ class ElectronState {
this.state.isDev = isDev
}
addWindow(window: BrowserWindow): void {
this.windows.set(window.id, window)
}
removeWindow(id: number): void {
this.windows.delete(id)
}
getWindow(id: number): BrowserWindow | undefined {
return this.windows.get(id)
}
getAllWindows(): BrowserWindow[] {
return Array.from(this.windows.values())
}
reset(): void {
this.windows.clear()
this.state = {
mainWindow: null,
serverPort: 3001,