️ perf: move settings into one page (#10229)

* move settings into one page

* fix: change the jump link to react-router-dom

---------

Co-authored-by: ONLY-yours <1349021570@qq.com>
This commit is contained in:
Arvin Xu
2025-11-24 15:10:41 +08:00
committed by GitHub
parent 3e820fd6b7
commit 1fb7b292ca
8 changed files with 64 additions and 204 deletions

View File

@@ -33,12 +33,6 @@ export interface RouteInterceptConfig {
* 定义了所有需要特殊处理的路由
*/
export const interceptRoutes: RouteInterceptConfig[] = [
{
description: '设置页面',
enabled: true,
pathPrefix: '/settings',
targetWindow: 'settings',
},
{
description: '开发者工具',
enabled: true,

View File

@@ -3,7 +3,6 @@ import type { BrowserWindowOpts } from './core/browser/Browser';
export const BrowsersIdentifiers = {
chat: 'chat',
devtools: 'devtools',
settings: 'settings',
};
export const appBrowsers = {
@@ -32,18 +31,6 @@ export const appBrowsers = {
vibrancy: 'under-window',
width: 1000,
},
settings: {
autoHideMenuBar: true,
height: 800,
identifier: 'settings',
keepAlive: true,
minWidth: 600,
parentIdentifier: 'chat',
path: '/settings',
titleBarStyle: 'hidden',
vibrancy: 'under-window',
width: 1000,
},
} satisfies Record<string, BrowserWindowOpts>;
// Window templates for multi-instance windows

View File

@@ -1,9 +1,8 @@
import { InterceptRouteParams, OpenSettingsWindowOptions } from '@lobechat/electron-client-ipc';
import { extractSubPath, findMatchingRoute } from '~common/routes';
import { findMatchingRoute } from '~common/routes';
import {
AppBrowsersIdentifiers,
BrowsersIdentifiers,
WindowTemplateIdentifiers,
} from '@/appBrowsers';
import { IpcClientEventSender } from '@/types/ipcClientEvent';
@@ -24,14 +23,32 @@ export default class BrowserWindowsCtr extends ControllerModule {
? { tab: typeof options === 'string' ? options : undefined }
: options;
console.log('[BrowserWindowsCtr] Received request to open settings window', normalizedOptions);
console.log('[BrowserWindowsCtr] Received request to open settings', normalizedOptions);
try {
await this.app.browserManager.showSettingsWindowWithTab(normalizedOptions);
const query = new URLSearchParams();
if (normalizedOptions.searchParams) {
Object.entries(normalizedOptions.searchParams).forEach(([key, value]) => {
if (value !== undefined) query.set(key, value);
});
}
const tab = normalizedOptions.tab;
if (tab && tab !== 'common' && !query.has('active')) {
query.set('active', tab);
}
const queryString = query.toString();
const subPath = tab && !queryString ? `/${tab}` : '';
const fullPath = `/settings${subPath}${queryString ? `?${queryString}` : ''}`;
const mainWindow = this.app.browserManager.getMainWindow();
await mainWindow.loadUrl(fullPath);
mainWindow.show();
return { success: true };
} catch (error) {
console.error('[BrowserWindowsCtr] Failed to open settings window:', error);
console.error('[BrowserWindowsCtr] Failed to open settings:', error);
return { error: error.message, success: false };
}
}
@@ -76,50 +93,14 @@ export default class BrowserWindowsCtr extends ControllerModule {
);
try {
if (matchedRoute.targetWindow === BrowsersIdentifiers.settings) {
const extractedSubPath = extractSubPath(path, matchedRoute.pathPrefix);
const sanitizedSubPath =
extractedSubPath && !extractedSubPath.startsWith('?') ? extractedSubPath : undefined;
let searchParams: Record<string, string> | undefined;
try {
const url = new URL(params.url);
const entries = Array.from(url.searchParams.entries());
if (entries.length > 0) {
searchParams = entries.reduce<Record<string, string>>((acc, [key, value]) => {
acc[key] = value;
return acc;
}, {});
}
} catch (error) {
console.warn(
'[BrowserWindowsCtr] Failed to parse URL for settings route interception:',
params.url,
error,
);
}
await this.openTargetWindow(matchedRoute.targetWindow as AppBrowsersIdentifiers);
await this.app.browserManager.showSettingsWindowWithTab({
searchParams,
tab: sanitizedSubPath,
});
return {
intercepted: true,
path,
source,
subPath: sanitizedSubPath,
targetWindow: matchedRoute.targetWindow,
};
} else {
await this.openTargetWindow(matchedRoute.targetWindow as AppBrowsersIdentifiers);
return {
intercepted: true,
path,
source,
targetWindow: matchedRoute.targetWindow,
};
}
return {
intercepted: true,
path,
source,
targetWindow: matchedRoute.targetWindow,
};
} catch (error) {
console.error('[BrowserWindowsCtr] Error while processing route interception:', error);
return {

View File

@@ -9,36 +9,40 @@ import BrowserWindowsCtr from '../BrowserWindowsCtr';
// 模拟 App 及其依赖项
const mockToggleVisible = vi.fn();
const mockShowSettingsWindowWithTab = vi.fn();
const mockLoadUrl = vi.fn();
const mockShow = vi.fn();
const mockRedirectToPage = vi.fn();
const mockCloseWindow = vi.fn();
const mockMinimizeWindow = vi.fn();
const mockMaximizeWindow = vi.fn();
const mockRetrieveByIdentifier = vi.fn();
const mockGetMainWindow = vi.fn(() => ({
toggleVisible: mockToggleVisible,
loadUrl: mockLoadUrl,
show: mockShow,
}));
const mockShow = vi.fn();
const mockShowOther = vi.fn();
// mock findMatchingRoute and extractSubPath
vi.mock('~common/routes', async () => ({
findMatchingRoute: vi.fn(),
extractSubPath: vi.fn(),
}));
const { findMatchingRoute, extractSubPath } = await import('~common/routes');
const { findMatchingRoute } = await import('~common/routes');
const mockApp = {
browserManager: {
getMainWindow: mockGetMainWindow,
showSettingsWindowWithTab: mockShowSettingsWindowWithTab,
redirectToPage: mockRedirectToPage,
closeWindow: mockCloseWindow,
minimizeWindow: mockMinimizeWindow,
maximizeWindow: mockMaximizeWindow,
retrieveByIdentifier: mockRetrieveByIdentifier.mockImplementation(
(identifier: AppBrowsersIdentifiers | string) => {
if (identifier === BrowsersIdentifiers.settings || identifier === 'some-other-window') {
return { show: mockShow };
if (identifier === 'some-other-window') {
return { show: mockShowOther };
}
return { show: mockShow }; // Default mock for other identifiers
return { show: mockShowOther }; // Default mock for other identifiers
},
),
},
@@ -61,16 +65,18 @@ describe('BrowserWindowsCtr', () => {
});
describe('openSettingsWindow', () => {
it('should show the settings window with the specified tab', async () => {
it('should navigate to settings in main window with the specified tab', async () => {
const tab = 'appearance';
const result = await browserWindowsCtr.openSettingsWindow(tab);
expect(mockShowSettingsWindowWithTab).toHaveBeenCalledWith({ tab });
expect(mockGetMainWindow).toHaveBeenCalled();
expect(mockLoadUrl).toHaveBeenCalledWith('/settings?active=appearance');
expect(mockShow).toHaveBeenCalled();
expect(result).toEqual({ success: true });
});
it('should return error if showing settings window fails', async () => {
const errorMessage = 'Failed to show';
mockShowSettingsWindowWithTab.mockRejectedValueOnce(new Error(errorMessage));
it('should return error if navigation fails', async () => {
const errorMessage = 'Failed to navigate';
mockLoadUrl.mockRejectedValueOnce(new Error(errorMessage));
const result = await browserWindowsCtr.openSettingsWindow('display');
expect(result).toEqual({ error: errorMessage, success: false });
});
@@ -117,36 +123,7 @@ describe('BrowserWindowsCtr', () => {
expect(result).toEqual({ intercepted: false, path: params.path, source: params.source });
});
it('should show settings window if matched route target is settings', async () => {
const params: InterceptRouteParams = {
...baseParams,
path: '/settings/provider',
url: 'app://host/settings/provider?active=provider&provider=ollama',
};
const matchedRoute = { targetWindow: BrowsersIdentifiers.settings, pathPrefix: '/settings' };
const subPath = 'provider';
(findMatchingRoute as Mock).mockReturnValue(matchedRoute);
(extractSubPath as Mock).mockReturnValue(subPath);
const result = await browserWindowsCtr.interceptRoute(params);
expect(findMatchingRoute).toHaveBeenCalledWith(params.path);
expect(extractSubPath).toHaveBeenCalledWith(params.path, matchedRoute.pathPrefix);
expect(mockShowSettingsWindowWithTab).toHaveBeenCalledWith({
searchParams: { active: 'provider', provider: 'ollama' },
tab: subPath,
});
expect(result).toEqual({
intercepted: true,
path: params.path,
source: params.source,
subPath,
targetWindow: matchedRoute.targetWindow,
});
expect(mockShow).not.toHaveBeenCalled();
});
it('should open target window if matched route target is not settings', async () => {
it('should open target window if matched route is found', async () => {
const params: InterceptRouteParams = {
...baseParams,
path: '/other/page',
@@ -160,44 +137,16 @@ describe('BrowserWindowsCtr', () => {
expect(findMatchingRoute).toHaveBeenCalledWith(params.path);
expect(mockRetrieveByIdentifier).toHaveBeenCalledWith(targetWindowIdentifier);
expect(mockShow).toHaveBeenCalled();
expect(mockShowOther).toHaveBeenCalled();
expect(result).toEqual({
intercepted: true,
path: params.path,
source: params.source,
targetWindow: matchedRoute.targetWindow,
});
expect(mockShowSettingsWindowWithTab).not.toHaveBeenCalled();
});
it('should return error if processing route interception fails for settings', async () => {
const params: InterceptRouteParams = {
...baseParams,
path: '/settings',
url: 'app://host/settings?active=general',
};
const matchedRoute = { targetWindow: BrowsersIdentifiers.settings, pathPrefix: '/settings' };
const subPath = undefined;
const errorMessage = 'Processing error for settings';
(findMatchingRoute as Mock).mockReturnValue(matchedRoute);
(extractSubPath as Mock).mockReturnValue(subPath);
mockShowSettingsWindowWithTab.mockRejectedValueOnce(new Error(errorMessage));
const result = await browserWindowsCtr.interceptRoute(params);
expect(mockShowSettingsWindowWithTab).toHaveBeenCalledWith({
searchParams: { active: 'general' },
tab: subPath,
});
expect(result).toEqual({
error: errorMessage,
intercepted: false,
path: params.path,
source: params.source,
});
});
it('should return error if processing route interception fails for other window', async () => {
it('should return error if processing route interception fails', async () => {
const params: InterceptRouteParams = {
...baseParams,
path: '/another/custom',

View File

@@ -336,6 +336,7 @@ export default class Browser {
vibrancy: 'sidebar',
visualEffectState: 'active',
webPreferences: {
backgroundThrottling: false,
contextIsolation: true,
preload: join(preloadDir, 'index.js'),
},

View File

@@ -1,8 +1,4 @@
import {
MainBroadcastEventKey,
MainBroadcastParams,
OpenSettingsWindowOptions,
} from '@lobechat/electron-client-ipc';
import { MainBroadcastEventKey, MainBroadcastParams } from '@lobechat/electron-client-ipc';
import { WebContents } from 'electron';
import { createLogger } from '@/utils/logger';
@@ -42,13 +38,6 @@ export class BrowserManager {
window.show();
}
showSettingsWindow() {
logger.debug('Showing settings window');
const window = this.retrieveByIdentifier('settings');
window.show();
return window;
}
broadcastToAllWindows = <T extends MainBroadcastEventKey>(
event: T,
data: MainBroadcastParams<T>,
@@ -68,50 +57,6 @@ export class BrowserManager {
this.browsers.get(identifier)?.broadcast(event, data);
};
/**
* Display the settings window and navigate to a specific tab
* @param tab Settings window sub-path tab
*/
async showSettingsWindowWithTab(options?: OpenSettingsWindowOptions) {
const tab = options?.tab;
const searchParams = options?.searchParams;
const query = new URLSearchParams();
if (searchParams) {
Object.entries(searchParams).forEach(([key, value]) => {
if (value !== undefined) query.set(key, value);
});
}
if (tab && tab !== 'common' && !query.has('active')) {
query.set('active', tab);
}
const queryString = query.toString();
const activeTab = query.get('active') ?? tab;
logger.debug(
`Showing settings window with navigation: active=${activeTab || 'default'}, query=${
queryString || 'none'
}`,
);
if (queryString) {
const browser = await this.redirectToPage('settings', undefined, queryString);
// make provider page more large
if (activeTab?.startsWith('provider')) {
logger.debug('Resizing window for provider settings');
browser.setWindowSize({ height: 1000, width: 1400 });
browser.moveToCenter();
}
return browser;
} else {
return this.showSettingsWindow();
}
}
/**
* Navigate window to specific sub-path
* @param identifier Window identifier

View File

@@ -81,8 +81,10 @@ export class MacOSMenu extends BaseMenuPlatform implements IMenuPlatform {
{ type: 'separator' },
{
accelerator: 'Command+,',
click: () => {
this.app.browserManager.showSettingsWindow();
click: async () => {
const mainWindow = this.app.browserManager.getMainWindow();
await mainWindow.loadUrl('/settings');
mainWindow.show();
},
label: t('macOS.preferences'),
},
@@ -337,7 +339,11 @@ export class MacOSMenu extends BaseMenuPlatform implements IMenuPlatform {
label: t('tray.show', { appName }),
},
{
click: () => this.app.browserManager.retrieveByIdentifier('settings').show(),
click: async () => {
const mainWindow = this.app.browserManager.getMainWindow();
await mainWindow.loadUrl('/settings');
mainWindow.show();
},
label: t('file.preferences'),
},
{ type: 'separator' },