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 1/2] 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 2/2] 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; +}