diff --git a/frontend/src/App.tool-center.test.ts b/frontend/src/App.tool-center.test.ts index e85f16d..5ad5a57 100644 --- a/frontend/src/App.tool-center.test.ts +++ b/frontend/src/App.tool-center.test.ts @@ -162,6 +162,8 @@ describe('tool center menu entries', () => { ['runQuery', 'gonavi:run-active-query'], ['focusSidebarSearch', 'gonavi:focus-sidebar-search'], ['newQueryTab', 'handleNewQuery();'], + ['switchToNextTab', 'switchActiveTabByOffset(1);'], + ['switchToPreviousTab', 'switchActiveTabByOffset(-1);'], ['newConnection', 'handleCreateConnection();'], ['toggleAIPanel', 'toggleAIPanel();'], ['toggleLogPanel', 'handleToggleLogPanel();'], @@ -174,8 +176,11 @@ describe('tool center menu entries', () => { for (const [action, handler] of expectedHandlers) { expect(getGlobalShortcutCaseBlock(action)).toContain(handler); } + expect(appSource).toContain('const switchActiveTabByOffset = useCallback((offset: 1 | -1) => {'); + expect(appSource).toContain('const nextIndex = (baseIndex + offset + tabs.length) % tabs.length;'); + expect(appSource).toContain('setActiveTab(tabs[nextIndex].id);'); expect(appSource).toContain('handleCreateConnection, handleManualResetWindowZoom'); - expect(appSource).toContain('setTheme, toggleAIPanel, useNativeMacWindowControls'); + expect(appSource).toContain('switchActiveTabByOffset, themeMode'); }); it('captures global shortcuts before Monaco/editor defaults consume them', () => { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 21ef1ba..a7757bb 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1141,6 +1141,7 @@ function App() { const connections = useStore(state => state.connections); const tabs = useStore(state => state.tabs); const activeTabId = useStore(state => state.activeTabId); + const setActiveTab = useStore(state => state.setActiveTab); const openSecurityUpdateSettings = useCallback((focusTarget: SecurityUpdateSettingsFocusTarget | null = null) => { setIsSecurityUpdateIntroOpen(false); setSecurityUpdateSettingsFocusTarget(focusTarget); @@ -1893,6 +1894,14 @@ function App() { }); }, [activeTabId, tabs, connections, activeContext, addTab]); + const switchActiveTabByOffset = useCallback((offset: 1 | -1) => { + if (tabs.length < 2) return; + const activeIndex = tabs.findIndex(tab => tab.id === activeTabId); + const baseIndex = activeIndex >= 0 ? activeIndex : 0; + const nextIndex = (baseIndex + offset + tabs.length) % tabs.length; + setActiveTab(tabs[nextIndex].id); + }, [activeTabId, setActiveTab, tabs]); + const closeConnectionPackageDialog = useCallback(() => { setConnectionPackageDialog(createClosedConnectionPackageDialogState()); setPendingConnectionImportPayload(null); @@ -2927,6 +2936,12 @@ function App() { case 'newQueryTab': handleNewQuery(); break; + case 'switchToNextTab': + switchActiveTabByOffset(1); + break; + case 'switchToPreviousTab': + switchActiveTabByOffset(-1); + break; case 'newConnection': handleCreateConnection(); break; @@ -2957,7 +2972,7 @@ function App() { return () => { window.removeEventListener('keydown', handleGlobalShortcut, true); }; - }, [activeShortcutPlatform, handleCreateConnection, handleManualResetWindowZoom, handleNewQuery, handleTitleBarWindowToggle, handleToggleLogPanel, isMacRuntime, shortcutOptions, themeMode, setTheme, toggleAIPanel, useNativeMacWindowControls]); + }, [activeShortcutPlatform, handleCreateConnection, handleManualResetWindowZoom, handleNewQuery, handleTitleBarWindowToggle, handleToggleLogPanel, isMacRuntime, shortcutOptions, switchActiveTabByOffset, themeMode, setTheme, toggleAIPanel, useNativeMacWindowControls]); useEffect(() => { if (!capturingShortcutAction) { diff --git a/frontend/src/utils/shortcuts.test.ts b/frontend/src/utils/shortcuts.test.ts index 1bf18c3..c896a3e 100644 --- a/frontend/src/utils/shortcuts.test.ts +++ b/frontend/src/utils/shortcuts.test.ts @@ -200,6 +200,14 @@ describe('shortcut defaults', () => { mac: { combo: 'Meta+N', enabled: true }, windows: { combo: 'Ctrl+N', enabled: true }, }); + expect(DEFAULT_SHORTCUT_OPTIONS.switchToNextTab).toEqual({ + mac: { combo: 'Ctrl+Tab', enabled: true }, + windows: { combo: 'Ctrl+Tab', enabled: true }, + }); + expect(DEFAULT_SHORTCUT_OPTIONS.switchToPreviousTab).toEqual({ + mac: { combo: 'Ctrl+Shift+Tab', enabled: true }, + windows: { combo: 'Ctrl+Shift+Tab', enabled: true }, + }); expect(DEFAULT_SHORTCUT_OPTIONS.toggleLogPanel).toEqual({ mac: { combo: 'Meta+Shift+H', enabled: true }, windows: { combo: 'Ctrl+H', enabled: true }, diff --git a/frontend/src/utils/shortcuts.ts b/frontend/src/utils/shortcuts.ts index 7308469..937b609 100644 --- a/frontend/src/utils/shortcuts.ts +++ b/frontend/src/utils/shortcuts.ts @@ -7,6 +7,8 @@ export type ShortcutAction = | 'sendAIChatMessage' | 'focusSidebarSearch' | 'newQueryTab' + | 'switchToNextTab' + | 'switchToPreviousTab' | 'newConnection' | 'toggleAIPanel' | 'toggleLogPanel' @@ -92,6 +94,8 @@ export const SHORTCUT_ACTION_ORDER: ShortcutAction[] = [ 'sendAIChatMessage', 'focusSidebarSearch', 'newQueryTab', + 'switchToNextTab', + 'switchToPreviousTab', 'newConnection', 'toggleAIPanel', 'toggleLogPanel', @@ -135,6 +139,16 @@ export const SHORTCUT_ACTION_META: Record = label: '新建查询页', description: '创建一个新的 SQL 查询标签页', }, + switchToNextTab: { + label: '切换到下一个标签页', + description: '在打开的标签页中向右切换', + allowInEditable: true, + }, + switchToPreviousTab: { + label: '切换到上一个标签页', + description: '在打开的标签页中向左切换', + allowInEditable: true, + }, newConnection: { label: '新建数据源', description: '创建新的数据库、运行时或其他数据源连接', @@ -194,6 +208,14 @@ export const DEFAULT_SHORTCUT_OPTIONS: ShortcutOptions = { mac: { combo: 'Meta+N', enabled: true }, windows: { combo: 'Ctrl+N', enabled: true }, }, + switchToNextTab: { + mac: { combo: 'Ctrl+Tab', enabled: true }, + windows: { combo: 'Ctrl+Tab', enabled: true }, + }, + switchToPreviousTab: { + mac: { combo: 'Ctrl+Shift+Tab', enabled: true }, + windows: { combo: 'Ctrl+Shift+Tab', enabled: true }, + }, newConnection: { mac: { combo: 'Meta+Shift+N', enabled: true }, windows: { combo: 'Ctrl+Shift+N', enabled: true },