diff --git a/common/changes/@visactor/vtable/4848-bug-vtable-sheet-deleteCol-withMergeCellState_2026-01-06-11-47.json b/common/changes/@visactor/vtable/4848-bug-vtable-sheet-deleteCol-withMergeCellState_2026-01-06-11-47.json new file mode 100644 index 0000000000..355a1e2827 --- /dev/null +++ b/common/changes/@visactor/vtable/4848-bug-vtable-sheet-deleteCol-withMergeCellState_2026-01-06-11-47.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "refactor: when has merge cells to delete column #4848\n\n", + "type": "none", + "packageName": "@visactor/vtable" + } + ], + "packageName": "@visactor/vtable", + "email": "892739385@qq.com" +} \ No newline at end of file diff --git a/common/changes/@visactor/vtable/4848-bug-vtable-sheet-deleteCol-withMergeCellState_2026-01-07-07-53.json b/common/changes/@visactor/vtable/4848-bug-vtable-sheet-deleteCol-withMergeCellState_2026-01-07-07-53.json new file mode 100644 index 0000000000..f2450fff35 --- /dev/null +++ b/common/changes/@visactor/vtable/4848-bug-vtable-sheet-deleteCol-withMergeCellState_2026-01-07-07-53.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "refactor: update cell merge delete records logic #4848\n\n", + "type": "none", + "packageName": "@visactor/vtable" + } + ], + "packageName": "@visactor/vtable", + "email": "892739385@qq.com" +} \ No newline at end of file diff --git a/docs/assets/api/en/methods.md b/docs/assets/api/en/methods.md index 223c0aec0c..06afbe1af8 100644 --- a/docs/assets/api/en/methods.md +++ b/docs/assets/api/en/methods.md @@ -2324,3 +2324,35 @@ Usage: // Collapse all column header tree nodes tableInstance.collapseAllForColumnTree(); ``` + +## updateCellContent(Function) + +Update the content of a single cell. This interface only refreshes the content of the scenegraph node, not rendering. The render() interface will not actively update the content of the scenegraph node. + +```ts + /** + * Update the content of a single cell + */ + updateCellContent: (col: number, row: number) => void; +``` +## updateCellContentRange(Function) + +Update the content of a range of cells. This interface only refreshes the content of the scenegraph node, not rendering. The render() interface will not actively update the content of the scenegraph node. + +```ts + /** + * Update the content of a range of cells + */ + updateCellContentRange: (startCol: number, startRow: number, endCol: number, endRow: number) => void; +``` + +## updateCellContentRanges(Function) + +Update the content of a range of cells. This interface only refreshes the content of the scenegraph node, not rendering. The render() interface will not actively update the content of the scenegraph node. + +```ts + /** + * Update the content of a range of cells + */ + updateCellContentRanges: (ranges: CellRange[]) => void; +``` \ No newline at end of file diff --git a/docs/assets/api/zh/methods.md b/docs/assets/api/zh/methods.md index aaeda6f201..a6efa60f52 100644 --- a/docs/assets/api/zh/methods.md +++ b/docs/assets/api/zh/methods.md @@ -2326,3 +2326,35 @@ tableInstance.expandAllForColumnTree(); // 折叠列表头树的所有节点 tableInstance.collapseAllForColumnTree(); ``` +## updateCellContent(Function) + +更新某个单元格内容. 这个接口仅是刷新场景树节点内容而非渲染。重新渲染接口render()不会主动更新场景树节点内容。 + +```ts + /** + * 更新某个单元格内容 + */ + updateCellContent: (col: number, row: number) => void; +``` + +## updateCellContentRange(Function) + +更新某个区域单元格内容. 这个接口仅是刷新场景树节点内容而非渲染。重新渲染接口render()不会主动更新场景树节点内容。 + +```ts + /** + * 更新某个区域单元格内容 + */ + updateCellContentRange: (startCol: number, startRow: number, endCol: number, endRow: number) => void; +``` + +## updateCellContentRanges(Function) + +更新某个区域单元格内容. 这个接口仅是刷新场景树节点内容而非渲染。重新渲染接口render()不会主动更新场景树节点内容。 + +```ts + /** + * 更新某个区域单元格内容 + */ + updateCellContentRanges: (ranges: CellRange[]) => void; +``` \ No newline at end of file diff --git a/packages/vtable-sheet/src/core/WorkSheet.ts b/packages/vtable-sheet/src/core/WorkSheet.ts index abe1789839..80eb909d4c 100644 --- a/packages/vtable-sheet/src/core/WorkSheet.ts +++ b/packages/vtable-sheet/src/core/WorkSheet.ts @@ -448,6 +448,76 @@ export class WorkSheet extends EventTarget implements IWorkSheetAPI { // 为了简化,我们假设删除的是连续的行,从最小的索引开始 const minIndex = Math.min(...rowIndexs.flat()); this.vtableSheet.formulaManager.removeRows(sheetKey, minIndex, deletedCount); + // 删除行后,需要更新合并单元格状态 + // 完全在删除范围内:删除合并单元格 + // 与删除范围有重叠(startRow <= deleteEndIndex && endRow >= minIndex): + // 起始行在删除范围内:移到 minIndex + // 起始行在删除范围之前:保持不变 + // 结束行在删除范围内:移到 minIndex - 1 + // 结束行在删除范围之后:减去 deletedCount + // 完全在删除范围之后(startRow > deleteEndIndex):起始行和结束行都减去 deletedCount + if (Array.isArray(this.tableInstance.options.customMergeCell)) { + const mergeCellsToRemove: number[] = []; + const deleteEndIndex = minIndex + deletedCount - 1; + const customMergeCellArray = this.tableInstance.options.customMergeCell; + // 需要clone一份mergeCellArray,因为后续会修改mergeCellArray + const cloneMergeCellArray = customMergeCellArray.map(mergeCell => ({ + ...mergeCell, + range: { + start: { ...mergeCell.range.start }, + end: { ...mergeCell.range.end } + } + })); + customMergeCellArray.forEach((mergeCell, index) => { + const startRow = mergeCell.range.start.row; + const endRow = mergeCell.range.end.row; + + // 如果合并单元格完全在删除范围内,标记为删除 + if (startRow >= minIndex && endRow <= deleteEndIndex) { + mergeCellsToRemove.push(index); + return; + } + + // 如果合并单元格与删除范围有重叠 + if (startRow <= deleteEndIndex && endRow >= minIndex) { + // 如果起始行在删除范围内,将起始行移到删除范围的起始位置(删除后这个位置不存在,所以移到 minIndex) + if (startRow >= minIndex) { + mergeCell.range.start.row = minIndex; + } + // 如果起始行在删除范围之前,不需要调整(保持不变) + + // 如果结束行在删除范围内,将结束行移到删除范围之前 + if (endRow <= deleteEndIndex) { + mergeCell.range.end.row = minIndex - 1; + } else { + // 结束行在删除范围之后,需要减去删除的行数 + mergeCell.range.end.row -= deletedCount; + } + + // 如果调整后起始行大于结束行,标记为删除 + if (mergeCell.range.start.row > mergeCell.range.end.row) { + mergeCellsToRemove.push(index); + } + } + // 如果合并单元格完全在删除范围之后,只需要向前移动行索引 + else if (startRow > deleteEndIndex) { + mergeCell.range.start.row -= deletedCount; + mergeCell.range.end.row -= deletedCount; + } + }); + + // 从后往前删除,避免索引变化影响 + mergeCellsToRemove + .sort((a, b) => b - a) + .forEach(index => { + customMergeCellArray.splice(index, 1); + }); + const updateRanges = cloneMergeCellArray.map(mergeCell => ({ + start: { ...mergeCell.range.start }, + end: { ...mergeCell.range.end } + })); + (this.tableInstance as any).updateCellContentRange(updateRanges); + } } } // update 事件不需要调整引用,因为只是数据内容变更 @@ -489,6 +559,77 @@ export class WorkSheet extends EventTarget implements IWorkSheetAPI { const minIndex = Math.min(...deleteColIndexs.flat()); const deletedCount = deleteColIndexs.length; this.vtableSheet.formulaManager.removeColumns(sheetKey, minIndex, deletedCount); + // 删除列后,需要更新合并单元格状态 + // 完全在删除范围内:删除合并单元格 + // 与删除范围有重叠(startCol <= deleteEndIndex && endCol >= minIndex): + // 起始列在删除范围内:移到 minIndex + // 起始列在删除范围之前:保持不变 + // 结束列在删除范围内:移到 minIndex - 1 + // 结束列在删除范围之后:减去 deletedCount + // 完全在删除范围之后(startCol > deleteEndIndex):起始列和结束列都减去 deletedCount + if (Array.isArray(this.tableInstance.options.customMergeCell)) { + const mergeCellsToRemove: number[] = []; + const deleteEndIndex = minIndex + deletedCount - 1; + const customMergeCellArray = this.tableInstance.options.customMergeCell; + // 需要clone一份mergeCellArray,因为后续会修改mergeCellArray + const cloneMergeCellArray = customMergeCellArray.map(mergeCell => ({ + ...mergeCell, + range: { + start: { ...mergeCell.range.start }, + end: { ...mergeCell.range.end } + } + })); + customMergeCellArray.forEach((mergeCell, index) => { + const startCol = mergeCell.range.start.col; + const endCol = mergeCell.range.end.col; + + // 如果合并单元格完全在删除范围内,标记为删除 + if (startCol >= minIndex && endCol <= deleteEndIndex) { + mergeCellsToRemove.push(index); + return; + } + + // 如果合并单元格与删除范围有重叠 + if (startCol <= deleteEndIndex && endCol >= minIndex) { + // 如果起始列在删除范围内,将起始列移到删除范围的起始位置(删除后这个位置不存在,所以移到 minIndex) + if (startCol >= minIndex) { + mergeCell.range.start.col = minIndex; + } + // 如果起始列在删除范围之前,不需要调整(保持不变) + + // 如果结束列在删除范围内,将结束列移到删除范围之前 + if (endCol <= deleteEndIndex) { + mergeCell.range.end.col = minIndex - 1; + } else { + // 结束列在删除范围之后,需要减去删除的列数 + mergeCell.range.end.col -= deletedCount; + } + + // 如果调整后起始列大于结束列,标记为删除 + if (mergeCell.range.start.col > mergeCell.range.end.col) { + mergeCellsToRemove.push(index); + } + } + // 如果合并单元格完全在删除范围之后,只需要向前移动列索引 + else if (startCol > deleteEndIndex) { + mergeCell.range.start.col -= deletedCount; + mergeCell.range.end.col -= deletedCount; + } + }); + + // 从后往前删除,避免索引变化影响 + mergeCellsToRemove + .sort((a, b) => b - a) + .forEach(index => { + customMergeCellArray.splice(index, 1); + }); + + const updateRanges = cloneMergeCellArray.map(mergeCell => ({ + start: { ...mergeCell.range.start }, + end: { ...mergeCell.range.end } + })); + (this.tableInstance as any).updateCellContentRange(updateRanges); + } } } // update 事件不需要调整引用,因为只是数据内容变更 diff --git a/packages/vtable/examples/list/list.ts b/packages/vtable/examples/list/list.ts index 7f8d59b387..84d73c2a34 100644 --- a/packages/vtable/examples/list/list.ts +++ b/packages/vtable/examples/list/list.ts @@ -304,7 +304,7 @@ export function createTable() { }; const tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID)!, option); window.tableInstance = tableInstance; - + tableInstance.mergeCells(3, 4, 5, 4); bindDebugTool(tableInstance.scenegraph.stage, { customGrapicKeys: ['col', 'row'] }); diff --git a/packages/vtable/src/core/BaseTable.ts b/packages/vtable/src/core/BaseTable.ts index 92a094191e..f9c831a9fc 100644 --- a/packages/vtable/src/core/BaseTable.ts +++ b/packages/vtable/src/core/BaseTable.ts @@ -3256,6 +3256,8 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { this.options.select?.makeSelectCellVisible ?? true, true ); + //防止触发到pointertap事件执行endSelectCells方法 会导致select.ranges被合并扩大范围 + this.stateManager.select.selecting = false; } /** * 拖拽选择列. 当结合插件table-series-number使用时,需要使用这个方法来拖拽选择整列 @@ -3277,13 +3279,16 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { this.options.select?.makeSelectCellVisible ?? true, true ); + //防止触发到pointertap事件执行endSelectCells方法 会导致select.ranges被合并扩大范围 + this.stateManager.select.selecting = false; } /** * 结束拖拽选择列. 当结合插件table-series-number使用时,需要使用这个方法来结束拖拽选择整列或者整行 */ endDragSelect() { this.stateManager.updateInteractionState(InteractionState.default); - this.stateManager.endSelectCells(false, false); + //上面方法dragSelectCol和startDragSelectCol方法中已经设置了select.selecting = false,所以这里不需要再调用endSelectCells方法 + // this.stateManager.endSelectCells(false, false); } /** @@ -3317,6 +3322,8 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { this.options.select?.makeSelectCellVisible ?? true, true ); + //防止触发到pointertap事件执行endSelectCells方法 会导致select.ranges被合并扩大范围 + this.stateManager.select.selecting = false; } /** * 拖拽选择行. 当结合插件table-series-number使用时,需要使用这个方法来拖拽选择整行 @@ -3338,6 +3345,8 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { this.options.select?.makeSelectCellVisible ?? true, true ); + //防止触发到pointertap事件执行endSelectCells方法 会导致select.ranges被合并扩大范围 + this.stateManager.select.selecting = false; } abstract isListTable(): boolean; @@ -5028,4 +5037,26 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { } return false; } + /** 更新某个单元格内容 ,重新渲染不会更新。 这个接口也仅是更新而非渲染*/ + updateCellContent(col: number, row: number) { + this.scenegraph.updateCellContent(col, row); + } + + /** 更新某个区域单元格内容 ,重新渲染不会更新。 这个接口也仅是更新而非渲染*/ + updateCellContentRange(startCol: number, startRow: number, endCol: number, endRow: number) { + for (let i = startCol; i <= endCol; i++) { + for (let j = startRow; j <= endRow; j++) { + this.scenegraph.updateCellContent(i, j); + } + } + } + + /** 更新某个区域单元格内容 ,重新渲染不会更新。 这个接口也仅是更新而非渲染*/ + updateCellContentRanges(ranges: CellRange[]) { + //ranges中每个range都调用updateCellContent + for (let i = 0; i < ranges.length; i++) { + const range = ranges[i]; + this.updateCellContentRange(range.start.col, range.start.row, range.end.col, range.end.row); + } + } } diff --git a/packages/vtable/src/state/select/update-position.ts b/packages/vtable/src/state/select/update-position.ts index 46f8dc279a..b617adce1b 100644 --- a/packages/vtable/src/state/select/update-position.ts +++ b/packages/vtable/src/state/select/update-position.ts @@ -1,3 +1,4 @@ +import { isValid } from '@visactor/vutils'; import type { ListTable } from '../..'; import type { SimpleHeaderLayoutMap } from '../../layout'; import type { Scenegraph } from '../../scenegraph/scenegraph'; @@ -160,7 +161,7 @@ export function updateSelectPosition( // // 更新select border // scenegraph.updateCellSelectBorder(cellPos); } else { - let extendSelectRange = true; + let extendSelectRange = isValid(skipBodyMerge) ? !skipBodyMerge : true; // 单选或多选开始 if (cellPos.col !== -1 && cellPos.row !== -1 && !enableCtrlSelectMode) { state.select.ranges = []; @@ -349,7 +350,7 @@ export function updateSelectPosition( (interactionState === InteractionState.grabing || table.eventManager.isDraging) && !table.stateManager.isResizeCol() ) { - let extendSelectRange = true; + let extendSelectRange = isValid(skipBodyMerge) ? !skipBodyMerge : true; // 可能有cellPosStart从-1开始grabing的情况 if (cellPos.col === -1) { cellPos.col = col; diff --git a/packages/vtable/src/ts-types/base-table.ts b/packages/vtable/src/ts-types/base-table.ts index 7af8a81757..b39ef2d358 100644 --- a/packages/vtable/src/ts-types/base-table.ts +++ b/packages/vtable/src/ts-types/base-table.ts @@ -1072,6 +1072,9 @@ export interface BaseTableAPI { _getComputedFrozenColCount: (frozenColCount: number) => number; isColumnSelected: (col: number) => boolean; isRowSelected: (row: number) => boolean; + updateCellContentRanges: (ranges: CellRange[]) => void; + updateCellContent: (col: number, row: number) => void; + updateCellContentRange: (startCol: number, startRow: number, endCol: number, endRow: number) => void; } export interface ListTableProtected extends IBaseTableProtected { /** 表格数据 */