From 5999b3184c7fdf015e566eca267f0048936b62a1 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Thu, 20 Aug 2026 18:56:14 +0800 Subject: [PATCH 01/12] fix(vtable): refresh custom merged corner layout Recalculate custom merged corner content after resize. Keep it on one render cell to avoid clipping artifacts. Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com --- .../demo/en/custom-render/complex-corner.md | 37 +- .../demo/zh/custom-render/complex-corner.md | 37 +- .../vtable/examples/custom/complex-corner.ts | 766 +++++++++--------- .../scenegraph/group-creater/cell-helper.ts | 50 +- .../group-creater/cell-type/text-cell.ts | 10 +- .../src/scenegraph/layout/update-height.ts | 31 +- .../src/scenegraph/layout/update-width.ts | 31 +- 7 files changed, 529 insertions(+), 433 deletions(-) diff --git a/docs/assets/demo/en/custom-render/complex-corner.md b/docs/assets/demo/en/custom-render/complex-corner.md index e3fd9a5d76..8d4df3ba88 100644 --- a/docs/assets/demo/en/custom-render/complex-corner.md +++ b/docs/assets/demo/en/custom-render/complex-corner.md @@ -71,9 +71,18 @@ const option = { }, corner: { titleOnDimension: 'row', + disableHeaderHover: true, + disableHeaderSelect: true, customLayout: args => { const { table, row, col, rect } = args; const { height, width } = rect ?? table.getCellRect(col, row); + const provinceWidth = table.getColWidth(0); + const cityWidth = table.getColWidth(1); + const categoryHeight = table.getRowHeight(0); + const provinceCenterX = provinceWidth / 2; + const cityCenterX = provinceWidth + cityWidth / 2; + const indicatorX = provinceWidth + cityWidth + 6; + const endCategoryX = width - 80; const container = createGroup({ height, width @@ -81,12 +90,12 @@ const option = { // 定义文本内容的数组 const texts = [ - { text: 'province', fontSize: 18, x: 10, y: rect.height - 25 }, - { text: 'city', fontSize: 18, x: 105, y: rect.height - 25 }, - { text: 'data', fontSize: 18, x: rect.width - 50, y: rect.height - 35 }, - { text: 'subCategory', fontSize: 18, x: rect.width - 100, y: rect.height - 85 }, - { text: 'category', fontSize: 18, x: rect.width - 80, y: 18 }, - { text: 'indicator', fontSize: 16, x: 176, y: rect.height - 20 } + { text: 'province', fontSize: 18, x: provinceCenterX - 36, y: height - 25 }, + { text: 'city', fontSize: 18, x: cityCenterX - 14, y: height - 25 }, + { text: 'data', fontSize: 18, x: endCategoryX + 30, y: height - 35 }, + { text: 'subCategory', fontSize: 18, x: endCategoryX - 20, y: height - 85 }, + { text: 'category', fontSize: 18, x: endCategoryX, y: Math.max(18, categoryHeight / 2 - 9) }, + { text: 'indicator', fontSize: 16, x: indicatorX, y: height - 20 } ]; // 循环添加文本 @@ -105,17 +114,15 @@ const option = { // 定义线段的点 const linePoints = [ - { x: rect.left, y: rect.top }, { x: 0, y: 0 }, - { x: rect.width - 40, y: rect.height }, + { x: width - 40, y: height }, + { x: provinceWidth + cityWidth + 3, y: height }, { x: 0, y: 0 }, - { x: 173, y: rect.height }, + { x: provinceWidth, y: height }, { x: 0, y: 0 }, - { x: 84, y: rect.height }, + { x: width, y: categoryHeight }, { x: 0, y: 0 }, - { x: rect.width, y: rect.height - 90 }, - { x: 0, y: 0 }, - { x: rect.width, y: rect.height - 38 }, + { x: width, y: height - 38 }, { x: 0, y: 0 } ]; @@ -398,6 +405,10 @@ const option = { rightFrozenColCount: 1, dragOrder:{ dragHeaderMode: 'all' + }, + resize: { + columnResizeMode: 'all', + rowResizeMode: 'all' } }; diff --git a/docs/assets/demo/zh/custom-render/complex-corner.md b/docs/assets/demo/zh/custom-render/complex-corner.md index 7c60a70928..95aa161557 100644 --- a/docs/assets/demo/zh/custom-render/complex-corner.md +++ b/docs/assets/demo/zh/custom-render/complex-corner.md @@ -71,9 +71,18 @@ const option = { }, corner: { titleOnDimension: 'row', + disableHeaderHover: true, + disableHeaderSelect: true, customLayout: args => { const { table, row, col, rect } = args; const { height, width } = rect ?? table.getCellRect(col, row); + const provinceWidth = table.getColWidth(0); + const cityWidth = table.getColWidth(1); + const categoryHeight = table.getRowHeight(0); + const provinceCenterX = provinceWidth / 2; + const cityCenterX = provinceWidth + cityWidth / 2; + const indicatorX = provinceWidth + cityWidth + 6; + const endCategoryX = width - 50; const container = createGroup({ height, width @@ -81,12 +90,12 @@ const option = { // 定义文本内容的数组 const texts = [ - { text: '省份', fontSize: 18, x: 30, y: rect.height - 25 }, - { text: '城市', fontSize: 18, x: 105, y: rect.height - 25 }, - { text: '数据', fontSize: 18, x: rect.width - 50, y: rect.height - 35 }, - { text: '子类别', fontSize: 18, x: rect.width - 50, y: rect.height - 85 }, - { text: '类别', fontSize: 18, x: rect.width - 50, y: 18 }, - { text: '指标', fontSize: 16, x: 176, y: rect.height - 20 } + { text: '省份', fontSize: 18, x: provinceCenterX - 18, y: height - 25 }, + { text: '城市', fontSize: 18, x: cityCenterX - 18, y: height - 25 }, + { text: '数据', fontSize: 18, x: endCategoryX, y: height - 35 }, + { text: '子类别', fontSize: 18, x: endCategoryX, y: height - 85 }, + { text: '类别', fontSize: 18, x: endCategoryX, y: Math.max(18, categoryHeight / 2 - 9) }, + { text: '指标', fontSize: 16, x: indicatorX, y: height - 20 } ]; // 循环添加文本 @@ -105,17 +114,15 @@ const option = { // 定义线段的点 const linePoints = [ - { x: rect.left, y: rect.top }, { x: 0, y: 0 }, - { x: rect.width - 40, y: rect.height }, + { x: width - 40, y: height }, + { x: provinceWidth + cityWidth + 3, y: height }, { x: 0, y: 0 }, - { x: 173, y: rect.height }, + { x: provinceWidth, y: height }, { x: 0, y: 0 }, - { x: 84, y: rect.height }, + { x: width, y: categoryHeight }, { x: 0, y: 0 }, - { x: rect.width, y: rect.height - 90 }, - { x: 0, y: 0 }, - { x: rect.width, y: rect.height - 38 }, + { x: width, y: height - 38 }, { x: 0, y: 0 } ]; @@ -398,6 +405,10 @@ const option = { rightFrozenColCount: 1, dragOrder: { dragHeaderMode: 'all' + }, + resize: { + columnResizeMode: 'all', + rowResizeMode: 'all' } }; diff --git a/packages/vtable/examples/custom/complex-corner.ts b/packages/vtable/examples/custom/complex-corner.ts index 5948f97184..70c586ba30 100644 --- a/packages/vtable/examples/custom/complex-corner.ts +++ b/packages/vtable/examples/custom/complex-corner.ts @@ -1,397 +1,405 @@ import * as VTable from '../../src'; -import { bindDebugTool } from '../../src/scenegraph/debug-tool'; +import VChart from '@visactor/vchart'; const BUGSERVER_CONTAINER_ID = 'vTable'; -const VCHART_NPM_CDN = 'https://unpkg.com/@visactor/vchart@latest/build/index.min.js'; +VTable.register.chartModule('vchart', VChart); -// your code here -import('https://unpkg.com/@visactor/vchart@latest/build/index.min.js') - .then(async () => { - VTable.register.chartModule('vchart', window.VChart.default); - const dom = document.querySelector('#' + BUGSERVER_CONTAINER_ID); - dom.style.width = '800px'; - dom.style.height = '800px'; - const option = { - autoFillHeight: true, - rows: ['province', 'city'], - columns: ['category', 'sub_category'], - indicators: ['sales', 'number'], - defaultHeaderColWidth: ['auto', 'auto', 120], - defaultHeaderRowHeight: ['auto', 90], - indicatorTitle: '指标名称', - indicatorsAsCol: false, - customMergeCell: (col, row, table) => { - if (col >= 0 && col < 3 && row <= 1) { - return { - text: 'merge text', - range: { - start: { - col: 0, - row: 0 - }, - end: { - col: 2, - row: 1 - } +export function createTable() { + const dom = document.querySelector('#' + BUGSERVER_CONTAINER_ID); + dom.style.width = '800px'; + dom.style.height = '800px'; + const option = { + autoFillHeight: true, + rows: ['province', 'city'], + columns: ['category', 'sub_category'], + indicators: ['sales', 'number'], + defaultHeaderColWidth: ['auto', 'auto', 120], + defaultHeaderRowHeight: ['auto', 90], + indicatorTitle: '指标名称', + indicatorsAsCol: false, + customMergeCell: (col, row, table) => { + if (col >= 0 && col < 3 && row <= 1) { + return { + text: 'merge text', + range: { + start: { + col: 0, + row: 0 }, - style: { - bgColor: '#ECF1F5' + end: { + col: 2, + row: 1 } - }; - } - }, - dataConfig: { - totals: { - row: { - showGrandTotals: true, - showSubTotals: true, - subTotalsDimensions: ['province'], - grandTotalLabel: '行总计', - subTotalLabel: '小计' }, - column: { - showGrandTotals: true, - showSubTotals: true, - subTotalsDimensions: ['category'], - grandTotalLabel: '列总计', - subTotalLabel: '小计' + style: { + bgColor: '#ECF1F5' } + }; + } + }, + dataConfig: { + totals: { + row: { + showGrandTotals: true, + showSubTotals: true, + subTotalsDimensions: ['province'], + grandTotalLabel: '行总计', + subTotalLabel: '小计' + }, + column: { + showGrandTotals: true, + showSubTotals: true, + subTotalsDimensions: ['category'], + grandTotalLabel: '列总计', + subTotalLabel: '小计' } - }, - corner: { - merge: true, - titleOnDimension: 'row', - customLayout: (args: any) => { - const {table, row, col, rect} = args; - const {height, width} = rect ?? table.getCellRect(col, row); - const container = new VTable.CustomLayout.Group({ - height, - width, - }); - - // 定义文本内容的数组 - const texts = [ - {text: '省', fontSize: 18, x: 30, y: rect.height - 25}, - {text: '市', fontSize: 18, x: 105, y: rect.height - 25}, - {text: '数据', fontSize: 18, x: rect.width - 50, y: rect.height - 35}, - {text: '物品', fontSize: 18, x: rect.width - 50, y: rect.height - 85}, - {text: '种类', fontSize: 18, x: rect.width - 50, y: 18}, - {text: '类型', fontSize: 16, x: 176, y: rect.height - 20}, - ]; + } + }, + corner: { + merge: true, + titleOnDimension: 'row', + disableHeaderHover: true, + disableHeaderSelect: true, + customLayout: (args: any) => { + const { table, row, col, rect } = args; + const { height, width } = rect ?? table.getCellRect(col, row); + const provinceWidth = table.getColWidth(0); + const cityWidth = table.getColWidth(1); + const categoryHeight = table.getRowHeight(0); + const provinceCenterX = provinceWidth / 2; + const cityCenterX = provinceWidth + cityWidth / 2; + const indicatorX = provinceWidth + cityWidth + 6; + const endCategoryX = width - 50; + const container = new VTable.CustomLayout.Group({ + height, + width + }); - // 循环添加文本 - texts.forEach(({text, fontSize, x, y}) => { - container.addChild( - new VTable.CustomLayout.Text({ - text, - fontSize, - fontFamily: 'sans-serif', - fill: 'black', - x, - y, - }) - ); - }); + // 定义文本内容的数组 + const texts = [ + { text: '省份', fontSize: 18, x: provinceCenterX - 18, y: height - 25 }, + { text: '城市', fontSize: 18, x: cityCenterX - 18, y: height - 25 }, + { text: '数据', fontSize: 18, x: endCategoryX, y: height - 35 }, + { text: '子类别', fontSize: 18, x: endCategoryX, y: height - 85 }, + { text: '类别', fontSize: 18, x: endCategoryX, y: Math.max(18, categoryHeight / 2 - 9) }, + { text: '指标', fontSize: 16, x: indicatorX, y: height - 20 } + ]; - // 定义线段的点 - const linePoints = [ - {x: rect.left, y: rect.top}, - {x: rect.width - 40, y: rect.height}, - {x: 173, y: rect.height}, - {x: 0, y: 0}, - {x: 84, y: rect.height}, - {x: 0, y: 0}, - {x: rect.width, y: rect.height - 90}, - {x: 0, y: 0}, - {x: rect.width, y: rect.height - 38}, - {x: 0, y: 0}, - ]; - - // 添加线段 + // 循环添加文本 + texts.forEach(({ text, fontSize, x, y }) => { container.addChild( - new VTable.CustomLayout.Line({ - points: linePoints, - lineWidth: 1, - stroke: '#ccc', + new VTable.CustomLayout.Text({ + text, + fontSize, + fontFamily: 'sans-serif', + fill: 'black', + x, + y }) ); + }); - return { - rootContainer: container, - renderDefault: false, - enableCellPadding: false, - }; - } + // 定义线段的点 + const linePoints = [ + { x: 0, y: 0 }, + { x: width - 40, y: height }, + { x: provinceWidth + cityWidth + 3, y: height }, + { x: 0, y: 0 }, + { x: provinceWidth, y: height }, + { x: 0, y: 0 }, + { x: width, y: categoryHeight }, + { x: 0, y: 0 }, + { x: width, y: height - 38 }, + { x: 0, y: 0 } + ]; + + // 添加线段 + container.addChild( + new VTable.CustomLayout.Line({ + points: linePoints, + lineWidth: 1, + stroke: '#ccc' + }) + ); + + return { + rootContainer: container, + renderDefault: false, + enableCellPadding: false + }; + } + }, + records: [ + { + sales: 891, + number: 77899999, + province: '浙江省', + city: '杭州市', + category: '家具', + sub_category: '桌子' }, - records: [ - { - sales: 891, - number: 77899999, - province: '浙江省', - city: '杭州市', - category: '家具', - sub_category: '桌子' - }, - { - sales: 792, - number: 2367, - province: '浙江省', - city: '绍兴市', - category: '家具', - sub_category: '桌子' - }, - { - sales: 893, - number: 3877, - province: '浙江省', - city: '宁波市', - category: '家具', - sub_category: '桌子' - }, - { - sales: 1094, - number: 4342, - province: '浙江省', - city: '舟山市', - category: '家具', - sub_category: '桌子' - }, - { - sales: 1295, - number: 5343, - province: '浙江省', - city: '杭州市', - category: '家具', - sub_category: '沙发' - }, - { - sales: 496, - number: 632, - province: '浙江省', - city: '绍兴市', - category: '家具', - sub_category: '沙发' - }, - { - sales: 1097, - number: 7234, - province: '浙江省', - city: '宁波市', - category: '家具', - sub_category: '沙发' - }, - { - sales: 998, - number: 834, - province: '浙江省', - city: '舟山市', - category: '家具', - sub_category: '沙发' - }, - { - sales: 766, - number: 945, - province: '浙江省', - city: '杭州市', - category: '办公用品', - sub_category: '笔' - }, - { - sales: 990, - number: 1304, - province: '浙江省', - city: '绍兴市', - category: '办公用品', - sub_category: '笔' - }, - { - sales: 891, - number: 1145, - province: '浙江省', - city: '宁波市', - category: '办公用品', - sub_category: '笔' - }, - { - sales: 792, - number: 1432, - province: '浙江省', - city: '舟山市', - category: '办公用品', - sub_category: '笔' - }, - { - sales: 745, - number: 1343, - province: '浙江省', - city: '杭州市', - category: '办公用品', - sub_category: '纸张' - }, - { - sales: 843, - number: 1354, - province: '浙江省', - city: '绍兴市', - category: '办公用品', - sub_category: '纸张' - }, - { - sales: 895, - number: 1523, - province: '浙江省', - city: '宁波市', - category: '办公用品', - sub_category: '纸张' - }, - { - sales: 965, - number: 1634, - province: '浙江省', - city: '舟山市', - category: '办公用品', - sub_category: '纸张' - }, - { - sales: 776, - number: 1723, - province: '四川省', - city: '成都市', - category: '家具', - sub_category: '桌子' - }, - { - sales: 634, - number: 1822, - province: '四川省', - city: '绵阳市', - category: '家具', - sub_category: '桌子' - }, - { - sales: 909, - number: 1943, - province: '四川省', - city: '南充市', - category: '家具', - sub_category: '桌子' - }, - { - sales: 399, - number: 2330, - province: '四川省', - city: '乐山市', - category: '家具', - sub_category: '桌子' - }, - { - sales: 700, - number: 2451, - province: '四川省', - city: '成都市', - category: '家具', - sub_category: '沙发' - }, - { - sales: 689, - number: 2244, - province: '四川省', - city: '绵阳市', - category: '家具', - sub_category: '沙发' - }, - { - sales: 500, - number: 2333, - province: '四川省', - city: '南充市', - category: '家具', - sub_category: '沙发' - }, - { - sales: 800, - number: 2445, - province: '四川省', - city: '乐山市', - category: '家具', - sub_category: '沙发' - }, - { - sales: 1044, - number: 2335, - province: '四川省', - city: '成都市', - category: '办公用品', - sub_category: '笔' - }, - { - sales: 689, - number: 245, - province: '四川省', - city: '绵阳市', - category: '办公用品', - sub_category: '笔' - }, - { - sales: 794, - number: 2457, - province: '四川省', - city: '南充市', - category: '办公用品', - sub_category: '笔' - }, - { - sales: 566, - number: 2458, - province: '四川省', - city: '乐山市', - category: '办公用品', - sub_category: '笔' - }, - { - sales: 865, - number: 4004, - province: '四川省', - city: '成都市', - category: '办公用品', - sub_category: '纸张' - }, - { - sales: 999, - number: 3077, - province: '四川省', - city: '绵阳市', - category: '办公用品', - sub_category: '纸张' - }, - { - sales: 999, - number: 3551, - province: '四川省', - city: '南充市', - category: '办公用品', - sub_category: '纸张' - }, - { - sales: 999, - number: 352, - province: '四川省', - city: '乐山市', - category: '办公用品', - sub_category: '纸张' - } - ], - widthMode: 'standard', // 宽度模式:standard 标准模式; adaptive 自动填满容器 - bottomFrozenRowCount: 2, - rightFrozenColCount: 1, + { + sales: 792, + number: 2367, + province: '浙江省', + city: '绍兴市', + category: '家具', + sub_category: '桌子' + }, + { + sales: 893, + number: 3877, + province: '浙江省', + city: '宁波市', + category: '家具', + sub_category: '桌子' + }, + { + sales: 1094, + number: 4342, + province: '浙江省', + city: '舟山市', + category: '家具', + sub_category: '桌子' + }, + { + sales: 1295, + number: 5343, + province: '浙江省', + city: '杭州市', + category: '家具', + sub_category: '沙发' + }, + { + sales: 496, + number: 632, + province: '浙江省', + city: '绍兴市', + category: '家具', + sub_category: '沙发' + }, + { + sales: 1097, + number: 7234, + province: '浙江省', + city: '宁波市', + category: '家具', + sub_category: '沙发' + }, + { + sales: 998, + number: 834, + province: '浙江省', + city: '舟山市', + category: '家具', + sub_category: '沙发' + }, + { + sales: 766, + number: 945, + province: '浙江省', + city: '杭州市', + category: '办公用品', + sub_category: '笔' + }, + { + sales: 990, + number: 1304, + province: '浙江省', + city: '绍兴市', + category: '办公用品', + sub_category: '笔' + }, + { + sales: 891, + number: 1145, + province: '浙江省', + city: '宁波市', + category: '办公用品', + sub_category: '笔' + }, + { + sales: 792, + number: 1432, + province: '浙江省', + city: '舟山市', + category: '办公用品', + sub_category: '笔' + }, + { + sales: 745, + number: 1343, + province: '浙江省', + city: '杭州市', + category: '办公用品', + sub_category: '纸张' + }, + { + sales: 843, + number: 1354, + province: '浙江省', + city: '绍兴市', + category: '办公用品', + sub_category: '纸张' + }, + { + sales: 895, + number: 1523, + province: '浙江省', + city: '宁波市', + category: '办公用品', + sub_category: '纸张' + }, + { + sales: 965, + number: 1634, + province: '浙江省', + city: '舟山市', + category: '办公用品', + sub_category: '纸张' + }, + { + sales: 776, + number: 1723, + province: '四川省', + city: '成都市', + category: '家具', + sub_category: '桌子' + }, + { + sales: 634, + number: 1822, + province: '四川省', + city: '绵阳市', + category: '家具', + sub_category: '桌子' + }, + { + sales: 909, + number: 1943, + province: '四川省', + city: '南充市', + category: '家具', + sub_category: '桌子' + }, + { + sales: 399, + number: 2330, + province: '四川省', + city: '乐山市', + category: '家具', + sub_category: '桌子' + }, + { + sales: 700, + number: 2451, + province: '四川省', + city: '成都市', + category: '家具', + sub_category: '沙发' + }, + { + sales: 689, + number: 2244, + province: '四川省', + city: '绵阳市', + category: '家具', + sub_category: '沙发' + }, + { + sales: 500, + number: 2333, + province: '四川省', + city: '南充市', + category: '家具', + sub_category: '沙发' + }, + { + sales: 800, + number: 2445, + province: '四川省', + city: '乐山市', + category: '家具', + sub_category: '沙发' + }, + { + sales: 1044, + number: 2335, + province: '四川省', + city: '成都市', + category: '办公用品', + sub_category: '笔' + }, + { + sales: 689, + number: 245, + province: '四川省', + city: '绵阳市', + category: '办公用品', + sub_category: '笔' + }, + { + sales: 794, + number: 2457, + province: '四川省', + city: '南充市', + category: '办公用品', + sub_category: '笔' + }, + { + sales: 566, + number: 2458, + province: '四川省', + city: '乐山市', + category: '办公用品', + sub_category: '笔' + }, + { + sales: 865, + number: 4004, + province: '四川省', + city: '成都市', + category: '办公用品', + sub_category: '纸张' + }, + { + sales: 999, + number: 3077, + province: '四川省', + city: '绵阳市', + category: '办公用品', + sub_category: '纸张' + }, + { + sales: 999, + number: 3551, + province: '四川省', + city: '南充市', + category: '办公用品', + sub_category: '纸张' + }, + { + sales: 999, + number: 352, + province: '四川省', + city: '乐山市', + category: '办公用品', + sub_category: '纸张' + } + ], + widthMode: 'standard', // 宽度模式:standard 标准模式; adaptive 自动填满容器 + bottomFrozenRowCount: 2, + rightFrozenColCount: 1, + dragOrder: { dragHeaderMode: 'all' - }; + }, + resize: { + columnResizeMode: 'all', + rowResizeMode: 'all' + } + }; - option.container = dom; - const instance = new VTable.PivotTable(option); - bindDebugTool(instance.scenegraph.stage, { customGrapicKeys: ['col', 'row'] }); - window['tableInstance'] = instance; - }) - .catch(err => { - console.error('Failed import CDN resource: ', err); - }); + option.container = dom; + const instance = new VTable.PivotTable(option); + window.tableInstance = instance; +} diff --git a/packages/vtable/src/scenegraph/group-creater/cell-helper.ts b/packages/vtable/src/scenegraph/group-creater/cell-helper.ts index bc7d557538..0327e021bd 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-helper.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-helper.ts @@ -46,6 +46,19 @@ import type { CreateButtonCellGroup } from './cell-type/button-cell'; const PROMISE_CELL_UPDATE_TOKEN_KEY = '__vtable_promise_cell_update_token__'; +export function isCornerCustomMergeRange(range: CellRange | undefined, table: BaseTableAPI): boolean { + return !!range?.isCustom && table.isCornerHeader(range.start.col, range.start.row); +} + +export function shouldRenderCornerCustomMergeContent( + col: number, + row: number, + range: CellRange | undefined, + table: BaseTableAPI +): boolean { + return !isCornerCustomMergeRange(range, table) || (col === range.end.col && row === range.end.row); +} + export function nextPromiseCellUpdateToken(cellGroup: Group): number { const token = ((cellGroup as any)[PROMISE_CELL_UPDATE_TOKEN_KEY] ?? 0) + 1; (cellGroup as any)[PROMISE_CELL_UPDATE_TOKEN_KEY] = token; @@ -592,7 +605,9 @@ function _generateCustomElementsGroup( let renderDefault = true; if (customResult) { // custom merge custom render - customElementsGroup = customResult.elementsGroup; + customElementsGroup = shouldRenderCornerCustomMergeContent(col, row, range, table) + ? customResult.elementsGroup + : undefined; renderDefault = customResult.renderDefault; } else if (range?.isCustom && !table.isCornerHeader(col, row)) { // 判断不是角头单元格,来兼容corner中设置的customLayout @@ -623,7 +638,9 @@ function _generateCustomElementsGroup( range, table ); - customElementsGroup = customResult.elementsGroup; + customElementsGroup = shouldRenderCornerCustomMergeContent(col, row, range, table) + ? customResult.elementsGroup + : undefined; renderDefault = customResult.renderDefault; } } @@ -1364,6 +1381,24 @@ export function getCustomCellMergeCustom(col: number, row: number, cellGroup: Gr } = customMerge; if (customMergeLayout || customMergeRender) { + const rangeHeight = table.getRowHeight(row); + const rangeWidth = table.getColWidth(col); + const contentWidth = table.getColsWidth(customMergeRange.start.col, customMergeRange.end.col); + const contentHeight = table.getRowsHeight(customMergeRange.start.row, customMergeRange.end.row); + + cellGroup.contentWidth = contentWidth; + cellGroup.contentHeight = contentHeight; + + if (!shouldRenderCornerCustomMergeContent(col, row, customMergeRange, table)) { + const customContainer = cellGroup.getChildByName(CUSTOM_CONTAINER_NAME); + if (customContainer) { + cellGroup.removeChild(customContainer); + } + cellGroup.setAttribute('clip', true); + resizeCellGroup(cellGroup, rangeWidth, rangeHeight, customMergeRange, table); + return customMergeRange; + } + const customResult = dealWithCustom( customMergeLayout, customMergeRender, @@ -1386,15 +1421,10 @@ export function getCustomCellMergeCustom(col: number, row: number, cellGroup: Gr cellGroup.appendChild(customElementsGroup); } - const rangeHeight = table.getRowHeight(row); - const rangeWidth = table.getColWidth(col); - - const { width: contentWidth } = cellGroup.attribute; - const { height: contentHeight } = cellGroup.attribute; - cellGroup.contentWidth = contentWidth; - cellGroup.contentHeight = contentHeight; - resizeCellGroup(cellGroup, rangeWidth, rangeHeight, customMergeRange, table); + if (isCornerCustomMergeRange(customMergeRange, table)) { + cellGroup.setAttribute('clip', false); + } return customMergeRange; } diff --git a/packages/vtable/src/scenegraph/group-creater/cell-type/text-cell.ts b/packages/vtable/src/scenegraph/group-creater/cell-type/text-cell.ts index 8ce3149af2..a93eb26142 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-type/text-cell.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-type/text-cell.ts @@ -48,6 +48,12 @@ export function createCellGroup( range: CellRange | undefined, isAsync: boolean ): Group { + const isCornerCustomMergeContentCell = + !!customElementsGroup && + !!range?.isCustom && + table.isCornerHeader(range.start.col, range.start.row) && + col === range.end.col && + row === range.end.row; const headerStyle = table._getCellStyle(col, row); // to be fixed const functionalPadding = getFunctionalProp('padding', headerStyle, col, row, table); if (isValid(functionalPadding)) { @@ -84,7 +90,7 @@ export function createCellGroup( cursor: (cellTheme?.group as any)?.cursor ?? undefined, lineDash: cellTheme?.group?.lineDash ?? undefined, lineCap: 'butt', - clip: true, + clip: !isCornerCustomMergeContentCell, cornerRadius: cellTheme.group.cornerRadius } as any); } @@ -106,7 +112,7 @@ export function createCellGroup( lineCap: 'butt', - clip: true, + clip: !isCornerCustomMergeContentCell, cornerRadius: cellTheme.group.cornerRadius } as any); diff --git a/packages/vtable/src/scenegraph/layout/update-height.ts b/packages/vtable/src/scenegraph/layout/update-height.ts index 55a70bc19b..bae4956126 100644 --- a/packages/vtable/src/scenegraph/layout/update-height.ts +++ b/packages/vtable/src/scenegraph/layout/update-height.ts @@ -13,7 +13,12 @@ import { updateImageCellContentWhileResize } from '../group-creater/cell-type/im import { getStyleTheme } from '../../core/tableHelper'; import { isMergeCellGroup } from '../utils/is-merge-cell-group'; import type { BaseTableAPI, HeaderData, ListTableProtected } from '../../ts-types/base-table'; -import { resizeCellGroup, getCustomCellMergeCustom } from '../group-creater/cell-helper'; +import { + resizeCellGroup, + getCustomCellMergeCustom, + isCornerCustomMergeRange, + shouldRenderCornerCustomMergeContent +} from '../group-creater/cell-helper'; import type { IGraphic } from '@src/vrender'; import { getCellMergeRange } from '../../tools/merge-range'; import type { ColumnDefine, ListTableConstructorOptions } from '../../ts-types'; @@ -254,6 +259,9 @@ export function updateCellHeight( const padding = getQuadProps(getProp('padding', style, col, row, scene.table)); let width = cell.attribute.width; let height = cell.attribute.height; + const mergeRange = isMergeCellGroup(cell) + ? scene.table.getCellRange(cell.mergeStartCol, cell.mergeStartRow) + : undefined; if (isMergeCellGroup(cell)) { width = scene.table.getColsWidth(cell.mergeStartCol, cell.mergeEndCol); height = scene.table.getRowsHeight(cell.mergeStartRow, cell.mergeEndRow); @@ -268,15 +276,12 @@ export function updateCellHeight( false, scene.table.isAutoRowHeight(row), padding, - isMergeCellGroup(cell) - ? { - start: { col: cell.mergeStartCol, row: cell.mergeStartRow }, - end: { col: cell.mergeEndCol, row: cell.mergeEndRow } - } - : undefined, + mergeRange, scene.table ); - customElementsGroup = customResult.elementsGroup; + customElementsGroup = shouldRenderCornerCustomMergeContent(col, row, mergeRange, scene.table) + ? customResult.elementsGroup + : undefined; renderDefault = customResult.renderDefault; } @@ -285,6 +290,12 @@ export function updateCellHeight( } else if (customElementsGroup) { cell.appendChild(customElementsGroup); } + if (isMergeCellGroup(cell)) { + const mergeRange = scene.table.getCellRange(cell.mergeStartCol, cell.mergeStartRow); + if (isCornerCustomMergeRange(mergeRange, scene.table)) { + cell.setAttribute('clip', !shouldRenderCornerCustomMergeContent(col, row, mergeRange, scene.table)); + } + } } } // if (renderDefault) { @@ -375,6 +386,10 @@ function updateMergeCellContentHeight( } } } + const mergeRange = table.getCellRange(cellGroup.mergeStartCol, cellGroup.mergeStartRow); + if (isCornerCustomMergeRange(mergeRange, table)) { + table.scenegraph.updateCellContent(mergeRange.end.col, mergeRange.end.row); + } } else { const style = table._getCellStyle(cellGroup.col, cellGroup.row); const padding = getQuadProps(getProp('padding', style, cellGroup.col, cellGroup.row, table)); diff --git a/packages/vtable/src/scenegraph/layout/update-width.ts b/packages/vtable/src/scenegraph/layout/update-width.ts index ac06b5a68e..2c835d386a 100644 --- a/packages/vtable/src/scenegraph/layout/update-width.ts +++ b/packages/vtable/src/scenegraph/layout/update-width.ts @@ -10,7 +10,12 @@ import type { Group } from '../graphic/group'; import { updateImageCellContentWhileResize } from '../group-creater/cell-type/image-cell'; import type { CreateProgressBarCell } from '../group-creater/cell-type/progress-bar-cell'; import type { CreateSparkLineCellGroup } from '../group-creater/cell-type/spark-line-cell'; -import { resizeCellGroup, getCustomCellMergeCustom } from '../group-creater/cell-helper'; +import { + resizeCellGroup, + getCustomCellMergeCustom, + isCornerCustomMergeRange, + shouldRenderCornerCustomMergeContent +} from '../group-creater/cell-helper'; import type { Scenegraph } from '../scenegraph'; import { getCellMergeInfo } from '../utils/get-cell-merge'; import { getProp } from '../utils/get-prop'; @@ -417,6 +422,9 @@ function updateCellWidth( const padding = getQuadProps(getProp('padding', style, col, row, scene.table)); let width = cellGroup.attribute.width; let height = cellGroup.attribute.height; + const mergeRange = isMergeCellGroup(cellGroup) + ? scene.table.getCellRange(cellGroup.mergeStartCol, cellGroup.mergeStartRow) + : undefined; if (isMergeCellGroup(cellGroup)) { width = scene.table.getColsWidth(cellGroup.mergeStartCol, cellGroup.mergeEndCol); height = scene.table.getRowsHeight(cellGroup.mergeStartRow, cellGroup.mergeEndRow); @@ -431,15 +439,12 @@ function updateCellWidth( false, scene.table.isAutoRowHeight(row), padding, - isMergeCellGroup(cellGroup) - ? { - start: { col: cellGroup.mergeStartCol, row: cellGroup.mergeStartRow }, - end: { col: cellGroup.mergeEndCol, row: cellGroup.mergeEndRow } - } - : undefined, + mergeRange, scene.table ); - customElementsGroup = customResult.elementsGroup; + customElementsGroup = shouldRenderCornerCustomMergeContent(col, row, mergeRange, scene.table) + ? customResult.elementsGroup + : undefined; renderDefault = customResult.renderDefault; isHeightChange = true; } @@ -449,6 +454,12 @@ function updateCellWidth( } else if (customElementsGroup) { cell.appendChild(customElementsGroup); } + if (isMergeCellGroup(cellGroup)) { + const mergeRange = scene.table.getCellRange(cellGroup.mergeStartCol, cellGroup.mergeStartRow); + if (isCornerCustomMergeRange(mergeRange, scene.table)) { + cell.setAttribute('clip', !shouldRenderCornerCustomMergeContent(col, row, mergeRange, scene.table)); + } + } } } const cellChange = updateMergeCellContentWidth( @@ -567,6 +578,10 @@ function updateMergeCellContentWidth( isHeightChange = isHeightChange || changed; } } + const mergeRange = table.getCellRange(cellGroup.mergeStartCol, cellGroup.mergeStartRow); + if (isCornerCustomMergeRange(mergeRange, table)) { + table.scenegraph.updateCellContent(mergeRange.end.col, mergeRange.end.row); + } return isHeightChange; } From ec25f7d7e3c1dd60c14ae07790a37390adb9ed3d Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Fri, 21 Aug 2026 11:08:56 +0800 Subject: [PATCH 02/12] fix(vtable): harden custom merged corner rendering Avoid resize crashes and constrain custom merged corner content to a single clipped container. Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com --- .../scenegraph/group-creater/cell-helper.ts | 40 +++++++-------- .../group-creater/cell-type/text-cell.ts | 14 ++++-- .../src/scenegraph/layout/update-height.ts | 25 +++++----- .../src/scenegraph/layout/update-width.ts | 25 +++++----- .../scenegraph/utils/corner-custom-merge.ts | 49 +++++++++++++++++++ 5 files changed, 106 insertions(+), 47 deletions(-) create mode 100644 packages/vtable/src/scenegraph/utils/corner-custom-merge.ts diff --git a/packages/vtable/src/scenegraph/group-creater/cell-helper.ts b/packages/vtable/src/scenegraph/group-creater/cell-helper.ts index 0327e021bd..e897e949ff 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-helper.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-helper.ts @@ -43,22 +43,14 @@ import { onBeforeAttributeUpdateForInvertHighlight } from '../../plugins/invert- import { getCellBorderStrokeWidth } from '../utils/cell-border-stroke-width'; import type { CreateSwitchCellGroup } from './cell-type/switch-cell'; import type { CreateButtonCellGroup } from './cell-type/button-cell'; +import { + createCornerCustomMergeContainer, + isCornerCustomMergeRange, + shouldRenderCornerCustomMergeContent +} from '../utils/corner-custom-merge'; const PROMISE_CELL_UPDATE_TOKEN_KEY = '__vtable_promise_cell_update_token__'; -export function isCornerCustomMergeRange(range: CellRange | undefined, table: BaseTableAPI): boolean { - return !!range?.isCustom && table.isCornerHeader(range.start.col, range.start.row); -} - -export function shouldRenderCornerCustomMergeContent( - col: number, - row: number, - range: CellRange | undefined, - table: BaseTableAPI -): boolean { - return !isCornerCustomMergeRange(range, table) || (col === range.end.col && row === range.end.row); -} - export function nextPromiseCellUpdateToken(cellGroup: Group): number { const token = ((cellGroup as any)[PROMISE_CELL_UPDATE_TOKEN_KEY] ?? 0) + 1; (cellGroup as any)[PROMISE_CELL_UPDATE_TOKEN_KEY] = token; @@ -605,10 +597,11 @@ function _generateCustomElementsGroup( let renderDefault = true; if (customResult) { // custom merge custom render - customElementsGroup = shouldRenderCornerCustomMergeContent(col, row, range, table) - ? customResult.elementsGroup + const shouldRenderContent = shouldRenderCornerCustomMergeContent(col, row, range, table); + customElementsGroup = shouldRenderContent + ? createCornerCustomMergeContainer(customResult.elementsGroup, cellWidth, cellHeight, range, table) : undefined; - renderDefault = customResult.renderDefault; + renderDefault = shouldRenderContent ? customResult.renderDefault : false; } else if (range?.isCustom && !table.isCornerHeader(col, row)) { // 判断不是角头单元格,来兼容corner中设置的customLayout // custom merge && no custom render @@ -638,10 +631,11 @@ function _generateCustomElementsGroup( range, table ); - customElementsGroup = shouldRenderCornerCustomMergeContent(col, row, range, table) - ? customResult.elementsGroup + const shouldRenderContent = shouldRenderCornerCustomMergeContent(col, row, range, table); + customElementsGroup = shouldRenderContent + ? createCornerCustomMergeContainer(customResult.elementsGroup, cellWidth, cellHeight, range, table) : undefined; - renderDefault = customResult.renderDefault; + renderDefault = shouldRenderContent ? customResult.renderDefault : false; } } return { @@ -1413,7 +1407,13 @@ export function getCustomCellMergeCustom(col: number, row: number, cellGroup: Gr table ); - const customElementsGroup = customResult.elementsGroup; + const customElementsGroup = createCornerCustomMergeContainer( + customResult.elementsGroup, + contentWidth, + contentHeight, + customMergeRange, + table + ); if (cellGroup.childrenCount > 0 && customElementsGroup) { cellGroup.insertBefore(customElementsGroup, cellGroup.firstChild); diff --git a/packages/vtable/src/scenegraph/group-creater/cell-type/text-cell.ts b/packages/vtable/src/scenegraph/group-creater/cell-type/text-cell.ts index a93eb26142..04e7f8deef 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-type/text-cell.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-type/text-cell.ts @@ -9,6 +9,7 @@ import type { BaseTableAPI } from '../../../ts-types/base-table'; import type { CellRange } from '../../../ts-types'; import { getCellBorderStrokeWidth } from '../../utils/cell-border-stroke-width'; import { createMark } from '../../graphic/mark'; +import { isCornerCustomMergeRange } from '../../utils/corner-custom-merge'; /** * @description: 创建单元格场景节点 @@ -49,11 +50,8 @@ export function createCellGroup( isAsync: boolean ): Group { const isCornerCustomMergeContentCell = - !!customElementsGroup && - !!range?.isCustom && - table.isCornerHeader(range.start.col, range.start.row) && - col === range.end.col && - row === range.end.row; + !!customElementsGroup && isCornerCustomMergeRange(range, table) && col === range.end.col && row === range.end.row; + // The carrier cell must allow the clipped merge container to span internal cell bounds. const headerStyle = table._getCellStyle(col, row); // to be fixed const functionalPadding = getFunctionalProp('padding', headerStyle, col, row, table); if (isValid(functionalPadding)) { @@ -124,6 +122,12 @@ export function createCellGroup( if (customElementsGroup) { cellGroup.appendChild(customElementsGroup); } + if (isCornerCustomMergeContentCell) { + cellGroup.setAttributes({ + width: Math.max(cellGroup.attribute.width, customElementsGroup.attribute.width ?? 0), + height: Math.max(cellGroup.attribute.height, customElementsGroup.attribute.height ?? 0) + }); + } if (renderDefault) { const textStr: string = value; let icons; diff --git a/packages/vtable/src/scenegraph/layout/update-height.ts b/packages/vtable/src/scenegraph/layout/update-height.ts index bae4956126..4df8519720 100644 --- a/packages/vtable/src/scenegraph/layout/update-height.ts +++ b/packages/vtable/src/scenegraph/layout/update-height.ts @@ -13,16 +13,16 @@ import { updateImageCellContentWhileResize } from '../group-creater/cell-type/im import { getStyleTheme } from '../../core/tableHelper'; import { isMergeCellGroup } from '../utils/is-merge-cell-group'; import type { BaseTableAPI, HeaderData, ListTableProtected } from '../../ts-types/base-table'; -import { - resizeCellGroup, - getCustomCellMergeCustom, - isCornerCustomMergeRange, - shouldRenderCornerCustomMergeContent -} from '../group-creater/cell-helper'; +import { resizeCellGroup, getCustomCellMergeCustom } from '../group-creater/cell-helper'; import type { IGraphic } from '@src/vrender'; import { getCellMergeRange } from '../../tools/merge-range'; import type { ColumnDefine, ListTableConstructorOptions } from '../../ts-types'; import { Factory } from '../../core/factory'; +import { + createCornerCustomMergeContainer, + isCornerCustomMergeRange, + shouldRenderCornerCustomMergeContent +} from '../utils/corner-custom-merge'; export function updateRowHeight(scene: Scenegraph, row: number, detaY: number, skipTableHeightMap?: boolean) { // 更新table行高存储 @@ -221,8 +221,10 @@ export function updateCellHeight( const customContainer = (mergedCell.getChildByName(CUSTOM_CONTAINER_NAME) as Group) || (mergedCell.getChildByName(CUSTOM_MERGE_CONTAINER_NAME) as Group); - customContainer.removeAllChild(); - mergedCell.removeChild(customContainer); + if (customContainer) { + customContainer.removeAllChild(); + mergedCell.removeChild(customContainer); + } getCustomCellMergeCustom(col, mergeRow, mergedCell, scene.table); } } else { @@ -279,10 +281,11 @@ export function updateCellHeight( mergeRange, scene.table ); - customElementsGroup = shouldRenderCornerCustomMergeContent(col, row, mergeRange, scene.table) - ? customResult.elementsGroup + const shouldRenderContent = shouldRenderCornerCustomMergeContent(col, row, mergeRange, scene.table); + customElementsGroup = shouldRenderContent + ? createCornerCustomMergeContainer(customResult.elementsGroup, width, height, mergeRange, scene.table) : undefined; - renderDefault = customResult.renderDefault; + renderDefault = shouldRenderContent ? customResult.renderDefault : false; } if (cell.childrenCount > 0 && customElementsGroup) { diff --git a/packages/vtable/src/scenegraph/layout/update-width.ts b/packages/vtable/src/scenegraph/layout/update-width.ts index 2c835d386a..a86d2eeccd 100644 --- a/packages/vtable/src/scenegraph/layout/update-width.ts +++ b/packages/vtable/src/scenegraph/layout/update-width.ts @@ -10,18 +10,18 @@ import type { Group } from '../graphic/group'; import { updateImageCellContentWhileResize } from '../group-creater/cell-type/image-cell'; import type { CreateProgressBarCell } from '../group-creater/cell-type/progress-bar-cell'; import type { CreateSparkLineCellGroup } from '../group-creater/cell-type/spark-line-cell'; -import { - resizeCellGroup, - getCustomCellMergeCustom, - isCornerCustomMergeRange, - shouldRenderCornerCustomMergeContent -} from '../group-creater/cell-helper'; +import { resizeCellGroup, getCustomCellMergeCustom } from '../group-creater/cell-helper'; import type { Scenegraph } from '../scenegraph'; import { getCellMergeInfo } from '../utils/get-cell-merge'; import { getProp } from '../utils/get-prop'; import { isMergeCellGroup } from '../utils/is-merge-cell-group'; import { getQuadProps } from '../utils/padding'; import { updateCellContentWidth } from '../utils/text-icon-layout'; +import { + createCornerCustomMergeContainer, + isCornerCustomMergeRange, + shouldRenderCornerCustomMergeContent +} from '../utils/corner-custom-merge'; import { computeRowHeight } from './compute-row-height'; import { updateCellHeightForRow } from './update-height'; import { getHierarchyOffset } from '../utils/get-hierarchy-offset'; @@ -384,8 +384,10 @@ function updateCellWidth( const customContainer = (mergedCell.getChildByName(CUSTOM_CONTAINER_NAME) as Group) || (mergedCell.getChildByName(CUSTOM_MERGE_CONTAINER_NAME) as Group); - customContainer.removeAllChild(); - mergedCell.removeChild(customContainer); + if (customContainer) { + customContainer.removeAllChild(); + mergedCell.removeChild(customContainer); + } getCustomCellMergeCustom(mergeCol, row, mergedCell, scene.table); } } else { @@ -442,10 +444,11 @@ function updateCellWidth( mergeRange, scene.table ); - customElementsGroup = shouldRenderCornerCustomMergeContent(col, row, mergeRange, scene.table) - ? customResult.elementsGroup + const shouldRenderContent = shouldRenderCornerCustomMergeContent(col, row, mergeRange, scene.table); + customElementsGroup = shouldRenderContent + ? createCornerCustomMergeContainer(customResult.elementsGroup, width, height, mergeRange, scene.table) : undefined; - renderDefault = customResult.renderDefault; + renderDefault = shouldRenderContent ? customResult.renderDefault : false; isHeightChange = true; } diff --git a/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts b/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts new file mode 100644 index 0000000000..0fcc2f101a --- /dev/null +++ b/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts @@ -0,0 +1,49 @@ +import type { Group as VGroup } from '@src/vrender'; +import type { CellRange } from '../../ts-types'; +import type { BaseTableAPI } from '../../ts-types/base-table'; +import { CUSTOM_CONTAINER_NAME } from '../component/custom'; +import { Group } from '../graphic/group'; + +export function isCornerCustomMergeRange(range: CellRange | undefined, table: BaseTableAPI): boolean { + return ( + !!range?.isCustom && + table.isCornerHeader(range.start.col, range.start.row) && + table.isCornerHeader(range.end.col, range.end.row) + ); +} + +export function shouldRenderCornerCustomMergeContent( + col: number, + row: number, + range: CellRange | undefined, + table: BaseTableAPI +): boolean { + return !isCornerCustomMergeRange(range, table) || (col === range.end.col && row === range.end.row); +} + +export function createCornerCustomMergeContainer( + customElementsGroup: VGroup | undefined, + width: number, + height: number, + range: CellRange | undefined, + table: BaseTableAPI +): VGroup | undefined { + if (!customElementsGroup || !isCornerCustomMergeRange(range, table)) { + return customElementsGroup; + } + + const customContainer = new Group({ + x: 0, + y: 0, + width, + height, + fill: false, + stroke: false, + pickable: false, + clip: true + }); + customContainer.name = CUSTOM_CONTAINER_NAME; + customContainer.appendChild(customElementsGroup); + + return customContainer as unknown as VGroup; +} From f08617c2b3b19b3920070b3d62e05ec241266b5e Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Thu, 27 Aug 2026 14:29:10 +0800 Subject: [PATCH 03/12] fix(vtable): harden custom corner merge cleanup Keep corner merged cells unclipped on default fallback. Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com --- .../vtable/src/scenegraph/group-creater/cell-helper.ts | 8 +++++--- .../scenegraph/group-creater/cell-type/text-cell.ts | 8 ++++---- packages/vtable/src/scenegraph/layout/update-height.ts | 10 +++++++++- packages/vtable/src/scenegraph/layout/update-width.ts | 10 +++++++++- .../vtable/src/scenegraph/utils/corner-custom-merge.ts | 6 +++++- 5 files changed, 32 insertions(+), 10 deletions(-) diff --git a/packages/vtable/src/scenegraph/group-creater/cell-helper.ts b/packages/vtable/src/scenegraph/group-creater/cell-helper.ts index e897e949ff..11a6400017 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-helper.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-helper.ts @@ -599,7 +599,7 @@ function _generateCustomElementsGroup( // custom merge custom render const shouldRenderContent = shouldRenderCornerCustomMergeContent(col, row, range, table); customElementsGroup = shouldRenderContent - ? createCornerCustomMergeContainer(customResult.elementsGroup, cellWidth, cellHeight, range, table) + ? createCornerCustomMergeContainer(customResult.elementsGroup, cellWidth, cellHeight, range, table, col, row) : undefined; renderDefault = shouldRenderContent ? customResult.renderDefault : false; } else if (range?.isCustom && !table.isCornerHeader(col, row)) { @@ -633,7 +633,7 @@ function _generateCustomElementsGroup( ); const shouldRenderContent = shouldRenderCornerCustomMergeContent(col, row, range, table); customElementsGroup = shouldRenderContent - ? createCornerCustomMergeContainer(customResult.elementsGroup, cellWidth, cellHeight, range, table) + ? createCornerCustomMergeContainer(customResult.elementsGroup, cellWidth, cellHeight, range, table, col, row) : undefined; renderDefault = shouldRenderContent ? customResult.renderDefault : false; } @@ -1412,7 +1412,9 @@ export function getCustomCellMergeCustom(col: number, row: number, cellGroup: Gr contentWidth, contentHeight, customMergeRange, - table + table, + col, + row ); if (cellGroup.childrenCount > 0 && customElementsGroup) { diff --git a/packages/vtable/src/scenegraph/group-creater/cell-type/text-cell.ts b/packages/vtable/src/scenegraph/group-creater/cell-type/text-cell.ts index 04e7f8deef..36ed060fc2 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-type/text-cell.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-type/text-cell.ts @@ -43,14 +43,14 @@ export function createCellGroup( textAlign: CanvasTextAlign, textBaseline: CanvasTextBaseline, mayHaveIcon: boolean, - customElementsGroup: VGroup, + customElementsGroup: VGroup | undefined, renderDefault: boolean, cellTheme: IThemeSpec, range: CellRange | undefined, isAsync: boolean ): Group { const isCornerCustomMergeContentCell = - !!customElementsGroup && isCornerCustomMergeRange(range, table) && col === range.end.col && row === range.end.row; + isCornerCustomMergeRange(range, table) && col === range.end.col && row === range.end.row; // The carrier cell must allow the clipped merge container to span internal cell bounds. const headerStyle = table._getCellStyle(col, row); // to be fixed const functionalPadding = getFunctionalProp('padding', headerStyle, col, row, table); @@ -124,8 +124,8 @@ export function createCellGroup( } if (isCornerCustomMergeContentCell) { cellGroup.setAttributes({ - width: Math.max(cellGroup.attribute.width, customElementsGroup.attribute.width ?? 0), - height: Math.max(cellGroup.attribute.height, customElementsGroup.attribute.height ?? 0) + width: Math.max(cellGroup.attribute.width, customElementsGroup?.attribute.width ?? 0), + height: Math.max(cellGroup.attribute.height, customElementsGroup?.attribute.height ?? 0) }); } if (renderDefault) { diff --git a/packages/vtable/src/scenegraph/layout/update-height.ts b/packages/vtable/src/scenegraph/layout/update-height.ts index 4df8519720..386e22f4d4 100644 --- a/packages/vtable/src/scenegraph/layout/update-height.ts +++ b/packages/vtable/src/scenegraph/layout/update-height.ts @@ -283,7 +283,15 @@ export function updateCellHeight( ); const shouldRenderContent = shouldRenderCornerCustomMergeContent(col, row, mergeRange, scene.table); customElementsGroup = shouldRenderContent - ? createCornerCustomMergeContainer(customResult.elementsGroup, width, height, mergeRange, scene.table) + ? createCornerCustomMergeContainer( + customResult.elementsGroup, + width, + height, + mergeRange, + scene.table, + col, + row + ) : undefined; renderDefault = shouldRenderContent ? customResult.renderDefault : false; } diff --git a/packages/vtable/src/scenegraph/layout/update-width.ts b/packages/vtable/src/scenegraph/layout/update-width.ts index a86d2eeccd..e62ba1d848 100644 --- a/packages/vtable/src/scenegraph/layout/update-width.ts +++ b/packages/vtable/src/scenegraph/layout/update-width.ts @@ -446,7 +446,15 @@ function updateCellWidth( ); const shouldRenderContent = shouldRenderCornerCustomMergeContent(col, row, mergeRange, scene.table); customElementsGroup = shouldRenderContent - ? createCornerCustomMergeContainer(customResult.elementsGroup, width, height, mergeRange, scene.table) + ? createCornerCustomMergeContainer( + customResult.elementsGroup, + width, + height, + mergeRange, + scene.table, + col, + row + ) : undefined; renderDefault = shouldRenderContent ? customResult.renderDefault : false; isHeightChange = true; diff --git a/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts b/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts index 0fcc2f101a..9c9599fb5f 100644 --- a/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts +++ b/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts @@ -26,7 +26,9 @@ export function createCornerCustomMergeContainer( width: number, height: number, range: CellRange | undefined, - table: BaseTableAPI + table: BaseTableAPI, + col: number, + row: number ): VGroup | undefined { if (!customElementsGroup || !isCornerCustomMergeRange(range, table)) { return customElementsGroup; @@ -43,6 +45,8 @@ export function createCornerCustomMergeContainer( clip: true }); customContainer.name = CUSTOM_CONTAINER_NAME; + customContainer.col = col; + customContainer.row = row; customContainer.appendChild(customElementsGroup); return customContainer as unknown as VGroup; From 1db8cd8f079c7fd22ecbaada041704b496b29f7e Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Thu, 27 Aug 2026 15:34:52 +0800 Subject: [PATCH 04/12] fix(vtable): guard custom corner merge rendering Avoid creating custom layout roots for non-carrier cells. Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com --- .../scenegraph/group-creater/cell-helper.ts | 46 ++++++++++++------- .../src/scenegraph/layout/update-height.ts | 42 +++++++++-------- .../src/scenegraph/layout/update-width.ts | 42 +++++++++-------- .../scenegraph/utils/corner-custom-merge.ts | 9 ++-- 4 files changed, 80 insertions(+), 59 deletions(-) diff --git a/packages/vtable/src/scenegraph/group-creater/cell-helper.ts b/packages/vtable/src/scenegraph/group-creater/cell-helper.ts index 11a6400017..bdc4cf3e64 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-helper.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-helper.ts @@ -595,13 +595,24 @@ function _generateCustomElementsGroup( ) { let customElementsGroup; let renderDefault = true; + const shouldRenderContent = shouldRenderCornerCustomMergeContent(col, row, range, table); + if (!shouldRenderContent) { + return { + customElementsGroup, + renderDefault: false + }; + } + if (customResult) { // custom merge custom render - const shouldRenderContent = shouldRenderCornerCustomMergeContent(col, row, range, table); - customElementsGroup = shouldRenderContent - ? createCornerCustomMergeContainer(customResult.elementsGroup, cellWidth, cellHeight, range, table, col, row) - : undefined; - renderDefault = shouldRenderContent ? customResult.renderDefault : false; + customElementsGroup = createCornerCustomMergeContainer( + customResult.elementsGroup, + cellWidth, + cellHeight, + range, + table + ); + renderDefault = customResult.renderDefault; } else if (range?.isCustom && !table.isCornerHeader(col, row)) { // 判断不是角头单元格,来兼容corner中设置的customLayout // custom merge && no custom render @@ -618,11 +629,13 @@ function _generateCustomElementsGroup( customLayout = define?.customLayout; } if (customLayout || customRender) { + const customCol = range && isCornerCustomMergeRange(range, table) ? range.start.col : col; + const customRow = range && isCornerCustomMergeRange(range, table) ? range.start.row : row; const customResult = dealWithCustom( customLayout, customRender, - col, - row, + customCol, + customRow, cellWidth, cellHeight, false, @@ -631,11 +644,14 @@ function _generateCustomElementsGroup( range, table ); - const shouldRenderContent = shouldRenderCornerCustomMergeContent(col, row, range, table); - customElementsGroup = shouldRenderContent - ? createCornerCustomMergeContainer(customResult.elementsGroup, cellWidth, cellHeight, range, table, col, row) - : undefined; - renderDefault = shouldRenderContent ? customResult.renderDefault : false; + customElementsGroup = createCornerCustomMergeContainer( + customResult.elementsGroup, + cellWidth, + cellHeight, + range, + table + ); + renderDefault = customResult.renderDefault; } } return { @@ -686,7 +702,7 @@ export function updateCell( // cellTheme.group.cornerRadius = getCellCornerRadius(col, row, table); // } - if (customLayout || customRender) { + if ((customLayout || customRender) && shouldRenderCornerCustomMergeContent(col, row, customMergeRange, table)) { customResult = dealWithCustom( customLayout, customRender, @@ -1412,9 +1428,7 @@ export function getCustomCellMergeCustom(col: number, row: number, cellGroup: Gr contentWidth, contentHeight, customMergeRange, - table, - col, - row + table ); if (cellGroup.childrenCount > 0 && customElementsGroup) { diff --git a/packages/vtable/src/scenegraph/layout/update-height.ts b/packages/vtable/src/scenegraph/layout/update-height.ts index 386e22f4d4..af51c64138 100644 --- a/packages/vtable/src/scenegraph/layout/update-height.ts +++ b/packages/vtable/src/scenegraph/layout/update-height.ts @@ -255,24 +255,33 @@ export function updateCellHeight( } } - if (customLayout || customRender) { + const mergeRange = isMergeCellGroup(cell) + ? scene.table.getCellRange(cell.mergeStartCol, cell.mergeStartRow) + : undefined; + const shouldRenderContent = shouldRenderCornerCustomMergeContent(col, row, mergeRange, scene.table); + if (!shouldRenderContent) { + renderDefault = false; + } else if (customLayout || customRender) { // const { autoRowHeight } = table.internalProps; const style = scene.table._getCellStyle(col, row) as ProgressBarStyle; const padding = getQuadProps(getProp('padding', style, col, row, scene.table)); let width = cell.attribute.width; let height = cell.attribute.height; - const mergeRange = isMergeCellGroup(cell) - ? scene.table.getCellRange(cell.mergeStartCol, cell.mergeStartRow) - : undefined; if (isMergeCellGroup(cell)) { width = scene.table.getColsWidth(cell.mergeStartCol, cell.mergeEndCol); height = scene.table.getRowsHeight(cell.mergeStartRow, cell.mergeEndRow); } + let customCol = col; + let customRow = row; + if (mergeRange && isCornerCustomMergeRange(mergeRange, scene.table)) { + customCol = mergeRange.start.col; + customRow = mergeRange.start.row; + } const customResult = dealWithCustom( customLayout, customRender, - col, - row, + customCol, + customRow, width, height, false, @@ -281,19 +290,14 @@ export function updateCellHeight( mergeRange, scene.table ); - const shouldRenderContent = shouldRenderCornerCustomMergeContent(col, row, mergeRange, scene.table); - customElementsGroup = shouldRenderContent - ? createCornerCustomMergeContainer( - customResult.elementsGroup, - width, - height, - mergeRange, - scene.table, - col, - row - ) - : undefined; - renderDefault = shouldRenderContent ? customResult.renderDefault : false; + customElementsGroup = createCornerCustomMergeContainer( + customResult.elementsGroup, + width, + height, + mergeRange, + scene.table + ); + renderDefault = customResult.renderDefault; } if (cell.childrenCount > 0 && customElementsGroup) { diff --git a/packages/vtable/src/scenegraph/layout/update-width.ts b/packages/vtable/src/scenegraph/layout/update-width.ts index e62ba1d848..6aa45fef52 100644 --- a/packages/vtable/src/scenegraph/layout/update-width.ts +++ b/packages/vtable/src/scenegraph/layout/update-width.ts @@ -418,24 +418,33 @@ function updateCellWidth( } } - if (customLayout || customRender) { + const mergeRange = isMergeCellGroup(cellGroup) + ? scene.table.getCellRange(cellGroup.mergeStartCol, cellGroup.mergeStartRow) + : undefined; + const shouldRenderContent = shouldRenderCornerCustomMergeContent(col, row, mergeRange, scene.table); + if (!shouldRenderContent) { + renderDefault = false; + } else if (customLayout || customRender) { // const { autoRowHeight } = table.internalProps; const style = scene.table._getCellStyle(col, row) as ProgressBarStyle; const padding = getQuadProps(getProp('padding', style, col, row, scene.table)); let width = cellGroup.attribute.width; let height = cellGroup.attribute.height; - const mergeRange = isMergeCellGroup(cellGroup) - ? scene.table.getCellRange(cellGroup.mergeStartCol, cellGroup.mergeStartRow) - : undefined; if (isMergeCellGroup(cellGroup)) { width = scene.table.getColsWidth(cellGroup.mergeStartCol, cellGroup.mergeEndCol); height = scene.table.getRowsHeight(cellGroup.mergeStartRow, cellGroup.mergeEndRow); } + let customCol = col; + let customRow = row; + if (mergeRange && isCornerCustomMergeRange(mergeRange, scene.table)) { + customCol = mergeRange.start.col; + customRow = mergeRange.start.row; + } const customResult = dealWithCustom( customLayout, customRender, - col, - row, + customCol, + customRow, width, height, false, @@ -444,19 +453,14 @@ function updateCellWidth( mergeRange, scene.table ); - const shouldRenderContent = shouldRenderCornerCustomMergeContent(col, row, mergeRange, scene.table); - customElementsGroup = shouldRenderContent - ? createCornerCustomMergeContainer( - customResult.elementsGroup, - width, - height, - mergeRange, - scene.table, - col, - row - ) - : undefined; - renderDefault = shouldRenderContent ? customResult.renderDefault : false; + customElementsGroup = createCornerCustomMergeContainer( + customResult.elementsGroup, + width, + height, + mergeRange, + scene.table + ); + renderDefault = customResult.renderDefault; isHeightChange = true; } diff --git a/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts b/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts index 9c9599fb5f..2a01a34b1c 100644 --- a/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts +++ b/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts @@ -7,6 +7,7 @@ import { Group } from '../graphic/group'; export function isCornerCustomMergeRange(range: CellRange | undefined, table: BaseTableAPI): boolean { return ( !!range?.isCustom && + (range.start.col !== range.end.col || range.start.row !== range.end.row) && table.isCornerHeader(range.start.col, range.start.row) && table.isCornerHeader(range.end.col, range.end.row) ); @@ -26,9 +27,7 @@ export function createCornerCustomMergeContainer( width: number, height: number, range: CellRange | undefined, - table: BaseTableAPI, - col: number, - row: number + table: BaseTableAPI ): VGroup | undefined { if (!customElementsGroup || !isCornerCustomMergeRange(range, table)) { return customElementsGroup; @@ -45,8 +44,8 @@ export function createCornerCustomMergeContainer( clip: true }); customContainer.name = CUSTOM_CONTAINER_NAME; - customContainer.col = col; - customContainer.row = row; + customContainer.col = range.start.col; + customContainer.row = range.start.row; customContainer.appendChild(customElementsGroup); return customContainer as unknown as VGroup; From ad0315697494f0779043dee53a4e96bddd2f934e Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Thu, 27 Aug 2026 16:36:28 +0800 Subject: [PATCH 05/12] fix(vtable): preserve corner custom layout during resize Avoid a second carrier-cell refresh after resize rebuilds the custom layout. Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com --- .../vtable/src/scenegraph/layout/update-height.ts | 15 ++++++++++++--- .../vtable/src/scenegraph/layout/update-width.ts | 8 +++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/vtable/src/scenegraph/layout/update-height.ts b/packages/vtable/src/scenegraph/layout/update-height.ts index af51c64138..ef51cd57f9 100644 --- a/packages/vtable/src/scenegraph/layout/update-height.ts +++ b/packages/vtable/src/scenegraph/layout/update-height.ts @@ -327,7 +327,15 @@ export function updateCellHeight( // scene.table // ); // } - updateMergeCellContentHeight(cell, distHeight, detaY, scene.table.isAutoRowHeight(row), renderDefault, scene.table); + updateMergeCellContentHeight( + cell, + distHeight, + detaY, + scene.table.isAutoRowHeight(row), + renderDefault, + scene.table, + !customContainer + ); } } @@ -337,7 +345,8 @@ function updateMergeCellContentHeight( detaY: number, autoRowHeight: boolean, renderDefault: boolean, - table: BaseTableAPI + table: BaseTableAPI, + refreshCornerCustomMergeContent = true ) { if (isMergeCellGroup(cellGroup)) { distHeight = 0; @@ -402,7 +411,7 @@ function updateMergeCellContentHeight( } } const mergeRange = table.getCellRange(cellGroup.mergeStartCol, cellGroup.mergeStartRow); - if (isCornerCustomMergeRange(mergeRange, table)) { + if (refreshCornerCustomMergeContent && isCornerCustomMergeRange(mergeRange, table)) { table.scenegraph.updateCellContent(mergeRange.end.col, mergeRange.end.row); } } else { diff --git a/packages/vtable/src/scenegraph/layout/update-width.ts b/packages/vtable/src/scenegraph/layout/update-width.ts index 6aa45fef52..aa57183055 100644 --- a/packages/vtable/src/scenegraph/layout/update-width.ts +++ b/packages/vtable/src/scenegraph/layout/update-width.ts @@ -483,7 +483,8 @@ function updateCellWidth( detaX, autoRowHeight, renderDefault, - scene.table + scene.table, + !customContainer ); isHeightChange = isHeightChange || cellChange; } @@ -500,7 +501,8 @@ function updateMergeCellContentWidth( detaX: number, autoRowHeight: boolean, renderDefault: boolean, - table: BaseTableAPI + table: BaseTableAPI, + refreshCornerCustomMergeContent = true ) { if (isMergeCellGroup(cellGroup)) { distWidth = 0; @@ -594,7 +596,7 @@ function updateMergeCellContentWidth( } } const mergeRange = table.getCellRange(cellGroup.mergeStartCol, cellGroup.mergeStartRow); - if (isCornerCustomMergeRange(mergeRange, table)) { + if (refreshCornerCustomMergeContent && isCornerCustomMergeRange(mergeRange, table)) { table.scenegraph.updateCellContent(mergeRange.end.col, mergeRange.end.row); } return isHeightChange; From e1ed11ed7df9e94b3655b76abd0daa2669858b39 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Thu, 27 Aug 2026 16:50:39 +0800 Subject: [PATCH 06/12] fix(vtable): refresh corner merge layout safely Clear the old React container before rebuilding the merged corner carrier. Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com --- .../src/scenegraph/layout/update-height.ts | 5 +++-- .../vtable/src/scenegraph/layout/update-width.ts | 5 +++-- .../src/scenegraph/utils/corner-custom-merge.ts | 16 +++++++++++++++- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/vtable/src/scenegraph/layout/update-height.ts b/packages/vtable/src/scenegraph/layout/update-height.ts index ef51cd57f9..7cce555246 100644 --- a/packages/vtable/src/scenegraph/layout/update-height.ts +++ b/packages/vtable/src/scenegraph/layout/update-height.ts @@ -21,7 +21,8 @@ import { Factory } from '../../core/factory'; import { createCornerCustomMergeContainer, isCornerCustomMergeRange, - shouldRenderCornerCustomMergeContent + shouldRenderCornerCustomMergeContent, + updateCornerCustomMergeContent } from '../utils/corner-custom-merge'; export function updateRowHeight(scene: Scenegraph, row: number, detaY: number, skipTableHeightMap?: boolean) { @@ -412,7 +413,7 @@ function updateMergeCellContentHeight( } const mergeRange = table.getCellRange(cellGroup.mergeStartCol, cellGroup.mergeStartRow); if (refreshCornerCustomMergeContent && isCornerCustomMergeRange(mergeRange, table)) { - table.scenegraph.updateCellContent(mergeRange.end.col, mergeRange.end.row); + updateCornerCustomMergeContent(mergeRange, table); } } else { const style = table._getCellStyle(cellGroup.col, cellGroup.row); diff --git a/packages/vtable/src/scenegraph/layout/update-width.ts b/packages/vtable/src/scenegraph/layout/update-width.ts index aa57183055..5637eb1643 100644 --- a/packages/vtable/src/scenegraph/layout/update-width.ts +++ b/packages/vtable/src/scenegraph/layout/update-width.ts @@ -20,7 +20,8 @@ import { updateCellContentWidth } from '../utils/text-icon-layout'; import { createCornerCustomMergeContainer, isCornerCustomMergeRange, - shouldRenderCornerCustomMergeContent + shouldRenderCornerCustomMergeContent, + updateCornerCustomMergeContent } from '../utils/corner-custom-merge'; import { computeRowHeight } from './compute-row-height'; import { updateCellHeightForRow } from './update-height'; @@ -597,7 +598,7 @@ function updateMergeCellContentWidth( } const mergeRange = table.getCellRange(cellGroup.mergeStartCol, cellGroup.mergeStartRow); if (refreshCornerCustomMergeContent && isCornerCustomMergeRange(mergeRange, table)) { - table.scenegraph.updateCellContent(mergeRange.end.col, mergeRange.end.row); + updateCornerCustomMergeContent(mergeRange, table); } return isHeightChange; } diff --git a/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts b/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts index 2a01a34b1c..faf07ebc8d 100644 --- a/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts +++ b/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts @@ -1,7 +1,7 @@ import type { Group as VGroup } from '@src/vrender'; import type { CellRange } from '../../ts-types'; import type { BaseTableAPI } from '../../ts-types/base-table'; -import { CUSTOM_CONTAINER_NAME } from '../component/custom'; +import { CUSTOM_CONTAINER_NAME, CUSTOM_MERGE_CONTAINER_NAME } from '../component/custom'; import { Group } from '../graphic/group'; export function isCornerCustomMergeRange(range: CellRange | undefined, table: BaseTableAPI): boolean { @@ -50,3 +50,17 @@ export function createCornerCustomMergeContainer( return customContainer as unknown as VGroup; } + +export function updateCornerCustomMergeContent(range: CellRange, table: BaseTableAPI): void { + const cellGroup = table.scenegraph.getCell(range.end.col, range.end.row); + const customContainer = + cellGroup.getChildByName(CUSTOM_CONTAINER_NAME) || cellGroup.getChildByName(CUSTOM_MERGE_CONTAINER_NAME); + + if (customContainer) { + table.reactCustomLayout?.removeCustomCell(range.start.col, range.start.row); + customContainer.removeAllChild(); + cellGroup.removeChild(customContainer); + } + + table.scenegraph.updateCellContent(range.end.col, range.end.row); +} From 2ec2275b921937f28bf1e9f1e8bc9fe60d814938 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Thu, 27 Aug 2026 17:03:24 +0800 Subject: [PATCH 07/12] fix(vtable): correct complex corner divider paths Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com --- docs/assets/demo/en/custom-render/complex-corner.md | 1 + docs/assets/demo/zh/custom-render/complex-corner.md | 1 + packages/vtable/examples/custom/complex-corner.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/docs/assets/demo/en/custom-render/complex-corner.md b/docs/assets/demo/en/custom-render/complex-corner.md index 8d4df3ba88..18fcf213f2 100644 --- a/docs/assets/demo/en/custom-render/complex-corner.md +++ b/docs/assets/demo/en/custom-render/complex-corner.md @@ -116,6 +116,7 @@ const option = { const linePoints = [ { x: 0, y: 0 }, { x: width - 40, y: height }, + { x: 0, y: 0 }, { x: provinceWidth + cityWidth + 3, y: height }, { x: 0, y: 0 }, { x: provinceWidth, y: height }, diff --git a/docs/assets/demo/zh/custom-render/complex-corner.md b/docs/assets/demo/zh/custom-render/complex-corner.md index 95aa161557..c14b8794f7 100644 --- a/docs/assets/demo/zh/custom-render/complex-corner.md +++ b/docs/assets/demo/zh/custom-render/complex-corner.md @@ -116,6 +116,7 @@ const option = { const linePoints = [ { x: 0, y: 0 }, { x: width - 40, y: height }, + { x: 0, y: 0 }, { x: provinceWidth + cityWidth + 3, y: height }, { x: 0, y: 0 }, { x: provinceWidth, y: height }, diff --git a/packages/vtable/examples/custom/complex-corner.ts b/packages/vtable/examples/custom/complex-corner.ts index 70c586ba30..6701fecb0a 100644 --- a/packages/vtable/examples/custom/complex-corner.ts +++ b/packages/vtable/examples/custom/complex-corner.ts @@ -103,6 +103,7 @@ export function createTable() { const linePoints = [ { x: 0, y: 0 }, { x: width - 40, y: height }, + { x: 0, y: 0 }, { x: provinceWidth + cityWidth + 3, y: height }, { x: 0, y: 0 }, { x: provinceWidth, y: height }, From 3107a0f48a3cea9d00c5d675a1a2494afaf3f599 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Thu, 27 Aug 2026 17:57:23 +0800 Subject: [PATCH 08/12] fix(react-vtable): flush custom layout unmounts Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com --- .../table-components/custom/custom-layout.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/react-vtable/src/table-components/custom/custom-layout.tsx b/packages/react-vtable/src/table-components/custom/custom-layout.tsx index 9731ded216..31f3f84d6a 100644 --- a/packages/react-vtable/src/table-components/custom/custom-layout.tsx +++ b/packages/react-vtable/src/table-components/custom/custom-layout.tsx @@ -106,7 +106,7 @@ export const CustomLayout: React.FC = (props: PropsWithChildr const key = `${col}-${row}`; if (container.current.has(key)) { const currentContainer = container.current.get(key); - module.reconcilor.updateContainer(null, currentContainer, null); + reconcilorUnmountContainer(module, currentContainer); // group = currentContainer.containerInfo; currentContainer.containerInfo.delete(); container.current.delete(key); @@ -121,7 +121,7 @@ export const CustomLayout: React.FC = (props: PropsWithChildr } container.current.forEach((value, key) => { const currentContainer = value; - module.reconcilor.updateContainer(null, currentContainer, null); + reconcilorUnmountContainer(module, currentContainer); currentContainer.containerInfo.delete(); }); container.current.clear(); @@ -237,6 +237,20 @@ function reconcilorUpdateContainer(module: ReconcilerModule, children: ReactElem // } } +function reconcilorUnmountContainer(module: ReconcilerModule, currentContainer: any) { + const { reconcilor } = module; + const updateContainerSync = (reconcilor as any).updateContainerSync; + if (typeof updateContainerSync === 'function') { + updateContainerSync(null, currentContainer, null); + const flushSyncWork = (reconcilor as any).flushSyncWork; + if (typeof flushSyncWork === 'function') { + flushSyncWork(); + } + return; + } + reconcilor.updateContainer(null, currentContainer, null); +} + function getCellRect(col: number, row: number, table: any) { const range = table.getCellRange(col, row); const rect = table.getCellsRect(range.start.col, range.start.row, range.end.col, range.end.row); From 45e4f589052e1624a2263d20af961fb174e1f98c Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Thu, 27 Aug 2026 19:13:37 +0800 Subject: [PATCH 09/12] fix(vtable): stabilize corner custom layout refresh Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com --- .../table-components/custom/custom-layout.tsx | 96 ++++++++++++++++--- .../components/react/react-custom-layout.ts | 11 ++- .../src/scenegraph/layout/update-height.ts | 41 ++++++-- .../src/scenegraph/layout/update-width.ts | 28 ++++-- .../scenegraph/utils/corner-custom-merge.ts | 40 +++++++- 5 files changed, 179 insertions(+), 37 deletions(-) diff --git a/packages/react-vtable/src/table-components/custom/custom-layout.tsx b/packages/react-vtable/src/table-components/custom/custom-layout.tsx index 31f3f84d6a..bc74c726dc 100644 --- a/packages/react-vtable/src/table-components/custom/custom-layout.tsx +++ b/packages/react-vtable/src/table-components/custom/custom-layout.tsx @@ -98,19 +98,30 @@ export const CustomLayout: React.FC = (props: PropsWithChildr [children, reportReconcilerError] ); - const removeContainer = useCallback((col: number, row: number) => { + const removeContainer = useCallback((col: number, row: number, afterRemove?: () => void) => { const module = reconcilerModule.current; if (!module) { - return; + return true; } const key = `${col}-${row}`; if (container.current.has(key)) { const currentContainer = container.current.get(key); - reconcilorUnmountContainer(module, currentContainer); - // group = currentContainer.containerInfo; - currentContainer.containerInfo.delete(); - container.current.delete(key); + const syncUnmounted = reconcilorUnmountContainer(module, currentContainer); + const finalizeRemove = () => { + currentContainer.containerInfo.delete(); + container.current.delete(key); + }; + if (syncUnmounted) { + finalizeRemove(); + return true; + } + scheduleAfterReconcilerCommit(module, () => { + finalizeRemove(); + afterRemove?.(); + }); + return false; } + return true; }, []); const removeAllContainer = useCallback(() => { @@ -119,12 +130,25 @@ export const CustomLayout: React.FC = (props: PropsWithChildr container.current.clear(); return; } + const pendingContainers: Array<{ key: string; currentContainer: FiberRoot }> = []; + let canFlushSync = true; container.current.forEach((value, key) => { const currentContainer = value; - reconcilorUnmountContainer(module, currentContainer); - currentContainer.containerInfo.delete(); + canFlushSync = requestReconcilerUnmountContainer(module, currentContainer) && canFlushSync; + pendingContainers.push({ key, currentContainer }); }); - container.current.clear(); + const finalizeRemove = () => { + pendingContainers.forEach(({ key, currentContainer }) => { + currentContainer.containerInfo.delete(); + container.current.delete(key); + }); + }; + + if (canFlushSync && flushReconcilerWork(module)) { + finalizeRemove(); + } else { + scheduleAfterReconcilerCommit(module, finalizeRemove); + } }, []); useLayoutEffect(() => { @@ -237,18 +261,60 @@ function reconcilorUpdateContainer(module: ReconcilerModule, children: ReactElem // } } -function reconcilorUnmountContainer(module: ReconcilerModule, currentContainer: any) { +function reconcilorUnmountContainer(module: ReconcilerModule, currentContainer: any): boolean { + if (!requestReconcilerUnmountContainer(module, currentContainer)) { + return false; + } + return flushReconcilerWork(module); +} + +function requestReconcilerUnmountContainer(module: ReconcilerModule, currentContainer: any): boolean { const { reconcilor } = module; const updateContainerSync = (reconcilor as any).updateContainerSync; if (typeof updateContainerSync === 'function') { - updateContainerSync(null, currentContainer, null); - const flushSyncWork = (reconcilor as any).flushSyncWork; - if (typeof flushSyncWork === 'function') { - flushSyncWork(); + try { + updateContainerSync(null, currentContainer, null); + return true; + } catch { + reconcilor.updateContainer(null, currentContainer, null); + return false; } - return; } reconcilor.updateContainer(null, currentContainer, null); + return false; +} + +function flushReconcilerWork(module: ReconcilerModule): boolean { + const { reconcilor } = module; + const flushSyncWork = (reconcilor as any).flushSyncWork; + if (typeof flushSyncWork === 'function') { + try { + const result = flushSyncWork(); + if (result === false) { + return false; + } + } catch { + return false; + } + } + + const flushPassiveEffects = (reconcilor as any).flushPassiveEffects; + if (typeof flushPassiveEffects === 'function') { + try { + flushPassiveEffects(); + } catch { + return false; + } + } + return true; +} + +function scheduleAfterReconcilerCommit(module: ReconcilerModule, callback: () => void) { + // eslint-disable-next-line no-undef + setTimeout(() => { + flushReconcilerWork(module); + callback(); + }, 0); } function getCellRect(col: number, row: number, table: any) { diff --git a/packages/vtable/src/components/react/react-custom-layout.ts b/packages/vtable/src/components/react/react-custom-layout.ts index 8805a7e89f..dd6d966fcc 100644 --- a/packages/vtable/src/components/react/react-custom-layout.ts +++ b/packages/vtable/src/components/react/react-custom-layout.ts @@ -16,10 +16,10 @@ export class ReactCustomLayout { removeAllContainer: () => void; table: BaseTableAPI; customLayoutFuncCache: Map; - reactRemoveGraphicCache: Map void>; + reactRemoveGraphicCache: Map void) => boolean | void>; reactRemoveAllGraphicCache: Map void>; headerCustomLayoutFuncCache: Map; - headerReactRemoveGraphicCache: Map void>; + headerReactRemoveGraphicCache: Map void) => boolean | void>; headerReactRemoveAllGraphicCache: Map void>; // reactContainerCache: Map>; constructor(table: BaseTableAPI) { @@ -58,7 +58,7 @@ export class ReactCustomLayout { setReactRemoveGraphic( componentId: string, - removeGraphic: (col: number, row: number) => void, + removeGraphic: (col: number, row: number, afterRemove?: () => void) => boolean | void, isHeaderCustomLayout?: boolean ) { if (isHeaderCustomLayout) { @@ -134,7 +134,7 @@ export class ReactCustomLayout { return this.customLayoutFuncCache.get(componentId) || emptyCustomLayout; } - removeCustomCell(col: number, row: number) { + removeCustomCell(col: number, row: number, afterRemove?: () => void): boolean { // const { startInTotal } = this.table.getBodyColumnDefine(col, row) as any; const isHeader = this.table.isHeader(col, row); let removeFun; @@ -148,8 +148,9 @@ export class ReactCustomLayout { removeFun = this.reactRemoveGraphicCache.get(componentId); } if (removeFun) { - removeFun(col, row); + return removeFun(col, row, afterRemove) !== false; } + return true; } clearCache() { diff --git a/packages/vtable/src/scenegraph/layout/update-height.ts b/packages/vtable/src/scenegraph/layout/update-height.ts index 7cce555246..6c557929be 100644 --- a/packages/vtable/src/scenegraph/layout/update-height.ts +++ b/packages/vtable/src/scenegraph/layout/update-height.ts @@ -22,7 +22,7 @@ import { createCornerCustomMergeContainer, isCornerCustomMergeRange, shouldRenderCornerCustomMergeContent, - updateCornerCustomMergeContent + updateCornerCustomMergeContentOnce } from '../utils/corner-custom-merge'; export function updateRowHeight(scene: Scenegraph, row: number, detaY: number, skipTableHeightMap?: boolean) { @@ -31,6 +31,7 @@ export function updateRowHeight(scene: Scenegraph, row: number, detaY: number, s scene.table._setRowHeight(row, scene.table.getRowHeight(row) + detaY, true); } + const refreshedCornerCustomMergeRanges = new Set(); for (let col = 0; col < scene.table.colCount; col++) { const cell = scene.getCell(col, row); if (cell.role === 'empty') { @@ -43,7 +44,16 @@ export function updateRowHeight(scene: Scenegraph, row: number, detaY: number, s const height = cell.attribute.height; // cell.setAttribute('height', height); // (cell.firstChild as Rect).setAttribute('height', cell.attribute.height); - updateCellHeightForRow(scene, cell, col, row, height + detaY, detaY, scene.table.isHeader(col, row)); + updateCellHeightForRow( + scene, + cell, + col, + row, + height + detaY, + detaY, + scene.table.isHeader(col, row), + refreshedCornerCustomMergeRanges + ); scene.updateCellContentWhileResize(col, row); } @@ -89,7 +99,8 @@ export function updateCellHeightForRow( row: number, height: number, detaY: number, - isHeader: boolean + isHeader: boolean, + refreshedCornerCustomMergeRanges?: Set // autoRowHeight: boolean ) { // cell.setAttribute('height', height); @@ -101,7 +112,7 @@ export function updateCellHeightForRow( return; } - updateCellHeight(scene, cellGroup, col, row, distHeight, detaY, isHeader); + updateCellHeight(scene, cellGroup, col, row, distHeight, detaY, isHeader, refreshedCornerCustomMergeRanges); } export function updateCellHeightForColumn( @@ -125,7 +136,8 @@ export function updateCellHeight( row: number, distHeight: number, detaY: number, - isHeader: boolean + isHeader: boolean, + refreshedCornerCustomMergeRanges?: Set ) { if (cell.attribute.height === distHeight && !cell.needUpdateHeight) { return; @@ -174,7 +186,16 @@ export function updateCellHeight( oldBarCell.release(); // deal with text - updateMergeCellContentHeight(cell, distHeight, detaY, scene.table.isAutoRowHeight(row), true, scene.table); + updateMergeCellContentHeight( + cell, + distHeight, + detaY, + scene.table.isAutoRowHeight(row), + true, + scene.table, + true, + refreshedCornerCustomMergeRanges + ); } else if (type === 'sparkline') { // 目前先采用重新生成节点的方案 cell.removeAllChild(); @@ -335,7 +356,8 @@ export function updateCellHeight( scene.table.isAutoRowHeight(row), renderDefault, scene.table, - !customContainer + !customContainer, + refreshedCornerCustomMergeRanges ); } } @@ -347,7 +369,8 @@ function updateMergeCellContentHeight( autoRowHeight: boolean, renderDefault: boolean, table: BaseTableAPI, - refreshCornerCustomMergeContent = true + refreshCornerCustomMergeContent = true, + refreshedCornerCustomMergeRanges?: Set ) { if (isMergeCellGroup(cellGroup)) { distHeight = 0; @@ -413,7 +436,7 @@ function updateMergeCellContentHeight( } const mergeRange = table.getCellRange(cellGroup.mergeStartCol, cellGroup.mergeStartRow); if (refreshCornerCustomMergeContent && isCornerCustomMergeRange(mergeRange, table)) { - updateCornerCustomMergeContent(mergeRange, table); + updateCornerCustomMergeContentOnce(mergeRange, table, refreshedCornerCustomMergeRanges); } } else { const style = table._getCellStyle(cellGroup.col, cellGroup.row); diff --git a/packages/vtable/src/scenegraph/layout/update-width.ts b/packages/vtable/src/scenegraph/layout/update-width.ts index 5637eb1643..1c51193b99 100644 --- a/packages/vtable/src/scenegraph/layout/update-width.ts +++ b/packages/vtable/src/scenegraph/layout/update-width.ts @@ -21,7 +21,7 @@ import { createCornerCustomMergeContainer, isCornerCustomMergeRange, shouldRenderCornerCustomMergeContent, - updateCornerCustomMergeContent + updateCornerCustomMergeContentOnce } from '../utils/corner-custom-merge'; import { computeRowHeight } from './compute-row-height'; import { updateCellHeightForRow } from './update-height'; @@ -133,6 +133,7 @@ function updateColunmWidth( scene: Scenegraph ) { let needRerangeRow = false; + const refreshedCornerCustomMergeRanges = new Set(); // const colOrCornerHeaderColumn = scene.getColGroup(col, true) as Group; const oldColumnWidth = columnGroup?.attribute.width ?? 0; columnGroup?.setAttribute('width', oldColumnWidth + detaX); @@ -147,7 +148,8 @@ function updateColunmWidth( oldColumnWidth + detaX, detaX, mode === 'row-body' ? cell.col < scene.table.rowHeaderLevelCount : true, - scene.table.internalProps.autoWrapText + scene.table.internalProps.autoWrapText, + refreshedCornerCustomMergeRanges ); if (isHeightChange) { const mergeInfo = getCellMergeInfo(scene.table, cell.col, cell.row); @@ -252,7 +254,8 @@ function updateCellWidth( detaX: number, isHeader: boolean, // autoColWidth: boolean, - autoWrapText: boolean + autoWrapText: boolean, + refreshedCornerCustomMergeRanges?: Set ): boolean { if (cell.attribute.width === distWidth && !cell.needUpdateWidth) { return false; @@ -314,7 +317,16 @@ function updateCellWidth( oldBarCell.release(); // deal width text - const cellChange = updateMergeCellContentWidth(cellGroup, distWidth, detaX, autoRowHeight, true, scene.table); + const cellChange = updateMergeCellContentWidth( + cellGroup, + distWidth, + detaX, + autoRowHeight, + true, + scene.table, + true, + refreshedCornerCustomMergeRanges + ); isHeightChange = isHeightChange || cellChange; } else if (type === 'sparkline') { // 目前先采用重新生成节点的方案 @@ -485,7 +497,8 @@ function updateCellWidth( autoRowHeight, renderDefault, scene.table, - !customContainer + !customContainer, + refreshedCornerCustomMergeRanges ); isHeightChange = isHeightChange || cellChange; } @@ -503,7 +516,8 @@ function updateMergeCellContentWidth( autoRowHeight: boolean, renderDefault: boolean, table: BaseTableAPI, - refreshCornerCustomMergeContent = true + refreshCornerCustomMergeContent = true, + refreshedCornerCustomMergeRanges?: Set ) { if (isMergeCellGroup(cellGroup)) { distWidth = 0; @@ -598,7 +612,7 @@ function updateMergeCellContentWidth( } const mergeRange = table.getCellRange(cellGroup.mergeStartCol, cellGroup.mergeStartRow); if (refreshCornerCustomMergeContent && isCornerCustomMergeRange(mergeRange, table)) { - updateCornerCustomMergeContent(mergeRange, table); + updateCornerCustomMergeContentOnce(mergeRange, table, refreshedCornerCustomMergeRanges); } return isHeightChange; } diff --git a/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts b/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts index faf07ebc8d..1efbd8b021 100644 --- a/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts +++ b/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts @@ -52,12 +52,50 @@ export function createCornerCustomMergeContainer( } export function updateCornerCustomMergeContent(range: CellRange, table: BaseTableAPI): void { + updateCornerCustomMergeContentOnce(range, table); +} + +export function updateCornerCustomMergeContentOnce( + range: CellRange, + table: BaseTableAPI, + refreshedRanges?: Set +): void { + if (refreshedRanges) { + const rangeKey = `${range.start.col}-${range.start.row}-${range.end.col}-${range.end.row}`; + if (refreshedRanges.has(rangeKey)) { + return; + } + refreshedRanges.add(rangeKey); + } + + refreshCornerCustomMergeContent(range, table); +} + +function refreshCornerCustomMergeContent(range: CellRange, table: BaseTableAPI): void { + const cellGroup = table.scenegraph.getCell(range.end.col, range.end.row); + const customContainer = + cellGroup.getChildByName(CUSTOM_CONTAINER_NAME) || cellGroup.getChildByName(CUSTOM_MERGE_CONTAINER_NAME); + + if (customContainer) { + const removed = table.reactCustomLayout?.removeCustomCell(range.start.col, range.start.row, () => { + removeCornerCustomContainerAndUpdate(range, table); + }); + if (removed === false) { + return; + } + removeCornerCustomContainerAndUpdate(range, table); + return; + } + + table.scenegraph.updateCellContent(range.end.col, range.end.row); +} + +function removeCornerCustomContainerAndUpdate(range: CellRange, table: BaseTableAPI): void { const cellGroup = table.scenegraph.getCell(range.end.col, range.end.row); const customContainer = cellGroup.getChildByName(CUSTOM_CONTAINER_NAME) || cellGroup.getChildByName(CUSTOM_MERGE_CONTAINER_NAME); if (customContainer) { - table.reactCustomLayout?.removeCustomCell(range.start.col, range.start.row); customContainer.removeAllChild(); cellGroup.removeChild(customContainer); } From 8e565ff2015a0ae4c22e7071a5d4172f5ff683ee Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Fri, 28 Aug 2026 10:48:45 +0800 Subject: [PATCH 10/12] fix: stabilize react custom layout refresh Keep React custom layout teardown synchronous across refresh paths. Avoid deferred cleanup deleting newly mounted containers. Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com --- .../table-components/custom/custom-layout.tsx | 74 +++++++------------ .../components/react/react-custom-layout.ts | 11 ++- .../src/scenegraph/layout/update-height.ts | 21 ++++-- .../src/scenegraph/layout/update-width.ts | 36 ++++++--- packages/vtable/src/scenegraph/scenegraph.ts | 25 ++++++- .../scenegraph/utils/corner-custom-merge.ts | 20 +---- .../src/state/resize/update-resize-column.ts | 36 +++++++-- .../src/state/resize/update-resize-row.ts | 23 ++++-- 8 files changed, 139 insertions(+), 107 deletions(-) diff --git a/packages/react-vtable/src/table-components/custom/custom-layout.tsx b/packages/react-vtable/src/table-components/custom/custom-layout.tsx index bc74c726dc..7f3f904a47 100644 --- a/packages/react-vtable/src/table-components/custom/custom-layout.tsx +++ b/packages/react-vtable/src/table-components/custom/custom-layout.tsx @@ -98,30 +98,20 @@ export const CustomLayout: React.FC = (props: PropsWithChildr [children, reportReconcilerError] ); - const removeContainer = useCallback((col: number, row: number, afterRemove?: () => void) => { + const removeContainer = useCallback((col: number, row: number) => { const module = reconcilerModule.current; if (!module) { - return true; + return; } const key = `${col}-${row}`; if (container.current.has(key)) { const currentContainer = container.current.get(key); - const syncUnmounted = reconcilorUnmountContainer(module, currentContainer); - const finalizeRemove = () => { + reconcilorUnmountContainer(module, currentContainer); + if (container.current.get(key) === currentContainer) { currentContainer.containerInfo.delete(); container.current.delete(key); - }; - if (syncUnmounted) { - finalizeRemove(); - return true; } - scheduleAfterReconcilerCommit(module, () => { - finalizeRemove(); - afterRemove?.(); - }); - return false; } - return true; }, []); const removeAllContainer = useCallback(() => { @@ -130,25 +120,20 @@ export const CustomLayout: React.FC = (props: PropsWithChildr container.current.clear(); return; } - const pendingContainers: Array<{ key: string; currentContainer: FiberRoot }> = []; - let canFlushSync = true; - container.current.forEach((value, key) => { - const currentContainer = value; - canFlushSync = requestReconcilerUnmountContainer(module, currentContainer) && canFlushSync; - pendingContainers.push({ key, currentContainer }); + const pendingContainers = Array.from(container.current.entries()); + batchReconcilerUpdates(module, () => { + pendingContainers.forEach(([, currentContainer]) => { + requestReconcilerUnmountContainer(module, currentContainer); + }); }); - const finalizeRemove = () => { - pendingContainers.forEach(({ key, currentContainer }) => { + flushReconcilerWork(module); + + pendingContainers.forEach(([key, currentContainer]) => { + if (container.current.get(key) === currentContainer) { currentContainer.containerInfo.delete(); container.current.delete(key); - }); - }; - - if (canFlushSync && flushReconcilerWork(module)) { - finalizeRemove(); - } else { - scheduleAfterReconcilerCommit(module, finalizeRemove); - } + } + }); }, []); useLayoutEffect(() => { @@ -243,10 +228,7 @@ function reconcilorUpdateContainer(module: ReconcilerModule, children: ReactElem const updateContainerSync = (reconcilor as any).updateContainerSync; if (typeof updateContainerSync === 'function') { updateContainerSync(element, currentContainer, null); - const flushSyncWork = (reconcilor as any).flushSyncWork; - if (typeof flushSyncWork === 'function') { - flushSyncWork(); - } + flushReconcilerWork(module); return; } reconcilor.updateContainer(element, currentContainer, null); @@ -262,26 +244,23 @@ function reconcilorUpdateContainer(module: ReconcilerModule, children: ReactElem } function reconcilorUnmountContainer(module: ReconcilerModule, currentContainer: any): boolean { - if (!requestReconcilerUnmountContainer(module, currentContainer)) { - return false; - } + requestReconcilerUnmountContainer(module, currentContainer); return flushReconcilerWork(module); } -function requestReconcilerUnmountContainer(module: ReconcilerModule, currentContainer: any): boolean { +function requestReconcilerUnmountContainer(module: ReconcilerModule, currentContainer: any) { const { reconcilor } = module; const updateContainerSync = (reconcilor as any).updateContainerSync; if (typeof updateContainerSync === 'function') { try { updateContainerSync(null, currentContainer, null); - return true; + return; } catch { reconcilor.updateContainer(null, currentContainer, null); - return false; + return; } } reconcilor.updateContainer(null, currentContainer, null); - return false; } function flushReconcilerWork(module: ReconcilerModule): boolean { @@ -290,7 +269,7 @@ function flushReconcilerWork(module: ReconcilerModule): boolean { if (typeof flushSyncWork === 'function') { try { const result = flushSyncWork(); - if (result === false) { + if (result === true) { return false; } } catch { @@ -309,12 +288,13 @@ function flushReconcilerWork(module: ReconcilerModule): boolean { return true; } -function scheduleAfterReconcilerCommit(module: ReconcilerModule, callback: () => void) { - // eslint-disable-next-line no-undef - setTimeout(() => { - flushReconcilerWork(module); +function batchReconcilerUpdates(module: ReconcilerModule, callback: () => void) { + const batchedUpdates = (module.reconcilor as any).batchedUpdates; + if (typeof batchedUpdates === 'function') { + batchedUpdates(callback); + } else { callback(); - }, 0); + } } function getCellRect(col: number, row: number, table: any) { diff --git a/packages/vtable/src/components/react/react-custom-layout.ts b/packages/vtable/src/components/react/react-custom-layout.ts index dd6d966fcc..8805a7e89f 100644 --- a/packages/vtable/src/components/react/react-custom-layout.ts +++ b/packages/vtable/src/components/react/react-custom-layout.ts @@ -16,10 +16,10 @@ export class ReactCustomLayout { removeAllContainer: () => void; table: BaseTableAPI; customLayoutFuncCache: Map; - reactRemoveGraphicCache: Map void) => boolean | void>; + reactRemoveGraphicCache: Map void>; reactRemoveAllGraphicCache: Map void>; headerCustomLayoutFuncCache: Map; - headerReactRemoveGraphicCache: Map void) => boolean | void>; + headerReactRemoveGraphicCache: Map void>; headerReactRemoveAllGraphicCache: Map void>; // reactContainerCache: Map>; constructor(table: BaseTableAPI) { @@ -58,7 +58,7 @@ export class ReactCustomLayout { setReactRemoveGraphic( componentId: string, - removeGraphic: (col: number, row: number, afterRemove?: () => void) => boolean | void, + removeGraphic: (col: number, row: number) => void, isHeaderCustomLayout?: boolean ) { if (isHeaderCustomLayout) { @@ -134,7 +134,7 @@ export class ReactCustomLayout { return this.customLayoutFuncCache.get(componentId) || emptyCustomLayout; } - removeCustomCell(col: number, row: number, afterRemove?: () => void): boolean { + removeCustomCell(col: number, row: number) { // const { startInTotal } = this.table.getBodyColumnDefine(col, row) as any; const isHeader = this.table.isHeader(col, row); let removeFun; @@ -148,9 +148,8 @@ export class ReactCustomLayout { removeFun = this.reactRemoveGraphicCache.get(componentId); } if (removeFun) { - return removeFun(col, row, afterRemove) !== false; + removeFun(col, row); } - return true; } clearCache() { diff --git a/packages/vtable/src/scenegraph/layout/update-height.ts b/packages/vtable/src/scenegraph/layout/update-height.ts index 6c557929be..2e7f9c60ce 100644 --- a/packages/vtable/src/scenegraph/layout/update-height.ts +++ b/packages/vtable/src/scenegraph/layout/update-height.ts @@ -25,13 +25,18 @@ import { updateCornerCustomMergeContentOnce } from '../utils/corner-custom-merge'; -export function updateRowHeight(scene: Scenegraph, row: number, detaY: number, skipTableHeightMap?: boolean) { +export function updateRowHeight( + scene: Scenegraph, + row: number, + detaY: number, + skipTableHeightMap?: boolean, + refreshedCornerCustomMergeRanges = new Set() +) { // 更新table行高存储 if (!skipTableHeightMap && detaY) { scene.table._setRowHeight(row, scene.table.getRowHeight(row) + detaY, true); } - const refreshedCornerCustomMergeRanges = new Set(); for (let col = 0; col < scene.table.colCount; col++) { const cell = scene.getCell(col, row); if (cell.role === 'empty') { @@ -226,9 +231,15 @@ export function updateCellHeight( (cell.getChildByName(CUSTOM_CONTAINER_NAME) as Group) || (cell.getChildByName(CUSTOM_MERGE_CONTAINER_NAME) as Group); if (customContainer) { - // if (scene.table.reactCustomLayout) { - // scene.table.reactCustomLayout.removeCustomCell(col, row); - // } + if (scene.table.reactCustomLayout) { + const mergeRange = isMergeCellGroup(cell) + ? scene.table.getCellRange(cell.mergeStartCol, cell.mergeStartRow) + : undefined; + const shouldUseMergeStart = mergeRange && isCornerCustomMergeRange(mergeRange, scene.table); + const removeCol = shouldUseMergeStart ? mergeRange.start.col : col; + const removeRow = shouldUseMergeStart ? mergeRange.start.row : row; + scene.table.reactCustomLayout.removeCustomCell(removeCol, removeRow); + } // customContainer.removeAllChild(); let customElementsGroup; cell.removeChild(customContainer); diff --git a/packages/vtable/src/scenegraph/layout/update-width.ts b/packages/vtable/src/scenegraph/layout/update-width.ts index 1c51193b99..ea3eef6f39 100644 --- a/packages/vtable/src/scenegraph/layout/update-width.ts +++ b/packages/vtable/src/scenegraph/layout/update-width.ts @@ -37,7 +37,13 @@ import type { ColumnDefine, ListTableConstructorOptions } from '../../ts-types'; * @param {number} detaX * @return {*} */ -export function updateColWidth(scene: Scenegraph, col: number, detaX: number, skipTableWidthMap?: boolean) { +export function updateColWidth( + scene: Scenegraph, + col: number, + detaX: number, + skipTableWidthMap?: boolean, + refreshedCornerCustomMergeRanges = new Set() +) { if (!skipTableWidthMap) { scene.table._setColWidth(col, scene.table.getColWidth(col) + detaX, true); } @@ -46,33 +52,33 @@ export function updateColWidth(scene: Scenegraph, col: number, detaX: number, sk const colOrCornerHeaderColumn = scene.getColGroup(col, true) as Group; const rightTopColumn = scene.getColGroupInRightTopCorner(col); if (colOrCornerHeaderColumn && !rightTopColumn) { - updateColunmWidth(colOrCornerHeaderColumn, detaX, 'col-corner', scene); + updateColunmWidth(colOrCornerHeaderColumn, detaX, 'col-corner', scene, refreshedCornerCustomMergeRanges); } // deal with right bottom frozen cells if (rightTopColumn) { - updateColunmWidth(rightTopColumn, detaX, 'right-top', scene); + updateColunmWidth(rightTopColumn, detaX, 'right-top', scene, refreshedCornerCustomMergeRanges); } // deal with row header or body or right frozen cells const rowHeaderOrBodyColumn = scene.getColGroup(col) as Group; if (rowHeaderOrBodyColumn) { - updateColunmWidth(rowHeaderOrBodyColumn, detaX, 'row-body', scene); + updateColunmWidth(rowHeaderOrBodyColumn, detaX, 'row-body', scene, refreshedCornerCustomMergeRanges); } const leftBottomColumn = scene.getColGroupInLeftBottomCorner(col); // deal with left bottom frozen cells if (leftBottomColumn) { - updateColunmWidth(leftBottomColumn, detaX, 'left-bottom', scene); + updateColunmWidth(leftBottomColumn, detaX, 'left-bottom', scene, refreshedCornerCustomMergeRanges); } // deal with bottom frozen cells const bottomColumn = scene.getColGroupInBottom(col); if (bottomColumn) { - updateColunmWidth(bottomColumn, detaX, 'bottom', scene); + updateColunmWidth(bottomColumn, detaX, 'bottom', scene, refreshedCornerCustomMergeRanges); } // deal with right bottom frozen cells const rightBottomColumn = scene.getColGroupInRightBottomCorner(col); if (rightBottomColumn) { - updateColunmWidth(rightBottomColumn, detaX, 'right-bottom', scene); + updateColunmWidth(rightBottomColumn, detaX, 'right-bottom', scene, refreshedCornerCustomMergeRanges); } // 更新剩余列位置 @@ -130,10 +136,10 @@ function updateColunmWidth( detaX: number, // autoRowHeight: boolean, mode: 'col-corner' | 'row-body' | 'bottom' | 'left-bottom' | 'right-top' | 'right-bottom', - scene: Scenegraph + scene: Scenegraph, + refreshedCornerCustomMergeRanges: Set ) { let needRerangeRow = false; - const refreshedCornerCustomMergeRanges = new Set(); // const colOrCornerHeaderColumn = scene.getColGroup(col, true) as Group; const oldColumnWidth = columnGroup?.attribute.width ?? 0; columnGroup?.setAttribute('width', oldColumnWidth + detaX); @@ -380,9 +386,15 @@ function updateCellWidth( (cell.getChildByName(CUSTOM_MERGE_CONTAINER_NAME) as Group); if (customContainer) { // fix for custom component flash - // if (scene.table.reactCustomLayout) { - // scene.table.reactCustomLayout.removeCustomCell(col, row); - // } + if (scene.table.reactCustomLayout) { + const mergeRange = isMergeCellGroup(cellGroup) + ? scene.table.getCellRange(cellGroup.mergeStartCol, cellGroup.mergeStartRow) + : undefined; + const shouldUseMergeStart = mergeRange && isCornerCustomMergeRange(mergeRange, scene.table); + const removeCol = shouldUseMergeStart ? mergeRange.start.col : col; + const removeRow = shouldUseMergeStart ? mergeRange.start.row : row; + scene.table.reactCustomLayout.removeCustomCell(removeCol, removeRow); + } // customContainer.removeAllChild(); let customElementsGroup; cell.removeChild(customContainer); diff --git a/packages/vtable/src/scenegraph/scenegraph.ts b/packages/vtable/src/scenegraph/scenegraph.ts index fb43b590b6..09ce2dedf8 100644 --- a/packages/vtable/src/scenegraph/scenegraph.ts +++ b/packages/vtable/src/scenegraph/scenegraph.ts @@ -512,6 +512,8 @@ export class Scenegraph { } releaseStage() { + this.clear = true; + this._needUpdateContainer = false; const releaseAppRef = this.releaseVRenderAppRef; this.releaseVRenderAppRef = undefined; @@ -994,8 +996,14 @@ export class Scenegraph { * @param {number} detaX 改变的宽度值 * @return {*} */ - updateColWidth(col: number, detaX: number, skipUpdateContainer?: boolean, skipTableWidthMap?: boolean) { - updateColWidth(this, col, Math.round(detaX), skipTableWidthMap); + updateColWidth( + col: number, + detaX: number, + skipUpdateContainer?: boolean, + skipTableWidthMap?: boolean, + refreshedCornerCustomMergeRanges?: Set + ) { + updateColWidth(this, col, Math.round(detaX), skipTableWidthMap, refreshedCornerCustomMergeRanges); // this.updateContainerWidth(col, detaX); if (!skipUpdateContainer) { // this.updateContainerAttrWidthAndX(); @@ -1538,9 +1546,14 @@ export class Scenegraph { } } - updateRowHeight(row: number, detaY: number, skipTableHeightMap?: boolean) { + updateRowHeight( + row: number, + detaY: number, + skipTableHeightMap?: boolean, + refreshedCornerCustomMergeRanges?: Set + ) { detaY = Math.round(detaY); - updateRowHeight(this, row, detaY, skipTableHeightMap); + updateRowHeight(this, row, detaY, skipTableHeightMap, refreshedCornerCustomMergeRanges); this.updateContainerHeight(row, detaY); } updateRowsHeight(rows: number[], detaYs: number[], skipTableHeightMap?: boolean) { @@ -2223,6 +2236,10 @@ export class Scenegraph { if (!this._needUpdateContainer) { this._needUpdateContainer = true; setTimeout(() => { + if (!this._needUpdateContainer || this.clear) { + this._needUpdateContainer = false; + return; + } this.updateContainerSync(updateConfig.needUpdateCellY ?? false); }, 0); } diff --git a/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts b/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts index 1efbd8b021..3a2f28d677 100644 --- a/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts +++ b/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts @@ -77,25 +77,7 @@ function refreshCornerCustomMergeContent(range: CellRange, table: BaseTableAPI): cellGroup.getChildByName(CUSTOM_CONTAINER_NAME) || cellGroup.getChildByName(CUSTOM_MERGE_CONTAINER_NAME); if (customContainer) { - const removed = table.reactCustomLayout?.removeCustomCell(range.start.col, range.start.row, () => { - removeCornerCustomContainerAndUpdate(range, table); - }); - if (removed === false) { - return; - } - removeCornerCustomContainerAndUpdate(range, table); - return; - } - - table.scenegraph.updateCellContent(range.end.col, range.end.row); -} - -function removeCornerCustomContainerAndUpdate(range: CellRange, table: BaseTableAPI): void { - const cellGroup = table.scenegraph.getCell(range.end.col, range.end.row); - const customContainer = - cellGroup.getChildByName(CUSTOM_CONTAINER_NAME) || cellGroup.getChildByName(CUSTOM_MERGE_CONTAINER_NAME); - - if (customContainer) { + table.reactCustomLayout?.removeCustomCell(range.start.col, range.start.row); customContainer.removeAllChild(); cellGroup.removeChild(customContainer); } diff --git a/packages/vtable/src/state/resize/update-resize-column.ts b/packages/vtable/src/state/resize/update-resize-column.ts index 3c7add4245..fe8a31d7fa 100644 --- a/packages/vtable/src/state/resize/update-resize-column.ts +++ b/packages/vtable/src/state/resize/update-resize-column.ts @@ -133,19 +133,38 @@ export function updateResizeColumn(xInTable: number, yInTable: number, state: St } function updateResizeColForColumn(detaX: number, state: StateManager) { + const refreshedCornerCustomMergeRanges = new Set(); if (state.table.widthMode === 'adaptive' && state.columnResize.col < state.table.colCount - 1) { // in adaptive mode, the right column width can not be negative // const rightColWidth = state.table.getColWidth(state.columnResize.col + 1); // if (rightColWidth - detaX < 0) { // detaX = rightColWidth; // } - state.table.scenegraph.updateColWidth(state.columnResize.col, detaX); - state.table.scenegraph.updateColWidth(state.columnResize.col + 1, -detaX); + state.table.scenegraph.updateColWidth( + state.columnResize.col, + detaX, + undefined, + undefined, + refreshedCornerCustomMergeRanges + ); + state.table.scenegraph.updateColWidth( + state.columnResize.col + 1, + -detaX, + undefined, + undefined, + refreshedCornerCustomMergeRanges + ); state.table.internalProps._widthResizedColMap.add(state.columnResize.col); state.table.internalProps._widthResizedColMap.add(state.columnResize.col + 1); } else { - state.table.scenegraph.updateColWidth(state.columnResize.col, detaX); + state.table.scenegraph.updateColWidth( + state.columnResize.col, + detaX, + undefined, + undefined, + refreshedCornerCustomMergeRanges + ); state.table.internalProps._widthResizedColMap.add(state.columnResize.col); } } @@ -153,6 +172,7 @@ function updateResizeColForColumn(detaX: number, state: StateManager) { function updateResizeColForAll(detaX: number, state: StateManager) { // 全列调整 const layout = state.table.internalProps.layoutMap as PivotHeaderLayoutMap; + const refreshedCornerCustomMergeRanges = new Set(); for (let col = state.table.frozenColCount; col < state.table.colCount - state.table.rightFrozenColCount; col++) { // 是否禁止调整列宽disableColumnResize 对应canResizeColumn的逻辑判断 if (!(state.table.internalProps.transpose || (state.table.isPivotTable() && !layout.indicatorsAsCol))) { @@ -162,7 +182,7 @@ function updateResizeColForAll(detaX: number, state: StateManager) { } } // state.table.setColWidth(col, afterSize); - state.table.scenegraph.updateColWidth(col, detaX); + state.table.scenegraph.updateColWidth(col, detaX, undefined, undefined, refreshedCornerCustomMergeRanges); state.table.internalProps._widthResizedColMap.add(col); } } @@ -180,16 +200,17 @@ function updateResizeColForIndicator(detaX: number, state: StateManager) { resizeDimensionKey = headerPath.dimensionKey; resizeDimensionValue = headerPath.value; } + const refreshedCornerCustomMergeRanges = new Set(); for (let col = state.table.rowHeaderLevelCount; col < state.table.colCount - state.table.rightFrozenColCount; col++) { const indicatorKey = layout.getIndicatorKey(col, state.table.columnHeaderLevelCount); if (layout.indicatorsAsCol && indicatorKey === resizeIndicatorKey) { - state.table.scenegraph.updateColWidth(col, detaX); + state.table.scenegraph.updateColWidth(col, detaX, undefined, undefined, refreshedCornerCustomMergeRanges); state.table.internalProps._widthResizedColMap.add(col); } else if (!layout.indicatorsAsCol) { const headerPaths = layout.getCellHeaderPaths(col, state.table.columnHeaderLevelCount - 1); const headerPath = headerPaths?.colHeaderPaths[headerPaths.colHeaderPaths.length - 1]; if (headerPath && resizeDimensionKey === headerPath.dimensionKey && resizeDimensionValue === headerPath.value) { - state.table.scenegraph.updateColWidth(col, detaX); + state.table.scenegraph.updateColWidth(col, detaX, undefined, undefined, refreshedCornerCustomMergeRanges); state.table.internalProps._widthResizedColMap.add(col); } } @@ -210,6 +231,7 @@ function updateResizeColForIndicatorGroup(detaX: number, state: StateManager) { // 计算当前受影响列的总宽度 后面会利用这个计算比例 const totalColWidth = state.table.getColsWidth(startCol, endCol); const moveX = detaX; // 纠正moveX 用于指标列均分该值 + const refreshedCornerCustomMergeRanges = new Set(); for (let col = startCol; col <= endCol; col++) { // 是否禁止调整列宽disableColumnResize 对应canResizeColumn的逻辑判断 if (!(state.table.internalProps.transpose || (state.table.isPivotTable() && !layout.indicatorsAsCol))) { @@ -227,7 +249,7 @@ function updateResizeColForIndicatorGroup(detaX: number, state: StateManager) { } else if (deltaWidth < 0 && deltaWidth >= -0.5) { deltaWidth = -0.5; } - state.table.scenegraph.updateColWidth(col, deltaWidth); + state.table.scenegraph.updateColWidth(col, deltaWidth, undefined, undefined, refreshedCornerCustomMergeRanges); state.table.internalProps._widthResizedColMap.add(col); } } diff --git a/packages/vtable/src/state/resize/update-resize-row.ts b/packages/vtable/src/state/resize/update-resize-row.ts index 3622b6f58d..a22fa8bfb7 100644 --- a/packages/vtable/src/state/resize/update-resize-row.ts +++ b/packages/vtable/src/state/resize/update-resize-row.ts @@ -69,14 +69,20 @@ export function updateResizeRow(xInTable: number, yInTable: number, state: State } function updateResizeColForRow(detaY: number, state: StateManager) { + const refreshedCornerCustomMergeRanges = new Set(); if (state.table.heightMode === 'adaptive' && state.rowResize.row < state.table.rowCount - 1) { - state.table.scenegraph.updateRowHeight(state.rowResize.row, detaY); - state.table.scenegraph.updateRowHeight(state.rowResize.row + 1, -detaY); + state.table.scenegraph.updateRowHeight(state.rowResize.row, detaY, undefined, refreshedCornerCustomMergeRanges); + state.table.scenegraph.updateRowHeight( + state.rowResize.row + 1, + -detaY, + undefined, + refreshedCornerCustomMergeRanges + ); state.table.internalProps._heightResizedRowMap.add(state.rowResize.row); state.table.internalProps._heightResizedRowMap.add(state.rowResize.row + 1); } else { - state.table.scenegraph.updateRowHeight(state.rowResize.row, detaY); + state.table.scenegraph.updateRowHeight(state.rowResize.row, detaY, undefined, refreshedCornerCustomMergeRanges); state.table.internalProps._heightResizedRowMap.add(state.rowResize.row); } } @@ -84,6 +90,7 @@ function updateResizeColForRow(detaY: number, state: StateManager) { function updateResizeColForAll(detaY: number, state: StateManager) { // 全列调整 // const layout = state.table.internalProps.layoutMap as PivotHeaderLayoutMap; + const refreshedCornerCustomMergeRanges = new Set(); for (let row = state.table.frozenRowCount; row < state.table.rowCount - state.table.bottomFrozenRowCount; row++) { // // 是否禁止调整列宽disableRowResize 对应canResizeRow的逻辑判断 // if (!(state.table.internalProps.transpose || (state.table.isPivotTable() && !layout.indicatorsAsCol))) { @@ -92,7 +99,7 @@ function updateResizeColForAll(detaY: number, state: StateManager) { // continue; // } // } - state.table.scenegraph.updateRowHeight(row, detaY); + state.table.scenegraph.updateRowHeight(row, detaY, undefined, refreshedCornerCustomMergeRanges); state.table.internalProps._heightResizedRowMap.add(row); } } @@ -110,6 +117,7 @@ function updateResizeColForIndicator(detaY: number, state: StateManager) { resizeDimensionKey = headerPath?.dimensionKey; resizeDimensionValue = headerPath?.value; } + const refreshedCornerCustomMergeRanges = new Set(); for ( let row = state.table.columnHeaderLevelCount; row < state.table.rowCount - state.table.bottomFrozenRowCount; @@ -117,13 +125,13 @@ function updateResizeColForIndicator(detaY: number, state: StateManager) { ) { const indicatorKey = layout.getIndicatorKey(state.table.rowHeaderLevelCount, row); if (!layout.indicatorsAsCol && indicatorKey === resizeIndicatorKey) { - state.table.scenegraph.updateRowHeight(row, detaY); + state.table.scenegraph.updateRowHeight(row, detaY, undefined, refreshedCornerCustomMergeRanges); state.table.internalProps._heightResizedRowMap.add(row); } else if (layout.indicatorsAsCol) { const headerPaths = layout.getCellHeaderPaths(state.table.rowHeaderLevelCount - 1, row); const headerPath = headerPaths?.rowHeaderPaths?.[headerPaths.rowHeaderPaths.length - 1]; if (headerPath && resizeDimensionKey === headerPath.dimensionKey && resizeDimensionValue === headerPath.value) { - state.table.scenegraph.updateRowHeight(row, detaY); + state.table.scenegraph.updateRowHeight(row, detaY, undefined, refreshedCornerCustomMergeRanges); state.table.internalProps._heightResizedRowMap.add(row); } } @@ -144,6 +152,7 @@ function updateResizeColForIndicatorGroup(detaY: number, state: StateManager) { // 计算当前受影响列的总宽度 后面会利用这个计算比例 const totalRowHeight = state.table.getRowsHeight(startRow, endRow); const moveY = detaY; // 纠正moveX 用于指标列均分该值 + const refreshedCornerCustomMergeRanges = new Set(); for (let row = startRow; row <= endRow; row++) { // 是否禁止调整列宽disableRowResize 对应canResizeRow的逻辑判断 // if (!(state.table.internalProps.transpose || (state.table.isPivotTable() && !layout.indicatorsAsCol))) { @@ -161,7 +170,7 @@ function updateResizeColForIndicatorGroup(detaY: number, state: StateManager) { } else if (deltaHeight < 0 && deltaHeight >= -0.5) { deltaHeight = -0.51; } - state.table.scenegraph.updateRowHeight(row, deltaHeight); + state.table.scenegraph.updateRowHeight(row, deltaHeight, undefined, refreshedCornerCustomMergeRanges); state.table.internalProps._heightResizedRowMap.add(row); } } From 4fa3405a3973163454464745afebce6d9816987e Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Fri, 28 Aug 2026 11:54:56 +0800 Subject: [PATCH 11/12] fix: defer corner merge refresh during resize Refresh corner custom merge content after grouped row or column resize updates. Use final dimensions instead of the first intermediate resize state. Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com --- .../src/scenegraph/layout/update-height.ts | 31 +++++++++----- .../src/scenegraph/layout/update-width.ts | 41 +++++++++++-------- packages/vtable/src/scenegraph/scenegraph.ts | 8 ++-- .../scenegraph/utils/corner-custom-merge.ts | 33 ++++++++++----- .../src/state/resize/update-resize-column.ts | 27 +++++++----- .../src/state/resize/update-resize-row.ts | 32 +++++++-------- 6 files changed, 104 insertions(+), 68 deletions(-) diff --git a/packages/vtable/src/scenegraph/layout/update-height.ts b/packages/vtable/src/scenegraph/layout/update-height.ts index 2e7f9c60ce..517622040d 100644 --- a/packages/vtable/src/scenegraph/layout/update-height.ts +++ b/packages/vtable/src/scenegraph/layout/update-height.ts @@ -19,10 +19,12 @@ import { getCellMergeRange } from '../../tools/merge-range'; import type { ColumnDefine, ListTableConstructorOptions } from '../../ts-types'; import { Factory } from '../../core/factory'; import { + type CornerCustomMergeRangeUpdateMap, createCornerCustomMergeContainer, + flushCornerCustomMergeContentUpdates, isCornerCustomMergeRange, - shouldRenderCornerCustomMergeContent, - updateCornerCustomMergeContentOnce + queueCornerCustomMergeContentUpdate, + shouldRenderCornerCustomMergeContent } from '../utils/corner-custom-merge'; export function updateRowHeight( @@ -30,8 +32,11 @@ export function updateRowHeight( row: number, detaY: number, skipTableHeightMap?: boolean, - refreshedCornerCustomMergeRanges = new Set() + pendingCornerCustomMergeRanges?: CornerCustomMergeRangeUpdateMap ) { + const shouldFlushCornerCustomMergeRanges = !pendingCornerCustomMergeRanges; + pendingCornerCustomMergeRanges ??= new Map(); + // 更新table行高存储 if (!skipTableHeightMap && detaY) { scene.table._setRowHeight(row, scene.table.getRowHeight(row) + detaY, true); @@ -57,7 +62,7 @@ export function updateRowHeight( height + detaY, detaY, scene.table.isHeader(col, row), - refreshedCornerCustomMergeRanges + pendingCornerCustomMergeRanges ); scene.updateCellContentWhileResize(col, row); @@ -91,6 +96,10 @@ export function updateRowHeight( } } } + + if (shouldFlushCornerCustomMergeRanges) { + flushCornerCustomMergeContentUpdates(pendingCornerCustomMergeRanges, scene.table); + } } /** @@ -105,7 +114,7 @@ export function updateCellHeightForRow( height: number, detaY: number, isHeader: boolean, - refreshedCornerCustomMergeRanges?: Set + pendingCornerCustomMergeRanges?: CornerCustomMergeRangeUpdateMap // autoRowHeight: boolean ) { // cell.setAttribute('height', height); @@ -117,7 +126,7 @@ export function updateCellHeightForRow( return; } - updateCellHeight(scene, cellGroup, col, row, distHeight, detaY, isHeader, refreshedCornerCustomMergeRanges); + updateCellHeight(scene, cellGroup, col, row, distHeight, detaY, isHeader, pendingCornerCustomMergeRanges); } export function updateCellHeightForColumn( @@ -142,7 +151,7 @@ export function updateCellHeight( distHeight: number, detaY: number, isHeader: boolean, - refreshedCornerCustomMergeRanges?: Set + pendingCornerCustomMergeRanges?: CornerCustomMergeRangeUpdateMap ) { if (cell.attribute.height === distHeight && !cell.needUpdateHeight) { return; @@ -199,7 +208,7 @@ export function updateCellHeight( true, scene.table, true, - refreshedCornerCustomMergeRanges + pendingCornerCustomMergeRanges ); } else if (type === 'sparkline') { // 目前先采用重新生成节点的方案 @@ -368,7 +377,7 @@ export function updateCellHeight( renderDefault, scene.table, !customContainer, - refreshedCornerCustomMergeRanges + pendingCornerCustomMergeRanges ); } } @@ -381,7 +390,7 @@ function updateMergeCellContentHeight( renderDefault: boolean, table: BaseTableAPI, refreshCornerCustomMergeContent = true, - refreshedCornerCustomMergeRanges?: Set + pendingCornerCustomMergeRanges?: CornerCustomMergeRangeUpdateMap ) { if (isMergeCellGroup(cellGroup)) { distHeight = 0; @@ -447,7 +456,7 @@ function updateMergeCellContentHeight( } const mergeRange = table.getCellRange(cellGroup.mergeStartCol, cellGroup.mergeStartRow); if (refreshCornerCustomMergeContent && isCornerCustomMergeRange(mergeRange, table)) { - updateCornerCustomMergeContentOnce(mergeRange, table, refreshedCornerCustomMergeRanges); + queueCornerCustomMergeContentUpdate(mergeRange, table, pendingCornerCustomMergeRanges); } } else { const style = table._getCellStyle(cellGroup.col, cellGroup.row); diff --git a/packages/vtable/src/scenegraph/layout/update-width.ts b/packages/vtable/src/scenegraph/layout/update-width.ts index ea3eef6f39..55d156e3dd 100644 --- a/packages/vtable/src/scenegraph/layout/update-width.ts +++ b/packages/vtable/src/scenegraph/layout/update-width.ts @@ -18,10 +18,12 @@ import { isMergeCellGroup } from '../utils/is-merge-cell-group'; import { getQuadProps } from '../utils/padding'; import { updateCellContentWidth } from '../utils/text-icon-layout'; import { + type CornerCustomMergeRangeUpdateMap, createCornerCustomMergeContainer, + flushCornerCustomMergeContentUpdates, isCornerCustomMergeRange, - shouldRenderCornerCustomMergeContent, - updateCornerCustomMergeContentOnce + queueCornerCustomMergeContentUpdate, + shouldRenderCornerCustomMergeContent } from '../utils/corner-custom-merge'; import { computeRowHeight } from './compute-row-height'; import { updateCellHeightForRow } from './update-height'; @@ -42,8 +44,11 @@ export function updateColWidth( col: number, detaX: number, skipTableWidthMap?: boolean, - refreshedCornerCustomMergeRanges = new Set() + pendingCornerCustomMergeRanges?: CornerCustomMergeRangeUpdateMap ) { + const shouldFlushCornerCustomMergeRanges = !pendingCornerCustomMergeRanges; + pendingCornerCustomMergeRanges ??= new Map(); + if (!skipTableWidthMap) { scene.table._setColWidth(col, scene.table.getColWidth(col) + detaX, true); } @@ -52,33 +57,33 @@ export function updateColWidth( const colOrCornerHeaderColumn = scene.getColGroup(col, true) as Group; const rightTopColumn = scene.getColGroupInRightTopCorner(col); if (colOrCornerHeaderColumn && !rightTopColumn) { - updateColunmWidth(colOrCornerHeaderColumn, detaX, 'col-corner', scene, refreshedCornerCustomMergeRanges); + updateColunmWidth(colOrCornerHeaderColumn, detaX, 'col-corner', scene, pendingCornerCustomMergeRanges); } // deal with right bottom frozen cells if (rightTopColumn) { - updateColunmWidth(rightTopColumn, detaX, 'right-top', scene, refreshedCornerCustomMergeRanges); + updateColunmWidth(rightTopColumn, detaX, 'right-top', scene, pendingCornerCustomMergeRanges); } // deal with row header or body or right frozen cells const rowHeaderOrBodyColumn = scene.getColGroup(col) as Group; if (rowHeaderOrBodyColumn) { - updateColunmWidth(rowHeaderOrBodyColumn, detaX, 'row-body', scene, refreshedCornerCustomMergeRanges); + updateColunmWidth(rowHeaderOrBodyColumn, detaX, 'row-body', scene, pendingCornerCustomMergeRanges); } const leftBottomColumn = scene.getColGroupInLeftBottomCorner(col); // deal with left bottom frozen cells if (leftBottomColumn) { - updateColunmWidth(leftBottomColumn, detaX, 'left-bottom', scene, refreshedCornerCustomMergeRanges); + updateColunmWidth(leftBottomColumn, detaX, 'left-bottom', scene, pendingCornerCustomMergeRanges); } // deal with bottom frozen cells const bottomColumn = scene.getColGroupInBottom(col); if (bottomColumn) { - updateColunmWidth(bottomColumn, detaX, 'bottom', scene, refreshedCornerCustomMergeRanges); + updateColunmWidth(bottomColumn, detaX, 'bottom', scene, pendingCornerCustomMergeRanges); } // deal with right bottom frozen cells const rightBottomColumn = scene.getColGroupInRightBottomCorner(col); if (rightBottomColumn) { - updateColunmWidth(rightBottomColumn, detaX, 'right-bottom', scene, refreshedCornerCustomMergeRanges); + updateColunmWidth(rightBottomColumn, detaX, 'right-bottom', scene, pendingCornerCustomMergeRanges); } // 更新剩余列位置 @@ -128,6 +133,10 @@ export function updateColWidth( }); } + if (shouldFlushCornerCustomMergeRanges) { + flushCornerCustomMergeContentUpdates(pendingCornerCustomMergeRanges, scene.table); + } + // scene.table.setColWidth(col, rowHeaderOrBodyColumn.attribute.width, true); } @@ -137,7 +146,7 @@ function updateColunmWidth( // autoRowHeight: boolean, mode: 'col-corner' | 'row-body' | 'bottom' | 'left-bottom' | 'right-top' | 'right-bottom', scene: Scenegraph, - refreshedCornerCustomMergeRanges: Set + pendingCornerCustomMergeRanges: CornerCustomMergeRangeUpdateMap ) { let needRerangeRow = false; // const colOrCornerHeaderColumn = scene.getColGroup(col, true) as Group; @@ -155,7 +164,7 @@ function updateColunmWidth( detaX, mode === 'row-body' ? cell.col < scene.table.rowHeaderLevelCount : true, scene.table.internalProps.autoWrapText, - refreshedCornerCustomMergeRanges + pendingCornerCustomMergeRanges ); if (isHeightChange) { const mergeInfo = getCellMergeInfo(scene.table, cell.col, cell.row); @@ -261,7 +270,7 @@ function updateCellWidth( isHeader: boolean, // autoColWidth: boolean, autoWrapText: boolean, - refreshedCornerCustomMergeRanges?: Set + pendingCornerCustomMergeRanges?: CornerCustomMergeRangeUpdateMap ): boolean { if (cell.attribute.width === distWidth && !cell.needUpdateWidth) { return false; @@ -331,7 +340,7 @@ function updateCellWidth( true, scene.table, true, - refreshedCornerCustomMergeRanges + pendingCornerCustomMergeRanges ); isHeightChange = isHeightChange || cellChange; } else if (type === 'sparkline') { @@ -510,7 +519,7 @@ function updateCellWidth( renderDefault, scene.table, !customContainer, - refreshedCornerCustomMergeRanges + pendingCornerCustomMergeRanges ); isHeightChange = isHeightChange || cellChange; } @@ -529,7 +538,7 @@ function updateMergeCellContentWidth( renderDefault: boolean, table: BaseTableAPI, refreshCornerCustomMergeContent = true, - refreshedCornerCustomMergeRanges?: Set + pendingCornerCustomMergeRanges?: CornerCustomMergeRangeUpdateMap ) { if (isMergeCellGroup(cellGroup)) { distWidth = 0; @@ -624,7 +633,7 @@ function updateMergeCellContentWidth( } const mergeRange = table.getCellRange(cellGroup.mergeStartCol, cellGroup.mergeStartRow); if (refreshCornerCustomMergeContent && isCornerCustomMergeRange(mergeRange, table)) { - updateCornerCustomMergeContentOnce(mergeRange, table, refreshedCornerCustomMergeRanges); + queueCornerCustomMergeContentUpdate(mergeRange, table, pendingCornerCustomMergeRanges); } return isHeightChange; } diff --git a/packages/vtable/src/scenegraph/scenegraph.ts b/packages/vtable/src/scenegraph/scenegraph.ts index 09ce2dedf8..f2a18bbad8 100644 --- a/packages/vtable/src/scenegraph/scenegraph.ts +++ b/packages/vtable/src/scenegraph/scenegraph.ts @@ -1001,9 +1001,9 @@ export class Scenegraph { detaX: number, skipUpdateContainer?: boolean, skipTableWidthMap?: boolean, - refreshedCornerCustomMergeRanges?: Set + pendingCornerCustomMergeRanges?: Map ) { - updateColWidth(this, col, Math.round(detaX), skipTableWidthMap, refreshedCornerCustomMergeRanges); + updateColWidth(this, col, Math.round(detaX), skipTableWidthMap, pendingCornerCustomMergeRanges); // this.updateContainerWidth(col, detaX); if (!skipUpdateContainer) { // this.updateContainerAttrWidthAndX(); @@ -1550,10 +1550,10 @@ export class Scenegraph { row: number, detaY: number, skipTableHeightMap?: boolean, - refreshedCornerCustomMergeRanges?: Set + pendingCornerCustomMergeRanges?: Map ) { detaY = Math.round(detaY); - updateRowHeight(this, row, detaY, skipTableHeightMap, refreshedCornerCustomMergeRanges); + updateRowHeight(this, row, detaY, skipTableHeightMap, pendingCornerCustomMergeRanges); this.updateContainerHeight(row, detaY); } updateRowsHeight(rows: number[], detaYs: number[], skipTableHeightMap?: boolean) { diff --git a/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts b/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts index 3a2f28d677..b28c51d30e 100644 --- a/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts +++ b/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts @@ -4,6 +4,8 @@ import type { BaseTableAPI } from '../../ts-types/base-table'; import { CUSTOM_CONTAINER_NAME, CUSTOM_MERGE_CONTAINER_NAME } from '../component/custom'; import { Group } from '../graphic/group'; +export type CornerCustomMergeRangeUpdateMap = Map; + export function isCornerCustomMergeRange(range: CellRange | undefined, table: BaseTableAPI): boolean { return ( !!range?.isCustom && @@ -52,23 +54,34 @@ export function createCornerCustomMergeContainer( } export function updateCornerCustomMergeContent(range: CellRange, table: BaseTableAPI): void { - updateCornerCustomMergeContentOnce(range, table); + refreshCornerCustomMergeContent(range, table); } -export function updateCornerCustomMergeContentOnce( +export function queueCornerCustomMergeContentUpdate( range: CellRange, table: BaseTableAPI, - refreshedRanges?: Set + pendingRanges?: CornerCustomMergeRangeUpdateMap ): void { - if (refreshedRanges) { - const rangeKey = `${range.start.col}-${range.start.row}-${range.end.col}-${range.end.row}`; - if (refreshedRanges.has(rangeKey)) { - return; - } - refreshedRanges.add(rangeKey); + if (!pendingRanges) { + refreshCornerCustomMergeContent(range, table); + return; } - refreshCornerCustomMergeContent(range, table); + pendingRanges.set(getCornerCustomMergeRangeKey(range), range); +} + +export function flushCornerCustomMergeContentUpdates( + pendingRanges: CornerCustomMergeRangeUpdateMap, + table: BaseTableAPI +): void { + pendingRanges.forEach(range => { + refreshCornerCustomMergeContent(range, table); + }); + pendingRanges.clear(); +} + +function getCornerCustomMergeRangeKey(range: CellRange): string { + return `${range.start.col}-${range.start.row}-${range.end.col}-${range.end.row}`; } function refreshCornerCustomMergeContent(range: CellRange, table: BaseTableAPI): void { diff --git a/packages/vtable/src/state/resize/update-resize-column.ts b/packages/vtable/src/state/resize/update-resize-column.ts index fe8a31d7fa..7d7c7cf41e 100644 --- a/packages/vtable/src/state/resize/update-resize-column.ts +++ b/packages/vtable/src/state/resize/update-resize-column.ts @@ -1,5 +1,6 @@ import type { ListTable } from '../../ListTable'; import type { PivotHeaderLayoutMap } from '../../layout/pivot-header-layout'; +import { flushCornerCustomMergeContentUpdates } from '../../scenegraph/utils/corner-custom-merge'; import type { IndicatorData } from '../../ts-types/list-table/layout-map/api'; import type { StateManager } from '../state'; @@ -133,7 +134,7 @@ export function updateResizeColumn(xInTable: number, yInTable: number, state: St } function updateResizeColForColumn(detaX: number, state: StateManager) { - const refreshedCornerCustomMergeRanges = new Set(); + const pendingCornerCustomMergeRanges = new Map(); if (state.table.widthMode === 'adaptive' && state.columnResize.col < state.table.colCount - 1) { // in adaptive mode, the right column width can not be negative // const rightColWidth = state.table.getColWidth(state.columnResize.col + 1); @@ -145,14 +146,14 @@ function updateResizeColForColumn(detaX: number, state: StateManager) { detaX, undefined, undefined, - refreshedCornerCustomMergeRanges + pendingCornerCustomMergeRanges ); state.table.scenegraph.updateColWidth( state.columnResize.col + 1, -detaX, undefined, undefined, - refreshedCornerCustomMergeRanges + pendingCornerCustomMergeRanges ); state.table.internalProps._widthResizedColMap.add(state.columnResize.col); @@ -163,16 +164,17 @@ function updateResizeColForColumn(detaX: number, state: StateManager) { detaX, undefined, undefined, - refreshedCornerCustomMergeRanges + pendingCornerCustomMergeRanges ); state.table.internalProps._widthResizedColMap.add(state.columnResize.col); } + flushCornerCustomMergeContentUpdates(pendingCornerCustomMergeRanges, state.table); } function updateResizeColForAll(detaX: number, state: StateManager) { // 全列调整 const layout = state.table.internalProps.layoutMap as PivotHeaderLayoutMap; - const refreshedCornerCustomMergeRanges = new Set(); + const pendingCornerCustomMergeRanges = new Map(); for (let col = state.table.frozenColCount; col < state.table.colCount - state.table.rightFrozenColCount; col++) { // 是否禁止调整列宽disableColumnResize 对应canResizeColumn的逻辑判断 if (!(state.table.internalProps.transpose || (state.table.isPivotTable() && !layout.indicatorsAsCol))) { @@ -182,9 +184,10 @@ function updateResizeColForAll(detaX: number, state: StateManager) { } } // state.table.setColWidth(col, afterSize); - state.table.scenegraph.updateColWidth(col, detaX, undefined, undefined, refreshedCornerCustomMergeRanges); + state.table.scenegraph.updateColWidth(col, detaX, undefined, undefined, pendingCornerCustomMergeRanges); state.table.internalProps._widthResizedColMap.add(col); } + flushCornerCustomMergeContentUpdates(pendingCornerCustomMergeRanges, state.table); } function updateResizeColForIndicator(detaX: number, state: StateManager) { @@ -200,21 +203,22 @@ function updateResizeColForIndicator(detaX: number, state: StateManager) { resizeDimensionKey = headerPath.dimensionKey; resizeDimensionValue = headerPath.value; } - const refreshedCornerCustomMergeRanges = new Set(); + const pendingCornerCustomMergeRanges = new Map(); for (let col = state.table.rowHeaderLevelCount; col < state.table.colCount - state.table.rightFrozenColCount; col++) { const indicatorKey = layout.getIndicatorKey(col, state.table.columnHeaderLevelCount); if (layout.indicatorsAsCol && indicatorKey === resizeIndicatorKey) { - state.table.scenegraph.updateColWidth(col, detaX, undefined, undefined, refreshedCornerCustomMergeRanges); + state.table.scenegraph.updateColWidth(col, detaX, undefined, undefined, pendingCornerCustomMergeRanges); state.table.internalProps._widthResizedColMap.add(col); } else if (!layout.indicatorsAsCol) { const headerPaths = layout.getCellHeaderPaths(col, state.table.columnHeaderLevelCount - 1); const headerPath = headerPaths?.colHeaderPaths[headerPaths.colHeaderPaths.length - 1]; if (headerPath && resizeDimensionKey === headerPath.dimensionKey && resizeDimensionValue === headerPath.value) { - state.table.scenegraph.updateColWidth(col, detaX, undefined, undefined, refreshedCornerCustomMergeRanges); + state.table.scenegraph.updateColWidth(col, detaX, undefined, undefined, pendingCornerCustomMergeRanges); state.table.internalProps._widthResizedColMap.add(col); } } } + flushCornerCustomMergeContentUpdates(pendingCornerCustomMergeRanges, state.table); } function updateResizeColForIndicatorGroup(detaX: number, state: StateManager) { @@ -231,7 +235,7 @@ function updateResizeColForIndicatorGroup(detaX: number, state: StateManager) { // 计算当前受影响列的总宽度 后面会利用这个计算比例 const totalColWidth = state.table.getColsWidth(startCol, endCol); const moveX = detaX; // 纠正moveX 用于指标列均分该值 - const refreshedCornerCustomMergeRanges = new Set(); + const pendingCornerCustomMergeRanges = new Map(); for (let col = startCol; col <= endCol; col++) { // 是否禁止调整列宽disableColumnResize 对应canResizeColumn的逻辑判断 if (!(state.table.internalProps.transpose || (state.table.isPivotTable() && !layout.indicatorsAsCol))) { @@ -249,7 +253,8 @@ function updateResizeColForIndicatorGroup(detaX: number, state: StateManager) { } else if (deltaWidth < 0 && deltaWidth >= -0.5) { deltaWidth = -0.5; } - state.table.scenegraph.updateColWidth(col, deltaWidth, undefined, undefined, refreshedCornerCustomMergeRanges); + state.table.scenegraph.updateColWidth(col, deltaWidth, undefined, undefined, pendingCornerCustomMergeRanges); state.table.internalProps._widthResizedColMap.add(col); } + flushCornerCustomMergeContentUpdates(pendingCornerCustomMergeRanges, state.table); } diff --git a/packages/vtable/src/state/resize/update-resize-row.ts b/packages/vtable/src/state/resize/update-resize-row.ts index a22fa8bfb7..0d61f43790 100644 --- a/packages/vtable/src/state/resize/update-resize-row.ts +++ b/packages/vtable/src/state/resize/update-resize-row.ts @@ -1,5 +1,6 @@ import type { ListTable } from '../../ListTable'; import type { PivotHeaderLayoutMap } from '../../layout/pivot-header-layout'; +import { flushCornerCustomMergeContentUpdates } from '../../scenegraph/utils/corner-custom-merge'; import type { IndicatorData } from '../../ts-types/list-table/layout-map/api'; import type { StateManager } from '../state'; @@ -69,28 +70,24 @@ export function updateResizeRow(xInTable: number, yInTable: number, state: State } function updateResizeColForRow(detaY: number, state: StateManager) { - const refreshedCornerCustomMergeRanges = new Set(); + const pendingCornerCustomMergeRanges = new Map(); if (state.table.heightMode === 'adaptive' && state.rowResize.row < state.table.rowCount - 1) { - state.table.scenegraph.updateRowHeight(state.rowResize.row, detaY, undefined, refreshedCornerCustomMergeRanges); - state.table.scenegraph.updateRowHeight( - state.rowResize.row + 1, - -detaY, - undefined, - refreshedCornerCustomMergeRanges - ); + state.table.scenegraph.updateRowHeight(state.rowResize.row, detaY, undefined, pendingCornerCustomMergeRanges); + state.table.scenegraph.updateRowHeight(state.rowResize.row + 1, -detaY, undefined, pendingCornerCustomMergeRanges); state.table.internalProps._heightResizedRowMap.add(state.rowResize.row); state.table.internalProps._heightResizedRowMap.add(state.rowResize.row + 1); } else { - state.table.scenegraph.updateRowHeight(state.rowResize.row, detaY, undefined, refreshedCornerCustomMergeRanges); + state.table.scenegraph.updateRowHeight(state.rowResize.row, detaY, undefined, pendingCornerCustomMergeRanges); state.table.internalProps._heightResizedRowMap.add(state.rowResize.row); } + flushCornerCustomMergeContentUpdates(pendingCornerCustomMergeRanges, state.table); } function updateResizeColForAll(detaY: number, state: StateManager) { // 全列调整 // const layout = state.table.internalProps.layoutMap as PivotHeaderLayoutMap; - const refreshedCornerCustomMergeRanges = new Set(); + const pendingCornerCustomMergeRanges = new Map(); for (let row = state.table.frozenRowCount; row < state.table.rowCount - state.table.bottomFrozenRowCount; row++) { // // 是否禁止调整列宽disableRowResize 对应canResizeRow的逻辑判断 // if (!(state.table.internalProps.transpose || (state.table.isPivotTable() && !layout.indicatorsAsCol))) { @@ -99,9 +96,10 @@ function updateResizeColForAll(detaY: number, state: StateManager) { // continue; // } // } - state.table.scenegraph.updateRowHeight(row, detaY, undefined, refreshedCornerCustomMergeRanges); + state.table.scenegraph.updateRowHeight(row, detaY, undefined, pendingCornerCustomMergeRanges); state.table.internalProps._heightResizedRowMap.add(row); } + flushCornerCustomMergeContentUpdates(pendingCornerCustomMergeRanges, state.table); } function updateResizeColForIndicator(detaY: number, state: StateManager) { @@ -117,7 +115,7 @@ function updateResizeColForIndicator(detaY: number, state: StateManager) { resizeDimensionKey = headerPath?.dimensionKey; resizeDimensionValue = headerPath?.value; } - const refreshedCornerCustomMergeRanges = new Set(); + const pendingCornerCustomMergeRanges = new Map(); for ( let row = state.table.columnHeaderLevelCount; row < state.table.rowCount - state.table.bottomFrozenRowCount; @@ -125,17 +123,18 @@ function updateResizeColForIndicator(detaY: number, state: StateManager) { ) { const indicatorKey = layout.getIndicatorKey(state.table.rowHeaderLevelCount, row); if (!layout.indicatorsAsCol && indicatorKey === resizeIndicatorKey) { - state.table.scenegraph.updateRowHeight(row, detaY, undefined, refreshedCornerCustomMergeRanges); + state.table.scenegraph.updateRowHeight(row, detaY, undefined, pendingCornerCustomMergeRanges); state.table.internalProps._heightResizedRowMap.add(row); } else if (layout.indicatorsAsCol) { const headerPaths = layout.getCellHeaderPaths(state.table.rowHeaderLevelCount - 1, row); const headerPath = headerPaths?.rowHeaderPaths?.[headerPaths.rowHeaderPaths.length - 1]; if (headerPath && resizeDimensionKey === headerPath.dimensionKey && resizeDimensionValue === headerPath.value) { - state.table.scenegraph.updateRowHeight(row, detaY, undefined, refreshedCornerCustomMergeRanges); + state.table.scenegraph.updateRowHeight(row, detaY, undefined, pendingCornerCustomMergeRanges); state.table.internalProps._heightResizedRowMap.add(row); } } } + flushCornerCustomMergeContentUpdates(pendingCornerCustomMergeRanges, state.table); } function updateResizeColForIndicatorGroup(detaY: number, state: StateManager) { @@ -152,7 +151,7 @@ function updateResizeColForIndicatorGroup(detaY: number, state: StateManager) { // 计算当前受影响列的总宽度 后面会利用这个计算比例 const totalRowHeight = state.table.getRowsHeight(startRow, endRow); const moveY = detaY; // 纠正moveX 用于指标列均分该值 - const refreshedCornerCustomMergeRanges = new Set(); + const pendingCornerCustomMergeRanges = new Map(); for (let row = startRow; row <= endRow; row++) { // 是否禁止调整列宽disableRowResize 对应canResizeRow的逻辑判断 // if (!(state.table.internalProps.transpose || (state.table.isPivotTable() && !layout.indicatorsAsCol))) { @@ -170,7 +169,8 @@ function updateResizeColForIndicatorGroup(detaY: number, state: StateManager) { } else if (deltaHeight < 0 && deltaHeight >= -0.5) { deltaHeight = -0.51; } - state.table.scenegraph.updateRowHeight(row, deltaHeight, undefined, refreshedCornerCustomMergeRanges); + state.table.scenegraph.updateRowHeight(row, deltaHeight, undefined, pendingCornerCustomMergeRanges); state.table.internalProps._heightResizedRowMap.add(row); } + flushCornerCustomMergeContentUpdates(pendingCornerCustomMergeRanges, state.table); } From a26131ad0f664ca244087fdfb64f506cf6b4d496 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Fri, 28 Aug 2026 14:10:29 +0800 Subject: [PATCH 12/12] fix: preserve corner merge react state on resize Keep resize refreshes from unmounting corner custom merge React roots. Reuse the existing root while updating the resized container rect. Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com --- .../src/scenegraph/utils/corner-custom-merge.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts b/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts index b28c51d30e..434eac5545 100644 --- a/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts +++ b/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts @@ -75,7 +75,7 @@ export function flushCornerCustomMergeContentUpdates( table: BaseTableAPI ): void { pendingRanges.forEach(range => { - refreshCornerCustomMergeContent(range, table); + refreshCornerCustomMergeContentForResize(range, table); }); pendingRanges.clear(); } @@ -97,3 +97,15 @@ function refreshCornerCustomMergeContent(range: CellRange, table: BaseTableAPI): table.scenegraph.updateCellContent(range.end.col, range.end.row); } + +function refreshCornerCustomMergeContentForResize(range: CellRange, table: BaseTableAPI): void { + const cellGroup = table.scenegraph.getCell(range.end.col, range.end.row); + const customContainer = + cellGroup.getChildByName(CUSTOM_CONTAINER_NAME) || cellGroup.getChildByName(CUSTOM_MERGE_CONTAINER_NAME); + + if (customContainer) { + cellGroup.removeChild(customContainer); + } + + table.scenegraph.updateCellContent(range.end.col, range.end.row); +}