diff --git a/common/changes/@visactor/vtable/fix-issue-5298-clear-col-width-cache_2026-08-28-00-00.json b/common/changes/@visactor/vtable/fix-issue-5298-clear-col-width-cache_2026-08-28-00-00.json new file mode 100644 index 000000000..5c3ba761d --- /dev/null +++ b/common/changes/@visactor/vtable/fix-issue-5298-clear-col-width-cache_2026-08-28-00-00.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "fix: preserve pivot table column widths after updateOption (#5298)", + "type": "patch" + } + ], + "packageName": "@visactor/vtable", + "email": "biukam.w@gmail.com" +} diff --git a/packages/vtable/__tests__/create-group-for-first-screen-issue-5298.test.ts b/packages/vtable/__tests__/create-group-for-first-screen-issue-5298.test.ts new file mode 100644 index 000000000..0deaeddde --- /dev/null +++ b/packages/vtable/__tests__/create-group-for-first-screen-issue-5298.test.ts @@ -0,0 +1,282 @@ +// @ts-nocheck +import { createGroupForFirstScreen } from '../src/scenegraph/group-creater/progress/create-group-for-first-screen'; +import { computeColsWidth } from '../src/scenegraph/layout/compute-col-width'; +import { computeRowsHeight } from '../src/scenegraph/layout/compute-row-height'; + +const { computeRowHeight, computeRowsHeight: computeRowsHeightActual } = jest.requireActual( + '../src/scenegraph/layout/compute-row-height' +); + +jest.mock('../src/scenegraph/layout/compute-col-width', () => ({ + computeColsWidth: jest.fn() +})); +jest.mock('../src/scenegraph/layout/compute-row-height', () => ({ + computeRowsHeight: jest.fn() +})); +jest.mock('../src/scenegraph/group-creater/column', () => ({ + createColGroup: jest.fn() +})); + +describe('createGroupForFirstScreen issue #5298', () => { + const makeGroup = () => ({ + firstChild: null, + lastChild: null, + setAttribute: jest.fn(), + setAttributes: jest.fn() + }); + + const createMockTable = () => ({ + widthMode: 'standard', + heightMode: 'standard', + options: {}, + colCount: 3, + rowCount: 3, + frozenColCount: 1, + frozenRowCount: 1, + rowHeaderLevelCount: 1, + leftRowSeriesNumberCount: 0, + rightFrozenColCount: 0, + bottomFrozenRowCount: 0, + isListTable: () => false, + isPivotChart: () => false, + getFrozenColsWidth: () => 0, + getFrozenRowsHeight: () => 0, + scenegraph: { + leftBottomCornerGroup: makeGroup(), + rightTopCornerGroup: makeGroup(), + rightBottomCornerGroup: makeGroup(), + colHeaderGroup: makeGroup(), + rowHeaderGroup: makeGroup(), + bottomFrozenGroup: makeGroup(), + rightFrozenGroup: makeGroup(), + bodyGroup: makeGroup() + }, + internalProps: { + _widthResizedColMap: new Set([0]), + _heightResizedRowMap: new Set() + } + }); + + test('recalculates unresized columns when some columns keep manual widths', () => { + const table = createMockTable(); + const proxy = { + table, + firstScreenColLimit: 3, + firstScreenRowLimit: 3, + totalCol: 2, + totalRow: 2, + setParamsForRow: jest.fn(), + setParamsForColumn: jest.fn(), + progress: jest.fn(), + colStart: 0, + rowStart: 0 + }; + const groups = [makeGroup(), makeGroup(), makeGroup(), makeGroup(), makeGroup(), makeGroup()]; + + createGroupForFirstScreen(...groups, 0, 0, proxy); + + expect(computeColsWidth).toHaveBeenCalledWith(table, 0, 2); + }); + + test('stops scanning resized columns after finding an unresized column', () => { + const table = createMockTable(); + table.colCount = 10000; + table.widthMode = 'adaptive'; + table.internalProps._widthResizedColMap = { + has: jest.fn(col => col === 0) + }; + const proxy = { + table, + firstScreenColLimit: 3, + firstScreenRowLimit: 3, + totalCol: 2, + totalRow: 2, + setParamsForRow: jest.fn(), + setParamsForColumn: jest.fn(), + progress: jest.fn(), + colStart: 0, + rowStart: 0 + }; + const groups = [makeGroup(), makeGroup(), makeGroup(), makeGroup(), makeGroup(), makeGroup()]; + + createGroupForFirstScreen(...groups, 0, 0, proxy); + + expect(table.internalProps._widthResizedColMap.has).toHaveBeenCalledTimes(2); + expect(computeColsWidth).toHaveBeenCalledWith(table, 0, 9999); + }); + + test('recalculates unresized rows when some rows keep manual heights', () => { + const table = createMockTable(); + table.internalProps._heightResizedRowMap = new Set([0]); + const proxy = { + table, + firstScreenColLimit: 3, + firstScreenRowLimit: 3, + totalCol: 2, + totalRow: 2, + setParamsForRow: jest.fn(), + setParamsForColumn: jest.fn(), + progress: jest.fn(), + colStart: 0, + rowStart: 0 + }; + const groups = [makeGroup(), makeGroup(), makeGroup(), makeGroup(), makeGroup(), makeGroup()]; + + createGroupForFirstScreen(...groups, 0, 0, proxy); + + expect(computeRowsHeight).toHaveBeenCalledWith(table, 0, 2); + }); + + test('stops scanning resized rows after finding an unresized row', () => { + const table = createMockTable(); + table.rowCount = 10000; + table.heightMode = 'adaptive'; + table.internalProps._heightResizedRowMap = { + has: jest.fn(row => row === 0) + }; + const proxy = { + table, + firstScreenColLimit: 3, + firstScreenRowLimit: 3, + totalCol: 2, + totalRow: 2, + setParamsForRow: jest.fn(), + setParamsForColumn: jest.fn(), + progress: jest.fn(), + colStart: 0, + rowStart: 0 + }; + const groups = [makeGroup(), makeGroup(), makeGroup(), makeGroup(), makeGroup(), makeGroup()]; + + createGroupForFirstScreen(...groups, 0, 0, proxy); + + expect(table.internalProps._heightResizedRowMap.has).toHaveBeenCalledTimes(2); + expect(computeRowsHeight).toHaveBeenCalledWith(table, 0, 9999); + }); + + test('syncs autoHeight visible rows even when all first screen rows keep manual heights', () => { + const table = createMockTable(); + table.heightMode = 'autoHeight'; + table.rowCount = 5; + table.tableNoFrameHeight = 100; + table.getBottomFrozenRowsHeight = () => 0; + table.getRowsHeight = jest.fn((start, end) => (end < 4 ? 50 : 120)); + table.internalProps._heightResizedRowMap = new Set([0, 1, 2]); + const proxy = { + table, + firstScreenColLimit: 3, + firstScreenRowLimit: 3, + bodyBottomRow: 4, + bodyTopRow: 1, + totalActualBodyRowCount: 2, + totalCol: 2, + totalRow: 2, + setParamsForRow: jest.fn(), + setParamsForColumn: jest.fn(), + progress: jest.fn(), + colStart: 0, + rowStart: 0 + }; + const groups = [makeGroup(), makeGroup(), makeGroup(), makeGroup(), makeGroup(), makeGroup()]; + + createGroupForFirstScreen(...groups, 0, 0, proxy); + + expect(computeRowsHeight).not.toHaveBeenCalledWith(table, 0, 2); + expect(computeRowsHeight).toHaveBeenCalledWith(table, 3, 3, false); + expect(computeRowsHeight).toHaveBeenCalledWith(table, 4, 4, false); + expect(proxy.totalRow).toBe(4); + expect(proxy.totalActualBodyRowCount).toBe(4); + }); + + test('does not recompute manually resized rows while filling autoHeight visible rows', () => { + const table = createMockTable(); + table.heightMode = 'autoHeight'; + table.rowCount = 5; + table.tableNoFrameHeight = 100; + table.getBottomFrozenRowsHeight = () => 0; + table.getRowsHeight = jest.fn((start, end) => (end < 4 ? 50 : 120)); + table.internalProps._heightResizedRowMap = new Set([0, 1, 2, 3]); + const proxy = { + table, + firstScreenColLimit: 3, + firstScreenRowLimit: 3, + bodyBottomRow: 4, + bodyTopRow: 1, + totalActualBodyRowCount: 2, + totalCol: 2, + totalRow: 2, + setParamsForRow: jest.fn(), + setParamsForColumn: jest.fn(), + progress: jest.fn(), + colStart: 0, + rowStart: 0 + }; + const groups = [makeGroup(), makeGroup(), makeGroup(), makeGroup(), makeGroup(), makeGroup()]; + + createGroupForFirstScreen(...groups, 0, 0, proxy); + + expect(computeRowsHeight).not.toHaveBeenCalledWith(table, 3, 3, false); + expect(computeRowsHeight).toHaveBeenCalledWith(table, 4, 4, false); + }); + + test('keeps manually resized row height when computing row height directly', () => { + const table = { + internalProps: { + _heightResizedRowMap: new Set([1]) + }, + getRowHeight: jest.fn(() => 42), + isAutoRowHeight: jest.fn() + }; + + expect(computeRowHeight(1, 0, 2, table)).toBe(42); + expect(table.isAutoRowHeight).not.toHaveBeenCalled(); + }); + + test('uses an unresized representative row when filling fixed body row heights', () => { + const setRowHeight = jest.fn(); + const table = { + defaultHeaderRowHeight: 40, + defaultHeaderColWidth: 80, + defaultRowHeight: 33, + heightMode: 'standard', + heightAdaptiveMode: 'all', + autoFillHeight: false, + options: { + defaultRowHeight: 'auto' + }, + rowCount: 4, + colCount: 2, + columnHeaderLevelCount: 1, + rowHeaderLevelCount: 0, + bottomFrozenRowCount: 0, + internalProps: { + _heightResizedRowMap: new Set([1]), + layoutMap: { + getBody: jest.fn(() => ({ + cellType: 'text', + define: {} + })) + }, + transpose: false, + autoWrapText: false, + enableLineBreak: false, + useOneRowHeightFillAll: false + }, + isPivotTable: () => false, + isPivotChart: () => false, + isAutoRowHeight: () => false, + getDefaultRowHeight: jest.fn(row => (row === 1 ? 200 : 33)), + getRowHeight: jest.fn(row => (row === 1 ? 200 : 33)), + _getCellStyle: jest.fn(() => ({})), + _setRowHeight: setRowHeight, + _clearRowRangeHeightsMap: jest.fn() + }; + + computeRowsHeightActual(table, 0, 3); + + expect(table.getDefaultRowHeight).toHaveBeenCalledWith(2); + expect(setRowHeight).not.toHaveBeenCalledWith(1, expect.any(Number)); + expect(setRowHeight).toHaveBeenCalledWith(2, 33); + expect(setRowHeight).toHaveBeenCalledWith(3, 33); + }); +}); diff --git a/packages/vtable/src/scenegraph/group-creater/progress/create-group-for-first-screen.ts b/packages/vtable/src/scenegraph/group-creater/progress/create-group-for-first-screen.ts index 01b31ddeb..4e16f0183 100644 --- a/packages/vtable/src/scenegraph/group-creater/progress/create-group-for-first-screen.ts +++ b/packages/vtable/src/scenegraph/group-creater/progress/create-group-for-first-screen.ts @@ -14,7 +14,9 @@ function fillVisibleBodyRows(proxy: SceneProxy, distRow: number): number { while (targetRow < bodyBottomRow && table.getRowsHeight(table.frozenRowCount, targetRow) < visibleBodyHeight) { const nextRow = targetRow + 1; - computeRowsHeight(table, nextRow, nextRow, false); + if (!table.internalProps._heightResizedRowMap.has(nextRow)) { + computeRowsHeight(table, nextRow, nextRow, false); + } targetRow = nextRow; } @@ -75,25 +77,38 @@ export function createGroupForFirstScreen( distRow = Math.min(proxy.firstScreenRowLimit - 1, table.rowCount - 1); } let bodyDistRow = Math.min(proxy.bodyBottomRow, distRow - table.bottomFrozenRowCount); - if (table.internalProps._widthResizedColMap.size === 0) { - // compute colums width in first screen - computeColsWidth(table, 0, distColForCompute ?? distCol); + const colEndForCompute = distColForCompute ?? distCol; + let hasUnresizedColumns = false; + for (let col = 0; col <= colEndForCompute; col++) { + if (!table.internalProps._widthResizedColMap.has(col)) { + hasUnresizedColumns = true; + break; + } + } + if (hasUnresizedColumns) { + // Compute widths for new columns while preserving manually resized columns. + computeColsWidth(table, 0, colEndForCompute); } - if (table.internalProps._heightResizedRowMap.size === 0) { - // compute rows height in first screen - computeRowsHeight( - table, - 0, - table.options.canvasHeight === 'auto' || table.options.customConfig?.forceComputeAllRowHeight - ? table.rowCount - 1 - : distRowForCompute ?? distRow - ); //如果配置了 canvasHeight为 'auto', 则一次性将所有行高都计算出来才能满足后续赋值表格高度的使用 - if (table.heightMode === 'autoHeight') { - bodyDistRow = fillVisibleBodyRows(proxy, bodyDistRow); - syncVisibleBodyRows(proxy, bodyDistRow); + const rowEndForCompute = + table.options.canvasHeight === 'auto' || table.options.customConfig?.forceComputeAllRowHeight + ? table.rowCount - 1 + : distRowForCompute ?? distRow; + let hasUnresizedRows = false; + for (let row = 0; row <= rowEndForCompute; row++) { + if (!table.internalProps._heightResizedRowMap.has(row)) { + hasUnresizedRows = true; + break; } } + if (hasUnresizedRows) { + // compute rows height in first screen + computeRowsHeight(table, 0, rowEndForCompute); //如果配置了 canvasHeight为 'auto', 则一次性将所有行高都计算出来才能满足后续赋值表格高度的使用 + } + if (table.heightMode === 'autoHeight') { + bodyDistRow = fillVisibleBodyRows(proxy, bodyDistRow); + syncVisibleBodyRows(proxy, bodyDistRow); + } if (distCol < table.colCount - table.rightFrozenColCount) { // compute right frozen row height diff --git a/packages/vtable/src/scenegraph/layout/compute-row-height.ts b/packages/vtable/src/scenegraph/layout/compute-row-height.ts index 7ae9074b6..0806eda80 100644 --- a/packages/vtable/src/scenegraph/layout/compute-row-height.ts +++ b/packages/vtable/src/scenegraph/layout/compute-row-height.ts @@ -139,14 +139,13 @@ export function computeRowsHeight( ) { // check fixed style and no wrap situation, fill all row width single compute // traspose table and row indicator pivot table cannot use single row height - const height = computeRowHeight(table.columnHeaderLevelCount, 0, table.colCount - 1, table); - fillRowsHeight( - height, - table.columnHeaderLevelCount, - table.rowCount - 1 - table.bottomFrozenRowCount, - table, - update ? newHeights : undefined - ); + const fillStartRow = table.columnHeaderLevelCount; + const fillEndRow = table.rowCount - 1 - table.bottomFrozenRowCount; + const rowForCompute = getFirstUnresizedRow(table, fillStartRow, fillEndRow); + if (rowForCompute >= 0) { + const height = computeRowHeight(rowForCompute, 0, table.colCount - 1, table); + fillRowsHeight(height, fillStartRow, fillEndRow, table, update ? newHeights : undefined); + } //底部冻结的行行高需要单独计算 for (let row = table.rowCount - table.bottomFrozenRowCount; row <= rowEnd; row++) { const height = computeRowHeight(row, 0, table.colCount - 1, table); @@ -352,6 +351,10 @@ export function computeRowsHeight( } export function computeRowHeight(row: number, startCol: number, endCol: number, table: BaseTableAPI): number { + if (table.internalProps._heightResizedRowMap.has(row)) { + return table.getRowHeight(row); + } + return computeRowHeightInternal(row, startCol, endCol, table, true); } @@ -607,6 +610,9 @@ function fillRowsHeight( return; } for (let row = startRow; row <= endRow; row++) { + if (table.internalProps._heightResizedRowMap.has(row)) { + continue; + } if (newHeights) { newHeights[row] = height; } else { @@ -616,6 +622,15 @@ function fillRowsHeight( table.internalProps.useOneRowHeightFillAll = true; } +function getFirstUnresizedRow(table: BaseTableAPI, startRow: number, endRow: number): number { + for (let row = startRow; row <= endRow; row++) { + if (!table.internalProps._heightResizedRowMap.has(row)) { + return row; + } + } + return -1; +} + /** * @description: compute customRender height * @param {number} col