feat(query-editor): 支持结果区默认隐藏和快捷键切换

- 默认隐藏 SQL 结果区,执行成功或失败后自动展开

- 增加结果区显示/隐藏按钮和 Win/Mac 快捷键提示

- 在结果表工具栏提供隐藏入口并补充前端回归测试
This commit is contained in:
Syngnat
2026-06-09 14:13:06 +08:00
parent 77b58baff7
commit a6105f4807
6 changed files with 534 additions and 36 deletions

View File

@@ -1226,6 +1226,7 @@ interface DataGridProps {
onApplyQuickWhereCondition?: (condition: string) => void;
scrollSnapshot?: { top: number; left: number };
onScrollSnapshotChange?: (snapshot: { top: number; left: number }) => void;
toolbarExtraActions?: React.ReactNode;
}
type GridFilterCondition = FilterCondition & {
@@ -1487,7 +1488,7 @@ const DataGrid: React.FC<DataGridProps> = ({
data, columnNames, loading, tableName, objectType = 'table', exportScope = 'table', dbName, connectionId, pkColumns = [], editLocator, readOnly = false,
onReload, onSort, onPageChange, pagination, onRequestTotalCount, onCancelTotalCount, sortInfoExternal, showFilter, onToggleFilter, exportSqlWithFilter, onApplyFilter, appliedFilterConditions, quickWhereCondition,
onApplyQuickWhereCondition,
scrollSnapshot, onScrollSnapshotChange
scrollSnapshot, onScrollSnapshotChange, toolbarExtraActions
}) => {
const connections = useStore(state => state.connections);
const addTab = useStore(state => state.addTab);
@@ -7432,6 +7433,7 @@ const DataGrid: React.FC<DataGridProps> = ({
aiShortcutLabel={aiShortcutLabel}
legacyAiButtonStyle={legacyAiButtonStyle}
paginationTotalCountLoading={pagination?.totalCountLoading}
toolbarExtraActions={toolbarExtraActions}
filterConditions={filterConditions}
sortInfo={sortInfo}
displayColumnNames={displayColumnNames}

View File

@@ -73,6 +73,7 @@ export interface DataGridToolbarFrameProps {
aiShortcutLabel: string;
legacyAiButtonStyle?: React.CSSProperties;
paginationTotalCountLoading?: boolean;
toolbarExtraActions?: React.ReactNode;
filterConditions: GridFilterCondition[];
sortInfo: GridSortInfo[];
displayColumnNames: string[];
@@ -166,6 +167,7 @@ const DataGridToolbarFrame: React.FC<DataGridToolbarFrameProps> = ({
aiShortcutLabel,
legacyAiButtonStyle,
paginationTotalCountLoading,
toolbarExtraActions,
filterConditions,
sortInfo,
displayColumnNames,
@@ -410,6 +412,13 @@ const DataGridToolbarFrame: React.FC<DataGridToolbarFrameProps> = ({
</Tooltip>
</>
{toolbarExtraActions && (
<>
{renderToolbarDivider()}
{toolbarExtraActions}
</>
)}
{prefersManualTotalCount && (
<>
{renderToolbarDivider()}

View File

@@ -36,7 +36,12 @@ const storeState = vi.hoisted(() => ({
appearance: { uiVersion: 'legacy' as 'legacy' | 'v2' },
sqlFormatOptions: { keywordCase: 'upper' as const },
setSqlFormatOptions: vi.fn(),
queryOptions: { maxRows: 5000 },
queryOptions: {
maxRows: 5000,
showColumnComment: true,
showColumnType: true,
showQueryResultsPanel: false,
},
setQueryOptions: vi.fn(),
shortcutOptions: {
runQuery: {
@@ -51,6 +56,10 @@ const storeState = vi.hoisted(() => ({
mac: { enabled: true, combo: 'Meta+S' },
windows: { enabled: true, combo: 'Ctrl+S' },
},
toggleQueryResultsPanel: {
mac: { enabled: true, combo: 'Meta+Shift+M' },
windows: { enabled: true, combo: 'Ctrl+Shift+M' },
},
},
activeTabId: 'tab-1',
aiPanelVisible: false,
@@ -250,7 +259,7 @@ vi.mock('@monaco-editor/react', () => ({
onMount?.(editorState.editor, {
editor: { setTheme: vi.fn() },
KeyMod: { CtrlCmd: 2048, WinCtrl: 256 },
KeyCode: { KeyQ: 81, KeyS: 83 },
KeyCode: { KeyM: 77, KeyQ: 81, KeyS: 83 },
languages: {
CompletionItemKind: { Keyword: 1, Function: 2, Field: 3 },
CompletionItemInsertTextRule: { InsertAsSnippet: 1 },
@@ -298,7 +307,11 @@ vi.mock('@monaco-editor/react', () => ({
vi.mock('./DataGrid', () => ({
default: (props: any) => {
dataGridState.latestProps = props;
return <div data-grid="true" />;
return (
<div data-grid="true">
{props.toolbarExtraActions ?? null}
</div>
);
},
GONAVI_ROW_KEY: '__gonavi_row_key__',
}));
@@ -314,6 +327,8 @@ vi.mock('@ant-design/icons', () => {
StopOutlined: Icon,
RobotOutlined: Icon,
DatabaseOutlined: Icon,
EyeOutlined: Icon,
EyeInvisibleOutlined: Icon,
};
});
@@ -352,24 +367,27 @@ vi.mock('antd', () => {
),
Tooltip: ({ children }: any) => <>{children}</>,
Select: () => null,
Tabs: ({ activeKey, items, onChange }: any) => {
Tabs: ({ activeKey, items, onChange, tabBarExtraContent }: any) => {
const resolvedActiveKey = tabsState.activeKey ?? activeKey ?? items?.[0]?.key;
const activeItem = items?.find((item: any) => item.key === resolvedActiveKey) || items?.[0];
return (
<div>
<div>{items?.map((item: any) => (
<button
key={item.key}
type="button"
data-tab-key={item.key}
onClick={() => {
tabsState.activeKey = item.key;
onChange?.(item.key);
}}
>
{item.label}
</button>
))}</div>
<div>
{items?.map((item: any) => (
<button
key={item.key}
type="button"
data-tab-key={item.key}
onClick={() => {
tabsState.activeKey = item.key;
onChange?.(item.key);
}}
>
{item.label}
</button>
))}
{tabBarExtraContent?.right ?? null}
</div>
<div>{activeItem?.children}</div>
</div>
);
@@ -425,6 +443,34 @@ describe('QueryEditor external SQL save', () => {
storeState.saveQuery.mockReset();
storeState.savedQueries = [];
storeState.activeTabId = 'tab-1';
storeState.queryOptions = {
maxRows: 5000,
showColumnComment: true,
showColumnType: true,
showQueryResultsPanel: false,
};
storeState.shortcutOptions = {
runQuery: {
mac: { enabled: false, combo: '' },
windows: { enabled: false, combo: '' },
},
selectCurrentStatement: {
mac: { enabled: false, combo: '' },
windows: { enabled: false, combo: '' },
},
saveQuery: {
mac: { enabled: true, combo: 'Meta+S' },
windows: { enabled: true, combo: 'Ctrl+S' },
},
toggleQueryResultsPanel: {
mac: { enabled: true, combo: 'Meta+Shift+M' },
windows: { enabled: true, combo: 'Ctrl+Shift+M' },
},
};
storeState.setQueryOptions.mockReset();
storeState.setQueryOptions.mockImplementation((options: Record<string, unknown>) => {
storeState.queryOptions = { ...storeState.queryOptions, ...options };
});
messageApi.success.mockReset();
messageApi.error.mockReset();
messageApi.warning.mockReset();
@@ -486,6 +532,173 @@ describe('QueryEditor external SQL save', () => {
expect(editorState.value).toBe('SELECT * FROM ');
});
it('keeps the query results panel hidden by default on first entry', async () => {
storeState.appearance.uiVersion = 'v2';
let renderer!: ReactTestRenderer;
await act(async () => {
renderer = create(<QueryEditor tab={createTab()} />);
});
expect(textContent(renderer.toJSON())).not.toContain('等待执行 SQL');
});
it('shows the empty query results panel after toggling the results button', async () => {
storeState.appearance.uiVersion = 'v2';
let renderer!: ReactTestRenderer;
await act(async () => {
renderer = create(<QueryEditor tab={createTab()} />);
});
await act(async () => {
findButton(renderer, '结果').props.onClick();
});
expect(textContent(renderer.toJSON())).toContain('等待执行 SQL');
expect(storeState.setQueryOptions).toHaveBeenCalledWith({ showQueryResultsPanel: true });
});
it('hides the expanded empty query results panel from the inline hide action', async () => {
storeState.appearance.uiVersion = 'v2';
let renderer!: ReactTestRenderer;
await act(async () => {
renderer = create(<QueryEditor tab={createTab()} />);
});
await act(async () => {
findButton(renderer, '结果').props.onClick();
});
expect(textContent(renderer.toJSON())).toContain('等待执行 SQL');
await act(async () => {
findButton(renderer, '隐藏').props.onClick();
});
expect(textContent(renderer.toJSON())).not.toContain('等待执行 SQL');
expect(storeState.setQueryOptions).toHaveBeenLastCalledWith({ showQueryResultsPanel: false });
});
it('auto expands the query results panel after a successful execution returns rows', async () => {
storeState.appearance.uiVersion = 'v2';
backendApp.DBQueryMulti.mockResolvedValueOnce({
success: true,
data: [{ columns: ['value'], rows: [{ value: 1 }] }],
});
let renderer!: ReactTestRenderer;
await act(async () => {
renderer = create(<QueryEditor tab={createTab({ query: 'SELECT 1 AS value' })} />);
});
expect(textContent(renderer.toJSON())).not.toContain('结果 1');
await act(async () => {
await findButton(renderer, '运行').props.onClick();
});
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
expect(textContent(renderer.toJSON())).toContain('结果 1');
expect(storeState.setQueryOptions).toHaveBeenCalledWith({ showQueryResultsPanel: true });
});
it('keeps the inline hide action available after query results render rows', async () => {
storeState.appearance.uiVersion = 'v2';
backendApp.DBQueryMulti.mockResolvedValueOnce({
success: true,
data: [{ columns: ['value'], rows: [{ value: 1 }] }],
});
let renderer!: ReactTestRenderer;
await act(async () => {
renderer = create(<QueryEditor tab={createTab({ query: 'SELECT 1 AS value' })} />);
});
await act(async () => {
await findButton(renderer, '运行').props.onClick();
});
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
expect(textContent(renderer.toJSON())).toContain('结果 1');
await act(async () => {
findButton(renderer, '隐藏').props.onClick();
});
expect(textContent(renderer.toJSON())).not.toContain('结果 1');
expect(storeState.setQueryOptions).toHaveBeenLastCalledWith({ showQueryResultsPanel: false });
});
it('toggles the query results panel with Ctrl/Cmd+Shift+M', async () => {
storeState.appearance.uiVersion = 'v2';
const windowListeners: Record<string, ((event?: any) => void)[]> = {};
vi.stubGlobal('window', {
addEventListener: vi.fn((type: string, listener: (event?: any) => void) => {
windowListeners[type] ||= [];
windowListeners[type].push(listener);
}),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
requestAnimationFrame: vi.fn((callback: FrameRequestCallback) => {
callback(0);
return 1;
}),
cancelAnimationFrame: vi.fn(),
innerHeight: 900,
});
let renderer!: ReactTestRenderer;
await act(async () => {
renderer = create(<QueryEditor tab={createTab()} />);
});
const toggleAction = editorState.editor.addAction.mock.calls
.map((call: any[]) => call[0])
.find((action: any) => action?.id === 'gonavi.toggleQueryResultsPanel');
expect(toggleAction).toMatchObject({
label: 'GoNavi: 切换结果区',
});
expect(toggleAction?.keybindings?.[0]).toBeGreaterThan(0);
const isMacRuntime = /(Mac|iPhone|iPad|iPod)/i.test(`${navigator.platform || ''} ${navigator.userAgent || ''}`);
const createToggleEvent = () => ({
ctrlKey: !isMacRuntime,
metaKey: isMacRuntime,
altKey: false,
shiftKey: true,
key: 'm',
target: null,
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
});
const firstEvent = createToggleEvent();
await act(async () => {
windowListeners.keydown?.forEach((listener) => listener(firstEvent));
});
expect(firstEvent.preventDefault).toHaveBeenCalled();
expect(firstEvent.stopPropagation).toHaveBeenCalled();
expect(textContent(renderer.toJSON())).toContain('等待执行 SQL');
const secondEvent = createToggleEvent();
await act(async () => {
windowListeners.keydown?.forEach((listener) => listener(secondEvent));
});
expect(secondEvent.preventDefault).toHaveBeenCalled();
expect(secondEvent.stopPropagation).toHaveBeenCalled();
expect(textContent(renderer.toJSON())).not.toContain('等待执行 SQL');
});
it('keeps table name completion available after typing in a fresh query tab', async () => {
let renderer!: ReactTestRenderer;
autoFetchState.visible = true;
@@ -3033,6 +3246,7 @@ describe('QueryEditor external SQL save', () => {
const moveListeners: Array<(event: MouseEvent) => void> = [];
const upListeners: Array<() => void> = [];
const frameCallbacks: FrameRequestCallback[] = [];
storeState.queryOptions.showQueryResultsPanel = true;
vi.mocked(document.addEventListener).mockImplementation((type: string, listener: any) => {
if (type === 'mousemove') moveListeners.push(listener);
if (type === 'mouseup') upListeners.push(listener);

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import Editor, { type OnMount } from './MonacoEditor';
import { Button, message, Modal, Input, Form, Dropdown, MenuProps, Tooltip, Select, Tabs } from 'antd';
import { PlayCircleOutlined, SaveOutlined, FormatPainterOutlined, SettingOutlined, CloseOutlined, StopOutlined, RobotOutlined } from '@ant-design/icons';
import { PlayCircleOutlined, SaveOutlined, FormatPainterOutlined, SettingOutlined, CloseOutlined, StopOutlined, RobotOutlined, EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
import { format } from 'sql-formatter';
import { v4 as uuidv4 } from 'uuid';
import { TabData, ColumnDefinition, IndexDefinition } from '../types';
@@ -1977,6 +1977,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const runQueryActionRef = useRef<any>(null);
const selectCurrentStatementActionRef = useRef<any>(null);
const saveQueryActionRef = useRef<any>(null);
const toggleQueryResultsPanelActionRef = useRef<any>(null);
const lastExternalQueryRef = useRef<string>(getTabQueryValue(tab));
const lastEditorCursorPositionRef = useRef<any>(null);
const lastHoverTargetPositionRef = useRef<{ lineNumber: number; column: number } | null>(null);
@@ -2021,6 +2022,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const setSqlFormatOptions = useStore(state => state.setSqlFormatOptions);
const queryOptions = useStore(state => state.queryOptions);
const setQueryOptions = useStore(state => state.setQueryOptions);
const [isResultPanelVisible, setIsResultPanelVisible] = useState(Boolean(queryOptions?.showQueryResultsPanel));
const shortcutOptions = useStore(state => state.shortcutOptions);
const activeShortcutPlatform = getShortcutPlatform(isMacLikePlatform());
const runQueryShortcutBinding = useMemo(
@@ -2035,10 +2037,28 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
() => resolveShortcutBinding(shortcutOptions, 'saveQuery', activeShortcutPlatform),
[activeShortcutPlatform, shortcutOptions],
);
const toggleQueryResultsPanelShortcutBinding = useMemo(
() => resolveShortcutBinding(shortcutOptions, 'toggleQueryResultsPanel', activeShortcutPlatform),
[activeShortcutPlatform, shortcutOptions],
);
const primaryShortcutModifierLabel = useMemo(
() => getShortcutPrimaryModifierDisplayLabel(activeShortcutPlatform),
[activeShortcutPlatform],
);
useEffect(() => {
setIsResultPanelVisible(Boolean(queryOptions?.showQueryResultsPanel));
}, [queryOptions?.showQueryResultsPanel]);
const updateResultPanelVisibility = useCallback((visible: boolean) => {
setIsResultPanelVisible(visible);
setQueryOptions({ showQueryResultsPanel: visible });
}, [setQueryOptions]);
const toggleResultPanelVisibility = useCallback(() => {
setIsResultPanelVisible((previousVisible) => {
const nextVisible = !previousVisible;
setQueryOptions({ showQueryResultsPanel: nextVisible });
return nextVisible;
});
}, [setQueryOptions]);
const autoFetchVisible = useAutoFetchVisibility();
const currentSavedQuery = useMemo(() => {
@@ -3116,6 +3136,21 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
}
}
const toggleResultsBinding = toggleQueryResultsPanelShortcutBinding;
if (toggleResultsBinding?.enabled && toggleResultsBinding.combo) {
const keyBinding = comboToMonacoKeyBinding(
toggleResultsBinding.combo, monaco.KeyMod, monaco.KeyCode
);
if (keyBinding) {
toggleQueryResultsPanelActionRef.current = editor.addAction({
id: 'gonavi.toggleQueryResultsPanel',
label: 'GoNavi: 切换结果区',
keybindings: [keyBinding.keyMod | keyBinding.keyCode],
run: toggleResultPanelVisibility,
});
}
}
// HMR 重载或测试重置时,以全局状态为准,避免本地闭包状态和 provider 列表不同步。
sqlCompletionRegistered = Boolean(_g.__gonaviSqlCompletionState.registered);
sqlCompletionDisposables = _g.__gonaviSqlCompletionState.disposables;
@@ -4109,6 +4144,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
if (shellConvert.recognized) {
if (shellConvert.error) {
const prefix = statements.length > 1 ? `${idx + 1} 条语句执行失败:` : '';
updateResultPanelVisibility(true);
setExecutionError(formatSqlExecutionError(shellConvert.error, { prefix }));
setResultSets([]);
setActiveResultKey('');
@@ -4148,6 +4184,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
});
if (!res.success) {
const prefix = statements.length > 1 ? `${idx + 1} 条语句执行失败:` : '';
updateResultPanelVisibility(true);
setExecutionError(formatSqlExecutionError(res.message, { prefix }));
setResultSets([]);
setActiveResultKey('');
@@ -4214,6 +4251,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
}
}
}
if (nextResultSets.length > 0) {
updateResultPanelVisibility(true);
}
const shouldReplaceAllResults = didExecuteWholeEditor;
setResultSets(prev => {
const merged = mergeResultSets(prev, nextResultSets, shouldReplaceAllResults);
@@ -4313,6 +4353,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
return;
}
updateResultPanelVisibility(true);
setExecutionError(formatSqlExecutionError(res.message));
setResultSets([]);
setActiveResultKey('');
@@ -4424,6 +4465,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
});
}
if (nextResultSets.length > 0) {
updateResultPanelVisibility(true);
}
const shouldReplaceAllResults = didExecuteWholeEditor;
setResultSets(prev => {
const merged = mergeResultSets(prev, nextResultSets, shouldReplaceAllResults);
@@ -4461,6 +4505,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
message: e.message,
dbName: currentDb
});
updateResultPanelVisibility(true);
setExecutionError(formattedError);
setResultSets([]);
setActiveResultKey('');
@@ -4659,6 +4704,37 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
};
}, [saveQueryShortcutBinding]);
useEffect(() => {
if (toggleQueryResultsPanelActionRef.current) {
toggleQueryResultsPanelActionRef.current.dispose();
toggleQueryResultsPanelActionRef.current = null;
}
const editor = editorRef.current;
const monaco = monacoRef.current;
if (!editor || !monaco) return;
const binding = toggleQueryResultsPanelShortcutBinding;
if (!binding?.enabled || !binding.combo) return;
const keyBinding = comboToMonacoKeyBinding(binding.combo, monaco.KeyMod, monaco.KeyCode);
if (keyBinding) {
toggleQueryResultsPanelActionRef.current = editor.addAction({
id: 'gonavi.toggleQueryResultsPanel',
label: 'GoNavi: 切换结果区',
keybindings: [keyBinding.keyMod | keyBinding.keyCode],
run: toggleResultPanelVisibility,
});
}
return () => {
if (toggleQueryResultsPanelActionRef.current) {
toggleQueryResultsPanelActionRef.current.dispose();
toggleQueryResultsPanelActionRef.current = null;
}
};
}, [toggleQueryResultsPanelShortcutBinding, toggleResultPanelVisibility]);
useEffect(() => {
const handleRunActiveQuery = () => {
if (!isActive) {
@@ -4907,6 +4983,39 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
};
}, [isActive, saveQueryShortcutBinding, handleQuickSave]);
useEffect(() => {
const binding = toggleQueryResultsPanelShortcutBinding;
if (!binding?.enabled || !binding.combo) {
return;
}
const handleToggleResultsShortcut = (event: KeyboardEvent) => {
if (!isActive) {
return;
}
if (!isShortcutMatch(event, binding.combo)) {
return;
}
const editor = editorRef.current;
const targetNode = resolveEventTargetNode(event.target);
const editorHasFocus = !!editor?.hasTextFocus?.();
const inQueryEditor = !!(targetNode && queryEditorRootRef.current?.contains(targetNode));
if (!editorHasFocus && !inQueryEditor) {
return;
}
event.preventDefault();
event.stopPropagation();
toggleResultPanelVisibility();
};
window.addEventListener('keydown', handleToggleResultsShortcut, true);
return () => {
window.removeEventListener('keydown', handleToggleResultsShortcut, true);
};
}, [isActive, toggleQueryResultsPanelShortcutBinding, toggleResultPanelVisibility]);
useEffect(() => {
const handleSaveActiveQuery = () => {
if (!isActive) {
@@ -5012,6 +5121,65 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
},
];
const resolvedActiveResultKey = activeResultKey || resultSets[0]?.key || '';
const activeResultSet = resultSets.find((rs) => rs.key === resolvedActiveResultKey) || null;
const activeResultUsesDataGrid = Boolean(
activeResultSet &&
activeResultSet.resultType !== 'message' &&
!(activeResultSet.columns.length === 1 && activeResultSet.columns[0] === 'affectedRows')
);
const toggleQueryResultsPanelShortcutLabel =
toggleQueryResultsPanelShortcutBinding.enabled && toggleQueryResultsPanelShortcutBinding.combo
? getShortcutDisplayLabel(toggleQueryResultsPanelShortcutBinding.combo, activeShortcutPlatform)
: '';
const resultPanelHideTooltipTitle = toggleQueryResultsPanelShortcutLabel
? `隐藏结果区(${toggleQueryResultsPanelShortcutLabel}`
: '隐藏结果区';
const resultPanelHideButton = (
<Tooltip
title={resultPanelHideTooltipTitle}
>
<Button
className="query-result-panel-hide"
type="text"
size="small"
icon={<EyeInvisibleOutlined />}
onClick={() => updateResultPanelVisibility(false)}
>
</Button>
</Tooltip>
);
const resultPanelTabsHideButton = (
<Tooltip title={resultPanelHideTooltipTitle}>
<Button
aria-label="隐藏结果区"
className="query-result-panel-hide query-result-panel-hide-compact"
type="text"
size="small"
icon={<EyeInvisibleOutlined />}
onClick={() => updateResultPanelVisibility(false)}
/>
</Tooltip>
);
const resultPanelToolbarHideButton = (
<Tooltip title={resultPanelHideTooltipTitle}>
<Button
className={isV2Ui ? 'gn-v2-query-result-toolbar-hide' : undefined}
icon={<EyeInvisibleOutlined />}
onClick={() => updateResultPanelVisibility(false)}
>
<span></span>
{isV2Ui && toggleQueryResultsPanelShortcutLabel && (
<span className="gn-v2-toolbar-kbd">{toggleQueryResultsPanelShortcutLabel}</span>
)}
</Button>
</Tooltip>
);
return (
<div ref={queryEditorRootRef} className={isV2Ui ? 'gn-v2-query-editor' : undefined} style={{ flex: '1 1 auto', minHeight: 0, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
<style>{`
@@ -5026,11 +5194,17 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
flex: 0 0 auto;
margin: 0;
min-height: 38px;
padding-right: 8px;
}
.query-result-tabs .ant-tabs-nav-wrap {
flex: 0 1 auto;
min-width: 0;
}
.query-result-tabs .ant-tabs-extra-content {
display: inline-flex;
align-items: center;
padding-left: 8px;
}
.query-result-tabs .ant-tabs-nav-list {
align-items: center;
width: auto;
@@ -5141,8 +5315,38 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
background: rgba(0, 0, 0, 0.06);
color: #666;
}
.query-result-panel-header {
flex: 0 0 auto;
min-height: 38px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 0 12px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
background: rgba(255, 255, 255, 0.9);
}
.query-result-panel-header-title {
font-size: 13px;
font-weight: 600;
color: #666;
}
.query-result-panel-hide {
display: inline-flex;
align-items: center;
gap: 4px;
}
.query-result-panel-hide-compact {
min-width: 28px;
padding: 0 6px;
justify-content: center;
}
`}</style>
<div ref={editorPaneRef} className={isV2Ui ? 'gn-v2-query-editor-pane' : undefined}>
<div
ref={editorPaneRef}
className={isV2Ui ? 'gn-v2-query-editor-pane' : undefined}
style={{ display: 'flex', flexDirection: 'column', minHeight: 0, flex: isResultPanelVisible ? '0 0 auto' : '1 1 auto' }}
>
<div className={isV2Ui ? 'gn-v2-query-toolbar' : undefined} style={{ padding: '4px 8px 8px', display: 'flex', gap: '8px', flexShrink: 0, alignItems: 'center' }}>
<div
className={isV2Ui ? 'gn-v2-query-toolbar-selects' : undefined}
@@ -5233,6 +5437,21 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
</Dropdown>
</Button.Group>
<Tooltip
title={
toggleQueryResultsPanelShortcutBinding.enabled && toggleQueryResultsPanelShortcutBinding.combo
? `${isResultPanelVisible ? '隐藏结果区' : '显示结果区'}${getShortcutDisplayLabel(toggleQueryResultsPanelShortcutBinding.combo, activeShortcutPlatform)}`
: (isResultPanelVisible ? '隐藏结果区' : '显示结果区')
}
>
<Button
icon={isResultPanelVisible ? <EyeInvisibleOutlined /> : <EyeOutlined />}
onClick={toggleResultPanelVisibility}
>
</Button>
</Tooltip>
<Dropdown menu={{ items: [
{ key: 'ai-generate', label: '生成 SQL', icon: <RobotOutlined />, onClick: () => handleAIAction('generate') },
{ key: 'ai-explain', label: '解释 SQL', icon: <RobotOutlined />, onClick: () => handleAIAction('explain') },
@@ -5245,7 +5464,11 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
</div>
</div>
<div ref={editorShellRef} className={isV2Ui ? 'gn-v2-query-monaco-shell' : undefined} style={{ height: editorHeight, minHeight: '100px' }}>
<div
ref={editorShellRef}
className={isV2Ui ? 'gn-v2-query-monaco-shell' : undefined}
style={isResultPanelVisible ? { height: editorHeight, minHeight: '100px' } : { flex: '1 1 auto', minHeight: 0 }}
>
<Editor
height="100%"
gonaviTypography="code"
@@ -5273,28 +5496,32 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
/>
</div>
<div
className={isV2Ui ? 'gn-v2-query-resizer' : undefined}
onMouseDown={handleMouseDown}
style={{
height: '5px',
cursor: 'row-resize',
background: darkMode ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.04)',
flexShrink: 0,
zIndex: 10
}}
title="拖动调整高度"
/>
{isResultPanelVisible && (
<div
className={isV2Ui ? 'gn-v2-query-resizer' : undefined}
onMouseDown={handleMouseDown}
style={{
height: '5px',
cursor: 'row-resize',
background: darkMode ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.04)',
flexShrink: 0,
zIndex: 10
}}
title="拖动调整高度"
/>
)}
</div>
<div className={isV2Ui ? 'gn-v2-query-results' : undefined} style={{ flex: 1, minHeight: 0, overflow: 'hidden', padding: 0, display: 'flex', flexDirection: 'column' }}>
{isResultPanelVisible && (
<div className={isV2Ui ? 'gn-v2-query-results' : undefined} style={{ position: 'relative', flex: 1, minHeight: 0, overflow: 'hidden', padding: 0, display: 'flex', flexDirection: 'column' }}>
{resultSets.length > 0 ? (
<Tabs
className="query-result-tabs"
activeKey={activeResultKey || resultSets[0]?.key}
activeKey={resolvedActiveResultKey}
onChange={setActiveResultKey}
animated={false}
style={{ flex: 1, minHeight: 0 }}
tabBarExtraContent={!activeResultUsesDataGrid ? { right: resultPanelTabsHideButton } : undefined}
items={resultSets.map((rs, idx) => ({
key: rs.key,
label: (
@@ -5427,6 +5654,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
editLocator={rs.editLocator}
onReload={() => handleReloadResult(rs.key, rs.sql)}
readOnly={rs.readOnly}
toolbarExtraActions={resolvedActiveResultKey === rs.key ? resultPanelToolbarHideButton : null}
/>
</div>
);
@@ -5434,6 +5662,11 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
}))}
/>
) : executionError ? (
<>
<div className={isV2Ui ? 'query-result-panel-header gn-v2-query-result-panel-header' : 'query-result-panel-header'}>
<span className="query-result-panel-header-title"></span>
{resultPanelHideButton}
</div>
<div className={isV2Ui ? 'gn-v2-query-error' : undefined} style={{ flex: 1, minHeight: 0, padding: 24, display: 'flex', flexDirection: 'column', gap: 16, background: darkMode ? '#1e1e1e' : '#fafafa', overflow: 'auto' }}>
<div style={{ color: '#ff4d4f', fontWeight: 'bold', fontSize: 16, display: 'flex', alignItems: 'center', gap: 8 }}>
<CloseOutlined />
@@ -5462,7 +5695,13 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
</Button>
</div>
</div>
</>
) : (
<>
<div className={isV2Ui ? 'query-result-panel-header gn-v2-query-result-panel-header' : 'query-result-panel-header'}>
<span className="query-result-panel-header-title"></span>
{resultPanelHideButton}
</div>
<div className={isV2Ui ? 'gn-v2-query-empty' : undefined} style={{ flex: 1, minHeight: 0 }}>
{isV2Ui && (
<div>
@@ -5471,8 +5710,10 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
</div>
)}
</div>
</>
)}
</div>
)}
<Modal
title={saveModalMode === 'rename' ? '重命名查询' : '保存查询'}

View File

@@ -1111,6 +1111,7 @@ export interface QueryOptions {
maxRows: number;
showColumnComment: boolean;
showColumnType: boolean;
showQueryResultsPanel: boolean;
}
interface AppState {
@@ -1581,13 +1582,16 @@ const sanitizeQueryOptions = (value: unknown): QueryOptions => {
typeof raw.showColumnComment === "boolean" ? raw.showColumnComment : true;
const showColumnType =
typeof raw.showColumnType === "boolean" ? raw.showColumnType : true;
const showQueryResultsPanel =
typeof raw.showQueryResultsPanel === "boolean" ? raw.showQueryResultsPanel : false;
if (!Number.isFinite(maxRows) || maxRows <= 0) {
return { maxRows: 5000, showColumnComment, showColumnType };
return { maxRows: 5000, showColumnComment, showColumnType, showQueryResultsPanel };
}
return {
maxRows: Math.min(50000, Math.trunc(maxRows)),
showColumnComment,
showColumnType,
showQueryResultsPanel,
};
};
@@ -1975,6 +1979,7 @@ export const useStore = create<AppState>()(
maxRows: 5000,
showColumnComment: true,
showColumnType: true,
showQueryResultsPanel: false,
},
shortcutOptions: cloneShortcutOptions(DEFAULT_SHORTCUT_OPTIONS),
sqlSnippets: DEFAULT_SQL_SNIPPETS,

View File

@@ -3277,6 +3277,18 @@ body[data-ui-version="v2"] .gn-v2-data-grid .gn-v2-ai-insight-button .gn-v2-tool
color: var(--gn-info);
}
body[data-ui-version="v2"] .gn-v2-data-grid .gn-v2-query-result-toolbar-hide {
border-color: var(--gn-br-1) !important;
background: var(--gn-bg-panel) !important;
color: var(--gn-fg-2) !important;
font-weight: 650 !important;
}
body[data-ui-version="v2"] .gn-v2-data-grid .gn-v2-query-result-toolbar-hide:hover {
background: var(--gn-bg-hover) !important;
color: var(--gn-fg-1) !important;
}
body[data-ui-version="v2"] .gn-v2-smart-filter-panel {
background: var(--gn-bg-panel-2) !important;
border-top: 0.5px solid var(--gn-br-1);
@@ -4920,6 +4932,12 @@ body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-n
min-height: 38px;
}
body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-extra-content {
display: inline-flex;
align-items: center;
padding-right: 8px;
}
body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-tab {
width: auto !important;
min-width: 0 !important;
@@ -4984,6 +5002,15 @@ body[data-ui-version="v2"] .gn-v2-query-results .query-result-tab-count {
color: var(--gn-fg-2);
}
body[data-ui-version="v2"] .gn-v2-query-result-panel-header {
background: var(--gn-bg-panel-2);
border-bottom-color: var(--gn-br-1);
}
body[data-ui-version="v2"] .gn-v2-query-result-panel-header .query-result-panel-header-title {
color: var(--gn-fg-3);
}
body[data-ui-version="v2"] .gn-v2-query-empty,
body[data-ui-version="v2"] .gn-v2-query-success {
display: flex;