🐛 fix(query-editor/data-grid): 修复UPDATE影响行数为0及虚拟表Shift+滚轮横向滚动失效

- 后端修复:DBQueryMulti 包含写操作时跳过原生 QueryMulti,走逐条 Exec 路径获取 RowsAffected
- 结果展示:UPDATE/INSERT/DELETE 结果改为简洁的执行成功提示,不再展示 DataGrid 全套操作按钮
- Tab标签:写操作结果集标签改为「结果 N ✓」替代原来的行数计数
- 横向滚动:修复虚拟表守卫检查选择器不匹配(.rc-virtual-list-holder → .ant-table-tbody-virtual-holder)
- 事件处理:使用 event.isTrusted 区分合成事件,通过 applyVirtualHorizontalOffset 驱动 rc-virtual-list
- 目标检查:isTableDataAreaTarget 改为黑名单模式,兼容 rc-virtual-list 包裹元素
This commit is contained in:
Syngnat
2026-03-19 17:13:38 +08:00
parent ab61e703b1
commit 18cb66b893
4 changed files with 71 additions and 29 deletions

View File

@@ -3815,6 +3815,7 @@ const DataGrid: React.FC<DataGridProps> = ({
// 通过合成 WheelEvent 驱动 rc-virtual-list 内部 offsetLeft state
// 让 rc-table onInternalScroll 自动同步 header scrollLeft。
// 不直接操作 DOM marginLeft避免 React re-render 覆盖。
holderEl.dispatchEvent(new WheelEvent('wheel', {
deltaX: deltaX,
deltaY: 0,
@@ -3987,27 +3988,29 @@ const DataGrid: React.FC<DataGridProps> = ({
const isTableDataAreaTarget = (target: EventTarget | null) => {
const element = target instanceof HTMLElement ? target : null;
if (!element) return false;
// 排除外部滚动条与工具栏,其余容器内元素一律视为数据区域
if (element.closest('.data-grid-external-horizontal-scroll')) return false;
return !!element.closest('.ant-table-body, .ant-table-content, .ant-table-cell, .ant-table-row, .ant-table-tbody, .ant-table-placeholder');
if (element.closest('.data-grid-toolbar')) return false;
return true;
};
const handleContainerHorizontalWheel = (event: WheelEvent) => {
// applyVirtualHorizontalOffset 分发的合成 WheelEventisTrusted=false
// 需要传播到 rc-virtual-list 的内部 handler此处不拦截。
if (!event.isTrusted) return;
const horizontalDelta = resolveHorizontalDelta(event);
if (!Number.isFinite(horizontalDelta) || Math.abs(horizontalDelta) < 0.5) return;
if (!isTableDataAreaTarget(event.target)) return;
if (enableVirtual) {
// 虚拟模式:不拦截事件,让 rc-virtual-list 原生处理 wheel。
// rc-virtual-list 会通过内部 setOffsetLeft → re-render → onVirtualScroll
// 自动同步 header scrollLeft。
// 仅需在状态更新后同步外部横向滚动条。
event.preventDefault();
event.stopPropagation();
horizontalSyncSourceRef.current = 'table';
// 空数据回退virtual-holder 不存在时,手动滚动表头
const virtualHolder = container.querySelector('.rc-virtual-list-holder') as HTMLElement | null;
const virtualHolder = container.querySelector('.ant-table-tbody-virtual-holder') as HTMLElement | null;
if (!virtualHolder) {
event.preventDefault();
event.stopPropagation();
const headerEl = container.querySelector('.ant-table-header') as HTMLElement | null;
const contentEl = container.querySelector('.ant-table-content') as HTMLElement | null;
const fallbackTargets = [headerEl, contentEl].filter((el): el is HTMLElement => el instanceof HTMLElement && el.scrollWidth > el.clientWidth + 1);
@@ -4027,6 +4030,9 @@ const DataGrid: React.FC<DataGridProps> = ({
return;
}
// 有数据:通过 applyVirtualHorizontalOffset 合成 WheelEvent 驱动 rc-virtual-list
const currentOffset = readVirtualHorizontalOffset(container);
applyVirtualHorizontalOffset(container, currentOffset + horizontalDelta);
requestAnimationFrame(() => {
const nextScrollLeft = readVirtualHorizontalOffset(container);
lastTableScrollLeftRef.current = nextScrollLeft;
@@ -4076,7 +4082,7 @@ const DataGrid: React.FC<DataGridProps> = ({
return () => {
container.removeEventListener('wheel', handleContainerHorizontalWheel, { capture: true } as EventListenerOptions);
};
}, [enableVirtual, pickHorizontalScrollTargets, readVirtualHorizontalOffset, viewMode]);
}, [applyVirtualHorizontalOffset, enableVirtual, pickHorizontalScrollTargets, readVirtualHorizontalOffset, viewMode]);
useEffect(() => {
if (viewMode !== 'table') return;

View File

@@ -2005,7 +2005,11 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => {
label: (
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
<Tooltip title={rs.sql}>
<span>{`结果 ${idx + 1}${Array.isArray(rs.rows) ? ` (${rs.rows.length}${rs.truncated ? '+' : ''})` : ''}`}</span>
<span>{(() => {
const isAffected = rs.columns.length === 1 && rs.columns[0] === 'affectedRows';
if (isAffected) return `结果 ${idx + 1}`;
return `结果 ${idx + 1}${Array.isArray(rs.rows) ? ` (${rs.rows.length}${rs.truncated ? '+' : ''})` : ''}`;
})()}</span>
</Tooltip>
<Tooltip title="关闭结果">
<span
@@ -2021,23 +2025,40 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => {
</Tooltip>
</div>
),
children: (
<div style={{ flex: 1, minHeight: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<DataGrid
data={rs.rows}
columnNames={rs.columns}
loading={loading}
tableName={rs.tableName}
exportScope="queryResult"
resultSql={rs.exportSql || rs.sql}
dbName={currentDb}
connectionId={currentConnectionId}
pkColumns={rs.pkColumns}
onReload={handleRun}
readOnly={rs.readOnly}
/>
</div>
)
children: (() => {
// 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 (
<div style={{
flex: 1, minHeight: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
flexDirection: 'column', gap: 8, color: '#666', userSelect: 'text',
}}>
<span style={{ fontSize: 36, color: '#52c41a' }}></span>
<span style={{ fontSize: 14, fontWeight: 500 }}></span>
<span style={{ fontSize: 13, color: '#999' }}>{affected}</span>
</div>
);
}
return (
<div style={{ flex: 1, minHeight: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<DataGrid
data={rs.rows}
columnNames={rs.columns}
loading={loading}
tableName={rs.tableName}
exportScope="queryResult"
resultSql={rs.exportSql || rs.sql}
dbName={currentDb}
connectionId={currentConnectionId}
pkColumns={rs.pkColumns}
onReload={handleRun}
readOnly={rs.readOnly}
/>
</div>
);
})()
}))}
/>
) : (

View File

@@ -1462,7 +1462,8 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
type: 'table-overview' as any,
connectionId: id,
dbName,
});
schemaName,
} as any);
return;
}
if (node.type === 'table') {

View File

@@ -525,8 +525,22 @@ func (a *App) DBQueryMulti(config connection.ConnectionConfig, dbName string, qu
a.queryMu.Unlock()
}()
// 尝试使用驱动原生多结果集支持
// 尝试使用驱动原生多结果集支持
// 注意:原生 conn.Query() 执行写操作UPDATE/INSERT/DELETE
// sql.Rows 不暴露 RowsAffected导致影响行数丢失。
// 因此仅在全部语句皆为读操作时才使用原生路径。
allReadOnly := true
for _, stmt := range splitSQLStatements(query) {
if strings.TrimSpace(stmt) != "" && !isReadOnlySQLQuery(runConfig.Type, stmt) {
allReadOnly = false
break
}
}
runMultiQuery := func(inst db.Database) ([]connection.ResultSetData, error) {
if !allReadOnly {
return nil, nil // 包含写操作,走逐条执行路径
}
if q, ok := inst.(db.MultiResultQuerierContext); ok {
return q.QueryMultiContext(ctx, query)
}