mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-19 21:29:32 +08:00
🐛 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:
@@ -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 分发的合成 WheelEvent(isTrusted=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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})()
|
||||
}))}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user