mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-24 18:00:04 +08:00
🐛 fix(data-grid): 修复窄表场景表头与数据列错位
This commit is contained in:
@@ -31,7 +31,7 @@ import 'react-resizable/css/styles.css';
|
||||
import { buildOrderBySQL, buildPaginatedSelectSQL, buildWhereSQL, escapeLiteral, quoteIdentPart, quoteQualifiedIdent, withSortBufferTuningSQL, type FilterCondition } from '../utils/sql';
|
||||
import { isMacLikePlatform, normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
|
||||
import { getDataSourceCapabilities } from '../utils/dataSourceCapabilities';
|
||||
import { calculateTableBodyBottomPadding } from './dataGridLayout';
|
||||
import { calculateTableBodyBottomPadding, calculateVirtualTableScrollX } from './dataGridLayout';
|
||||
|
||||
// --- Error Boundary ---
|
||||
interface DataGridErrorBoundaryState {
|
||||
@@ -1374,11 +1374,6 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
.${gridId} .ant-table-tbody .ant-table-row > .ant-table-cell { background: transparent !important; border-bottom: 1px solid ${darkMode ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)'} !important; border-inline-end: 1px solid transparent !important; }
|
||||
.${gridId} .ant-table-thead > tr > th { background: transparent !important; border-bottom: 1px solid ${darkMode ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)'} !important; border-inline-end: 1px solid transparent !important; }
|
||||
/* 选择列对齐:header TH 无 class(Ant Design 虚拟模式),需用 :first-child 匹配 */
|
||||
.${gridId} .ant-table-selection-col,
|
||||
.${gridId} .ant-table-bordered .ant-table-selection-col,
|
||||
.${gridId} .ant-table-selection-col.ant-table-selection-col-with-dropdown {
|
||||
width: ${selectionColumnWidth}px !important;
|
||||
}
|
||||
.${gridId} .ant-table-header th:first-child,
|
||||
.${gridId} .ant-table-thead > tr > th:first-child {
|
||||
text-align: center !important;
|
||||
@@ -1392,6 +1387,17 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
padding-inline-start: 0 !important;
|
||||
padding-inline-end: 0 !important;
|
||||
}
|
||||
/* 窄表场景下 rc-table 会按视口等比放大选择列宽度,不能再额外锁死 header 宽度;
|
||||
这里只统一 header/body 的内边距与对齐方式,避免第一列把后续数据列整体顶偏。 */
|
||||
.${gridId} .ant-table-tbody > tr > td.ant-table-selection-column,
|
||||
.${gridId} .ant-table-tbody .ant-table-row > .ant-table-cell.ant-table-selection-column,
|
||||
.${gridId} .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell.ant-table-selection-column {
|
||||
text-align: center !important;
|
||||
padding-inline-start: 0 !important;
|
||||
padding-inline-end: 0 !important;
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
.${gridId} .ant-table-thead > tr:first-child > th:first-child,
|
||||
.${gridId} .ant-table-header table > thead > tr:first-child > th:first-child {
|
||||
border-top-left-radius: ${panelRadius}px !important;
|
||||
@@ -1457,6 +1463,11 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
.${gridId} .ant-table-sticky-scroll {
|
||||
display: none !important;
|
||||
}
|
||||
/* 虚拟表列对齐:阻止 header <table> 通过 min-width:100% 拉伸到视口,
|
||||
使 header 列宽与虚拟 body 单元格宽度精确一致 */
|
||||
.${gridId} .ant-table-header > table {
|
||||
min-width: 0 !important;
|
||||
}
|
||||
.${gridId} .ant-table-tbody-virtual-scrollbar.ant-table-tbody-virtual-scrollbar-horizontal {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -3764,10 +3775,13 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
const totalWidth = columns.reduce((sum: number, col: any) => sum + (Number(col.width) || 200), 0) + selectionColumnWidth;
|
||||
const useContextMenuRow = false;
|
||||
const tableScrollX = useMemo(() => {
|
||||
const baseWidth = Math.max(totalWidth, 1000);
|
||||
if (!isMacLike || tableViewportWidth <= 0) return baseWidth;
|
||||
// macOS 在“自动隐藏滚动条”模式下容易误判为无横向滚动,预留 2px 触发稳定滚动轨道。
|
||||
return Math.max(baseWidth, tableViewportWidth + 2);
|
||||
// rc-table 在 scroll.x 小于容器宽度时会把实际列宽按视口补齐。
|
||||
// 这里必须与其使用同一套 scroll.x 口径,否则少字段场景下 header/body 会错位。
|
||||
return calculateVirtualTableScrollX({
|
||||
totalWidth,
|
||||
tableViewportWidth,
|
||||
isMacLike,
|
||||
});
|
||||
}, [totalWidth, isMacLike, tableViewportWidth]);
|
||||
const horizontalScrollVisible = viewMode === 'table' && tableScrollX > tableViewportWidth + 1;
|
||||
const horizontalScrollWidth = Math.max(externalScrollbarMinWidth, tableScrollX);
|
||||
@@ -4090,6 +4104,31 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
return () => cancelAnimationFrame(rafId);
|
||||
}, [viewMode, totalWidth, mergedDisplayData.length, pagination?.total, pagination?.pageSize, recalculateTableMetrics]);
|
||||
|
||||
// 虚拟表列对齐:antd 虚拟表 body 使用 <div>+<td>(非 <table>),
|
||||
// 不会自动拉伸列宽到视口。而 header <table> 会被 antd 的 CSS 或 JS
|
||||
// 设置为 width:100% 自动拉伸。强制 header table 宽度等于 scroll.x,
|
||||
// 使 header 列宽与 body 单元格宽度精确一致。
|
||||
useEffect(() => {
|
||||
if (viewMode !== 'table') return;
|
||||
const container = tableContainerRef.current;
|
||||
if (!container) return;
|
||||
const syncHeaderWidth = () => {
|
||||
const headerTable = container.querySelector('.ant-table-header > table') as HTMLElement;
|
||||
if (headerTable) {
|
||||
headerTable.style.setProperty('width', `${tableScrollX}px`, 'important');
|
||||
headerTable.style.setProperty('min-width', '0px', 'important');
|
||||
headerTable.style.setProperty('max-width', `${tableScrollX}px`, 'important');
|
||||
}
|
||||
};
|
||||
syncHeaderWidth();
|
||||
const rafId = requestAnimationFrame(syncHeaderWidth);
|
||||
// 监听 antd 可能的重渲染覆盖
|
||||
const observer = new MutationObserver(syncHeaderWidth);
|
||||
const headerEl = container.querySelector('.ant-table-header');
|
||||
if (headerEl) observer.observe(headerEl, { attributes: true, childList: true, subtree: true, attributeFilter: ['style'] });
|
||||
return () => { cancelAnimationFrame(rafId); observer.disconnect(); };
|
||||
}, [viewMode, tableScrollX, mergedDisplayData.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (viewMode !== 'table' || !onScrollSnapshotChange) return;
|
||||
const tableContainer = tableContainerRef.current;
|
||||
|
||||
@@ -194,7 +194,7 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
|
||||
const openTable = useCallback((tableName: string) => {
|
||||
if (!connection) return;
|
||||
addTab({
|
||||
id: `${connection.id}-${tab.dbName}-${tableName}`,
|
||||
id: `${connection.id}-${tab.dbName}-table-${tableName}`,
|
||||
title: tableName,
|
||||
type: 'table',
|
||||
connectionId: connection.id,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { calculateTableBodyBottomPadding } from './dataGridLayout';
|
||||
import { calculateTableBodyBottomPadding, calculateVirtualTableScrollX } from './dataGridLayout';
|
||||
|
||||
const assertEqual = (actual: unknown, expected: unknown, message: string) => {
|
||||
if (actual !== expected) {
|
||||
@@ -36,4 +36,34 @@ assertEqual(
|
||||
'较粗滚动条场景下应同步放大底部安全区'
|
||||
);
|
||||
|
||||
assertEqual(
|
||||
calculateVirtualTableScrollX({
|
||||
totalWidth: 646,
|
||||
tableViewportWidth: 1200,
|
||||
isMacLike: false,
|
||||
}),
|
||||
1200,
|
||||
'列总宽小于视口时应按视口宽度返回 scroll.x,避免 header/body 走两套宽度'
|
||||
);
|
||||
|
||||
assertEqual(
|
||||
calculateVirtualTableScrollX({
|
||||
totalWidth: 646,
|
||||
tableViewportWidth: 0,
|
||||
isMacLike: false,
|
||||
}),
|
||||
646,
|
||||
'未拿到视口宽度时应退回列宽总和'
|
||||
);
|
||||
|
||||
assertEqual(
|
||||
calculateVirtualTableScrollX({
|
||||
totalWidth: 1200,
|
||||
tableViewportWidth: 800,
|
||||
isMacLike: true,
|
||||
}),
|
||||
1202,
|
||||
'macOS 横向溢出时仍需额外预留 2px 以稳定滚动轨道'
|
||||
);
|
||||
|
||||
console.log('dataGridLayout tests passed');
|
||||
|
||||
@@ -4,6 +4,12 @@ export interface TableBodyBottomPaddingOptions {
|
||||
floatingScrollbarGap: number;
|
||||
}
|
||||
|
||||
export interface VirtualTableScrollXOptions {
|
||||
totalWidth: number;
|
||||
tableViewportWidth: number;
|
||||
isMacLike: boolean;
|
||||
}
|
||||
|
||||
const MIN_SCROLLBAR_CLEARANCE = 8;
|
||||
const FLOATING_SCROLLBAR_VISUAL_EXTRA = 4;
|
||||
|
||||
@@ -21,3 +27,22 @@ export const calculateTableBodyBottomPadding = ({
|
||||
|
||||
return safeScrollbarHeight + FLOATING_SCROLLBAR_VISUAL_EXTRA + safeScrollbarGap + MIN_SCROLLBAR_CLEARANCE;
|
||||
};
|
||||
|
||||
export const calculateVirtualTableScrollX = ({
|
||||
totalWidth,
|
||||
tableViewportWidth,
|
||||
isMacLike,
|
||||
}: VirtualTableScrollXOptions): number => {
|
||||
const safeTotalWidth = Math.max(0, Math.ceil(totalWidth));
|
||||
const safeViewportWidth = Math.max(0, Math.floor(tableViewportWidth));
|
||||
|
||||
if (safeViewportWidth > 0 && safeTotalWidth < safeViewportWidth) {
|
||||
return safeViewportWidth;
|
||||
}
|
||||
|
||||
if (isMacLike && safeViewportWidth > 0 && safeTotalWidth > safeViewportWidth) {
|
||||
return safeTotalWidth + 2;
|
||||
}
|
||||
|
||||
return safeTotalWidth;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user