From f67f76b241894d4743cdc045d169e8419434f883 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Mon, 27 Jul 2026 18:26:58 +0800 Subject: [PATCH 1/3] fix: keep master detail expandable after setRecords --- ...r-detail-set-records_2026-07-27-19-20.json | 11 +++ .../issue-5185-set-records-expand.ts | 97 +++++++++++++++++++ packages/vtable-plugins/demo/menu.ts | 4 + .../src/master-detail-plugin/config.ts | 6 +- .../src/master-detail-plugin/core.ts | 28 +++++- .../table-api-extensions.ts | 23 +++++ 6 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 common/changes/@visactor/vtable-plugins/fix-issue-5185-master-detail-set-records_2026-07-27-19-20.json create mode 100644 packages/vtable-plugins/demo/master-detail-plugin/issue-5185-set-records-expand.ts diff --git a/common/changes/@visactor/vtable-plugins/fix-issue-5185-master-detail-set-records_2026-07-27-19-20.json b/common/changes/@visactor/vtable-plugins/fix-issue-5185-master-detail-set-records_2026-07-27-19-20.json new file mode 100644 index 0000000000..f128fbcd56 --- /dev/null +++ b/common/changes/@visactor/vtable-plugins/fix-issue-5185-master-detail-set-records_2026-07-27-19-20.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable-plugins", + "comment": "fix: keep master detail expandable after list table setRecords", + "type": "patch" + } + ], + "packageName": "@visactor/vtable-plugins", + "email": "892739385@qq.com" +} diff --git a/packages/vtable-plugins/demo/master-detail-plugin/issue-5185-set-records-expand.ts b/packages/vtable-plugins/demo/master-detail-plugin/issue-5185-set-records-expand.ts new file mode 100644 index 0000000000..86c27fe46a --- /dev/null +++ b/packages/vtable-plugins/demo/master-detail-plugin/issue-5185-set-records-expand.ts @@ -0,0 +1,97 @@ +import * as VTable from '@visactor/vtable'; +import { MasterDetailPlugin } from '../../src'; + +const CONTAINER_ID = 'vTable'; + +const columns: VTable.ColumnsDefine = [ + { field: 'name', title: 'Name', width: 180 }, + { field: 'department', title: 'Department', width: 160 }, + { field: 'status', title: 'Status', width: 120 } +]; + +const detailColumns: VTable.ColumnsDefine = [ + { field: 'project', title: 'Project', width: 180 }, + { field: 'role', title: 'Role', width: 140 } +]; + +const createRecords = (prefix: string) => [ + { + id: `${prefix}-1`, + name: `${prefix} Employee 1`, + department: 'Engineering', + status: 'Active', + children: [ + { project: `${prefix} Project A`, role: 'Owner' }, + { project: `${prefix} Project B`, role: 'Reviewer' } + ] + }, + { + id: `${prefix}-2`, + name: `${prefix} Employee 2`, + department: 'Design', + status: 'Active', + children: [{ project: `${prefix} Project C`, role: 'Designer' }] + } +]; + +const createStatusBar = () => { + const container = document.getElementById(CONTAINER_ID)!; + const status = document.createElement('div'); + status.id = 'issue5185Status'; + status.style.cssText = 'height: 32px; line-height: 32px; font-size: 13px; color: #333;'; + status.textContent = 'Click "Check setRecords expand" to verify issue #5185.'; + + const button = document.createElement('button'); + button.textContent = 'Check setRecords expand'; + button.style.cssText = 'margin: 0 0 8px 8px;'; + button.onclick = () => checkSetRecordsExpand(); + + container.parentElement?.insertBefore(status, container); + status.appendChild(button); +}; + +const getSubTableCount = (tableInstance: VTable.ListTable) => + ((tableInstance as any).internalProps.subTableInstances as Map)?.size ?? 0; + +const checkSetRecordsExpand = () => { + const tableInstance = (window as any).tableInstance as VTable.ListTable; + const status = document.getElementById('issue5185Status')!; + + tableInstance.setRecords(createRecords('After')); + tableInstance.toggleHierarchyState(0, tableInstance.columnHeaderLevelCount); + + const subTableCount = getSubTableCount(tableInstance); + const firstRecord = tableInstance.records?.[0] as any; + const pass = subTableCount > 0 && firstRecord?.hierarchyState === VTable.TYPES.HierarchyState.expand; + + status.textContent = `${pass ? 'PASS' : 'FAIL'} | subTableCount=${subTableCount}, hierarchyState=${ + firstRecord?.hierarchyState + }`; + return status.textContent; +}; + +export function createTable() { + const option: VTable.ListTableConstructorOptions = { + records: createRecords('Initial'), + columns, + widthMode: 'standard', + defaultRowHeight: 36, + plugins: [ + new MasterDetailPlugin({ + detailTableOptions: { + columns: detailColumns, + heightMode: 'autoHeight', + defaultRowHeight: 30, + style: { + height: 90 + } + } + }) + ] + }; + + createStatusBar(); + const tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID)!, option); + (window as any).tableInstance = tableInstance; + (window as any).issue5185Run = checkSetRecordsExpand; +} diff --git a/packages/vtable-plugins/demo/menu.ts b/packages/vtable-plugins/demo/menu.ts index 8c16ad6b42..2c641801de 100644 --- a/packages/vtable-plugins/demo/menu.ts +++ b/packages/vtable-plugins/demo/menu.ts @@ -131,6 +131,10 @@ export const menus = [ path: 'master-detail-plugin', name: 'master-detail-plugin9' }, + { + path: 'master-detail-plugin', + name: 'issue-5185-set-records-expand' + }, { menu: 'pivot-plugin', children: [ diff --git a/packages/vtable-plugins/src/master-detail-plugin/config.ts b/packages/vtable-plugins/src/master-detail-plugin/config.ts index fe37a9d5b0..3f5552ed7e 100644 --- a/packages/vtable-plugins/src/master-detail-plugin/config.ts +++ b/packages/vtable-plugins/src/master-detail-plugin/config.ts @@ -161,7 +161,7 @@ export class ConfigManager { /** * 处理记录的层级状态 */ - private processRecordsHierarchyStates(records: unknown[]): void { + processRecordsHierarchyStates(records: unknown[], expandInitialRows: boolean = true): void { const HierarchyState = VTable.TYPES.HierarchyState; // 兼容处理headerExpandLevel const hierarchyExpandLevel = this.table.options.hierarchyExpandLevel || this.table.options.headerExpandLevel; @@ -193,7 +193,9 @@ export class ConfigManager { }); }; processRecords(records); - this.performInitialExpansion(); + if (expandInitialRows) { + this.performInitialExpansion(); + } } /** diff --git a/packages/vtable-plugins/src/master-detail-plugin/core.ts b/packages/vtable-plugins/src/master-detail-plugin/core.ts index cea11d6ff5..1a51eb54d7 100644 --- a/packages/vtable-plugins/src/master-detail-plugin/core.ts +++ b/packages/vtable-plugins/src/master-detail-plugin/core.ts @@ -171,13 +171,39 @@ export class MasterDetailPlugin implements pluginsDefinition.IVTablePlugin { collapseRow: (rowIndex: number) => this.collapseRow(rowIndex), updateSubTablePositions: () => this.subTableManager.recalculateAllSubTablePositions(), updateRowHeightForExpand: (rowIndex: number, deltaHeight: number) => - this.updateRowHeightForExpand(rowIndex, deltaHeight) + this.updateRowHeightForExpand(rowIndex, deltaHeight), + resetMasterDetailStateBeforeSetRecords: () => this.resetMasterDetailStateBeforeSetRecords() }); // 执行API扩展 this.tableAPIExtensions.extendTableAPI(); } + /** + * setRecords 前清理旧主从表状态,避免新数据复用旧展开行和子表实例 + */ + private resetMasterDetailStateBeforeSetRecords(): void { + const internalProps = getInternalProps(this.table); + const expandedRows = [...this.eventManager.getExpandedRows()]; + expandedRows.forEach(rowIndex => { + try { + this.collapseRowToNoRealRecordIndex(rowIndex); + } catch (error) { + console.warn(`Failed to collapse master detail row ${rowIndex} before setRecords:`, error); + } + }); + + const subTableRowIndices = Array.from(internalProps.subTableInstances?.keys() ?? []); + subTableRowIndices.forEach(bodyRowIndex => { + this.subTableManager.removeSubTable(bodyRowIndex); + }); + + internalProps.expandedRecordIndices?.splice(0); + internalProps.originalRowHeights?.clear(); + internalProps.subTableCheckboxStates?.clear(); + this.eventManager.setExpandedRows([]); + } + /** * 在 adaptive 处理后更新原始高度缓存 */ diff --git a/packages/vtable-plugins/src/master-detail-plugin/table-api-extensions.ts b/packages/vtable-plugins/src/master-detail-plugin/table-api-extensions.ts index 33b1ef3fb2..4d73c6b406 100644 --- a/packages/vtable-plugins/src/master-detail-plugin/table-api-extensions.ts +++ b/packages/vtable-plugins/src/master-detail-plugin/table-api-extensions.ts @@ -23,6 +23,7 @@ export class TableAPIExtensions { private originalUpdateChartSizeForResizeColWidth?: (col: number) => void; private originalUpdateChartSizeForResizeRowHeight?: (row: number) => void; private originalUpdateRowHeight?: (row: number, detaY: number, skipTableHeightMap?: boolean) => void; + private originalSetRecords?: (records: Array, option?: Parameters[1]) => void; private originalGetResizeColAt?: ( abstractX: number, abstractY: number, @@ -50,6 +51,7 @@ export class TableAPIExtensions { collapseRow: (rowIndex: number) => void; updateSubTablePositions: () => void; updateRowHeightForExpand: (rowIndex: number, deltaHeight: number) => void; + resetMasterDetailStateBeforeSetRecords: () => void; }; constructor( @@ -67,6 +69,7 @@ export class TableAPIExtensions { collapseRow: (rowIndex: number) => void; updateSubTablePositions: () => void; updateRowHeightForExpand: (rowIndex: number, deltaHeight: number) => void; + resetMasterDetailStateBeforeSetRecords: () => void; } ) { this.table = table; @@ -140,6 +143,8 @@ export class TableAPIExtensions { this.extendUpdateRowHeight(); // 处理展开行的列宽调整检测 this.extendGetResizeColAt(); + // 处理 setRecords 后主从表层级状态重建 + this.extendSetRecords(); } /** @@ -197,6 +202,24 @@ export class TableAPIExtensions { }; } + /** + * 扩展 setRecords 方法 + */ + private extendSetRecords(): void { + const table = this.table; + this.originalSetRecords = table.setRecords.bind(table); + table.setRecords = (records: Array, option?: Parameters[1]) => { + this.callbacks.resetMasterDetailStateBeforeSetRecords(); + if (Array.isArray(records)) { + this.configManager.processRecordsHierarchyStates(records, false); + } + this.originalSetRecords?.(records, option); + if (Array.isArray(records)) { + this.configManager.processRecordsHierarchyStates(records); + } + }; + } + /** * 扩展 updateResizeRow 方法 */ From 7fc98269ac1e7c76c135052980c31724b185dae2 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 28 Jul 2026 10:57:27 +0800 Subject: [PATCH 2/3] fix(plugins): cancel stale master detail expansion --- .../src/master-detail-plugin/config.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/vtable-plugins/src/master-detail-plugin/config.ts b/packages/vtable-plugins/src/master-detail-plugin/config.ts index 3f5552ed7e..cef00ebb1a 100644 --- a/packages/vtable-plugins/src/master-detail-plugin/config.ts +++ b/packages/vtable-plugins/src/master-detail-plugin/config.ts @@ -7,6 +7,7 @@ import type { DetailTableOptions, MasterDetailPluginOptions } from './types'; export class ConfigManager { private expandRowCallback?: (rowIndex: number) => void; private childrenKey: string; + private expansionVersion = 0; constructor(private pluginOptions: MasterDetailPluginOptions, private table: VTable.ListTable) { this.childrenKey = pluginOptions.childrenKey || 'children'; @@ -162,6 +163,7 @@ export class ConfigManager { * 处理记录的层级状态 */ processRecordsHierarchyStates(records: unknown[], expandInitialRows: boolean = true): void { + const expansionVersion = ++this.expansionVersion; const HierarchyState = VTable.TYPES.HierarchyState; // 兼容处理headerExpandLevel const hierarchyExpandLevel = this.table.options.hierarchyExpandLevel || this.table.options.headerExpandLevel; @@ -194,7 +196,7 @@ export class ConfigManager { }; processRecords(records); if (expandInitialRows) { - this.performInitialExpansion(); + this.performInitialExpansion(expansionVersion); } } @@ -202,7 +204,7 @@ export class ConfigManager { * 遍历所有记录,根据 hierarchyState 状态执行初始展开 * 与VTable的异步CellGroup创建过程同步,在每个CellGroup创建后检查是否需要展开 */ - private performInitialExpansion(): void { + private performInitialExpansion(expansionVersion: number): void { // 获取需要展开的记录索引列表 const expandableRecords = this.getExpandableRecords(); if (expandableRecords.length === 0) { @@ -210,7 +212,7 @@ export class ConfigManager { } // 开始异步展开过程,与VTable的渲染频率同步 - this.startAsyncExpansion(expandableRecords); + this.startAsyncExpansion(expandableRecords, expansionVersion); } /** @@ -270,11 +272,16 @@ export class ConfigManager { * 开始异步展开过程,与VTable的异步渲染同步 */ private startAsyncExpansion( - expandableRecords: Array<{ recordIndex: number; actualRowIndex: number; record: unknown }> + expandableRecords: Array<{ recordIndex: number; actualRowIndex: number; record: unknown }>, + expansionVersion: number ): void { let currentIndex = 0; const processNextExpansion = (): void => { + if (expansionVersion !== this.expansionVersion) { + return; + } + if (currentIndex >= expandableRecords.length) { return; // 所有展开操作完成 } From a95261f8cbdc71128cbd8a0b86ec77639f4b8a2e Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 28 Jul 2026 11:47:26 +0800 Subject: [PATCH 3/3] fix(plugins): invalidate expansion on release --- packages/vtable-plugins/src/master-detail-plugin/config.ts | 1 + packages/vtable-plugins/src/master-detail-plugin/core.ts | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/vtable-plugins/src/master-detail-plugin/config.ts b/packages/vtable-plugins/src/master-detail-plugin/config.ts index cef00ebb1a..36d98c12f7 100644 --- a/packages/vtable-plugins/src/master-detail-plugin/config.ts +++ b/packages/vtable-plugins/src/master-detail-plugin/config.ts @@ -346,6 +346,7 @@ export class ConfigManager { * 释放所有资源和引用 */ release(): void { + this.expansionVersion++; this.isRowExpanded = () => false; // 清理对表格的引用 (this as unknown as { table: VTable.ListTable | null }).table = null; diff --git a/packages/vtable-plugins/src/master-detail-plugin/core.ts b/packages/vtable-plugins/src/master-detail-plugin/core.ts index 1a51eb54d7..2d37acdac3 100644 --- a/packages/vtable-plugins/src/master-detail-plugin/core.ts +++ b/packages/vtable-plugins/src/master-detail-plugin/core.ts @@ -292,9 +292,6 @@ export class MasterDetailPlugin implements pluginsDefinition.IVTablePlugin { this.updateRowHeightForExpand(rowIndex, deltaHeight); this.table.scenegraph.updateContainerHeight(rowIndex, deltaHeight); internalProps._heightResizedRowMap.add(rowIndex); - if (rowIndex === 96) { - console.log('wokk'); - } this.subTableManager.renderSubTable(bodyRowIndex, childrenData, (record, bodyRowIndex) => this.configManager.getDetailConfigForRecord(record, bodyRowIndex) );