diff --git a/frontend/src/components/DataGrid.tsx b/frontend/src/components/DataGrid.tsx index 08e2e4e..dabe70a 100644 --- a/frontend/src/components/DataGrid.tsx +++ b/frontend/src/components/DataGrid.tsx @@ -3815,6 +3815,7 @@ const DataGrid: React.FC = ({ // 通过合成 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 = ({ 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 = ({ 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 = ({ return () => { container.removeEventListener('wheel', handleContainerHorizontalWheel, { capture: true } as EventListenerOptions); }; - }, [enableVirtual, pickHorizontalScrollTargets, readVirtualHorizontalOffset, viewMode]); + }, [applyVirtualHorizontalOffset, enableVirtual, pickHorizontalScrollTargets, readVirtualHorizontalOffset, viewMode]); useEffect(() => { if (viewMode !== 'table') return; diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index f54534b..fabc0aa 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -2005,7 +2005,11 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => { label: (
- {`结果 ${idx + 1}${Array.isArray(rs.rows) ? ` (${rs.rows.length}${rs.truncated ? '+' : ''})` : ''}`} + {(() => { + 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 ? '+' : ''})` : ''}`; + })()} = ({ tab }) => {
), - children: ( -
- -
- ) + 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 ( +
+ + 执行成功 + 影响行数:{affected} +
+ ); + } + return ( +
+ +
+ ); + })() }))} /> ) : ( diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 0e3b694..ae0a9e9 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -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') { diff --git a/internal/app/methods_db.go b/internal/app/methods_db.go index f95e55f..14a8bb1 100644 --- a/internal/app/methods_db.go +++ b/internal/app/methods_db.go @@ -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) }