🐛 fix(frontend): 修复 dev 构建类型错误

- 补齐 v2 外观配置与侧栏置顶状态的 store 类型和持久化兼容

- 按当前平台解析和录制快捷键配置,适配 mac/windows 双平台结构

- 恢复 AI 入口布局工具导出,修复 App 引用缺失

- 更新 store 快捷键持久化测试断言
This commit is contained in:
Syngnat
2026-05-23 11:20:31 +08:00
parent 24d9db4c51
commit 8b8a00b666
4 changed files with 132 additions and 37 deletions

View File

@@ -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<Record<ShortcutAction, ConflictInfo[]>> = {};
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<any>(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() {
</Button>
<Switch
checked={binding.enabled}
onChange={(checked) => updateShortcut(action, { enabled: checked })}
onChange={(checked) => updateShortcut(action, { enabled: checked }, activeShortcutPlatform)}
/>
</div>
</div>

View File

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

View File

@@ -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<string, string[]>;
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<QueryOptions>) => void;
updateShortcut: (
action: ShortcutAction,
binding: Partial<ShortcutBinding>,
binding: Partial<ShortcutPlatformBinding>,
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<AppearanceSettings> | 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<AppState>()(
enableColumnOrderMemory: true,
tableHiddenColumns: {},
enableHiddenColumnMemory: true,
pinnedSidebarTables: [],
windowBounds: null,
windowState: "normal" as const,
sidebarWidth: 330,
@@ -1823,14 +1873,18 @@ export const useStore = create<AppState>()(
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<AppState>()(
};
}),
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<AppState>()(
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<AppState>()(
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<AppState>()(
enableColumnOrderMemory: state.enableColumnOrderMemory,
tableHiddenColumns: state.tableHiddenColumns,
enableHiddenColumnMemory: state.enableHiddenColumnMemory,
pinnedSidebarTables: state.pinnedSidebarTables,
windowBounds: state.windowBounds,
windowState: state.windowState,
sidebarWidth: state.sidebarWidth,

View File

@@ -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;