diff --git a/docs/assets/option/en/common/option-secondary.md b/docs/assets/option/en/common/option-secondary.md index abc43c2089..4d3227f667 100644 --- a/docs/assets/option/en/common/option-secondary.md +++ b/docs/assets/option/en/common/option-secondary.md @@ -721,6 +721,13 @@ Whether to adjust the column width, support configuring functions, the function canResizeColumn?: (col: number, row: number, table: BaseTableAPI) => boolean; ``` +##${prefix} realtime(boolean) = true + +Whether to respond in real time when dragging to resize columns/rows. + +- `true` (default): Column widths / row heights update in real time during dragging, keeping the historical behavior. +- `false`: Only the indicator line moves while dragging; the final size is applied when the mouse is released. Recommended for large datasets to significantly reduce lag while dragging. + #${prefix} dragOrder(Object) #${prefix} dragOrder(Object) diff --git a/docs/assets/option/zh/common/option-secondary.md b/docs/assets/option/zh/common/option-secondary.md index cc560547bf..b96f8471f2 100644 --- a/docs/assets/option/zh/common/option-secondary.md +++ b/docs/assets/option/zh/common/option-secondary.md @@ -719,6 +719,13 @@ animationAppear?: boolean | { canResizeColumn?: (col: number, row: number, table: BaseTableAPI) => boolean; ``` +##${prefix} realtime(boolean) = true + +拖拽调整行列尺寸时是否实时响应。 + +- `true`(默认):拖拽过程中实时更新列宽/行高,与历史行为一致。 +- `false`:拖拽过程中仅移动指示线,鼠标释放后才应用最终尺寸变化。适用于大数据量场景,可显著缓解拖拽时的卡顿。 + #${prefix} dragOrder(Object) 拖拽移动位置的配置。 diff --git a/packages/vtable-gantt/__tests__/gantt-non-realtime-resize-sync.test.ts b/packages/vtable-gantt/__tests__/gantt-non-realtime-resize-sync.test.ts new file mode 100644 index 0000000000..c7189d99f4 --- /dev/null +++ b/packages/vtable-gantt/__tests__/gantt-non-realtime-resize-sync.test.ts @@ -0,0 +1,104 @@ +// @ts-nocheck + +global.__VERSION__ = 'none'; + +import { createDiv } from './dom'; +import { Gantt } from '../src/index'; +// 真正发出 RESIZE_COLUMN / RESIZE_ROW 事件的是 table-group 中的 end wrapper(pointerup 路径), +// 这里直接复用它来模拟拖拽结束,验证跨包布局同步。 +import { endResizeCol } from '../../vtable/src/event/listener/table-group'; + +function createGantt(resize: any) { + const container = createDiv(); + const option = { + records: [{ id: 1, title: '任务', startDate: '2024-07-01', endDate: '2024-07-02', progress: 50 }], + taskListTable: { + columns: [ + { field: 'title', title: '任务名称', width: 180 }, + { field: 'id', title: 'ID', width: 120 } + ], + tableWidth: 'auto', + resize + }, + timelineHeader: { + colWidth: 50, + scales: [{ unit: 'day', step: 1 }] + }, + minDate: '2024-07-01', + maxDate: '2024-07-07' + }; + const gantt = new Gantt(container, option); + return { gantt, container }; +} + +/** + * 忠实模拟一次把第 col 列加宽 delta 像素的拖拽:start -> move(含过程事件) -> up。 + * 过程事件 RESIZE_COLUMN 在真实场景中由 container-dom 的 pointermove 回调发出,这里一并复刻, + * 以便区分实时/非实时两种模式下过程事件携带的列宽是否为最新值。 + */ +function dragResizeCol(table: any, col: number, delta: number) { + const startX = table.getColsWidth(0, col); // 列右边界的大致 x + table.stateManager.startResizeCol(col, startX, 50); + // 一次 pointermove,传入完整位移 + table.stateManager.updateResizeCol(startX + delta, 50); + // container-dom 在每次 pointermove 后发出 RESIZE_COLUMN,携带“当前” getColWidth + if (table.hasListeners('resize_column')) { + table.fireListeners('resize_column', { col, colWidth: table.getColWidth(col) }); + } + // pointerup:走真实的 end wrapper,非实时模式在此提交并补发过程事件 + endResizeCol(table); +} + +describe('gantt non-realtime resize layout sync', () => { + test('non-realtime column resize should sync taskTableWidth after commit', () => { + const { gantt, container } = createGantt({ realtime: false }); + const table = gantt.taskListTableInstance; + const border = gantt.parsedOptions.outerFrameStyle.borderLineWidth as number; + expect(table.internalProps.resizeRealtime).toBe(false); + + const widthBefore = gantt.taskTableWidth; + const colWidthBefore = table.getColWidth(0); + + dragResizeCol(table, 0, 60); + + const colWidthAfter = table.getColWidth(0); + // 列宽确实变宽 + expect(colWidthAfter).toBeGreaterThan(colWidthBefore); + // 关键断言:甘特图任务列表宽度已随最终列宽同步,而不是停留在旧值。 + // 修复前,拖拽过程事件携带旧列宽、结束时又不再发 resize_column,taskTableWidth 会停在 widthBefore。 + expect(gantt.taskTableWidth).toBe(table.getAllColsWidth() + border); + expect(gantt.taskTableWidth).toBeGreaterThan(widthBefore); + + gantt.release?.(); + container.remove(); + }); + + test('non-realtime shrink should also sync (no leftover gap)', () => { + const { gantt, container } = createGantt({ realtime: false }); + const table = gantt.taskListTableInstance; + const border = gantt.parsedOptions.outerFrameStyle.borderLineWidth as number; + + const widthBefore = gantt.taskTableWidth; + dragResizeCol(table, 0, -60); + + expect(gantt.taskTableWidth).toBe(table.getAllColsWidth() + border); + expect(gantt.taskTableWidth).toBeLessThan(widthBefore); + + gantt.release?.(); + container.remove(); + }); + + test('realtime mode keeps syncing as before', () => { + const { gantt, container } = createGantt({ realtime: true }); + const table = gantt.taskListTableInstance; + const border = gantt.parsedOptions.outerFrameStyle.borderLineWidth as number; + expect(table.internalProps.resizeRealtime).not.toBe(false); + + dragResizeCol(table, 0, 60); + + expect(gantt.taskTableWidth).toBe(table.getAllColsWidth() + border); + + gantt.release?.(); + container.remove(); + }); +}); diff --git a/packages/vtable/__tests__/visual/cases/index.mjs b/packages/vtable/__tests__/visual/cases/index.mjs index 1072fdd97c..111ad6aeea 100644 --- a/packages/vtable/__tests__/visual/cases/index.mjs +++ b/packages/vtable/__tests__/visual/cases/index.mjs @@ -162,6 +162,12 @@ export const cases = [ { id: 'pivot-empty-string-dimension', purpose: '透视空字符串列维度与普通列并存', file: './pivot/empty-string-dimension.mjs', bundles: ['vtable'] }, { id: 'pivot-chart-pie-multi-select', purpose: '透视饼图连续扇区多选', file: './pivot-chart/pie-multi-select.mjs', bundles: ['vchart'] }, { id: 'resize-row-height', purpose: '列表选择后拖动行边界调整行高', file: './interaction/resize-row-height.mjs', bundles: ['vtable'] }, + { id: 'resize-column-non-realtime', purpose: 'resize.realtime=false 时列宽仅在鼠标松开后应用', file: './interaction/resize-column-non-realtime.mjs', bundles: ['vtable'] }, + { id: 'resize-column-realtime-default', purpose: 'resize.realtime 默认 true 时列宽实时随拖拽变化', file: './interaction/resize-column-realtime-default.mjs', bundles: ['vtable'] }, + { id: 'resize-column-non-realtime-min-clamp', purpose: '非实时模式越过 minWidth 时按目标方向 clamp 而非反向 resize', file: './interaction/resize-column-non-realtime-min-clamp.mjs', bundles: ['vtable'] }, + { id: 'resize-column-non-realtime-preview-clamp', purpose: '非实时拖拽预览指示线/标签按最终可应用尺寸约束', file: './interaction/resize-column-non-realtime-preview-clamp.mjs', bundles: ['vtable'] }, + { id: 'resize-column-adaptive-neighbor', purpose: 'adaptive 列宽相邻列位移不被重复扣减', file: './interaction/resize-column-adaptive-neighbor.mjs', bundles: ['vtable'] }, + { id: 'resize-row-adaptive-neighbor', purpose: 'adaptive 行高相邻行位移不被重复扣减', file: './interaction/resize-row-adaptive-neighbor.mjs', bundles: ['vtable'] }, { id: 'pivot-resize-indicator-row', purpose: '指标行上的透视表拖动行高', file: './pivot/resize-indicator-row.mjs', bundles: ['vtable'] }, { id: 'pivot-corner-sort-one-dimension', purpose: '单级角头维度初始降序状态与排序图标', file: './pivot/corner-sort-one-dimension.mjs', bundles: ['vtable'] }, { id: 'pivot-corner-sort-two-dimensions', purpose: '两级角头维度初始降序状态与排序图标', file: './pivot/corner-sort-two-dimensions.mjs', bundles: ['vtable'] }, diff --git a/packages/vtable/__tests__/visual/cases/interaction/resize-column-adaptive-neighbor.mjs b/packages/vtable/__tests__/visual/cases/interaction/resize-column-adaptive-neighbor.mjs new file mode 100644 index 0000000000..e894e6ba77 --- /dev/null +++ b/packages/vtable/__tests__/visual/cases/interaction/resize-column-adaptive-neighbor.mjs @@ -0,0 +1,65 @@ +/** + * 验证目的:adaptive 列宽下,相邻右列承接相反方向的位移后不应被重复扣减。 + * 右列已是 rightColWidthCache - detaX,若再用 rightColWidth - detaX 判断 limitMinWidth 会把位移错误截短, + * 导致最终列宽与指示线终点、实时拖动不一致。本例一次性拖动一个较大位移以触发该重复扣减。 + */ +export default { + mount(container) { + // 固定容器宽度让 adaptive 把两列各分到约 150px,便于数值断言。 + container.style.width = '300px'; + container.style.height = '300px'; + return new window.VTable.ListTable(container, { + columns: [ + { field: 'name', title: 'Name', width: 150 }, + { field: 'value', title: 'Value', width: 150 } + ], + records: Array.from({ length: 5 }, (_, index) => ({ + name: `Item ${index + 1}`, + value: index + 1 + })), + defaultRowHeight: 32, + widthMode: 'adaptive', + resize: { columnResizeMode: 'all' } + }); + }, + async exercise(page) { + const point = await page.evaluate(() => { + const table = window.__visualTable; + const b = table.getCellRect(0, 0).bounds; + const host = document.getElementById('table').getBoundingClientRect(); + window.__leftBefore = table.getColWidth(0); + window.__rightBefore = table.getColWidth(1); + // 选取使 rightBefore - drag >= limitMinWidth(10) 但 rightBefore - 2*drag < 10 的位移, + // 正确实现应保留全部位移,旧实现会二次扣减并截短。 + window.__drag = window.__rightBefore - 20; + return { edge: { x: host.x + b.x2 - 1, y: host.y + (b.y1 + b.y2) / 2 }, drag: window.__rightBefore - 20 }; + }); + // 单步拖动一个大位移,使一次 updateResizeColumn 收到完整 detaX。 + await page.mouse.move(point.edge.x, point.edge.y); + await page.mouse.down(); + await page.mouse.move(point.edge.x + point.drag, point.edge.y); + await page.mouse.up(); + }, + async verify(page) { + await page.evaluate(() => { + const table = window.__visualTable; + const leftBefore = window.__leftBefore; + const rightBefore = window.__rightBefore; + const drag = window.__drag; + const leftAfter = table.getColWidth(0); + const rightAfter = table.getColWidth(1); + // 右列应收缩到约 rightBefore - drag(≈20);重复扣减会把它停在 rightBefore - 10 附近。 + if (rightAfter > rightBefore - drag + 5) { + throw new Error( + `adaptive 右列位移被重复扣减截短:rightBefore=${rightBefore}, drag=${drag}, rightAfter=${rightAfter}` + ); + } + // 左列应吃满整段位移,验证尺寸与指示线终点一致。 + if (leftAfter < leftBefore + drag - 5) { + throw new Error( + `adaptive 左列未吃满位移:leftBefore=${leftBefore}, drag=${drag}, leftAfter=${leftAfter}` + ); + } + }); + } +}; diff --git a/packages/vtable/__tests__/visual/cases/interaction/resize-column-non-realtime-min-clamp.mjs b/packages/vtable/__tests__/visual/cases/interaction/resize-column-non-realtime-min-clamp.mjs new file mode 100644 index 0000000000..6e0f2cd827 --- /dev/null +++ b/packages/vtable/__tests__/visual/cases/interaction/resize-column-non-realtime-min-clamp.mjs @@ -0,0 +1,50 @@ +/** + * 验证目的:非实时模式把整段累计位移一次传入 min/max 约束时,应按目标值的越界方向 clamp。 + * 列宽 180、minWidth 100、maxWidth 200,向左累计拖动 100px 目标为 80,必须贴 minWidth 收到 100, + * 不能因“当前宽度离哪个边界更近”而反向扩到 maxWidth 200。 + */ +export default { + mount(container) { + return new window.VTable.ListTable(container, { + columns: [ + { field: 'name', title: 'Name', width: 180, minWidth: 100, maxWidth: 200 }, + { field: 'value', title: 'Value', width: 150 } + ], + records: Array.from({ length: 5 }, (_, index) => ({ + name: `Item ${index + 1}`, + value: index + 1 + })), + defaultRowHeight: 32, + resize: { columnResizeMode: 'all', realtime: false } + }); + }, + async exercise(page) { + const point = await page.evaluate(() => { + const table = window.__visualTable; + const b = table.getCellRect(0, 0).bounds; + const host = document.getElementById('table').getBoundingClientRect(); + window.__colWidthBefore = table.getColWidth(0); + return { edge: { x: host.x + b.x2 - 1, y: host.y + (b.y1 + b.y2) / 2 } }; + }); + // 非实时模式:拖拽过程只移动指示线,向左累计 100px,松开鼠标后统一提交。 + await page.mouse.move(point.edge.x, point.edge.y); + await page.mouse.down(); + await page.mouse.move(point.edge.x - 100, point.edge.y, { steps: 10 }); + await page.mouse.up(); + }, + async verify(page) { + await page.evaluate(() => { + const table = window.__visualTable; + const before = window.__colWidthBefore; + const after = table.getColWidth(0); + // 目标宽度 80 越过下界,必须 clamp 到 minWidth 100 并相对初始缩窄。 + if (after !== 100) { + throw new Error(`非实时越界列宽未按 minWidth clamp:before=${before}, after=${after}`); + } + // 反向 resize 会扩到 maxWidth 200,这里显式拦截该回归。 + if (after >= before) { + throw new Error(`非实时向左拖动反而没有缩窄,疑似反向 resize:before=${before}, after=${after}`); + } + }); + } +}; diff --git a/packages/vtable/__tests__/visual/cases/interaction/resize-column-non-realtime-preview-clamp.mjs b/packages/vtable/__tests__/visual/cases/interaction/resize-column-non-realtime-preview-clamp.mjs new file mode 100644 index 0000000000..07091c3c18 --- /dev/null +++ b/packages/vtable/__tests__/visual/cases/interaction/resize-column-non-realtime-preview-clamp.mjs @@ -0,0 +1,71 @@ +/** + * 验证目的:非实时模式拖拽越过 minWidth 时,指示线预览与标签应显示“最终可应用的尺寸”, + * 而不是未约束的 pendingDeltaX。列宽 180、minWidth 100,向左拖 100px 目标为 80, + * 预览标签必须显示 100px(与松手后的最终列宽一致),不能显示未约束的 80px。 + */ +export default { + mount(container) { + return new window.VTable.ListTable(container, { + columns: [ + { field: 'name', title: 'Name', width: 180, minWidth: 100, maxWidth: 200 }, + { field: 'value', title: 'Value', width: 150 } + ], + records: Array.from({ length: 5 }, (_, index) => ({ + name: `Item ${index + 1}`, + value: index + 1 + })), + defaultRowHeight: 32, + resize: { columnResizeMode: 'all', realtime: false } + }); + }, + async exercise(page) { + const point = await page.evaluate(() => { + const table = window.__visualTable; + const b = table.getCellRect(0, 0).bounds; + const host = document.getElementById('table').getBoundingClientRect(); + return { edge: { x: host.x + b.x2 - 1, y: host.y + (b.y1 + b.y2) / 2 } }; + }); + // 非实时模式:按下并向左累计拖动 100px,但先不松手,停在拖拽预览状态。 + await page.mouse.move(point.edge.x, point.edge.y); + await page.mouse.down(); + await page.mouse.move(point.edge.x - 100, point.edge.y, { steps: 10 }); + // 捕获松手前的预览标签文案与指示线位置。 + await page.evaluate(() => { + const table = window.__visualTable; + const component = table.scenegraph.component; + window.__previewLabel = component.columnResizeLabel.lastChild.attribute.text; + window.__previewLineX = component.columnResizeLine.attribute.x; + // 基准线:当前列右边界 x(无偏移)。 + const b = table.getCellRect(0, 0).bounds; + window.__colBaseX = b.x2; + window.__colWidthBefore = table.getColWidth(0); + }); + await page.mouse.up(); + }, + async verify(page) { + await page.evaluate(() => { + const table = window.__visualTable; + const previewLabel = window.__previewLabel; + const previewLineX = window.__previewLineX; + const colBaseX = window.__colBaseX; + const after = table.getColWidth(0); + + // 松手后最终列宽应 clamp 到 minWidth 100。 + if (after !== 100) { + throw new Error(`最终列宽未按 minWidth clamp:after=${after}`); + } + // 预览标签必须显示最终可应用尺寸 100px,而不是未约束的 80px。 + if (previewLabel !== '100px') { + throw new Error(`预览标签未按最终尺寸约束:previewLabel=${previewLabel}, 期望 100px`); + } + // 指示线位置应对应有效位移(最终宽度 100 相对初始 180 向左收 80px), + // 即基准线左移 (180-100)=80px;若用未约束的 100px 位移会多移 20px。 + const expectedLineX = colBaseX - 80; + if (Math.abs(previewLineX - expectedLineX) > 1) { + throw new Error( + `预览指示线位置未按最终尺寸约束:previewLineX=${previewLineX}, 期望≈${expectedLineX}(基准 ${colBaseX})` + ); + } + }); + } +}; diff --git a/packages/vtable/__tests__/visual/cases/interaction/resize-column-non-realtime.mjs b/packages/vtable/__tests__/visual/cases/interaction/resize-column-non-realtime.mjs new file mode 100644 index 0000000000..74f74c57b2 --- /dev/null +++ b/packages/vtable/__tests__/visual/cases/interaction/resize-column-non-realtime.mjs @@ -0,0 +1,66 @@ +/** + * 验证目的:resize.realtime=false 时,列宽调整仅在鼠标松开后应用一次。 + * 拖拽过程中列宽保持不变,松开鼠标时列宽等于累计偏移量。 + */ +export default { + mount(container) { + return new window.VTable.ListTable(container, { + columns: [ + { field: 'name', title: 'Name', width: 150 }, + { field: 'value', title: 'Value', width: 150 } + ], + records: Array.from({ length: 5 }, (_, index) => ({ + name: `Item ${index + 1}`, + value: index + 1 + })), + defaultRowHeight: 32, + resize: { columnResizeMode: 'all', realtime: false } + }); + }, + async exercise(page) { + const point = await page.evaluate(() => { + const table = window.__visualTable; + const b = table.getCellRect(0, 0).bounds; + const host = document.getElementById('table').getBoundingClientRect(); + window.__colWidthBefore = table.getColWidth(0); + return { + cell: { x: host.x + (b.x1 + b.x2) / 2, y: host.y + (b.y1 + b.y2) / 2 }, + edge: { x: host.x + b.x2 - 1, y: host.y + (b.y1 + b.y2) / 2 } + }; + }); + await page.mouse.move(point.edge.x, point.edge.y); + await page.mouse.down(); + // 拖拽过程中每步都记录列宽,确认拖拽时列宽未变化 + await page.mouse.move(point.edge.x + 40, point.edge.y, { steps: 10 }); + const widthDuringDrag = await page.evaluate(() => window.__visualTable.getColWidth(0)); + await page.mouse.move(point.edge.x + 80, point.edge.y, { steps: 10 }); + const widthDuringDrag2 = await page.evaluate(() => window.__visualTable.getColWidth(0)); + await page.mouse.up(); + await page.evaluate( + ({ w1, w2 }) => { + window.__widthDuringDrag = w1; + window.__widthDuringDrag2 = w2; + }, + { w1: widthDuringDrag, w2: widthDuringDrag2 } + ); + }, + async verify(page) { + await page.evaluate(() => { + const table = window.__visualTable; + const before = window.__colWidthBefore; + const during1 = window.__widthDuringDrag; + const during2 = window.__widthDuringDrag2; + const after = table.getColWidth(0); + // 拖拽中列宽不应变化 + if (during1 !== before || during2 !== before) { + throw new Error( + `非实时响应模式下拖拽过程中列宽发生了变化: before=${before}, during1=${during1}, during2=${during2}, after=${after}` + ); + } + // 松开鼠标后列宽应增加(累计偏移量 80px 应用) + if (after <= before + 40) { + throw new Error(`鼠标松开后列宽未按累计偏移量应用: before=${before}, after=${after}`); + } + }); + } +}; diff --git a/packages/vtable/__tests__/visual/cases/interaction/resize-column-realtime-default.mjs b/packages/vtable/__tests__/visual/cases/interaction/resize-column-realtime-default.mjs new file mode 100644 index 0000000000..9cb7cc3f77 --- /dev/null +++ b/packages/vtable/__tests__/visual/cases/interaction/resize-column-realtime-default.mjs @@ -0,0 +1,60 @@ +/** + * 验证目的:resize.realtime=true(默认)时保持历史行为,拖拽过程中列宽实时变化。 + * 作为 resize-column-non-realtime 的对照用例,防止改动回归默认行为。 + */ +export default { + mount(container) { + return new window.VTable.ListTable(container, { + columns: [ + { field: 'name', title: 'Name', width: 150 }, + { field: 'value', title: 'Value', width: 150 } + ], + records: Array.from({ length: 5 }, (_, index) => ({ + name: `Item ${index + 1}`, + value: index + 1 + })), + defaultRowHeight: 32, + resize: { columnResizeMode: 'all' } + }); + }, + async exercise(page) { + const point = await page.evaluate(() => { + const table = window.__visualTable; + const b = table.getCellRect(0, 0).bounds; + const host = document.getElementById('table').getBoundingClientRect(); + window.__colWidthBefore = table.getColWidth(0); + return { + edge: { x: host.x + b.x2 - 1, y: host.y + (b.y1 + b.y2) / 2 } + }; + }); + await page.mouse.move(point.edge.x, point.edge.y); + await page.mouse.down(); + await page.mouse.move(point.edge.x + 40, point.edge.y, { steps: 10 }); + const widthDuringDrag = await page.evaluate(() => window.__visualTable.getColWidth(0)); + await page.mouse.move(point.edge.x + 80, point.edge.y, { steps: 10 }); + const widthDuringDrag2 = await page.evaluate(() => window.__visualTable.getColWidth(0)); + await page.mouse.up(); + await page.evaluate( + ({ w1, w2 }) => { + window.__widthDuringDrag = w1; + window.__widthDuringDrag2 = w2; + }, + { w1: widthDuringDrag, w2: widthDuringDrag2 } + ); + }, + async verify(page) { + await page.evaluate(() => { + const before = window.__colWidthBefore; + const during1 = window.__widthDuringDrag; + const during2 = window.__widthDuringDrag2; + const after = window.__visualTable.getColWidth(0); + // 实时模式:拖拽中列宽应递增 + if (during1 <= before || during2 <= during1) { + throw new Error(`实时模式下列宽未随拖拽变化: before=${before}, during1=${during1}, during2=${during2}`); + } + if (after <= before + 40) { + throw new Error(`鼠标松开后列宽未达到预期: before=${before}, after=${after}`); + } + }); + } +}; diff --git a/packages/vtable/__tests__/visual/cases/interaction/resize-row-adaptive-neighbor.mjs b/packages/vtable/__tests__/visual/cases/interaction/resize-row-adaptive-neighbor.mjs new file mode 100644 index 0000000000..c3f6e90ecf --- /dev/null +++ b/packages/vtable/__tests__/visual/cases/interaction/resize-row-adaptive-neighbor.mjs @@ -0,0 +1,64 @@ +/** + * 验证目的:adaptive 行高下,下方相邻行承接相反方向的位移后不应被重复扣减。 + * 行高路径与列相同:bottomRowHeight 已是 bottomRowHeightCache - detaY,若再用 bottomRowHeight - detaY + * 判断 limitMinHeight 会把位移错误截短,使最终行高与指示线终点、实时拖动不一致。本例一次拖动大位移触发该重复扣减。 + */ +export default { + mount(container) { + // 固定容器高度让 adaptive 把数据行撑高,便于数值断言。 + container.style.width = '300px'; + container.style.height = '400px'; + return new window.VTable.ListTable(container, { + columns: [ + { field: 'name', title: 'Name', width: 150 }, + { field: 'value', title: 'Value', width: 150 } + ], + records: Array.from({ length: 3 }, (_, index) => ({ + name: `Item ${index + 1}`, + value: index + 1 + })), + defaultRowHeight: 80, + heightMode: 'adaptive', + resize: { rowResizeMode: 'all' } + }); + }, + async exercise(page) { + const point = await page.evaluate(() => { + const table = window.__visualTable; + const b = table.getCellRect(0, 1).bounds; + const host = document.getElementById('table').getBoundingClientRect(); + window.__topBefore = table.getRowHeight(1); + window.__bottomBefore = table.getRowHeight(2); + // 选取使 bottomBefore - drag >= limitMinHeight(10) 但 bottomBefore - 2*drag < 10 的位移。 + window.__drag = window.__bottomBefore - 20; + return { edge: { x: host.x + (b.x1 + b.x2) / 2, y: host.y + b.y2 - 1 }, drag: window.__bottomBefore - 20 }; + }); + // 单步向下拖动一个大位移,使一次 updateResizeRow 收到完整 detaY。 + await page.mouse.move(point.edge.x, point.edge.y); + await page.mouse.down(); + await page.mouse.move(point.edge.x, point.edge.y + point.drag); + await page.mouse.up(); + }, + async verify(page) { + await page.evaluate(() => { + const table = window.__visualTable; + const topBefore = window.__topBefore; + const bottomBefore = window.__bottomBefore; + const drag = window.__drag; + const topAfter = table.getRowHeight(1); + const bottomAfter = table.getRowHeight(2); + // 下方行应收缩到约 bottomBefore - drag(≈20);重复扣减会把它停在 bottomBefore - 10 附近。 + if (bottomAfter > bottomBefore - drag + 5) { + throw new Error( + `adaptive 下方行位移被重复扣减截短:bottomBefore=${bottomBefore}, drag=${drag}, bottomAfter=${bottomAfter}` + ); + } + // 拖动行应吃满整段位移,验证尺寸与指示线终点一致。 + if (topAfter < topBefore + drag - 5) { + throw new Error( + `adaptive 拖动行未吃满位移:topBefore=${topBefore}, drag=${drag}, topAfter=${topAfter}` + ); + } + }); + } +}; diff --git a/packages/vtable/examples/interactive/resize-realtime.ts b/packages/vtable/examples/interactive/resize-realtime.ts new file mode 100644 index 0000000000..e74c3a7a3d --- /dev/null +++ b/packages/vtable/examples/interactive/resize-realtime.ts @@ -0,0 +1,75 @@ +import * as VTable from '../../src'; + +const ListTable = VTable.ListTable; +const CONTAINER_ID = 'vTable'; + +export function createTable() { + // 生成较大数据量,便于对比实时与非实时响应下的拖拽流畅度 + const records = Array.from({ length: 10000 }, (_, index) => ({ + id: index + 1, + name: `记录 ${index + 1}`, + dept: ['研发', '产品', '运营', '销售'][index % 4], + city: ['杭州', '北京', '上海', '成都', '广州'][index % 5], + score: Math.round(Math.random() * 100), + salary: Math.round(4000 + Math.random() * 20000) + })); + + const option: any = { + columns: [ + { field: 'id', title: 'ID', width: 80 }, + { field: 'name', title: '姓名', width: 160 }, + { field: 'dept', title: '部门', width: 120 }, + { field: 'city', title: '城市', width: 120 }, + { field: 'score', title: '评分', width: 120 }, + { field: 'salary', title: '薪资', width: 140 } + ], + records, + defaultRowHeight: 32, + resize: { + columnResizeMode: 'all', + rowResizeMode: 'all', + // 关键:设为 false 时,拖拽过程只移动指示线,鼠标松开后才应用列宽/行高变化, + // 可显著缓解大数据量下的拖拽卡顿。默认为 true,保持原有行为。 + realtime: false + } + }; + + const instance = new ListTable(document.getElementById(CONTAINER_ID)!, option); + (window as any).tableInstance = instance; + + // 提供简单的运行时切换按钮,便于对比两种模式的手感 + const bar = document.createElement('div'); + bar.style.cssText = [ + 'position:absolute', + 'top:10px', + 'right:10px', + 'z-index:9999', + 'background:#fff', + 'padding:8px 12px', + 'border:1px solid #ddd', + 'border-radius:4px', + 'box-shadow:0 1px 4px rgba(0,0,0,.1)', + 'font-family:sans-serif', + 'font-size:12px' + ].join(';'); + bar.innerHTML = ` +
当前 resize.realtime = false
+ + `; + // 挂进表格根元素内部(position:relative)而非 document.body, + // 这样切换到其他 demo、容器被清空时按钮会随之移除,不会残留在页面右上角 + instance.getElement().appendChild(bar); + let current = false; + bar.querySelector('#__toggle_realtime')!.addEventListener('click', () => { + current = !current; + // 通过 updateOption 重新应用配置 + (instance as any).updateOption({ + ...option, + resize: { ...option.resize, realtime: current } + }); + (bar.querySelector('#__realtime_state') as HTMLElement).textContent = String(current); + (bar.querySelector('#__toggle_realtime') as HTMLElement).textContent = `切换为 ${ + current ? '非实时(false)' : '实时(true)' + }`; + }); +} diff --git a/packages/vtable/examples/menu.ts b/packages/vtable/examples/menu.ts index a6ad9e99ac..2a1ed18284 100644 --- a/packages/vtable/examples/menu.ts +++ b/packages/vtable/examples/menu.ts @@ -979,6 +979,10 @@ export const menus = [ path: 'interactive', name: 'row-resize' }, + { + path: 'interactive', + name: 'resize-realtime' + }, { path: 'interactive', name: 'resize-setRecords' diff --git a/packages/vtable/src/core/BaseTable.ts b/packages/vtable/src/core/BaseTable.ts index 8f880d7481..9ca2e75ad3 100644 --- a/packages/vtable/src/core/BaseTable.ts +++ b/packages/vtable/src/core/BaseTable.ts @@ -462,6 +462,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { internalProps.columnResizeMode = resize?.columnResizeMode ?? columnResizeMode; internalProps.canResizeColumn = resize?.canResizeColumn; internalProps.rowResizeMode = resize?.rowResizeMode ?? rowResizeMode; + internalProps.resizeRealtime = resize?.realtime !== false; internalProps.dragHeaderMode = dragOrder?.dragHeaderMode ?? dragHeaderMode ?? 'none'; internalProps.renderChartAsync = renderChartAsync; setBatchRenderChartCount(renderChartAsyncBatchCount); @@ -2911,6 +2912,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { internalProps.columnResizeMode = resize?.columnResizeMode ?? columnResizeMode; internalProps.canResizeColumn = resize?.canResizeColumn; internalProps.rowResizeMode = resize?.rowResizeMode ?? rowResizeMode; + internalProps.resizeRealtime = resize?.realtime !== false; internalProps.dragHeaderMode = dragOrder?.dragHeaderMode ?? dragHeaderMode ?? 'none'; internalProps.renderChartAsync = renderChartAsync; setBatchRenderChartCount(renderChartAsyncBatchCount); diff --git a/packages/vtable/src/event/listener/table-group.ts b/packages/vtable/src/event/listener/table-group.ts index 4023cac004..266d17901b 100644 --- a/packages/vtable/src/event/listener/table-group.ts +++ b/packages/vtable/src/event/listener/table-group.ts @@ -1309,7 +1309,18 @@ export function bindGesture(eventManager: EventManager) { }); } export function endResizeCol(table: BaseTableAPI) { + // 非实时模式下,拖拽过程中 container-dom 发出的 RESIZE_COLUMN 携带的是旧列宽, + // 真正的列宽在 endResizeCol() -> commitPendingColumnResize() 时才提交。 + const isNonRealtime = table.internalProps.resizeRealtime === false; table.stateManager.endResizeCol(); + // 提交后补发一次 RESIZE_COLUMN,让依赖过程事件做布局同步的调用方(如甘特图任务列表宽度同步、 + // list-tree-stick 插件)拿到最终列宽,避免最终尺寸正确却未同步。实时模式在拖拽过程中已持续发出,无需补发。 + if (isNonRealtime && (table as any).hasListeners(TABLE_EVENT_TYPE.RESIZE_COLUMN)) { + table.fireListeners(TABLE_EVENT_TYPE.RESIZE_COLUMN, { + col: table.stateManager.columnResize.col, + colWidth: table.getColWidth(table.stateManager.columnResize.col) + }); + } // textStick 依赖了这个事件 所以一定要触发RESIZE_COLUMN_END // if ((table as any).hasListeners(TABLE_EVENT_TYPE.RESIZE_COLUMN_END)) { // RESIZE_COLUMN_END事件触发,返回所有列宽 @@ -1326,7 +1337,16 @@ export function endResizeCol(table: BaseTableAPI) { } export function endResizeRow(table: BaseTableAPI) { + // 非实时模式下,拖拽过程中发出的 RESIZE_ROW 携带的是旧行高,真正的行高在提交时才确定。 + const isNonRealtime = table.internalProps.resizeRealtime === false; table.stateManager.endResizeRow(); + // 提交后补发一次 RESIZE_ROW,让依赖过程事件的调用方拿到最终行高。实时模式无需补发。 + if (isNonRealtime && (table as any).hasListeners(TABLE_EVENT_TYPE.RESIZE_ROW)) { + table.fireListeners(TABLE_EVENT_TYPE.RESIZE_ROW, { + row: table.stateManager.rowResize.row, + rowHeight: table.getRowHeight(table.stateManager.rowResize.row) + }); + } table.fireListeners(TABLE_EVENT_TYPE.RESIZE_ROW_END, { row: table.stateManager.rowResize.row, diff --git a/packages/vtable/src/scenegraph/component/table-component.ts b/packages/vtable/src/scenegraph/component/table-component.ts index 8352992d2c..33fae723b4 100644 --- a/packages/vtable/src/scenegraph/component/table-component.ts +++ b/packages/vtable/src/scenegraph/component/table-component.ts @@ -640,9 +640,12 @@ export class TableComponent { * @param {number} y 标签显示的y坐标 * @return {*} */ - updateResizeCol(col: number, y: number, isRightFrozen?: boolean) { + updateResizeCol(col: number, y: number, isRightFrozen?: boolean, previewDeltaX?: number) { // 基准线 - const colX = getColX(col, this.table, isRightFrozen); + const colBaseX = getColX(col, this.table, isRightFrozen); + const delta = previewDeltaX ?? 0; + // 非实时响应模式下指示线基于当前列右边界+累计偏移量渲染 + const colX = isRightFrozen ? colBaseX - delta : colBaseX + delta; // this.columnResizeLine.setAttribute('x', x); this.columnResizeLine.setAttributes({ x: colX, @@ -665,7 +668,8 @@ export class TableComponent { x: colX, y }); - (this.columnResizeLabel.lastChild as Text).setAttribute('text', `${Math.floor(this.table.getColWidth(col))}px`); + const previewWidth = Math.max(0, this.table.getColWidth(col) + delta); + (this.columnResizeLabel.lastChild as Text).setAttribute('text', `${Math.floor(previewWidth)}px`); } } @@ -725,9 +729,11 @@ export class TableComponent { * @param {number} y 标签显示的y坐标 * @return {*} */ - updateResizeRow(row: number, x: number, isBottomFrozen?: boolean) { + updateResizeRow(row: number, x: number, isBottomFrozen?: boolean, previewDeltaY?: number) { // 基准线 - const rowY = getRowY(row, this.table, isBottomFrozen); + const rowBaseY = getRowY(row, this.table, isBottomFrozen); + const delta = previewDeltaY ?? 0; + const rowY = isBottomFrozen ? rowBaseY - delta : rowBaseY + delta; // this.columnResizeLine.setAttribute('x', x); this.rowResizeLine.setAttributes({ y: rowY, @@ -750,7 +756,8 @@ export class TableComponent { y: rowY, x }); - (this.rowResizeLabel.lastChild as Text).setAttribute('text', `${Math.floor(this.table.getRowHeight(row))}px`); + const previewHeight = Math.max(0, this.table.getRowHeight(row) + delta); + (this.rowResizeLabel.lastChild as Text).setAttribute('text', `${Math.floor(previewHeight)}px`); } } diff --git a/packages/vtable/src/state/resize/update-resize-column.ts b/packages/vtable/src/state/resize/update-resize-column.ts index 7d7c7cf41e..4d4e039239 100644 --- a/packages/vtable/src/state/resize/update-resize-column.ts +++ b/packages/vtable/src/state/resize/update-resize-column.ts @@ -8,33 +8,71 @@ import type { StateManager } from '../state'; export function updateResizeColumn(xInTable: number, yInTable: number, state: StateManager) { xInTable = Math.ceil(xInTable); yInTable = Math.ceil(yInTable); - let detaX = state.columnResize.isRightFrozen ? state.columnResize.x - xInTable : xInTable - state.columnResize.x; + const detaX = state.columnResize.isRightFrozen ? state.columnResize.x - xInTable : xInTable - state.columnResize.x; // table.getColWidth会使用Math.round,因此这里直接跳过小于1px的修改 if (Math.abs(detaX) < 1) { return; } + // 非实时响应模式:仅累积偏移量并移动指示线,实际列宽在拖拽结束时统一应用 + if (state.table.internalProps.resizeRealtime === false) { + state.columnResize.pendingDeltaX = (state.columnResize.pendingDeltaX ?? 0) + detaX; + state.columnResize.x = xInTable; + // 预览指示线/标签复用提交时相同的有效位移计算(dry run), + // 避免越过 min/max 或 adaptive 相邻列约束时预览显示未约束的尺寸、与最终列宽不一致。 + const previewDeltaX = applyColumnResizeDelta(state.columnResize.pendingDeltaX, xInTable, yInTable, state, true); + state.table.scenegraph.component.updateResizeCol( + state.columnResize.col, + yInTable, + state.columnResize.isRightFrozen, + previewDeltaX + ); + state.table.scenegraph.updateNextFrame(); + return; + } + + applyColumnResizeDelta(detaX, xInTable, yInTable, state); +} + +/** 非实时响应模式下,在拖拽结束时提交累计的宽度变化 */ +export function commitPendingColumnResize(state: StateManager) { + const pending = state.columnResize.pendingDeltaX ?? 0; + state.columnResize.pendingDeltaX = 0; + if (Math.abs(pending) < 1) { + return; + } + // 用当前列坐标+累计偏移量作为最终位置,复用实时模式的处理逻辑 + const isRightFrozen = state.columnResize.isRightFrozen; + const currentX = state.columnResize.x; + const targetX = isRightFrozen ? currentX - pending : currentX + pending; + applyColumnResizeDelta(pending, targetX, currentX, state); +} + +function applyColumnResizeDelta( + detaX: number, + xInTable: number, + yInTable: number, + state: StateManager, + dryRun = false +): number { // 检查minWidth/maxWidth - // getColWidth会进行Math.round,所以先从colWidthsMap获取: - // 如果是数值,直接使用;如果不是,则通过getColWidth获取像素值 - // let widthCache = (state.table as any).colWidthsMap.get(state.columnResize.col); - // if (typeof widthCache === 'number') { - // widthCache = widthCache; - // } else { const widthCache = state.table.getColWidth(state.columnResize.col); - // } let width = widthCache; width += detaX; const minWidth = state.table.getMinColWidth(state.columnResize.col); const maxWidth = state.table.getMaxColWidth(state.columnResize.col); - if (width < minWidth || width > maxWidth) { - if (widthCache === minWidth || widthCache === maxWidth) { - return; - } else if (widthCache - minWidth > maxWidth - widthCache) { - detaX = maxWidth - widthCache; - } else { - detaX = minWidth - widthCache; + // 按目标宽度的越界方向 clamp:目标值过小时贴 minWidth,过大时贴 maxWidth。 + // 非实时模式会把整段 pendingDeltaX 一次传入,若按“当前宽度离哪个边界更近”选择边界会产生反向 resize。 + if (width < minWidth) { + if (widthCache === minWidth) { + return 0; + } + detaX = minWidth - widthCache; + } else if (width > maxWidth) { + if (widthCache === maxWidth) { + return 0; } + detaX = maxWidth - widthCache; } // limitMinWidth限制 @@ -47,23 +85,32 @@ export function updateResizeColumn(xInTable: number, yInTable: number, state: St const rightColWidthCache = state.table.getColWidth(state.columnResize.col + 1); const rightColMinWidth = state.table.getMinColWidth(state.columnResize.col + 1); const rightColMaxWidth = state.table.getMaxColWidth(state.columnResize.col + 1); - let rightColWidth = rightColWidthCache; - rightColWidth -= detaX; - if (rightColWidth < rightColMinWidth || rightColWidth > rightColMaxWidth) { - if (rightColWidthCache === rightColMinWidth || rightColWidthCache === rightColMaxWidth) { - return; - } else if (rightColWidthCache - rightColMinWidth > rightColMaxWidth - rightColWidthCache) { - detaX = rightColMaxWidth - rightColWidthCache; - } else { - detaX = rightColMinWidth - rightColWidthCache; + // 右侧相邻列承接相反方向的位移,目标宽度为 rightColWidthCache - detaX。 + const rightColWidth = rightColWidthCache - detaX; + // 按目标宽度的越界方向 clamp:右列过窄时贴 min,过宽时贴 max。 + if (rightColWidth < rightColMinWidth) { + if (rightColWidthCache === rightColMinWidth) { + return 0; } + detaX = rightColWidthCache - rightColMinWidth; + } else if (rightColWidth > rightColMaxWidth) { + if (rightColWidthCache === rightColMaxWidth) { + return 0; + } + detaX = rightColWidthCache - rightColMaxWidth; } - if (rightColWidth - detaX < state.table.internalProps.limitMinWidth) { - detaX = rightColWidth - state.table.internalProps.limitMinWidth; + // limitMinWidth 用目标宽度判断,不再二次扣减 detaX,避免与指示线终点、实时模式不一致。 + if (rightColWidthCache - detaX < state.table.internalProps.limitMinWidth) { + detaX = rightColWidthCache - state.table.internalProps.limitMinWidth; } } detaX = Math.ceil(detaX); + // dry run:仅返回约束后的有效位移,供非实时模式预览指示线/标签使用,不产生任何副作用。 + if (dryRun) { + return detaX; + } + if ( state.columnResize.col < state.table.rowHeaderLevelCount || state.columnResize.col >= state.table.colCount - state.table.rightFrozenColCount @@ -80,17 +127,6 @@ export function updateResizeColumn(xInTable: number, yInTable: number, state: St updateResizeColForColumn(detaX, state); } - // 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); - // } else { - // state.table.scenegraph.updateColWidth(state.columnResize.col, detaX); - // } state.columnResize.x = xInTable; // update resize column component @@ -131,6 +167,7 @@ export function updateResizeColumn(xInTable: number, yInTable: number, state: St // stage rerender state.table.scenegraph.updateNextFrame(); + return detaX; } function updateResizeColForColumn(detaX: number, state: StateManager) { diff --git a/packages/vtable/src/state/resize/update-resize-row.ts b/packages/vtable/src/state/resize/update-resize-row.ts index 0d61f43790..a773fea64d 100644 --- a/packages/vtable/src/state/resize/update-resize-row.ts +++ b/packages/vtable/src/state/resize/update-resize-row.ts @@ -8,12 +8,52 @@ import type { StateManager } from '../state'; export function updateResizeRow(xInTable: number, yInTable: number, state: StateManager) { xInTable = Math.ceil(xInTable); yInTable = Math.ceil(yInTable); - let detaY = state.rowResize.isBottomFrozen ? state.rowResize.y - yInTable : yInTable - state.rowResize.y; + const detaY = state.rowResize.isBottomFrozen ? state.rowResize.y - yInTable : yInTable - state.rowResize.y; // table.getColWidth会使用Math.round,因此这里直接跳过小于1px的修改 if (Math.abs(detaY) < 1) { return; } + // 非实时响应模式:仅累积偏移量并移动指示线,实际行高在拖拽结束时统一应用 + if (state.table.internalProps.resizeRealtime === false) { + state.rowResize.pendingDeltaY = (state.rowResize.pendingDeltaY ?? 0) + detaY; + state.rowResize.y = yInTable; + // 预览指示线/标签复用提交时相同的有效位移计算(dry run), + // 避免越过 limitMinHeight 或 adaptive 相邻行约束时预览显示未约束的尺寸、与最终行高不一致。 + const previewDeltaY = applyRowResizeDelta(state.rowResize.pendingDeltaY, xInTable, yInTable, state, true); + state.table.scenegraph.component.updateResizeRow( + state.rowResize.row, + xInTable, + state.rowResize.isBottomFrozen, + previewDeltaY + ); + state.table.scenegraph.updateNextFrame(); + return; + } + + applyRowResizeDelta(detaY, xInTable, yInTable, state); +} + +/** 非实时响应模式下,在拖拽结束时提交累计的高度变化 */ +export function commitPendingRowResize(state: StateManager) { + const pending = state.rowResize.pendingDeltaY ?? 0; + state.rowResize.pendingDeltaY = 0; + if (Math.abs(pending) < 1) { + return; + } + const isBottomFrozen = state.rowResize.isBottomFrozen; + const currentY = state.rowResize.y; + const targetY = isBottomFrozen ? currentY - pending : currentY + pending; + applyRowResizeDelta(pending, currentY, targetY, state); +} + +function applyRowResizeDelta( + detaY: number, + xInTable: number, + yInTable: number, + state: StateManager, + dryRun = false +): number { const heightCache = state.table.getRowHeight(state.rowResize.row); let height = heightCache; height += detaY; @@ -26,14 +66,20 @@ export function updateResizeRow(xInTable: number, yInTable: number, state: State } if (state.table.heightMode === 'adaptive' && state.rowResize.row < state.table.rowCount - 1) { const bottomRowHeightCache = state.table.getRowHeight(state.rowResize.row + 1); - let bottomRowHeight = bottomRowHeightCache; - bottomRowHeight -= detaY; - if (bottomRowHeight - detaY < state.table.internalProps.limitMinHeight) { - detaY = bottomRowHeight - state.table.internalProps.limitMinHeight; + // 下方相邻行承接相反方向的位移,目标高度为 bottomRowHeightCache - detaY。 + const bottomRowHeight = bottomRowHeightCache - detaY; + // limitMinHeight 用目标高度判断,不再二次扣减 detaY,避免与指示线终点、实时模式不一致。 + if (bottomRowHeight < state.table.internalProps.limitMinHeight) { + detaY = bottomRowHeightCache - state.table.internalProps.limitMinHeight; } } detaY = Math.ceil(detaY); + // dry run:仅返回约束后的有效位移,供非实时模式预览指示线/标签使用,不产生任何副作用。 + if (dryRun) { + return detaY; + } + if ( state.rowResize.row < state.table.columnHeaderLevelCount || state.rowResize.row >= state.table.rowCount - state.table.bottomFrozenRowCount @@ -67,6 +113,7 @@ export function updateResizeRow(xInTable: number, yInTable: number, state: State //#endregion // stage rerender state.table.scenegraph.updateNextFrame(); + return detaY; } function updateResizeColForRow(detaY: number, state: StateManager) { diff --git a/packages/vtable/src/state/state.ts b/packages/vtable/src/state/state.ts index b6af718f19..640cde7f2c 100644 --- a/packages/vtable/src/state/state.ts +++ b/packages/vtable/src/state/state.ts @@ -37,7 +37,7 @@ import { getIconAndPositionFromTarget } from '../scenegraph/utils/icon'; import type { BaseTableAPI, HeaderData } from '../ts-types/base-table'; import { getBodyHorizontalScrollRange } from '../scenegraph/component/util'; import { debounce } from '../tools/debounce'; -import { updateResizeColumn } from './resize/update-resize-column'; +import { updateResizeColumn, commitPendingColumnResize } from './resize/update-resize-column'; import { changeRadioOrder, setRadioState, syncRadioState } from './radio/radio'; import { changeCheckboxOrder, @@ -48,7 +48,7 @@ import { syncCheckedState, updateHeaderCheckedState } from './checkbox/checkbox'; -import { updateResizeRow } from './resize/update-resize-row'; +import { updateResizeRow, commitPendingRowResize } from './resize/update-resize-row'; import { deleteAllSelectingBorder } from '../scenegraph/select/delete-select-border'; import type { PivotTable } from '../PivotTable'; import { traverseObject } from '../tools/util'; @@ -135,6 +135,8 @@ export class StateManager { x: number; resizing: boolean; isRightFrozen?: boolean; + /** 非实时响应模式下,指示线相对原始列右边界的累计偏移量 */ + pendingDeltaX?: number; }; rowResize: { row: number; @@ -142,6 +144,8 @@ export class StateManager { y: number; resizing: boolean; isBottomFrozen?: boolean; + /** 非实时响应模式下,指示线相对原始行下边界的累计偏移量 */ + pendingDeltaY?: number; }; columnMove: { colSource: number; @@ -295,12 +299,14 @@ export class StateManager { this.columnResize = { col: -1, x: 0, - resizing: false + resizing: false, + pendingDeltaX: 0 }; this.rowResize = { row: -1, y: 0, - resizing: false + resizing: false, + pendingDeltaY: 0 }; this.columnMove = { colSource: -1, @@ -382,12 +388,14 @@ export class StateManager { this.columnResize = { col: -1, x: 0, - resizing: false + resizing: false, + pendingDeltaX: 0 }; this.rowResize = { row: -1, y: 0, - resizing: false + resizing: false, + pendingDeltaY: 0 }; this.columnMove = { colSource: -1, @@ -847,6 +855,9 @@ export class StateManager { } endResizeCol() { + if (this.columnResize.resizing && this.table.internalProps.resizeRealtime === false) { + commitPendingColumnResize(this); + } setTimeout(() => { this.columnResize.resizing = false; }, 0); @@ -860,6 +871,7 @@ export class StateManager { this.columnResize.col = col; this.columnResize.x = x; this.columnResize.isRightFrozen = isRightFrozen; + this.columnResize.pendingDeltaX = 0; this.table.scenegraph.component.showResizeCol(col, y, isRightFrozen); @@ -874,6 +886,9 @@ export class StateManager { } endResizeRow() { + if (this.rowResize.resizing && this.table.internalProps.resizeRealtime === false) { + commitPendingRowResize(this); + } setTimeout(() => { this.rowResize.resizing = false; }, 0); @@ -887,6 +902,7 @@ export class StateManager { this.rowResize.row = row; this.rowResize.y = y; this.rowResize.isBottomFrozen = isBottomFrozen; + this.rowResize.pendingDeltaY = 0; this.table.scenegraph.component.showResizeRow(row, x, isBottomFrozen); diff --git a/packages/vtable/src/ts-types/base-table.ts b/packages/vtable/src/ts-types/base-table.ts index 9c27788d55..32feb5ab1d 100644 --- a/packages/vtable/src/ts-types/base-table.ts +++ b/packages/vtable/src/ts-types/base-table.ts @@ -145,6 +145,9 @@ export interface IBaseTableProtected { rowResizeMode?: 'all' | 'none' | 'header' | 'body'; + /** 拖拽调整行列尺寸时是否实时响应,为 false 时仅拖拽结束后应用尺寸变化 */ + resizeRealtime?: boolean; + columnResizeType?: 'column' | 'indicator' | 'all' | 'indicatorGroup'; rowResizeType?: 'row' | 'indicator' | 'all' | 'indicatorGroup'; @@ -720,6 +723,12 @@ export interface BaseTableConstructorOptions { rowResizeMode?: 'all' | 'none' | 'header' | 'body'; /** 是否禁用双击列边框自动调整列宽 **/ disableDblclickAutoResizeColWidth?: boolean; + /** + * 拖拽调整行列尺寸时是否实时响应。 + * true(默认):拖拽过程中实时更新列宽/行高,与历史行为一致。 + * false:拖拽过程中仅移动指示线,鼠标释放时才应用最终尺寸变化,可缓解大数据量场景下的拖拽卡顿。 + */ + realtime?: boolean; }; dragOrder?: { /** 控制拖拽表头移动位置顺序开关 */