feat(shortcuts): 新增标签页切换快捷键

- 新增切换到下一个标签页动作,默认 Ctrl+Tab
- 新增切换到上一个标签页动作,默认 Ctrl+Shift+Tab
- 接入全局快捷键处理,按当前标签顺序首尾循环切换
- 补充快捷键默认值与全局执行链路测试

Refs #399
This commit is contained in:
Syngnat
2026-06-01 11:05:06 +08:00
parent 2fee3d1389
commit b85e7491a9
4 changed files with 52 additions and 2 deletions

View File

@@ -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', () => {

View File

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

View File

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

View File

@@ -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<ShortcutAction, ShortcutActionMeta> =
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 },