🐛 fix(query-editor/data-grid): 修复只读查询结果右键菜单失效及提交事务后数据丢失

- 右键菜单修复:移除 handleContextMenu 的 editable 守卫,只读模式也能弹出右键菜单
- 非编辑单元格绑定:EditableCell 非编辑模式增加 onContextMenu 包装,确保右键事件触发
- mergedColumns 统一:所有列通过 onCell 绑定 onContextMenu,不再跳过非 editable 列
- 表名正则增强:支持多行 SQL 和 schema.table 写法,复杂 SELECT 也能提取表名获得编辑能力
- 精准重查询:新增 handleReloadResult 函数,提交事务后只用当前结果集 SQL 重查,避免整个编辑器 SQL 二次处理导致数据丢失
- refs #267
This commit is contained in:
Syngnat
2026-03-20 12:11:09 +08:00
parent eaa76d8f04
commit 1b36f60821
2 changed files with 95 additions and 14 deletions

View File

@@ -567,12 +567,10 @@ const EditableCell: React.FC<EditableCellProps> = 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<EditableCellProps> = React.memo(({
{children}
</div>
);
} else if (cellContextMenuContext) {
// 非编辑模式(只读查询结果)也绑定右键菜单,支持复制为 INSERT/JSON/CSV 等操作
childNode = (
<div onContextMenu={handleContextMenu} style={{ minHeight: 20 }}>
{children}
</div>
);
}
const handleDoubleClick = () => {
@@ -3081,8 +3086,8 @@ const DataGrid: React.FC<DataGridProps> = ({
}, [displayColumnNames, columnWidths, sortInfo, handleResizeStart, canModifyData, onSort, renderColumnTitle]);
const mergedColumns = useMemo(() => columns.map((col): ColumnType<any> => {
if (!col.editable) return col as ColumnType<any>;
const dataIndex = String(col.dataIndex);
// 即使不可编辑,也需要通过 onCell/render 绑定右键菜单
return {
...col,
onCell: (record: Item) => {
@@ -3092,7 +3097,16 @@ const DataGrid: React.FC<DataGridProps> = ({
'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<DataGridProps> = ({
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;
},

View File

@@ -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);
// 支持多行 SQLSELECT * 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}
/>
</div>