diff --git a/frontend/src/components/DataGrid.tsx b/frontend/src/components/DataGrid.tsx index 6c7d350..d84b807 100644 --- a/frontend/src/components/DataGrid.tsx +++ b/frontend/src/components/DataGrid.tsx @@ -567,12 +567,10 @@ const EditableCell: React.FC = React.memo(({ }; const handleContextMenu = (e: React.MouseEvent) => { - if (!editable) return; + if (!cellContextMenuContext) return; e.preventDefault(); e.stopPropagation(); // 阻止冒泡到行级菜单 - if (cellContextMenuContext) { - cellContextMenuContext.showMenu(e, record, dataIndex, title); - } + cellContextMenuContext.showMenu(e, record, dataIndex, title); }; let childNode = children; @@ -611,6 +609,13 @@ const EditableCell: React.FC = React.memo(({ {children} ); + } else if (cellContextMenuContext) { + // 非编辑模式(只读查询结果)也绑定右键菜单,支持复制为 INSERT/JSON/CSV 等操作 + childNode = ( +
+ {children} +
+ ); } const handleDoubleClick = () => { @@ -3081,8 +3086,8 @@ const DataGrid: React.FC = ({ }, [displayColumnNames, columnWidths, sortInfo, handleResizeStart, canModifyData, onSort, renderColumnTitle]); const mergedColumns = useMemo(() => columns.map((col): ColumnType => { - if (!col.editable) return col as ColumnType; const dataIndex = String(col.dataIndex); + // 即使不可编辑,也需要通过 onCell/render 绑定右键菜单 return { ...col, onCell: (record: Item) => { @@ -3092,7 +3097,16 @@ const DataGrid: React.FC = ({ 'data-col-name': dataIndex, }; - if (!enableInlineEditableCell) { + if (col.editable && enableInlineEditableCell) { + // 可编辑模式(非虚拟):传递给 EditableCell 的 props + cellProps.record = record; + cellProps.editable = col.editable; + cellProps.dataIndex = col.dataIndex; + cellProps.title = dataIndex; + cellProps.handleSave = handleCellSave; + cellProps.focusCell = openCellEditor; + } else if (col.editable && !enableInlineEditableCell) { + // 可编辑但非 inline(虚拟模式下):双击和右键通过 onCell 绑定 cellProps.onDoubleClick = () => handleVirtualCellActivate(record, dataIndex, dataIndex); cellProps.onContextMenu = (e: React.MouseEvent) => { e.preventDefault(); @@ -3100,12 +3114,12 @@ const DataGrid: React.FC = ({ showCellContextMenu(e, record, dataIndex, dataIndex); }; } else { - cellProps.record = record; - cellProps.editable = col.editable; - cellProps.dataIndex = col.dataIndex; - cellProps.title = dataIndex; - cellProps.handleSave = handleCellSave; - cellProps.focusCell = openCellEditor; + // 不可编辑(只读查询结果):只绑定右键菜单 + cellProps.onContextMenu = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + showCellContextMenu(e, record, dataIndex, dataIndex); + }; } return cellProps; }, diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index 1a92a5f..8212a93 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -1313,6 +1313,72 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => { return selected; }; + // 精准重查询单个结果集(提交事务 / 刷新按钮使用),不会重跑整个编辑器 SQL + const handleReloadResult = async (resultKey: string, sql: string) => { + if (!sql?.trim() || !currentDb) return; + const conn = connections.find(c => c.id === currentConnectionId); + if (!conn) return; + + const config = { + ...conn.config, + port: Number(conn.config.port), + password: conn.config.password || "", + database: conn.config.database || "", + useSSH: conn.config.useSSH || false, + ssh: conn.config.ssh || { host: "", port: 22, user: "", password: "", keyPath: "" } + }; + + try { + setLoading(true); + // 使用 DBQueryMulti 保持和首次查询一致的后端路径 + let queryId: string; + try { + queryId = await GenerateQueryID(); + } catch { + queryId = 'reload-' + Date.now(); + } + const res = await DBQueryMulti(config as any, currentDb, sql, queryId); + if (!res?.success) { + message.error('刷新失败: ' + (res?.message || '未知错误')); + return; + } + + // 取第一个结果集(单条 SQL 只有一个结果集) + const resultSetDataArray = Array.isArray(res.data) ? (res.data as any[]) : []; + if (resultSetDataArray.length === 0) return; + const rsData = resultSetDataArray[0]; + const isAffectedResult = Array.isArray(rsData.rows) && rsData.rows.length === 1 + && rsData.columns && rsData.columns.length === 1 + && rsData.columns[0] === 'affectedRows'; + if (isAffectedResult) return; // 不应该出现,但保险起见 + + let rows = Array.isArray(rsData.rows) ? rsData.rows : []; + const maxRows = Number(queryOptions?.maxRows) || 0; + let truncated = false; + if (Number.isFinite(maxRows) && maxRows > 0 && rows.length > maxRows) { + truncated = true; + rows = rows.slice(0, maxRows); + } + const cols = (rsData.columns && rsData.columns.length > 0) + ? rsData.columns + : (rows.length > 0 ? Object.keys(rows[0]) : []); + rows.forEach((row: any, i: number) => { + if (row && typeof row === 'object') row[GONAVI_ROW_KEY] = i; + }); + + // 只更新匹配的结果集的 rows 和 columns,保留 tableName/pkColumns/readOnly 等元数据 + setResultSets(prev => prev.map(rs => + rs.key === resultKey + ? { ...rs, rows, columns: cols, truncated } + : rs + )); + } catch (err: any) { + message.error('刷新失败: ' + (err?.message || '未知错误')); + } finally { + setLoading(false); + } + }; + const handleRun = async () => { const currentQuery = getCurrentQuery(); if (!currentQuery.trim()) return; @@ -1601,7 +1667,8 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => { let simpleTableName: string | undefined = undefined; if (rawStatement) { - const tableMatch = rawStatement.match(/^\s*SELECT\s+\*\s+FROM\s+[`"]?(\w+)[`"]?\s*(?:WHERE.*)?(?:ORDER BY.*)?(?:LIMIT.*)?$/i); + // 支持多行 SQL:SELECT * FROM [schema.]table [WHERE...] [ORDER BY...] [LIMIT...] 等 + const tableMatch = rawStatement.match(/^\s*SELECT\s+\*\s+FROM\s+(?:[\w`"]+\.)?[`"]?(\w+)[`"]?\s*(?:$|[\s;])/im); if (tableMatch) { simpleTableName = tableMatch[1]; if (!forceReadOnlyResult) { @@ -2060,7 +2127,7 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => { dbName={currentDb} connectionId={currentConnectionId} pkColumns={rs.pkColumns} - onReload={handleRun} + onReload={() => handleReloadResult(rs.key, rs.sql)} readOnly={rs.readOnly} />