From c8f11d72586d96ebede11775fc10edfafb0bf2b1 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Tue, 9 Jun 2026 14:57:29 +0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(query-editor):=20?= =?UTF-8?q?=E6=8B=86=E5=88=86=20SQL=20=E7=BB=93=E6=9E=9C=E5=8C=BA=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QueryEditor.external-sql-save.test.tsx | 2 +- frontend/src/components/QueryEditor.tsx | 496 ++--------------- .../components/QueryEditorResultsPanel.tsx | 508 ++++++++++++++++++ 3 files changed, 544 insertions(+), 462 deletions(-) create mode 100644 frontend/src/components/QueryEditorResultsPanel.tsx diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index 5df034b..8d99fe3 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -3189,7 +3189,7 @@ describe('QueryEditor external SQL save', () => { }); it('keeps query result tabs compact, centered, and readable in v2 UI', () => { - const source = readFileSync(new URL('./QueryEditor.tsx', import.meta.url), 'utf8'); + const source = readFileSync(new URL('./QueryEditorResultsPanel.tsx', import.meta.url), 'utf8'); const css = readFileSync(new URL('../v2-theme.css', import.meta.url), 'utf8'); expect(source).toContain('.query-result-tabs .ant-tabs-tab {'); diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index da40aaf..cb0aa0b 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -1,13 +1,13 @@ 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, EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons'; +import { Button, message, Modal, Input, Form, Dropdown, MenuProps, Tooltip, Select } from 'antd'; +import { PlayCircleOutlined, SaveOutlined, FormatPainterOutlined, SettingOutlined, 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'; import { useStore } from '../store'; import { DBQuery, DBQueryWithCancel, DBQueryMulti, DBGetTables, DBGetAllColumns, DBGetDatabases, DBGetColumns, DBGetIndexes, CancelQuery, GenerateQueryID, WriteSQLFile, ExportSQLFile } from '../../wailsjs/go/app/App'; -import DataGrid, { GONAVI_ROW_KEY } from './DataGrid'; +import { GONAVI_ROW_KEY } from './DataGrid'; import { getDataSourceCapabilities } from '../utils/dataSourceCapabilities'; import { applyMongoQueryAutoLimit, convertMongoShellToJsonCommand } from "../utils/mongodb"; import { getShortcutDisplayLabel, getShortcutPlatform, getShortcutPrimaryModifierDisplayLabel, isEditableElement, isShortcutMatch, comboToMonacoKeyBinding, resolveShortcutBinding } from "../utils/shortcuts"; @@ -34,6 +34,7 @@ import { getColumnDefinitionKey, getColumnDefinitionName, } from '../utils/columnDefinition'; +import QueryEditorResultsPanel, { type QueryEditorResultSet } from './QueryEditorResultsPanel'; const SQL_KEYWORDS = [ 'SELECT', 'FROM', 'WHERE', 'LIMIT', 'INSERT', 'UPDATE', 'DELETE', 'JOIN', 'LEFT', 'RIGHT', @@ -1934,23 +1935,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc const isExternalSQLFileTab = Boolean(String(tab.filePath || '').trim()); const isObjectEditQueryTab = tab.type === 'query' && tab.queryMode === 'object-edit'; - type ResultSet = { - key: string; - sql: string; - exportSql?: string; - sourceStatementIndex?: number; - statementResultIndex?: number; - rows: any[]; - columns: string[]; - messages?: string[]; - resultType?: 'grid' | 'message'; - tableName?: string; - pkColumns: string[]; - editLocator?: EditRowLocator; - readOnly: boolean; - truncated?: boolean; - pkLoading?: boolean; - }; + type ResultSet = QueryEditorResultSet; // Result Sets const [resultSets, setResultSets] = useState([]); @@ -5093,255 +5078,24 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc setActiveResultKey(''); }; - const buildResultTabMenuItems = (key: string, index: number): MenuProps['items'] => [ - { - key: 'close-other', - label: '关闭其他页', - disabled: resultSets.length <= 1, - onClick: () => closeOtherResultTabs(key), - }, - { - key: 'close-left', - label: '关闭左侧', - disabled: index <= 0, - onClick: () => closeResultTabsToLeft(key), - }, - { - key: 'close-right', - label: '关闭右侧', - disabled: index >= resultSets.length - 1, - onClick: () => closeResultTabsToRight(key), - }, - { type: 'divider' }, - { - key: 'close-all', - label: '关闭所有', - disabled: resultSets.length === 0, - onClick: closeAllResultTabs, - }, - ]; - - 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 = ( - - - - ); - - const resultPanelTabsHideButton = ( - - - - ); + const handleDiagnoseExecutionError = () => { + const errSql = getCurrentQuery(); + const prompt = `我在执行以下 SQL 时遇到了错误:\n\`\`\`sql\n${errSql}\n\`\`\`\n\n数据库报错信息如下:\n\`\`\`text\n${executionError}\n\`\`\`\n\n请帮我分析错误原因,并给出修改建议。`; + const store = useStore.getState(); + const wasClosed = !store.aiPanelVisible; + if (wasClosed) store.setAIPanelVisible(true); + setTimeout(() => { + window.dispatchEvent(new CustomEvent('gonavi:ai:inject-prompt', { detail: { prompt } })); + }, wasClosed ? 350 : 0); + }; return (
-
= ({ tab, isAc
{isResultPanelVisible && ( -
- {resultSets.length > 0 ? ( - ({ - key: rs.key, - label: ( - -
{ - event.preventDefault(); - }} - > - - {rs.resultType === 'message' ? `消息 ${idx + 1}` : `结果 ${idx + 1}`} - - {(() => { - if (rs.resultType === 'message') { - return i; - } - const isAffected = rs.columns.length === 1 && rs.columns[0] === 'affectedRows'; - if (isAffected) { - return ; - } - if (!Array.isArray(rs.rows)) { - return null; - } - return {rs.rows.length}; - })()} - - { - e.preventDefault(); - e.stopPropagation(); - handleCloseResult(rs.key); - }} - > - - - -
-
- ), - children: (() => { - if (rs.resultType === 'message') { - return ( -
- 执行消息 -
- {(rs.messages || []).join('\n')} -
-
- ); - } - // affectedRows 类型结果集(UPDATE/INSERT/DELETE):简洁提示 - const isAffectedResult = rs.columns.length === 1 && rs.columns[0] === 'affectedRows'; - if (isAffectedResult) { - const affected = Number(rs.rows[0]?.affectedRows ?? 0); - return ( -
- - 执行成功 - 影响行数:{affected} - {Array.isArray(rs.messages) && rs.messages.length > 0 && ( -
- {rs.messages.join('\n')} -
- )} -
- ); - } - return ( -
- {Array.isArray(rs.messages) && rs.messages.length > 0 && ( -
- {rs.messages.join('\n')} -
- )} - handleReloadResult(rs.key, rs.sql)} - readOnly={rs.readOnly} - toolbarExtraActions={resolvedActiveResultKey === rs.key ? resultPanelToolbarHideButton : null} - /> -
- ); - })() - }))} - /> - ) : executionError ? ( - <> -
- 结果区 - {resultPanelHideButton} -
-
-
- - 执行失败 -
-
- {executionError} -
-
- -
-
- - ) : ( - <> -
- 结果区 - {resultPanelHideButton} -
-
- {isV2Ui && ( -
- 等待执行 SQL - 运行查询后,结果会在下方以新版数据网格展示。 -
- )} -
- - )} -
+ updateResultPanelVisibility(false)} + onCloseResult={handleCloseResult} + onCloseOtherResultTabs={closeOtherResultTabs} + onCloseResultTabsToLeft={closeResultTabsToLeft} + onCloseResultTabsToRight={closeResultTabsToRight} + onCloseAllResultTabs={closeAllResultTabs} + onReloadResult={handleReloadResult} + onDiagnoseExecutionError={handleDiagnoseExecutionError} + /> )} void; + onHide: () => void; + onCloseResult: (key: string) => void; + onCloseOtherResultTabs: (key: string) => void; + onCloseResultTabsToLeft: (key: string) => void; + onCloseResultTabsToRight: (key: string) => void; + onCloseAllResultTabs: () => void; + onReloadResult: (key: string, sql: string) => void; + onDiagnoseExecutionError: () => void; +} + +const isAffectedRowsResult = (result: QueryEditorResultSet): boolean => + result.columns.length === 1 && result.columns[0] === 'affectedRows'; + +const QueryEditorResultsPanel: React.FC = ({ + resultSets, + activeResultKey, + loading, + executionError, + darkMode, + isV2Ui, + currentDb, + currentConnectionId, + toggleShortcutLabel, + onActiveResultKeyChange, + onHide, + onCloseResult, + onCloseOtherResultTabs, + onCloseResultTabsToLeft, + onCloseResultTabsToRight, + onCloseAllResultTabs, + onReloadResult, + onDiagnoseExecutionError, +}) => { + const resolvedActiveResultKey = activeResultKey || resultSets[0]?.key || ''; + const activeResultSet = resultSets.find((rs) => rs.key === resolvedActiveResultKey) || null; + const activeResultUsesDataGrid = Boolean( + activeResultSet && + activeResultSet.resultType !== 'message' && + !isAffectedRowsResult(activeResultSet), + ); + const hideTooltipTitle = toggleShortcutLabel + ? `隐藏结果区(${toggleShortcutLabel})` + : '隐藏结果区'; + + const buildResultTabMenuItems = (key: string, index: number): MenuProps['items'] => [ + { + key: 'close-other', + label: '关闭其他页', + disabled: resultSets.length <= 1, + onClick: () => onCloseOtherResultTabs(key), + }, + { + key: 'close-left', + label: '关闭左侧', + disabled: index <= 0, + onClick: () => onCloseResultTabsToLeft(key), + }, + { + key: 'close-right', + label: '关闭右侧', + disabled: index >= resultSets.length - 1, + onClick: () => onCloseResultTabsToRight(key), + }, + { type: 'divider' }, + { + key: 'close-all', + label: '关闭所有', + disabled: resultSets.length === 0, + onClick: onCloseAllResultTabs, + }, + ]; + + const hideButton = ( + + + + ); + + const tabsHideButton = ( + + + + ); + + return ( + <> + +
+ {resultSets.length > 0 ? ( + ({ + key: rs.key, + label: ( + +
{ + event.preventDefault(); + }} + > + + {rs.resultType === 'message' ? `消息 ${idx + 1}` : `结果 ${idx + 1}`} + + {(() => { + if (rs.resultType === 'message') { + return i; + } + if (isAffectedRowsResult(rs)) { + return ; + } + if (!Array.isArray(rs.rows)) { + return null; + } + return {rs.rows.length}; + })()} + + { + e.preventDefault(); + e.stopPropagation(); + onCloseResult(rs.key); + }} + > + + + +
+
+ ), + children: (() => { + if (rs.resultType === 'message') { + return ( +
+ 执行消息 +
+ {(rs.messages || []).join('\n')} +
+
+ ); + } + if (isAffectedRowsResult(rs)) { + const affected = Number(rs.rows[0]?.affectedRows ?? 0); + return ( +
+ + 执行成功 + 影响行数:{affected} + {Array.isArray(rs.messages) && rs.messages.length > 0 && ( +
+ {rs.messages.join('\n')} +
+ )} +
+ ); + } + return ( +
+ {Array.isArray(rs.messages) && rs.messages.length > 0 && ( +
+ {rs.messages.join('\n')} +
+ )} + onReloadResult(rs.key, rs.sql)} + readOnly={rs.readOnly} + toolbarExtraActions={resolvedActiveResultKey === rs.key ? toolbarHideButton : null} + /> +
+ ); + })(), + }))} + /> + ) : executionError ? ( + <> +
+ 结果区 + {hideButton} +
+
+
+ + 执行失败 +
+
+ {executionError} +
+
+ +
+
+ + ) : ( + <> +
+ 结果区 + {hideButton} +
+
+ {isV2Ui && ( +
+ 等待执行 SQL + 运行查询后,结果会在下方以新版数据网格展示。 +
+ )} +
+ + )} +
+ + ); +}; + +export default QueryEditorResultsPanel;