diff --git a/common/changes/@visactor/vtable/feat-frozen-shadow-skip-merged-cells_2026-10-12-12-00.json b/common/changes/@visactor/vtable/feat-frozen-shadow-skip-merged-cells_2026-10-12-12-00.json new file mode 100644 index 0000000000..235726c928 --- /dev/null +++ b/common/changes/@visactor/vtable/feat-frozen-shadow-skip-merged-cells_2026-10-12-12-00.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "feat: support skipping cross-boundary merged cells in frozen column shadows", + "type": "minor" + } + ], + "packageName": "@visactor/vtable" +} diff --git a/docs/assets/option/en/common/frozen-column-line-style.md b/docs/assets/option/en/common/frozen-column-line-style.md index 5066d6b9d8..ba85bb1db8 100644 --- a/docs/assets/option/en/common/frozen-column-line-style.md +++ b/docs/assets/option/en/common/frozen-column-line-style.md @@ -24,3 +24,15 @@ Shadow Visible Time, default is `always`. - always: always show - scrolling: show when scrolling - overflow: show only at frozen boundaries with body content hidden beyond that edge. The left shadow appears after scrolling away from the start; the right shadow appears while content remains to the right. Shadows persist after scrolling stops and disappear at the corresponding edge. No shadow is shown without frozen columns or horizontal body overflow. + +###${prefix} skipMergedCells(boolean) + +Skip merged cells crossing a frozen-column boundary. Defaults to `false`. + +When enabled, the shadow is omitted within each cross-boundary merged cell and resumes below it. Supports multiple gaps, left and right frozen columns, and merged cells in the top, body and bottom frozen regions. Merges contained entirely on one side of the boundary keep the original shadow. Shadow width, colors and visibility are unchanged. + +```js +theme: VTable.themes.DEFAULT.extends({ + frozenColumnLine: { shadow: { skipMergedCells: true } } +}) +``` diff --git a/docs/assets/option/zh/common/frozen-column-line-style.md b/docs/assets/option/zh/common/frozen-column-line-style.md index 925ed5fd78..5b5bae7ec8 100644 --- a/docs/assets/option/zh/common/frozen-column-line-style.md +++ b/docs/assets/option/zh/common/frozen-column-line-style.md @@ -23,3 +23,15 @@ - always: 总是显示 - scrolling: 滚动时显示 - overflow: 仅在冻结边界外有被遮挡的表格主体内容时显示。左侧阴影在滚离起点后显示,右侧阴影在右边仍有内容时显示;停止滚动后保持,滚到对应边缘立即隐藏。没有冻结列或主体没有水平溢出时不显示。 + +###${prefix} skipMergedCells(boolean) + +是否跳过跨越冻结列边界的合并单元格,默认 `false`。 + +开启后,每个跨冻结边界的合并单元格所在区间不绘制阴影,区间结束后恢复绘制。支持多段截断、左右冻结列,以及顶部、滚动区和底部冻结区的合并单元格。完全位于边界同一侧的合并单元格保持原有阴影。阴影宽度、颜色和显示时机不变。 + +```js +theme: VTable.themes.DEFAULT.extends({ + frozenColumnLine: { shadow: { skipMergedCells: true } } +}) +``` diff --git a/docs/public/vtable/guide/frozen-shadow-merged-cells.png b/docs/public/vtable/guide/frozen-shadow-merged-cells.png new file mode 100644 index 0000000000..cf9a1f94df Binary files /dev/null and b/docs/public/vtable/guide/frozen-shadow-merged-cells.png differ diff --git a/packages/vtable/__tests__/frozen-shadow.test.ts b/packages/vtable/__tests__/frozen-shadow.test.ts new file mode 100644 index 0000000000..e3716fa15b --- /dev/null +++ b/packages/vtable/__tests__/frozen-shadow.test.ts @@ -0,0 +1,138 @@ +import { getFrozenShadowSegments, subtractShadowIntervals } from '../src/scenegraph/component/frozen-shadow'; +import type { BaseTableAPI } from '../src/ts-types/base-table'; +import type { CellRange } from '../src/ts-types'; + +type Merge = CellRange & { rect?: { top: number; bottom: number } }; + +function merge(startCol: number, endCol: number, startRow: number, endRow = startRow, rect?: Merge['rect']): Merge { + return { start: { col: startCol, row: startRow }, end: { col: endCol, row: endRow }, rect }; +} + +function createTable(merges: Merge[] = [], overrides: Partial = {}) { + const visited: number[] = []; + const table = { + colCount: 9, + rowCount: 12, + frozenRowCount: 2, + bottomFrozenRowCount: 1, + getFrozenRowsHeight: () => 80, + getBottomFrozenRowsHeight: () => 40, + getBodyVisibleCellRange: () => ({ rowStart: 2, rowEnd: 9 }), + getCellRange: (col: number, row: number) => { + visited.push(row); + return ( + merges.find(m => m.start.row <= row && m.end.row >= row && m.start.col <= col && m.end.col >= col) ?? { + start: { col, row }, + end: { col, row } + } + ); + }, + getCellRangeRelativeRect: (range: Merge) => { + if (range.rect) { + return range.rect; + } + const top = range.start.row === 11 ? 380 : range.start.row * 40; + return { top, bottom: top + (range.end.row - range.start.row + 1) * 40 }; + }, + ...overrides + } as unknown as BaseTableAPI; + return { table, visited }; +} + +describe('frozen column shadows skipping merged cells', () => { + test('truncates the shadow above a cross-boundary bottom summary', () => { + const { table } = createTable([merge(0, 8, 11)]); + expect(getFrozenShadowSegments(table, 1, 420)).toEqual([{ start: 0, end: 380 }]); + }); + + test('keeps the shadow for ordinary bottom frozen cells', () => { + const { table } = createTable(); + expect(getFrozenShadowSegments(table, 1, 420)).toEqual([{ start: 0, end: 420 }]); + }); + + test('resumes the shadow after multiple body merges including a multi-row merge', () => { + const { table } = createTable([merge(0, 3, 3), merge(0, 3, 6, 7), merge(0, 8, 11)]); + expect(getFrozenShadowSegments(table, 1, 420)).toEqual([ + { start: 0, end: 120 }, + { start: 160, end: 240 }, + { start: 320, end: 380 } + ]); + }); + + test('handles both header and bottom merges', () => { + const { table } = createTable([merge(0, 3, 0, 1), merge(0, 8, 11)]); + expect(getFrozenShadowSegments(table, 1, 420)).toEqual([{ start: 80, end: 380 }]); + }); + + test('does not skip merges contained on one side of the frozen boundary', () => { + const { table } = createTable([merge(0, 1, 3), merge(2, 4, 5)]); + expect(getFrozenShadowSegments(table, 1, 420)).toEqual([{ start: 0, end: 420 }]); + }); + + test('handles the right frozen boundary', () => { + const { table } = createTable([merge(5, 8, 3), merge(0, 8, 11)]); + expect(getFrozenShadowSegments(table, 6, 420)).toEqual([ + { start: 0, end: 120 }, + { start: 160, end: 380 } + ]); + }); + + test('clips a scrolled body merge to the body viewport and visits only visible rows', () => { + const { table, visited } = createTable([merge(0, 3, 2, 8, { top: -60, bottom: 220 })], { + getBodyVisibleCellRange: () => ({ rowStart: 5, rowEnd: 10, colStart: 2, colEnd: 8 }) + }); + expect(getFrozenShadowSegments(table, 1, 420)).toEqual([ + { start: 0, end: 80 }, + { start: 220, end: 420 } + ]); + expect(visited).toEqual([0, 1, 5, 9, 10, 11]); + }); + + test('handles an empty body and multiple bottom frozen rows', () => { + const { table } = createTable([merge(0, 8, 10, 11, { top: 340, bottom: 420 })], { + bottomFrozenRowCount: 2, + getBottomFrozenRowsHeight: () => 80, + getBodyVisibleCellRange: () => null + }); + expect(getFrozenShadowSegments(table, 1, 420)).toEqual([{ start: 0, end: 340 }]); + }); + + test('unions overlapping intervals and clips them to the drawing height', () => { + expect( + subtractShadowIntervals(420, [ + { start: 100, end: 200 }, + { start: 180, end: 240 }, + { start: -30, end: 20 }, + { start: 380, end: 900 }, + { start: 20, end: 20 } + ]) + ).toEqual([ + { start: 20, end: 100 }, + { start: 240, end: 380 } + ]); + expect(subtractShadowIntervals(0, [])).toEqual([]); + expect(subtractShadowIntervals(420, [{ start: 0, end: 420 }])).toEqual([]); + }); + + test('keeps the full-height shadow when there is no column boundary', () => { + const { table } = createTable([merge(0, 8, 11)]); + expect(getFrozenShadowSegments(table, -1, 420)).toEqual([{ start: 0, end: 420 }]); + expect(getFrozenShadowSegments(table, 8, 420)).toEqual([{ start: 0, end: 420 }]); + }); + + test('recomputes intervals after row geometry or merge definitions change', () => { + const merges = [merge(0, 3, 3, 3, { top: 120, bottom: 160 })]; + const { table } = createTable(merges); + expect(getFrozenShadowSegments(table, 1, 420)).toEqual([ + { start: 0, end: 120 }, + { start: 160, end: 420 } + ]); + merges[0].rect = { top: 120, bottom: 190 }; + expect(getFrozenShadowSegments(table, 1, 420)).toEqual([ + { start: 0, end: 120 }, + { start: 190, end: 420 } + ]); + merges.length = 0; + expect(getFrozenShadowSegments(table, 1, 420)).toEqual([{ start: 0, end: 420 }]); + }); +}); diff --git a/packages/vtable/__tests__/visual/cases/frozen/merged-shadow.mjs b/packages/vtable/__tests__/visual/cases/frozen/merged-shadow.mjs new file mode 100644 index 0000000000..8aa918be4c --- /dev/null +++ b/packages/vtable/__tests__/visual/cases/frozen/merged-shadow.mjs @@ -0,0 +1,78 @@ +/** + * 验证目的:左右冻结阴影跳过多个跨边界合并区间,并跟随真实滚动和主题切换。 + * 来源:独立固定数据样例;覆盖单行、多行、底部合并和默认主题样式。 + */ +export default { + mount(container) { + container.style.width = '720px'; + container.style.height = '480px'; + const columns = Array.from({ length: 8 }, (_, col) => ({ field: `c${col}`, title: `C${col + 1}`, width: 140 })); + const records = Array.from({ length: 14 }, (_, row) => + Object.fromEntries(columns.map((column, col) => [column.field, `R${row + 1} / C${col + 1}`])) + ); + const theme = skipMergedCells => window.VTable.themes.DEFAULT.extends({ + frozenColumnLine: { shadow: { skipMergedCells } } + }); + const table = new window.VTable.ListTable(container, { + columns, records, frozenColCount: 2, rightFrozenColCount: 1, bottomFrozenRowCount: 1, + defaultRowHeight: 36, defaultHeaderRowHeight: 44, + theme: theme(true), + customMergeCell: (col, row, instance) => { + const firstRow = row === 3 ? 3 : row >= 6 && row <= 7 ? 6 : instance.rowCount - 1; + const lastRow = firstRow === 6 ? 7 : firstRow; + if (row < firstRow || row > lastRow) return; + return { + text: firstRow === 3 ? 'Merged A' : firstRow === 6 ? 'Merged B (two rows)' : 'Summary', + range: { start: { col: 0, row: firstRow }, end: { col: instance.colCount - 1, row: lastRow } } + }; + } + }); + // 通过实际 checkbox 事件更新主题,验证运行时开关,而非直接写内部状态。 + const control = document.createElement('input'); + control.type = 'checkbox'; + control.checked = true; + control.setAttribute('aria-label', 'Skip merged shadows'); + control.addEventListener('change', () => table.updateTheme(theme(control.checked))); + container.after(control); + return table; + }, + async exercise(page) { + await page.getByRole('checkbox', { name: 'Skip merged shadows' }).uncheck(); + await page.waitForFunction(() => { + const component = window.__visualTable.scenegraph.component; + return !component.frozenShadowLine.pathProxy && !component.rightFrozenShadowLine.pathProxy; + }); + await page.getByRole('checkbox', { name: 'Skip merged shadows' }).check(); + const host = await page.locator('#table').boundingBox(); + await page.mouse.move(host.x + host.width / 2, host.y + 240); + await page.mouse.wheel(180, 0); + await page.waitForFunction(() => window.__visualTable.scrollLeft > 0); + await page.mouse.wheel(0, 36); + await page.waitForFunction(() => window.__visualTable.scrollTop > 0); + }, + async verify(page) { + // 检查路径对应的真实矩形区间,确保合并区域没有被阴影穿过且普通行恢复。 + await page.evaluate(() => { + const table = window.__visualTable; + const defaults = window.VTable.themes.DEFAULT.frozenColumnLine.shadow; + const theme = table.theme.frozenColumnLine.shadow; + if (!theme.skipMergedCells || theme.width !== defaults.width || + theme.startColor !== defaults.startColor || theme.endColor !== defaults.endColor) + throw new Error('阴影开关或默认样式错误'); + const mergedRows = [3, 6, 7, table.rowCount - 1]; + for (const shadow of [table.scenegraph.component.frozenShadowLine, table.scenegraph.component.rightFrozenShadowLine]) { + if (typeof shadow.pathProxy !== 'function') throw new Error('冻结阴影缺少截断路径'); + const segments = shadow.pathProxy(shadow.attribute).commandList.map(command => ({ + start: command[2], end: command[2] + command[4] + })); + for (const row of [2, 3, 4, 5, 6, 7, 8, table.rowCount - 1]) { + const rect = table.getCellRangeRelativeRect({ col: 1, row }); + const y = (rect.top + rect.bottom) / 2; + if (y < table.getFrozenRowsHeight() || y >= shadow.attribute.height) continue; + const covered = segments.some(segment => y >= segment.start && y < segment.end); + if (covered === mergedRows.includes(row)) throw new Error(`行 ${row} 的阴影片段错误`); + } + } + }); + } +}; diff --git a/packages/vtable/__tests__/visual/cases/index.mjs b/packages/vtable/__tests__/visual/cases/index.mjs index 1072fdd97c..b1d8f113e3 100644 --- a/packages/vtable/__tests__/visual/cases/index.mjs +++ b/packages/vtable/__tests__/visual/cases/index.mjs @@ -194,6 +194,7 @@ export const cases = [ { id: 'frozen-shadow-start', purpose: '冻结列 overflow 阴影在 start 位置的可见性', file: './frozen/frozen-shadow-start.mjs', bundles: ['vtable'] }, { id: 'frozen-shadow-end', purpose: '冻结列 overflow 阴影在 end 位置的可见性', file: './frozen/frozen-shadow-end.mjs', bundles: ['vtable'] }, { id: 'frozen-shadow-middle', purpose: '冻结列 overflow 阴影在 middle 位置的可见性', file: './frozen/frozen-shadow-middle.mjs', bundles: ['vtable'] }, + { id: 'frozen-shadow-merged-cells', purpose: '左右冻结阴影多段截断、真实滚动和主题切换', file: './frozen/merged-shadow.mjs', bundles: ['vtable'] }, { id: 'scrollbar-ignore-frozen', purpose: '主横向滚动条跨越冻结列宽度', file: './frozen/scrollbar-ignore-frozen.mjs', bundles: ['vtable'] }, { id: 'frozen-empty-records-header', purpose: '大量列滚动后清空记录保留冻结表头', file: './frozen/empty-records-header.mjs', bundles: ['vtable'] }, { id: 'checkbox-header-options', purpose: '复选框表头回调固定选中与禁用配置', file: './cells/checkbox-header-options.mjs', bundles: ['vtable'] }, diff --git a/packages/vtable/examples/frozen/list-table-merged-shadow.ts b/packages/vtable/examples/frozen/list-table-merged-shadow.ts new file mode 100644 index 0000000000..bfbb48c437 --- /dev/null +++ b/packages/vtable/examples/frozen/list-table-merged-shadow.ts @@ -0,0 +1,38 @@ +import * as VTable from '../../src'; + +/** 多个跨冻结边界的合并区域分别截断阴影,普通行继续显示默认阴影。 */ +export function createTable() { + const container = document.getElementById('vTable'); + container.style.width = '900px'; + container.style.height = '480px'; + const columns = Array.from({ length: 8 }, (_, col) => ({ + field: `c${col}`, + title: `Column ${col + 1}`, + width: 160 + })); + const records = Array.from({ length: 14 }, (_, row) => + Object.fromEntries(columns.map((column, col) => [column.field, `R${row + 1} / C${col + 1}`])) + ); + const table = new VTable.ListTable(container, { + columns, + records, + frozenColCount: 2, + rightFrozenColCount: 1, + bottomFrozenRowCount: 1, + theme: VTable.themes.DEFAULT.extends({ + frozenColumnLine: { shadow: { skipMergedCells: true } } + }), + customMergeCell: (col, row, instance) => { + const firstRow = row === 3 ? 3 : row >= 6 && row <= 7 ? 6 : instance.rowCount - 1; + const lastRow = firstRow === 6 ? 7 : firstRow; + if (row < firstRow || row > lastRow) { + return; + } + return { + text: firstRow === 3 ? 'Merged A' : firstRow === 6 ? 'Merged B (two rows)' : 'Frozen summary', + range: { start: { col: 0, row: firstRow }, end: { col: instance.colCount - 1, row: lastRow } } + }; + } + }); + window.tableInstance = table; +} diff --git a/packages/vtable/src/scenegraph/component/frozen-shadow.ts b/packages/vtable/src/scenegraph/component/frozen-shadow.ts new file mode 100644 index 0000000000..e0a8755eb0 --- /dev/null +++ b/packages/vtable/src/scenegraph/component/frozen-shadow.ts @@ -0,0 +1,66 @@ +import type { BaseTableAPI } from '../../ts-types/base-table'; + +export type ShadowSegment = { start: number; end: number }; + +/** Subtract merged-cell intervals from a vertical shadow. */ +export function subtractShadowIntervals(height: number, blocked: ShadowSegment[]): ShadowSegment[] { + const segments: ShadowSegment[] = []; + let cursor = 0; + for (const interval of blocked.slice().sort((a, b) => a.start - b.start)) { + const start = Math.max(0, Math.min(height, interval.start)); + const end = Math.max(0, Math.min(height, interval.end)); + if (end <= start) { + continue; + } + if (start > cursor) { + segments.push({ start: cursor, end: start }); + } + cursor = Math.max(cursor, end); + } + if (cursor < height) { + segments.push({ start: cursor, end: height }); + } + return segments; +} + +/** Only inspect visible rows at the boundary, including top/bottom frozen bands. */ +export function getFrozenShadowSegments(table: BaseTableAPI, boundaryCol: number, height: number): ShadowSegment[] { + if (boundaryCol < 0 || boundaryCol >= table.colCount - 1) { + return height > 0 ? [{ start: 0, end: height }] : []; + } + const blocked: ShadowSegment[] = []; + const top = Math.min(height, table.getFrozenRowsHeight()); + const bottom = Math.max(top, height - table.getBottomFrozenRowsHeight()); + const bottomRow = table.rowCount - table.bottomFrozenRowCount; + const visibleBody = table.getBodyVisibleCellRange(); + const bands = [ + { rowStart: 0, rowEnd: table.frozenRowCount - 1, start: 0, end: top }, + { + rowStart: Math.max(table.frozenRowCount, visibleBody?.rowStart ?? table.frozenRowCount), + rowEnd: Math.min(bottomRow - 1, visibleBody?.rowEnd ?? -1), + start: top, + end: bottom + }, + { rowStart: bottomRow, rowEnd: table.rowCount - 1, start: bottom, end: height } + ]; + + for (const band of bands) { + if (band.end <= band.start) { + continue; + } + let row = band.rowStart; + while (row <= band.rowEnd) { + const range = table.getCellRange(boundaryCol, row); + if (range && range.start.col <= boundaryCol && range.end.col > boundaryCol) { + const rect = table.getCellRangeRelativeRect(range); + const start = Math.max(band.start, rect.top); + const end = Math.min(band.end, rect.bottom); + if (end > start) { + blocked.push({ start, end }); + } + } + row = Math.max(row + 1, (range?.end.row ?? row) + 1); + } + } + return subtractShadowIntervals(height, blocked); +} diff --git a/packages/vtable/src/scenegraph/component/table-component.ts b/packages/vtable/src/scenegraph/component/table-component.ts index 8352992d2c..b459f98f7f 100644 --- a/packages/vtable/src/scenegraph/component/table-component.ts +++ b/packages/vtable/src/scenegraph/component/table-component.ts @@ -1,6 +1,6 @@ import { theme } from './../../themes'; import type { ILine, IRect, IGroup, FederatedPointerEvent, Text, IText } from '@src/vrender'; -import { createRect, createLine, createText, createGroup, createSymbol } from '@src/vrender'; +import { createRect, createLine, createText, createGroup, createSymbol, CustomPath2D } from '@src/vrender'; import { ScrollBar } from '@src/vrender'; import type { Group } from '../graphic/group'; import { MenuHandler } from './menu'; @@ -9,6 +9,7 @@ import { CellMover } from './cell-mover'; import { getBodyHorizontalScrollRange, getColX, getRowY } from './util'; import type { BaseTableAPI } from '../../ts-types/base-table'; import { isValid } from '@visactor/vutils'; +import { getFrozenShadowSegments } from './frozen-shadow'; /** * @description: 表格内容外组件 @@ -229,6 +230,8 @@ export class TableComponent { } }); + this.updateFrozenShadowPaths(); + // TO BE DONE 冻结列border(theme.frozenColumnLine?.border) // 弹出窗口 @@ -782,11 +785,35 @@ export class TableComponent { this.cellMover.update(backX, lineX, backY, lineY); } - /** - * @description: 显示冻结列shadow - * @param {number} col - * @return {*} - */ + /** Use the original rectangle renderer unless merged-cell clipping is enabled. */ + private updateFrozenShadowPaths() { + const skipMergedCells = this.table.theme.frozenColumnLine?.shadow?.skipMergedCells; + // Resolve at draw time so scrolling, resizing and merge updates stay in sync. + this.frozenShadowLine.pathProxy = skipMergedCells + ? () => this.createFrozenShadowPath(this.frozenShadowLine, this.table.frozenColCount - 1) + : undefined; + this.rightFrozenShadowLine.pathProxy = skipMergedCells + ? () => + this.createFrozenShadowPath( + this.rightFrozenShadowLine, + this.table.colCount - this.table.rightFrozenColCount - 1 + ) + : undefined; + } + + /** Draw the vertical intervals that do not intersect a cross-boundary merged cell. */ + private createFrozenShadowPath(shadow: IRect, boundaryCol: number): CustomPath2D { + const path = new CustomPath2D(); + const width = shadow.attribute.width ?? 0; + const height = shadow.attribute.height ?? 0; + const segments = getFrozenShadowSegments(this.table, boundaryCol, height); + for (const segment of segments) { + path.rect(0, segment.start, width, segment.end - segment.start); + } + return path; + } + + /** 显示冻结列阴影。 */ setFrozenColumnShadow(col: number, isRightFrozen?: boolean) { const colX = !isRightFrozen && col === this.table.frozenColCount - 1 && (this.table.getFrozenColsOffset?.() ?? 0) > 0 @@ -1135,6 +1162,7 @@ export class TableComponent { } }); + this.updateFrozenShadowPaths(); this.cellMover.updateStyle(); // this.menu.updateStyle(); // this.drillIcon.updateStyle(); diff --git a/packages/vtable/src/themes/theme-define.ts b/packages/vtable/src/themes/theme-define.ts index 0a1fa0d2fd..e65342e653 100644 --- a/packages/vtable/src/themes/theme-define.ts +++ b/packages/vtable/src/themes/theme-define.ts @@ -660,7 +660,13 @@ export class TableTheme implements ITableThemeDefine { ); this._frozenColumnLine = { get shadow(): - | { width: number; startColor: string; endColor: string; visible: 'always' | 'scrolling' | 'overflow' } + | { + width: number; + startColor: string; + endColor: string; + visible: 'always' | 'scrolling' | 'overflow'; + skipMergedCells: boolean; + } | undefined { if (frozenColumnLine.shadow) { return { @@ -675,6 +681,9 @@ export class TableTheme implements ITableThemeDefine { }, get visible(): 'always' | 'scrolling' | 'overflow' { return frozenColumnLine.shadow?.visible ?? 'always'; + }, + get skipMergedCells(): boolean { + return frozenColumnLine.shadow?.skipMergedCells ?? false; } }; } diff --git a/packages/vtable/src/ts-types/theme.ts b/packages/vtable/src/ts-types/theme.ts index 2eb114a36f..3f785f154b 100644 --- a/packages/vtable/src/ts-types/theme.ts +++ b/packages/vtable/src/ts-types/theme.ts @@ -136,6 +136,8 @@ export interface ITableThemeDefine { * 'overflow' shows each frozen boundary only while body content is scrolled beneath it. */ visible?: 'always' | 'scrolling' | 'overflow'; + /** Skip merged cells crossing the frozen-column boundary. Defaults to false. */ + skipMergedCells?: boolean; }; /** TODO 暂未生效 */ border?: {