From 8b8a00b6668693efb0c5a5329ca40aa0638b400f Mon Sep 17 00:00:00 2001 From: Syngnat Date: Sat, 23 May 2026 11:20:31 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(frontend):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20dev=20=E6=9E=84=E5=BB=BA=E7=B1=BB=E5=9E=8B=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 补齐 v2 外观配置与侧栏置顶状态的 store 类型和持久化兼容 - 按当前平台解析和录制快捷键配置,适配 mac/windows 双平台结构 - 恢复 AI 入口布局工具导出,修复 App 引用缺失 - 更新 store 快捷键持久化测试断言 --- frontend/src/App.tsx | 24 +++++---- frontend/src/store.test.ts | 51 ++++++++++-------- frontend/src/store.ts | 82 +++++++++++++++++++++++++++-- frontend/src/utils/aiEntryLayout.ts | 12 +++++ 4 files changed, 132 insertions(+), 37 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b3ffcd0..6dbf6ba 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -66,9 +66,11 @@ import { eventToShortcut, findReservedConflicts, getShortcutDisplay, + getShortcutPlatform, isEditableElement, isShortcutMatch, normalizeShortcutCombo, + resolveShortcutBinding, splitConflictsByContext, type ConflictInfo, } from './utils/shortcuts'; @@ -1270,6 +1272,7 @@ function App() { const isWindowsRuntime = runtimePlatform === 'windows' || (runtimePlatform === '' && isWindowsPlatform()); const useNativeMacWindowControls = isMacRuntime && appearance.useNativeMacWindowControls === true; + const activeShortcutPlatform = getShortcutPlatform(isMacRuntime); const macWindowDiagnosticsEnabled = shouldEnableMacWindowDiagnostics( isMacRuntime, import.meta.env.DEV, @@ -1956,7 +1959,7 @@ function App() { const shortcutConflictMap = useMemo(() => { const map: Partial> = {}; for (const action of SHORTCUT_ACTION_ORDER) { - const binding = shortcutOptions[action]; + const binding = resolveShortcutBinding(shortcutOptions, action, activeShortcutPlatform); if (!binding?.enabled || !binding.combo) continue; const conflicts = findReservedConflicts(normalizeShortcutCombo(binding.combo)); if (conflicts.length > 0) { @@ -1964,7 +1967,7 @@ function App() { } } return map; - }, [shortcutOptions]); + }, [activeShortcutPlatform, shortcutOptions]); const [isProxyModalOpen, setIsProxyModalOpen] = useState(false); const [isDataRootModalOpen, setIsDataRootModalOpen] = useState(false); const [dataRootInfo, setDataRootInfo] = useState(null); @@ -2470,9 +2473,10 @@ function App() { document.body.style.backgroundColor = 'transparent'; document.body.style.color = darkMode ? '#ffffff' : '#000000'; document.body.setAttribute('data-theme', darkMode ? 'dark' : 'light'); + document.body.setAttribute('data-ui-version', appearance.uiVersion); document.body.style.fontSize = `${effectiveFontSize}px`; document.documentElement.style.setProperty('--gonavi-font-size', `${effectiveFontSize}px`); - }, [darkMode, effectiveFontSize]); + }, [appearance.uiVersion, darkMode, effectiveFontSize]); useEffect(() => { isAboutOpenRef.current = isAboutOpen; @@ -2596,7 +2600,7 @@ function App() { if (meta.scope && meta.scope !== 'global') { return false; } - const binding = shortcutOptions[action]; + const binding = resolveShortcutBinding(shortcutOptions, action, activeShortcutPlatform); if (!binding?.enabled) { return false; } @@ -2647,7 +2651,7 @@ function App() { return () => { window.removeEventListener('keydown', handleGlobalShortcut); }; - }, [handleManualResetWindowZoom, handleNewQuery, handleTitleBarWindowToggle, isMacRuntime, shortcutOptions, themeMode, setTheme, useNativeMacWindowControls]); + }, [activeShortcutPlatform, handleManualResetWindowZoom, handleNewQuery, handleTitleBarWindowToggle, isMacRuntime, shortcutOptions, themeMode, setTheme, useNativeMacWindowControls]); useEffect(() => { if (!capturingShortcutAction) { @@ -2680,7 +2684,7 @@ function App() { if (action === capturingShortcutAction) { return false; } - const binding = shortcutOptions[action]; + const binding = resolveShortcutBinding(shortcutOptions, action, activeShortcutPlatform); if (!binding?.enabled) { return false; } @@ -2702,7 +2706,7 @@ function App() { } } - updateShortcut(capturingShortcutAction, { combo: normalizedCombo, enabled: true }); + updateShortcut(capturingShortcutAction, { combo: normalizedCombo, enabled: true }, activeShortcutPlatform); setCapturingShortcutAction(null); }; @@ -2710,7 +2714,7 @@ function App() { return () => { window.removeEventListener('keydown', handleShortcutCapture, true); }; - }, [capturingShortcutAction, shortcutOptions, updateShortcut]); + }, [activeShortcutPlatform, capturingShortcutAction, shortcutOptions, updateShortcut]); const linuxResizeHandleStyleBase = { position: 'fixed', @@ -3731,7 +3735,7 @@ function App() { if (meta.platformOnly === 'mac' && !isMacRuntime) { return null; } - const binding = shortcutOptions[action] ?? { combo: '', enabled: false }; + const binding = resolveShortcutBinding(shortcutOptions, action, activeShortcutPlatform); const isCapturing = capturingShortcutAction === action; const conflicts = shortcutConflictMap[action]; const conflictInfo = conflicts?.length ? splitConflictsByContext(conflicts) : null; @@ -3775,7 +3779,7 @@ function App() { updateShortcut(action, { enabled: checked })} + onChange={(checked) => updateShortcut(action, { enabled: checked }, activeShortcutPlatform)} /> diff --git a/frontend/src/store.test.ts b/frontend/src/store.test.ts index 8c83d4a..610efb2 100644 --- a/frontend/src/store.test.ts +++ b/frontend/src/store.test.ts @@ -64,12 +64,17 @@ describe('store appearance persistence', () => { const { useStore } = await importStore(); const appearance = useStore.getState().appearance; + expect(appearance.uiVersion).toBe('legacy'); expect(appearance.enabled).toBe(false); expect(appearance.opacity).toBe(0.75); expect(appearance.blur).toBe(6); expect(appearance.useNativeMacWindowControls).toBe(true); expect(appearance.showDataTableVerticalBorders).toBe(false); expect(appearance.dataTableDensity).toBe('comfortable'); + expect(appearance.dataTableFontSize).toBeNull(); + expect(appearance.dataTableFontSizeFollowGlobal).toBe(true); + expect(appearance.sidebarTreeFontSize).toBeNull(); + expect(appearance.sidebarTreeFontSizeFollowGlobal).toBe(true); }); it('persists DataGrid appearance settings and restores them after reload', async () => { @@ -565,8 +570,8 @@ describe('store appearance persistence', () => { const { useStore } = await importStore(); expect(useStore.getState().shortcutOptions.sendAIChatMessage).toEqual({ - combo: 'Enter', - enabled: true, + mac: { combo: 'Enter', enabled: true }, + windows: { combo: 'Enter', enabled: true }, }); }); @@ -576,19 +581,19 @@ describe('store appearance persistence', () => { useStore.getState().updateShortcut('sendAIChatMessage', { combo: 'Meta+Enter', enabled: true, - }); + }, 'mac'); const persisted = JSON.parse(storage.getItem('lite-db-storage') || '{}'); expect(persisted.state.shortcutOptions.sendAIChatMessage).toEqual({ - combo: 'Meta+Enter', - enabled: true, + mac: { combo: 'Meta+Enter', enabled: true }, + windows: { combo: 'Enter', enabled: true }, }); vi.resetModules(); const reloaded = await importStore(); expect(reloaded.useStore.getState().shortcutOptions.sendAIChatMessage).toEqual({ - combo: 'Meta+Enter', - enabled: true, + mac: { combo: 'Meta+Enter', enabled: true }, + windows: { combo: 'Enter', enabled: true }, }); }); @@ -608,8 +613,8 @@ describe('store appearance persistence', () => { const { useStore } = await importStore(); expect(useStore.getState().shortcutOptions.sendAIChatMessage).toEqual({ - combo: 'Enter', - enabled: true, + mac: { combo: 'Enter', enabled: true }, + windows: { combo: 'Enter', enabled: true }, }); }); @@ -618,14 +623,14 @@ describe('store appearance persistence', () => { useStore.getState().updateShortcut('sendAIChatMessage', { combo: 'Ctrl+Enter', enabled: true, - }); + }, 'windows'); useStore.getState().replaceConnections([]); const persisted = JSON.parse(storage.getItem('lite-db-storage') || '{}'); expect(persisted.state.shortcutOptions.sendAIChatMessage).toEqual({ - combo: 'Ctrl+Enter', - enabled: true, + mac: { combo: 'Enter', enabled: true }, + windows: { combo: 'Ctrl+Enter', enabled: true }, }); }); @@ -637,8 +642,8 @@ describe('store appearance persistence', () => { shortcutOptions: { ...shortcutOptions, sendAIChatMessage: { - combo: 'Meta+Enter', - enabled: true, + mac: { combo: 'Meta+Enter', enabled: true }, + windows: { combo: 'Ctrl+Enter', enabled: true }, }, }, }, @@ -648,8 +653,8 @@ describe('store appearance persistence', () => { shortcutOptions: { ...shortcutOptions, sendAIChatMessage: { - combo: 'Enter', - enabled: true, + mac: { combo: 'Enter', enabled: true }, + windows: { combo: 'Enter', enabled: true }, }, }, }); @@ -658,8 +663,8 @@ describe('store appearance persistence', () => { const persisted = JSON.parse(storage.getItem('lite-db-storage') || '{}'); expect(persisted.state.shortcutOptions.sendAIChatMessage).toEqual({ - combo: 'Meta+Enter', - enabled: true, + mac: { combo: 'Meta+Enter', enabled: true }, + windows: { combo: 'Ctrl+Enter', enabled: true }, }); }); @@ -670,13 +675,13 @@ describe('store appearance persistence', () => { useStore.getState().updateShortcut('sendAIChatMessage', { combo: 'Meta+Enter', enabled: true, - }); + }, 'mac'); useStore.setState({ shortcutOptions: { ...shortcutOptions, sendAIChatMessage: { - combo: 'Enter', - enabled: true, + mac: { combo: 'Enter', enabled: true }, + windows: { combo: 'Enter', enabled: true }, }, }, }); @@ -684,8 +689,8 @@ describe('store appearance persistence', () => { const persisted = JSON.parse(storage.getItem('lite-db-storage') || '{}'); expect(persisted.state.shortcutOptions.sendAIChatMessage).toEqual({ - combo: 'Meta+Enter', - enabled: true, + mac: { combo: 'Meta+Enter', enabled: true }, + windows: { combo: 'Enter', enabled: true }, }); }); }); diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 7548e5c..47e4216 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -17,11 +17,13 @@ import { } from "./types"; import { ShortcutAction, - ShortcutBinding, ShortcutOptions, DEFAULT_SHORTCUT_OPTIONS, cloneShortcutOptions, + getShortcutPlatform, sanitizeShortcutOptions, + type ShortcutPlatformBinding, + type ShortcutPlatform, } from "./utils/shortcuts"; import { buildExternalSQLDirectoryId } from "./utils/externalSqlTree"; import { @@ -41,6 +43,7 @@ import { } from "./utils/oceanBaseProtocol"; export interface AppearanceSettings extends DataGridDisplaySettings { + uiVersion: "legacy" | "v2"; enabled: boolean; opacity: number; blur: number; @@ -48,6 +51,7 @@ export interface AppearanceSettings extends DataGridDisplaySettings { } export const DEFAULT_APPEARANCE: AppearanceSettings = { + uiVersion: "legacy", enabled: true, opacity: 1.0, blur: 0, @@ -799,6 +803,7 @@ interface AppState { enableColumnOrderMemory: boolean; tableHiddenColumns: Record; enableHiddenColumnMemory: boolean; + pinnedSidebarTables: string[]; windowBounds: { width: number; height: number; x: number; y: number } | null; windowState: "normal" | "fullscreen" | "maximized"; sidebarWidth: number; @@ -876,7 +881,8 @@ interface AppState { setQueryOptions: (options: Partial) => void; updateShortcut: ( action: ShortcutAction, - binding: Partial, + binding: Partial, + platform?: ShortcutPlatform, ) => void; resetShortcutOptions: () => void; saveSqlSnippet: (snippet: SqlSnippet) => void; @@ -896,6 +902,13 @@ interface AppState { dbName: string, sortBy: "name" | "frequency", ) => void; + setSidebarTablePinned: ( + connectionId: string, + dbName: string, + tableName: string, + schemaName: string | undefined, + pinned: boolean, + ) => void; setTableColumnOrder: ( connectionId: string, dbName: string, @@ -1164,6 +1177,17 @@ const sanitizeTableHiddenColumns = ( return result; }; +const sanitizePinnedSidebarTables = (value: unknown): string[] => { + if (!Array.isArray(value)) return []; + return Array.from( + new Set( + value + .map((entry) => toTrimmedString(entry)) + .filter(Boolean), + ), + ); +}; + const sanitizeAppearance = ( appearance: Partial | undefined, version: number, @@ -1173,6 +1197,10 @@ const sanitizeAppearance = ( } const dataGridDisplaySettings = sanitizeDataGridDisplaySettings(appearance); const nextAppearance = { + uiVersion: + appearance.uiVersion === "v2" || appearance.uiVersion === "legacy" + ? appearance.uiVersion + : DEFAULT_APPEARANCE.uiVersion, enabled: typeof appearance.enabled === "boolean" ? appearance.enabled @@ -1192,6 +1220,12 @@ const sanitizeAppearance = ( showDataTableVerticalBorders: dataGridDisplaySettings.showDataTableVerticalBorders, dataTableDensity: dataGridDisplaySettings.dataTableDensity, + dataTableFontSize: dataGridDisplaySettings.dataTableFontSize, + dataTableFontSizeFollowGlobal: + dataGridDisplaySettings.dataTableFontSizeFollowGlobal, + sidebarTreeFontSize: dataGridDisplaySettings.sidebarTreeFontSize, + sidebarTreeFontSizeFollowGlobal: + dataGridDisplaySettings.sidebarTreeFontSizeFollowGlobal, }; if (version < 2 && isLegacyDefaultAppearance(appearance)) { return { ...DEFAULT_APPEARANCE }; @@ -1339,6 +1373,21 @@ const runWithExplicitShortcutPersistence = (callback: () => void): void => { } }; +export const buildSidebarTablePinKey = ( + connectionId: string, + dbName: string, + tableName: string, + schemaName = "", +): string => { + const parts = [ + toTrimmedString(connectionId), + toTrimmedString(dbName), + toTrimmedString(schemaName), + toTrimmedString(tableName), + ]; + return parts[0] && parts[1] && parts[3] ? JSON.stringify(parts) : ""; +}; + // --- AI 会话文件持久化辅助函数 --- /** 每个 session 独立防抖定时器(2秒) */ @@ -1448,6 +1497,7 @@ export const useStore = create()( enableColumnOrderMemory: true, tableHiddenColumns: {}, enableHiddenColumnMemory: true, + pinnedSidebarTables: [], windowBounds: null, windowState: "normal" as const, sidebarWidth: 330, @@ -1823,14 +1873,18 @@ export const useStore = create()( set((state) => ({ queryOptions: { ...state.queryOptions, ...options }, })), - updateShortcut: (action, binding) => { + updateShortcut: (action, binding, platform) => { runWithExplicitShortcutPersistence(() => { + const targetPlatform = platform ?? getShortcutPlatform(); set((state) => ({ shortcutOptions: { ...state.shortcutOptions, [action]: { ...state.shortcutOptions[action], - ...binding, + [targetPlatform]: { + ...state.shortcutOptions[action][targetPlatform], + ...binding, + }, }, }, })); @@ -1898,6 +1952,19 @@ export const useStore = create()( }; }), + setSidebarTablePinned: (connectionId, dbName, tableName, schemaName, pinned) => + set((state) => { + const key = buildSidebarTablePinKey(connectionId, dbName, tableName, schemaName); + if (!key) return state; + const current = new Set(state.pinnedSidebarTables); + if (pinned) { + current.add(key); + } else { + current.delete(key); + } + return { pinnedSidebarTables: Array.from(current) }; + }), + setTableColumnOrder: (connectionId, dbName, tableName, order) => set((state) => { const key = `${connectionId}-${dbName}-${tableName}`; @@ -2230,6 +2297,9 @@ export const useStore = create()( nextState.tableHiddenColumns = safeHidden; nextState.enableHiddenColumnMemory = state.enableHiddenColumnMemory !== false; + nextState.pinnedSidebarTables = sanitizePinnedSidebarTables( + state.pinnedSidebarTables, + ); nextState.windowBounds = sanitizeWindowBounds(state.windowBounds); nextState.windowState = sanitizeWindowState(state.windowState); nextState.sidebarWidth = sanitizeSidebarWidth(state.sidebarWidth); @@ -2272,6 +2342,9 @@ export const useStore = create()( state.tableHiddenColumns, ), enableHiddenColumnMemory: state.enableHiddenColumnMemory !== false, + pinnedSidebarTables: sanitizePinnedSidebarTables( + state.pinnedSidebarTables, + ), windowBounds: sanitizeWindowBounds(state.windowBounds), windowState: sanitizeWindowState(state.windowState), sidebarWidth: sanitizeSidebarWidth(state.sidebarWidth), @@ -2311,6 +2384,7 @@ export const useStore = create()( enableColumnOrderMemory: state.enableColumnOrderMemory, tableHiddenColumns: state.tableHiddenColumns, enableHiddenColumnMemory: state.enableHiddenColumnMemory, + pinnedSidebarTables: state.pinnedSidebarTables, windowBounds: state.windowBounds, windowState: state.windowState, sidebarWidth: state.sidebarWidth, diff --git a/frontend/src/utils/aiEntryLayout.ts b/frontend/src/utils/aiEntryLayout.ts index 55c844d..36372e4 100644 --- a/frontend/src/utils/aiEntryLayout.ts +++ b/frontend/src/utils/aiEntryLayout.ts @@ -2,7 +2,9 @@ import type { CSSProperties } from 'react'; export const SIDEBAR_UTILITY_ITEM_KEYS = ['tools', 'settings'] as const; +export type AIEntryPlacement = 'content-edge'; export type LegacyAIEdgeHandleAttachment = 'content-shell' | 'panel-shell'; +export type AIEdgeHandleAttachment = LegacyAIEdgeHandleAttachment; export interface ResolveLegacyAIEdgeHandleStyleInput { darkMode: boolean; @@ -10,10 +12,16 @@ export interface ResolveLegacyAIEdgeHandleStyleInput { effectiveUiScale: number; } +export type ResolveAIEdgeHandleStyleInput = ResolveLegacyAIEdgeHandleStyleInput; + +export const resolveAIEntryPlacement = (): AIEntryPlacement => 'content-edge'; + export const resolveLegacyAIEdgeHandleAttachment = ( aiPanelVisible: boolean, ): LegacyAIEdgeHandleAttachment => (aiPanelVisible ? 'panel-shell' : 'content-shell'); +export const resolveAIEdgeHandleAttachment = resolveLegacyAIEdgeHandleAttachment; + export const resolveLegacyAIEdgeHandleDockStyle = ( attachment: LegacyAIEdgeHandleAttachment, ): CSSProperties => ({ @@ -23,6 +31,8 @@ export const resolveLegacyAIEdgeHandleDockStyle = ( zIndex: 12, }); +export const resolveAIEdgeHandleDockStyle = resolveLegacyAIEdgeHandleDockStyle; + export const resolveLegacyAIEdgeHandleStyle = ({ darkMode, aiPanelVisible, @@ -54,3 +64,5 @@ export const resolveLegacyAIEdgeHandleStyle = ({ flexShrink: 0, }; }; + +export const resolveAIEdgeHandleStyle = resolveLegacyAIEdgeHandleStyle;