From a3f032b1d9be0b2ba2c91534195aba5ca5bb7b06 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Fri, 7 Aug 2026 10:06:22 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(titlebar):=20=E4=B8=BA?= =?UTF-8?q?=E6=96=B0=E5=BB=BA=E6=93=8D=E4=BD=9C=E6=8F=90=E7=A4=BA=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E5=BF=AB=E6=8D=B7=E9=94=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新建查询与新建连接悬浮提示展示当前生效快捷键 - 按 macOS 和 Windows 格式化组合键并跟随用户自定义绑定 - 快捷键禁用时回退操作名称并补齐平台与隔离回归测试 --- frontend/src/App.tsx | 16 ++++- .../TitleBarPrimaryActions.test.tsx | 67 ++++++++++++++++++- .../src/components/TitleBarPrimaryActions.tsx | 31 ++++++++- 3 files changed, 110 insertions(+), 4 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 75e75b60..1c0e7e8c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,7 +7,9 @@ import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } import { CSS } from '@dnd-kit/utilities'; import { BrowserOpenURL, Environment, EventsOn, WindowFullscreen, WindowGetPosition, WindowGetSize, WindowIsFullscreen, WindowIsMaximised, WindowIsMinimised, WindowIsNormal, WindowMaximise, WindowMinimise, WindowSetDarkTheme, WindowSetLightTheme, WindowSetPosition, WindowSetSize, WindowSetSystemDefaultTheme, WindowUnfullscreen, WindowUnmaximise } from '../wailsjs/runtime'; import Sidebar from './components/Sidebar'; -import TitleBarPrimaryActions from './components/TitleBarPrimaryActions'; +import TitleBarPrimaryActions, { + resolveTitleBarPrimaryActionShortcut, +} from './components/TitleBarPrimaryActions'; import TabManager from './components/TabManager'; import FloatingWorkbenchWindows from './components/FloatingWorkbenchWindows'; import FloatingAIChatWindow from './components/FloatingAIChatWindow'; @@ -2446,6 +2448,16 @@ function App() { || (runtimePlatform === '' && /mac/i.test(detectNavigatorPlatform())); const useNativeMacWindowControls = isMacRuntime; const activeShortcutPlatform = getShortcutPlatform(isMacRuntime); + const titleBarNewQueryShortcut = resolveTitleBarPrimaryActionShortcut( + shortcutOptions, + 'newQueryTab', + activeShortcutPlatform, + ); + const titleBarNewConnectionShortcut = resolveTitleBarPrimaryActionShortcut( + shortcutOptions, + 'newConnection', + activeShortcutPlatform, + ); const macWindowDiagnosticsEnabled = shouldEnableMacWindowDiagnostics( isMacRuntime, import.meta.env.DEV, @@ -7856,6 +7868,8 @@ function App() { diff --git a/frontend/src/components/TitleBarPrimaryActions.test.tsx b/frontend/src/components/TitleBarPrimaryActions.test.tsx index a781abff..303600e7 100644 --- a/frontend/src/components/TitleBarPrimaryActions.test.tsx +++ b/frontend/src/components/TitleBarPrimaryActions.test.tsx @@ -3,7 +3,13 @@ import { readFileSync } from 'node:fs'; import { create } from 'react-test-renderer'; import { describe, expect, it, vi } from 'vitest'; -import TitleBarPrimaryActions from './TitleBarPrimaryActions'; +import TitleBarPrimaryActions, { + resolveTitleBarPrimaryActionShortcut, +} from './TitleBarPrimaryActions'; +import { + cloneShortcutOptions, + DEFAULT_SHORTCUT_OPTIONS, +} from '../utils/shortcuts'; const appCss = readFileSync(new URL('../App.css', import.meta.url), 'utf8'); @@ -45,10 +51,13 @@ describe('TitleBarPrimaryActions', () => { it('shows both labels in query-first order and invokes their actions', () => { const onNewQuery = vi.fn(); const onNewConnection = vi.fn(); + const shortcutOptions = cloneShortcutOptions(DEFAULT_SHORTCUT_OPTIONS); const renderer = create( , @@ -58,6 +67,10 @@ describe('TitleBarPrimaryActions', () => { const buttons = actions.findAllByType('button'); expect(actions.props['data-no-titlebar-toggle']).toBe('true'); expect(buttons.map((button) => button.props['aria-label'])).toEqual(['新建查询', '新建连接']); + expect(buttons.map((button) => button.props.title)).toEqual([ + '新建查询 · ⌘N', + '新建连接 · ⌘⇧N', + ]); expect(buttons.map((button) => button.children[button.children.length - 1])).toEqual(['新建查询', '新建连接']); buttons[0].props.onClick(); @@ -65,4 +78,56 @@ describe('TitleBarPrimaryActions', () => { expect(onNewQuery).toHaveBeenCalledTimes(1); expect(onNewConnection).toHaveBeenCalledTimes(1); }); + + it('shows both Windows shortcut labels', () => { + const shortcutOptions = cloneShortcutOptions(DEFAULT_SHORTCUT_OPTIONS); + const renderer = create( + , + ); + + const buttons = renderer.root.findAllByType('button'); + expect(buttons.map((button) => button.props.title)).toEqual([ + 'New Query · Ctrl+N', + 'New Connection · Ctrl+Shift+N', + ]); + }); + + it('uses current platform custom bindings and hides disabled shortcuts', () => { + const shortcutOptions = cloneShortcutOptions(DEFAULT_SHORTCUT_OPTIONS); + shortcutOptions.newQueryTab.mac = { combo: 'Meta+Alt+Q', enabled: true }; + shortcutOptions.newQueryTab.windows = { combo: 'Ctrl+Alt+W', enabled: true }; + shortcutOptions.newConnection.mac = { combo: 'Meta+Shift+C', enabled: false }; + shortcutOptions.newConnection.windows = { combo: 'Ctrl+Alt+C', enabled: true }; + + expect(resolveTitleBarPrimaryActionShortcut(shortcutOptions, 'newQueryTab', 'mac')).toBe('⌘⌥Q'); + expect(resolveTitleBarPrimaryActionShortcut(shortcutOptions, 'newQueryTab', 'windows')).toBe('Ctrl+Alt+W'); + expect(resolveTitleBarPrimaryActionShortcut(shortcutOptions, 'newConnection', 'mac')).toBeUndefined(); + expect(resolveTitleBarPrimaryActionShortcut(shortcutOptions, 'newConnection', 'windows')).toBe('Ctrl+Alt+C'); + + const renderer = create( + , + ); + + const buttons = renderer.root.findAllByType('button'); + expect(buttons.map((button) => button.props.title)).toEqual([ + '新建查询 · ⌘⌥Q', + '新建连接', + ]); + expect(buttons.map((button) => button.props['aria-label'])).toEqual(['新建查询', '新建连接']); + expect(buttons.every((button) => button.props.disabled !== true)).toBe(true); + }); }); diff --git a/frontend/src/components/TitleBarPrimaryActions.tsx b/frontend/src/components/TitleBarPrimaryActions.tsx index 5a6efd6d..c69b4fb4 100644 --- a/frontend/src/components/TitleBarPrimaryActions.tsx +++ b/frontend/src/components/TitleBarPrimaryActions.tsx @@ -1,16 +1,43 @@ import React from 'react'; import { ConsoleSqlOutlined, PlusOutlined } from '@ant-design/icons'; +import { + getShortcutDisplayLabel, + resolveShortcutBinding, + type ShortcutOptions, + type ShortcutPlatform, +} from '../utils/shortcuts'; + +type TitleBarPrimaryShortcutAction = 'newQueryTab' | 'newConnection'; + +export const resolveTitleBarPrimaryActionShortcut = ( + shortcutOptions: Partial | null | undefined, + action: TitleBarPrimaryShortcutAction, + platform: ShortcutPlatform, +): string | undefined => { + const binding = resolveShortcutBinding(shortcutOptions, action, platform); + return binding.enabled && binding.combo + ? getShortcutDisplayLabel(binding.combo, platform) + : undefined; +}; interface TitleBarPrimaryActionsProps { newQueryLabel: string; newConnectionLabel: string; + newQueryShortcut?: string; + newConnectionShortcut?: string; onNewQuery: () => void; onNewConnection: () => void; } +const getActionTitle = (label: string, shortcut?: string): string => ( + shortcut ? `${label} \u00b7 ${shortcut}` : label +); + const TitleBarPrimaryActions: React.FC = ({ newQueryLabel, newConnectionLabel, + newQueryShortcut, + newConnectionShortcut, onNewQuery, onNewConnection, }) => ( @@ -24,7 +51,7 @@ const TitleBarPrimaryActions: React.FC = ({ type="button" className="gonavi-titlebar-primary-action" aria-label={newQueryLabel} - title={newQueryLabel} + title={getActionTitle(newQueryLabel, newQueryShortcut)} data-gonavi-new-query-action="true" onClick={onNewQuery} > @@ -35,7 +62,7 @@ const TitleBarPrimaryActions: React.FC = ({ type="button" className="gonavi-titlebar-primary-action" aria-label={newConnectionLabel} - title={newConnectionLabel} + title={getActionTitle(newConnectionLabel, newConnectionShortcut)} data-gonavi-create-connection-action="true" onClick={onNewConnection} >