diff --git a/docs/assets/api/en/methods.md b/docs/assets/api/en/methods.md index c52f07c5a8..b553343cf6 100644 --- a/docs/assets/api/en/methods.md +++ b/docs/assets/api/en/methods.md @@ -1649,6 +1649,44 @@ The pixelRatio can be obtained directly from the instance's pixelRatio property. /\*_ Set the canvas pixel ratio _/ setPixelRatio: (pixelRatio: number) => void; +```` + +## expandAllTreeNode(Function) + +Expand all tree nodes (including headers and data rows). + +**ListTable Proprietary** + +```ts + /** + * Expand all tree nodes (including headers and data rows). + */ + expandAllTreeNode(): void +```` + +Usage: + +```ts +// Expand all nodes +tableInstance.expandAllTreeNode(); ``` +## collapseAllTreeNode(Function) + +Collapse all tree nodes (including headers and data rows). + +**ListTable Proprietary** + +```ts + /** + * Collapse all tree nodes (including headers and data rows). + */ + collapseAllTreeNode(): void +``` + +Usage: + +```ts +// Collapse all nodes +tableInstance.collapseAllTreeNode(); ``` diff --git a/docs/assets/api/zh/methods.md b/docs/assets/api/zh/methods.md index c88d90041e..1be70dd20c 100644 --- a/docs/assets/api/zh/methods.md +++ b/docs/assets/api/zh/methods.md @@ -1462,3 +1462,43 @@ setLoadingHierarchyState: (col: number, row: number) => void; /** 设置画布的像素比 */ setPixelRatio: (pixelRatio: number) => void; ``` + +## expandAllTreeNode(Function) + +展开所有树形节点(包括表头和数据行)。 + +**ListTable 专有** + +```ts + /** + * 展开所有树形节点(包括表头和数据行)。 + */ + expandAllTreeNode(): void +``` + +使用: + +```ts +// 展开所有节点 +tableInstance.expandAllTreeNode(); +``` + +## collapseAllTreeNode(Function) + +折叠所有树形节点(包括表头和数据行)。 + +**ListTable 专有** + +```ts + /** + * 折叠所有树形节点(包括表头和数据行)。 + */ + collapseAllTreeNode(): void +``` + +使用: + +```ts +// 折叠所有节点 +tableInstance.collapseAllTreeNode(); +``` diff --git a/packages/vtable/examples/list/list-checkbox-tree-moveRow.ts b/packages/vtable/examples/list/list-checkbox-tree-moveRow.ts new file mode 100644 index 0000000000..30fccf7330 --- /dev/null +++ b/packages/vtable/examples/list/list-checkbox-tree-moveRow.ts @@ -0,0 +1,256 @@ +import * as VTable from '../../src'; +import { bindDebugTool } from '../../src/scenegraph/debug-tool'; +const ListTable = VTable.ListTable; +const CONTAINER_ID = 'vTable'; + +export function createTable() { + const data = [ + { + 类别: '办公用品', + 销售额: '129.696', + 数量: '2', + 利润: '60.704', + children: [ + { + 类别: '信封', // 对应原子类别 + 销售额: '125.44', + 数量: '2', + 利润: '42.56', + children: [ + { + 类别: '黄色信封', + 销售额: '125.44', + 数量: '2', + 利润: '42.56', + children: [ + { + 类别: '黄色大信封', + 销售额: '1375.92', + 数量: '3', + 利润: '550.2' + }, + { + 类别: '黄色小信封', + 销售额: '1375.92', + 数量: '3', + 利润: '550.2' + } + ] + }, + { + 类别: '白色信封', + 销售额: '1375.92', + 数量: '3', + 利润: '550.2' + } + ] + }, + { + 类别: '器具', // 对应原子类别 + 销售额: '1375.92', + 数量: '3', + 利润: '550.2', + children: [ + { + 类别: '订书机', + 销售额: '125.44', + 数量: '2', + 利润: '42.56' + }, + { + 类别: '计算器', + 销售额: '1375.92', + 数量: '3', + 利润: '550.2' + } + ] + } + ] + }, + { + 类别: '技术', + 销售额: '229.696', + 数量: '20', + 利润: '90.704', + children: [ + { + 类别: '设备', // 对应原子类别 + 销售额: '225.44', + 数量: '5', + 利润: '462.56' + }, + { + 类别: '配件', // 对应原子类别 + 销售额: '375.92', + 数量: '8', + 利润: '550.2' + }, + { + 类别: '复印机', // 对应原子类别 + 销售额: '425.44', + 数量: '7', + 利润: '34.56' + }, + { + 类别: '电话', // 对应原子类别 + 销售额: '175.92', + 数量: '6', + 利润: '750.2' + } + ] + }, + { + 类别: '家具', + 销售额: '129.696', + 数量: '2', + 利润: '-60.704', + children: [ + { + 类别: '桌子', // 对应原子类别 + 销售额: '125.44', + 数量: '2', + 利润: '42.56', + children: [ + { + 类别: '黄色桌子', + 销售额: '125.44', + 数量: '2', + 利润: '42.56' + }, + { + 类别: '白色桌子', + 销售额: '1375.92', + 数量: '3', + 利润: '550.2' + } + ] + }, + { + 类别: '椅子', // 对应原子类别 + 销售额: '1375.92', + 数量: '3', + 利润: '550.2', + children: [ + { + 类别: '老板椅', + 销售额: '125.44', + 数量: '2', + 利润: '42.56' + }, + { + 类别: '沙发椅', + 销售额: '1375.92', + 数量: '3', + 利润: '550.2' + } + ] + } + ] + }, + { + 类别: '生活家电(懒加载)', + 销售额: '229.696', + 数量: '20', + 利润: '90.704', + children: true + } + ]; + const option: VTable.ListTableConstructorOptions = { + container: document.getElementById(CONTAINER_ID), + columns: [ + { + field: '类别', + tree: true, + cellType: 'checkbox', + // headerType: 'checkbox', + title: '类别', + width: 'auto', + sort: true + }, + { + cellType: 'radio', + field: '销售额', + title: '销售额', + width: 'auto', + sort: true + // tree: true, + }, + { + field: '利润', + title: '利润', + width: 'auto', + sort: true + } + ], + showFrozenIcon: true, //显示VTable内置冻结列图标 + widthMode: 'standard', + // autoFillHeight: true, + // heightMode: 'adaptive', + allowFrozenColCount: 2, + records: data, + + hierarchyIndent: 20, + hierarchyExpandLevel: 2, + + // sortState: { + // field: '销售额', + // order: 'desc' + // }, + theme: VTable.themes.BRIGHT, + defaultRowHeight: 32, + select: { + disableDragSelect: true + }, + // transpose: true, + rowSeriesNumber: { + dragOrder: true + // enableTreeCheckbox: true + } + // enableCheckboxCascade: true + }; + + const instance = new ListTable(option); + window.tableInstance = instance; + bindDebugTool(instance.scenegraph.stage, { customGrapicKeys: ['col', 'row'] }); + + const { TREE_HIERARCHY_STATE_CHANGE } = VTable.ListTable.EVENT_TYPE; + instance.on(TREE_HIERARCHY_STATE_CHANGE, args => { + // TODO 调用接口插入设置子节点的数据 + if (args.hierarchyState === VTable.TYPES.HierarchyState.expand && !Array.isArray(args.originData.children)) { + const record = args.originData; + instance.setLoadingHierarchyState(args.col, args.row); + setTimeout(() => { + const children = [ + { + 类别: record['类别'] + ' - 分类1', // 对应原子类别 + 销售额: 2, + 数量: 5, + 利润: 4 + }, + { + 类别: record['类别'] + ' - 分类2', // 对应原子类别 + 销售额: 3, + 数量: 8, + 利润: 5 + }, + { + 类别: record['类别'] + ' - 分类3(懒加载)', + 销售额: 4, + 数量: 20, + 利润: 90.704, + children: true + }, + { + 类别: record['类别'] + ' - 分类4', // 对应原子类别 + 销售额: 5, + 数量: 6, + 利润: 7 + } + ]; + instance.setRecordChildren(children, args.col, args.row); + }, 2000); + } + }); + + window.tableInstance = instance; +} diff --git a/packages/vtable/examples/list/list-tree-expandAndCollapseAll.ts b/packages/vtable/examples/list/list-tree-expandAndCollapseAll.ts new file mode 100644 index 0000000000..0822b5afd2 --- /dev/null +++ b/packages/vtable/examples/list/list-tree-expandAndCollapseAll.ts @@ -0,0 +1,256 @@ +import * as VTable from '../../src'; +import { bindDebugTool } from '../../src/scenegraph/debug-tool'; +const ListTable = VTable.ListTable; +const CONTAINER_ID = 'vTable'; + +export function createTable() { + const data = [ + { + 类别: '办公用品', + 销售额: '129.696', + 数量: '2', + 利润: '60.704', + children: [ + { + 类别: '信封', // 对应原子类别 + 销售额: '125.44', + 数量: '2', + 利润: '42.56', + children: [ + { + 类别: '黄色信封', + 销售额: '125.44', + 数量: '2', + 利润: '42.56' + }, + { + 类别: '白色信封', + 销售额: '1375.92', + 数量: '3', + 利润: '550.2' + } + ] + }, + { + 类别: '器具', // 对应原子类别 + 销售额: '1375.92', + 数量: '3', + 利润: '550.2', + children: [ + { + 类别: '订书机', + 销售额: '125.44', + 数量: '2', + 利润: '42.56' + }, + { + 类别: '计算器', + 销售额: '1375.92', + 数量: '3', + 利润: '550.2' + } + ] + } + ] + }, + { + 类别: '技术', + 销售额: '229.696', + 数量: '20', + 利润: '90.704', + children: [ + { + 类别: '设备', // 对应原子类别 + 销售额: '225.44', + 数量: '5', + 利润: '462.56' + }, + { + 类别: '配件', // 对应原子类别 + 销售额: '375.92', + 数量: '8', + 利润: '550.2' + }, + { + 类别: '复印机', // 对应原子类别 + 销售额: '425.44', + 数量: '7', + 利润: '34.56' + }, + { + 类别: '电话', // 对应原子类别 + 销售额: '175.92', + 数量: '6', + 利润: '750.2' + } + ] + }, + { + 类别: '家具', + 销售额: '129.696', + 数量: '2', + 利润: '-60.704', + children: [ + { + 类别: '桌子', // 对应原子类别 + 销售额: '125.44', + 数量: '2', + 利润: '42.56', + children: [ + { + 类别: '黄色桌子', + 销售额: '125.44', + 数量: '2', + 利润: '42.56' + }, + { + 类别: '白色桌子', + 销售额: '1375.92', + 数量: '3', + 利润: '550.2' + } + ] + }, + { + 类别: '椅子', // 对应原子类别 + 销售额: '1375.92', + 数量: '3', + 利润: '550.2', + children: [ + { + 类别: '老板椅', + 销售额: '125.44', + 数量: '2', + 利润: '42.56' + }, + { + 类别: '沙发椅', + 销售额: '1375.92', + 数量: '3', + 利润: '550.2' + } + ] + } + ] + }, + { + 类别: '生活家电(懒加载)', + 销售额: '229.696', + 数量: '20', + 利润: '90.704', + children: true + } + ]; + const option: VTable.ListTableConstructorOptions = { + container: document.getElementById(CONTAINER_ID), + columns: [ + { + field: '类别', + tree: true, + title: '类别', + width: 'auto', + sort: true + }, + { + field: '销售额', + title: '销售额', + width: 'auto', + sort: true + // tree: true, + }, + { + field: '利润', + title: '利润', + width: 'auto', + sort: true + } + ], + showFrozenIcon: true, //显示VTable内置冻结列图标 + widthMode: 'standard', + // autoFillHeight: true, + // heightMode: 'adaptive', + allowFrozenColCount: 2, + records: data, + + hierarchyIndent: 20, + hierarchyExpandLevel: 2, + + sortState: { + field: '销售额', + order: 'desc' + }, + theme: VTable.themes.BRIGHT, + defaultRowHeight: 32, + select: { + disableDragSelect: true + } + }; + + const instance = new ListTable(option); + + // add buttons to expand all and collapse all tree nodes + const buttonsContainer = document.createElement('div'); + buttonsContainer.style.marginBottom = '10px'; + + const expandAllBtn = document.createElement('button'); + expandAllBtn.innerText = '全部展开'; + expandAllBtn.addEventListener('click', () => { + instance.expandAllTreeNode(); + }); + buttonsContainer.appendChild(expandAllBtn); + + const collapseAllBtn = document.createElement('button'); + collapseAllBtn.innerText = '全部折叠'; + collapseAllBtn.style.marginLeft = '10px'; + collapseAllBtn.addEventListener('click', () => { + instance.collapseAllTreeNode(); + }); + buttonsContainer.appendChild(collapseAllBtn); + + document + .getElementById(CONTAINER_ID) + .parentElement.insertBefore(buttonsContainer, document.getElementById(CONTAINER_ID)); + + window.tableInstance = instance; + bindDebugTool(instance.scenegraph.stage, { customGrapicKeys: ['col', 'row'] }); + + const { TREE_HIERARCHY_STATE_CHANGE } = VTable.ListTable.EVENT_TYPE; + instance.on(TREE_HIERARCHY_STATE_CHANGE, args => { + if (args.hierarchyState === VTable.TYPES.HierarchyState.expand && !Array.isArray(args.originData.children)) { + const record = args.originData; + instance.setLoadingHierarchyState(args.col, args.row); + setTimeout(() => { + const children = [ + { + 类别: record['类别'] + ' - 分类1', // 对应原子类别 + 销售额: 2, + 数量: 5, + 利润: 4 + }, + { + 类别: record['类别'] + ' - 分类2', // 对应原子类别 + 销售额: 3, + 数量: 8, + 利润: 5 + }, + { + 类别: record['类别'] + ' - 分类3(懒加载)', + 销售额: 4, + 数量: 20, + 利润: 90.704, + children: true + }, + { + 类别: record['类别'] + ' - 分类4', // 对应原子类别 + 销售额: 5, + 数量: 6, + 利润: 7 + } + ]; + instance.setRecordChildren(children, args.col, args.row); + }, 2000); + } + }); + + window.tableInstance = instance; +} diff --git a/packages/vtable/examples/menu.ts b/packages/vtable/examples/menu.ts index f0ec529f21..860d01aea3 100644 --- a/packages/vtable/examples/menu.ts +++ b/packages/vtable/examples/menu.ts @@ -25,6 +25,10 @@ export const menus = [ { path: 'debug', name: 'scroll' + }, + { + path: 'debug', + name: 'list' } ] }, @@ -63,10 +67,18 @@ export const menus = [ path: 'list', name: 'list-tree' }, + { + path: 'list', + name: 'list-tree-expandAndCollapseAll' + }, { path: 'list', name: 'list-checkbox-tree' }, + { + path: 'list', + name: 'list-checkbox-tree-moveRow' + }, { path: 'list', name: 'list-tree-20000' @@ -242,6 +254,10 @@ export const menus = [ path: 'pivot', name: 'pivot-tree-hide' }, + { + path: 'pivot', + name: 'pivot-moveRow' + }, { path: 'pivot', name: 'pivot-rowSeriesNumber' diff --git a/packages/vtable/examples/pivot/pivot-moveRow.ts b/packages/vtable/examples/pivot/pivot-moveRow.ts new file mode 100644 index 0000000000..e947bf69ec --- /dev/null +++ b/packages/vtable/examples/pivot/pivot-moveRow.ts @@ -0,0 +1,308 @@ +import * as VTable from '../../src'; +import { bindDebugTool } from '../../src/scenegraph/debug-tool'; +const PivotTable = VTable.PivotTable; +const CONTAINER_ID = 'vTable'; + +export function createTable() { + fetch('https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/North_American_Superstore_Pivot2_data.json') + .then(res => res.json()) + .then(data => { + VTable.register.icon('book', { + type: 'svg', //指定svg格式图标,其他还支持path,image,font + svg: ``, + width: 20, + height: 20, + name: 'book', + positionType: VTable.TYPES.IconPosition.inlineEnd, + marginLeft: 2, + marginRight: 0, + visibleTime: 'always', + hover: { + width: 22, + height: 22, + bgColor: 'rgba(22,44,66,0.2)' + }, + tooltip: { + title: '书籍', + placement: VTable.TYPES.Placement.left + } + }); + + const option: VTable.PivotTableConstructorOptions = { + container: document.getElementById(CONTAINER_ID), + records: data, + rowTree: [ + { + dimensionKey: 'Category', + value: 'Furniture', + hierarchyState: 'expand', + children: [ + { + dimensionKey: 'Sub-Category', + value: 'Bookcases', + hierarchyState: 'collapse' + }, + { + dimensionKey: 'Sub-Category', + value: 'Chairs', + hierarchyState: 'collapse' + }, + { + dimensionKey: 'Sub-Category', + value: 'Furnishings' + }, + { + dimensionKey: 'Sub-Category', + value: 'Tables' + } + ] + }, + { + dimensionKey: 'Category', + value: 'Office Supplies', + children: [ + { + dimensionKey: 'Sub-Category', + value: 'Appliances' + }, + { + dimensionKey: 'Sub-Category', + value: 'Art' + }, + { + dimensionKey: 'Sub-Category', + value: 'Binders' + }, + { + dimensionKey: 'Sub-Category', + value: 'Envelopes' + }, + { + dimensionKey: 'Sub-Category', + value: 'Fasteners' + }, + { + dimensionKey: 'Sub-Category', + value: 'Labels' + }, + { + dimensionKey: 'Sub-Category', + value: 'Paper' + }, + { + dimensionKey: 'Sub-Category', + value: 'Storage' + }, + { + dimensionKey: 'Sub-Category', + value: 'Supplies' + } + ] + }, + { + dimensionKey: 'Category', + value: 'Technology', + children: [ + { + dimensionKey: 'Sub-Category', + value: 'Accessories' + }, + { + dimensionKey: 'Sub-Category', + value: 'Copiers' + }, + { + dimensionKey: 'Sub-Category', + value: 'Machines' + }, + { + dimensionKey: 'Sub-Category', + value: 'Phones' + } + ] + } + ], + columnTree: [ + { + dimensionKey: 'Region', + value: 'West', + children: [ + { + value: 'Sales', + indicatorKey: 'Sales' + }, + { + value: 'Profit', + indicatorKey: 'Profit' + } + ] + }, + { + dimensionKey: 'Region', + value: 'South', + children: [ + { + value: 'Sales', + indicatorKey: 'Sales' + }, + { + value: 'Profit', + indicatorKey: 'Profit' + } + ] + }, + { + dimensionKey: 'Region', + value: 'Central', + children: [ + { + value: 'Sales', + indicatorKey: 'Sales' + }, + { + value: 'Profit', + indicatorKey: 'Profit' + } + ] + }, + { + dimensionKey: 'Region', + value: 'East', + children: [ + { + value: 'Sales', + indicatorKey: 'Sales' + }, + { + value: 'Profit', + indicatorKey: 'Profit' + } + ] + } + ], + rows: [ + { + dimensionKey: 'Category', + title: 'Catogery', // Changed from dimensionTitle to title + width: 'auto' + }, + { + dimensionKey: 'Sub-Category', + title: 'Sub-Catogery', // Changed from dimensionTitle to title + width: 'auto' + } + ], + columns: [ + { + dimensionKey: 'Region', + title: 'Region', // Changed from dimensionTitle to title + headerStyle: { + textStick: true + }, + width: 'auto' + } + ], + indicators: [ + { + indicatorKey: 'Sales', + title: 'Sales', // Changed from caption to title + width: 'auto', + showSort: false, + headerStyle: { + fontWeight: 'normal' + }, + format: value => { + if (value != null) { + return '$' + Number(value).toFixed(2); + } + return ''; + }, + style: { + padding: [16, 28, 16, 28], + color(args) { + if (args.dataValue >= 0) { + return 'black'; + } + return 'red'; + } + } + }, + { + indicatorKey: 'Profit', + title: 'Profit', // Changed from caption to title + width: 'auto', + showSort: false, + headerStyle: { + fontWeight: 'normal' + }, + format: value => { + if (value != null) { + return '$' + Number(value).toFixed(2); + } + return ''; + }, + style: { + padding: [16, 28, 16, 28], + color(args) { + if (args.dataValue >= 0) { + return 'black'; + } + return 'red'; + } + } + } + ], + corner: { + titleOnDimension: 'row', + headerStyle: { + textStick: true + } + }, + rowHierarchyType: 'tree', + widthMode: 'standard', + rowHierarchyIndent: 20, + rowExpandLevel: 1, + dragOrder: { + dragHeaderMode: 'all', + validateDragOrderOnEnd(source, target) { + if (source.row === 3) { + return false; + } + return true; + } + }, + rowSeriesNumber: { + enable: true, + title: '行号', + dragOrder: true, // dragOrder for rowSeriesNumber might need specific handling or might not be directly supported in PivotTable in the same way as ListTable. + width: 'auto', + icon: 'book', + headerStyle: { + color: 'black', + bgColor: 'pink' + }, + style: { + color: 'red' + } + }, + theme: VTable.themes.BRIGHT // Added a default theme + }; + + const instance = new PivotTable(option); + window.tableInstance = instance; + bindDebugTool(instance.scenegraph.stage, { customGrapicKeys: ['col', 'row'] }); + + // You might want to add event listeners or other logic here if needed + }) + .catch(error => console.error('Error fetching or processing data:', error)); +} + +// To run this example, you would typically call createTable() +// For example, in your main.ts or an HTML file: +// document.addEventListener('DOMContentLoaded', createTable); +// Or if you have a button to trigger it: +// const button = document.createElement('button'); +// button.innerText = 'Create Pivot Table'; +// button.onclick = createTable; +// document.body.appendChild(button); +// Ensure you have a div with id="vTable" in your HTML:
diff --git a/packages/vtable/src/ListTable.ts b/packages/vtable/src/ListTable.ts index 7faab784f5..7a528651df 100644 --- a/packages/vtable/src/ListTable.ts +++ b/packages/vtable/src/ListTable.ts @@ -241,6 +241,33 @@ export class ListTable extends BaseTable implements ListTableAPI { this.renderAsync(); this.eventManager.updateEventBinder(); } + + /** + * 作为 `updateColumns` 的轻量级替代方案,用于仅需重新创建场景图而无需重新定义列的场景, + * 例如展开/折叠树形节点。此方法避免了 `updateColumns` 中开销较大的 `cloneDeepSpec` 深拷贝操作。 + * + * 注意:此方法与 `updateColumns` 共享部分逻辑。如果将来修改了 `updateColumns`, + * 请务必检查此方法,以确保逻辑一致性,防止出现“逻辑漂移”。 + */ + private _recreateSceneForStateChange(): void { + this.scenegraph.clearCells(); + const oldHoverState = { col: this.stateManager.hover.cellPos.col, row: this.stateManager.hover.cellPos.row }; + + this._hasAutoImageColumn = undefined; + this.refreshHeader(); + if (this.records && checkHasAggregationOnColumnDefine(this.internalProps.columns)) { + this.dataSource.processRecords(this.dataSource.dataSourceObj?.records ?? this.dataSource.dataSourceObj); + } + this.internalProps.useOneRowHeightFillAll = false; + + this.headerStyleCache = new Map(); + this.bodyStyleCache = new Map(); + this.bodyBottomStyleCache = new Map(); + this.scenegraph.createSceneGraph(); + this.stateManager.updateHoverPos(oldHoverState.col, oldHoverState.row); + this.renderAsync(); + this.eventManager.updateEventBinder(); + } /** * 添加列 TODO: 需要优化 这个方法目前直接调用了updateColumns 可以避免调用 做优化性能 * @param column @@ -1557,4 +1584,82 @@ export class ListTable extends BaseTable implements ListTableAPI { this.editorManager.release(); super.release(); } + + /** + * 展开所有树形节点 + */ + expandAllTreeNode(): void { + let stateChanged = false; + + // 展开所有表头节点 + const headerObjects = this.internalProps.layoutMap.headerObjects; + for (const header of headerObjects) { + const headerDefine = header.define as any; + if (headerDefine.columns && headerDefine.columns.length > 0) { + if (headerDefine.hierarchyState !== HierarchyState.expand) { + headerDefine.hierarchyState = HierarchyState.expand; + stateChanged = true; + } + } + } + + // 展开所有数据行节点 + if (this.dataSource && typeof (this.dataSource as any).expandAllNodes === 'function') { + (this.dataSource as any).expandAllNodes(); + stateChanged = true; + } + + // 刷新视图 + if (stateChanged) { + // 使用轻量级的更新管道,而非重量级的 `updateColumns`。 + // 这个新方法会处理表头刷新、场景创建和渲染。 + this._recreateSceneForStateChange(); + + this.fireListeners(TABLE_EVENT_TYPE.TREE_HIERARCHY_STATE_CHANGE, { + col: -1, // 表示非特定单元格操作 + row: -1, + hierarchyState: HierarchyState.expand, + originData: { children: this.records } + }); + } + } + + /** + * 折叠所有树形节点 + */ + collapseAllTreeNode(): void { + let stateChanged = false; + + // 折叠所有表头节点 + const headerObjects = this.internalProps.layoutMap.headerObjects; + for (const header of headerObjects) { + const headerDefine = header.define as any; + if (headerDefine.columns && headerDefine.columns.length > 0) { + if (headerDefine.hierarchyState !== HierarchyState.collapse) { + headerDefine.hierarchyState = HierarchyState.collapse; + stateChanged = true; + } + } + } + + // 折叠所有数据行节点 + if (this.dataSource && typeof (this.dataSource as any).collapseAllNodes === 'function') { + (this.dataSource as any).collapseAllNodes(); + stateChanged = true; + } + + // 刷新视图 + if (stateChanged) { + // 使用轻量级的更新管道,而非重量级的 `updateColumns`。 + // 这个新方法会处理表头刷新、场景创建和渲染。 + this._recreateSceneForStateChange(); + + this.fireListeners(TABLE_EVENT_TYPE.TREE_HIERARCHY_STATE_CHANGE, { + col: -1, + row: -1, + hierarchyState: HierarchyState.collapse, + originData: { children: this.records } + }); + } + } } diff --git a/packages/vtable/src/data/DataSource.ts b/packages/vtable/src/data/DataSource.ts index 1f325e12ed..0411a6969a 100644 --- a/packages/vtable/src/data/DataSource.ts +++ b/packages/vtable/src/data/DataSource.ts @@ -1600,6 +1600,62 @@ export class DataSource extends EventTarget implements DataSourceAPI { } return childTotalLength; } + + private _setNodeStateRecursive(node: any, targetState: HierarchyState): void { + if (!node) { + return; + } + const children = (node as any).filteredChildren ?? (node as any).children; + // 仅为具有子节点(即可以展开/折叠)的节点设置状态 + if (children && (Array.isArray(children) ? children.length > 0 : children === true)) { + (node as any).hierarchyState = targetState; + } + + // 如果子节点作为数组存在,则递归应用于子节点 + if (children && Array.isArray(children)) { + for (const child of children) { + this._setNodeStateRecursive(child, targetState); + } + } + } + + expandAllNodes(): void { + if (Array.isArray(this._source)) { + for (const rootNode of this._source) { + this._setNodeStateRecursive(rootNode, HierarchyState.expand); + } + this.hasHierarchyStateExpand = true; + this.clearSortedIndexMap(); + this.restoreTreeHierarchyState(); + + if (this.lastSortStates && this.lastSortStates.length > 0) { + this.sort(this.lastSortStates); // sort 方法内部会调用 updatePagerData + } else { + this.updatePagerData(); + } + } else { + console.warn('DataSource._source is not an array, cannot expand all nodes.'); + } + } + + collapseAllNodes(): void { + if (Array.isArray(this._source)) { + for (const rootNode of this._source) { + this._setNodeStateRecursive(rootNode, HierarchyState.collapse); + } + // hasHierarchyStateExpand 将由 restoreTreeHierarchyState 正确更新 + this.clearSortedIndexMap(); + this.restoreTreeHierarchyState(); + + if (this.lastSortStates && this.lastSortStates.length > 0) { + this.sort(this.lastSortStates); // sort 方法内部会调用 updatePagerData + } else { + this.updatePagerData(); + } + } else { + console.warn('DataSource._source is not an array, cannot collapse all nodes.'); + } + } } /** diff --git a/packages/vtable/src/state/cell-move/index.ts b/packages/vtable/src/state/cell-move/index.ts index be41f3dbd4..9899c593bc 100644 --- a/packages/vtable/src/state/cell-move/index.ts +++ b/packages/vtable/src/state/cell-move/index.ts @@ -205,14 +205,29 @@ export function endMoveCol(state: StateManager): boolean { } } if ( - !(state.table as ListTable).transpose && + state.table.isListTable() && (state.table.internalProps.layoutMap as SimpleHeaderLayoutMap).isSeriesNumberInBody( state.columnMove.colSource, state.columnMove.rowSource ) ) { + let sourceRecordPath; + let targetRecordPath; + + if (!(state.table as ListTable).transpose) { + sourceRecordPath = (state.table as ListTable).getRecordIndexByCell(0, moveContext.sourceIndex); + targetRecordPath = (state.table as ListTable).getRecordIndexByCell(0, moveContext.targetIndex); + } else { + sourceRecordPath = (state.table as ListTable).getRecordIndexByCell(moveContext.sourceIndex, 0); + targetRecordPath = (state.table as ListTable).getRecordIndexByCell(moveContext.targetIndex, 0); + } + state.table.changeRecordOrder(moveContext.sourceIndex, moveContext.targetIndex); - state.changeCheckboxAndRadioOrder(moveContext.sourceIndex, moveContext.targetIndex); + + // 对于checkbox,使用真实完整的路径 + state.changeCheckboxOrder(sourceRecordPath, targetRecordPath); + + state.changeRadioOrder(moveContext.sourceIndex, moveContext.targetIndex); } // clear columns width and rows height cache if (moveContext.moveType === 'column') { diff --git a/packages/vtable/src/state/checkbox/checkbox.ts b/packages/vtable/src/state/checkbox/checkbox.ts index 8c70bee4ca..97fd0f3ca0 100644 --- a/packages/vtable/src/state/checkbox/checkbox.ts +++ b/packages/vtable/src/state/checkbox/checkbox.ts @@ -321,67 +321,128 @@ export function setCellCheckboxStateByAttribute( } } -export function changeCheckboxOrder(sourceIndex: number, targetIndex: number, state: StateManager) { - const { checkedState, table } = state; - let source; - let target; - if (table.internalProps.transpose) { - sourceIndex = table.getRecordShowIndexByCell(sourceIndex, 0); - targetIndex = table.getRecordShowIndexByCell(targetIndex, 0); - } else { - // sourceIndex = table.getRecordShowIndexByCell(0, sourceIndex); - // targetIndex = table.getRecordShowIndexByCell(0, targetIndex); +export function changeCheckboxOrder( + sourceRecordPath: number | number[], + targetRecordPath: number | number[], + state: StateManager +) { + const { checkedState } = state; - source = table.isPivotTable() ? undefined : (table as any).getRecordIndexByCell(0, sourceIndex); - target = table.isPivotTable() ? undefined : (table as any).getRecordIndexByCell(0, targetIndex); - } + // 处理组级别移动(单个数字或数组的第一个元素) + if ( + (!isArray(sourceRecordPath) && !isArray(targetRecordPath)) || + (isArray(sourceRecordPath) && + isArray(targetRecordPath) && + sourceRecordPath.length === 1 && + targetRecordPath.length === 1) + ) { + const sourceIndex = isArray(sourceRecordPath) ? sourceRecordPath[0] : sourceRecordPath; + const targetIndex = isArray(targetRecordPath) ? targetRecordPath[0] : targetRecordPath; + + const allKeys = Array.from(checkedState.keys()); + + // 将键转换为字符串进行比较 + const sourcePrefix = String(sourceIndex); + const sourceKeys = allKeys.filter(key => String(key).startsWith(sourcePrefix)); + + const sourceValues = sourceKeys.map(key => ({ + key, + value: checkedState.get(key) + })); - if (isNumber(source) && isNumber(target)) { - sourceIndex = source; - targetIndex = target; if (sourceIndex > targetIndex) { - const sourceRecord = checkedState.get(sourceIndex.toString()); - for (let i = sourceIndex; i > targetIndex; i--) { - // checkedState[i] = checkedState[i - 1]; - checkedState.set(i.toString(), checkedState.get((i - 1).toString())); + for (let i = sourceIndex - 1; i >= targetIndex; i--) { + const keysToMove = allKeys.filter(key => String(key).startsWith(String(i))); + + keysToMove.forEach(oldKey => { + const oldKeyStr = String(oldKey); + const newKey = oldKeyStr.replace(new RegExp(`^${i}([,]|$)`), `${i + 1}$1`); + const value = checkedState.get(oldKey); + checkedState.set(newKey, value); + }); } - // checkedState[targetIndex] = sourceRecord; - checkedState.set(targetIndex.toString(), sourceRecord); } else if (sourceIndex < targetIndex) { - const sourceRecord = checkedState.get(sourceIndex.toString()); - for (let i = sourceIndex; i < targetIndex; i++) { - // checkedState[i] = checkedState[i + 1]; - checkedState.set(i.toString(), checkedState.get((i + 1).toString())); + for (let i = sourceIndex + 1; i <= targetIndex; i++) { + const keysToMove = allKeys.filter(key => String(key).startsWith(String(i))); + keysToMove.forEach(oldKey => { + const oldKeyStr = String(oldKey); + const newKey = oldKeyStr.replace(new RegExp(`^${i}([,]|$)`), `${i - 1}$1`); + const value = checkedState.get(oldKey); + checkedState.set(newKey, value); + }); } - // checkedState[targetIndex] = sourceRecord; - checkedState.set(targetIndex.toString(), sourceRecord); } - } else if (isArray(source) && isArray(target)) { - sourceIndex = source[source.length - 1]; - targetIndex = target[target.length - 1]; - if (sourceIndex > targetIndex) { - const sourceRecord = checkedState.get(source.toString()); - for (let i = sourceIndex; i > targetIndex; i--) { - const now = [...source]; - now[now.length - 1] = i; - const last = [...source]; - last[last.length - 1] = i - 1; - checkedState.set(now.toString(), checkedState.get(last.toString())); + + sourceValues.forEach(({ key, value }) => { + const keyStr = String(key); + const newKey = keyStr.replace(new RegExp(`^${sourceIndex}([,]|$)`), `${targetIndex}$1`); + checkedState.set(newKey, value); + }); + + // 处理同一层级内的移动 + } else if ( + isArray(sourceRecordPath) && + isArray(targetRecordPath) && + sourceRecordPath.length === targetRecordPath.length && + sourceRecordPath.slice(0, -1).join(',') === targetRecordPath.slice(0, -1).join(',') + ) { + const parentPath = sourceRecordPath.slice(0, -1).join(','); + const sourceItemIndex = sourceRecordPath[sourceRecordPath.length - 1]; + const targetItemIndex = targetRecordPath[targetRecordPath.length - 1]; + + const allKeys = Array.from(checkedState.keys()); + + // 将键转换为字符串进行比较 + const sourcePrefix = parentPath ? `${parentPath},${sourceItemIndex}` : `${sourceItemIndex}`; + const sourceKeys = allKeys.filter(key => String(key).startsWith(sourcePrefix)); + + const sourceValues = sourceKeys.map(key => ({ + key, + value: checkedState.get(key) + })); + + if (sourceItemIndex > targetItemIndex) { + for (let i = sourceItemIndex - 1; i >= targetItemIndex; i--) { + const currentPrefix = parentPath ? `${parentPath},${i}` : `${i}`; + const keysToMove = allKeys.filter(key => String(key).startsWith(currentPrefix)); + + keysToMove.forEach(oldKey => { + const oldKeyStr = String(oldKey); + const newKey = parentPath + ? oldKeyStr.replace(new RegExp(`^${parentPath},${i}([,]|$)`), `${parentPath},${i + 1}$1`) + : oldKeyStr.replace(new RegExp(`^${i}([,]|$)`), `${i + 1}$1`); + const value = checkedState.get(oldKey); + checkedState.set(newKey, value); + }); } - // checkedState[targetIndex] = sourceRecord; - checkedState.set(target.toString(), sourceRecord); - } else if (sourceIndex < targetIndex) { - const sourceRecord = checkedState.get(source.toString()); - for (let i = sourceIndex; i < targetIndex; i++) { - const now = [...source]; - now[now.length - 1] = i; - const next = [...source]; - next[next.length - 1] = i + 1; - checkedState.set(now.toString(), checkedState.get(next.toString())); + } else if (sourceItemIndex < targetItemIndex) { + for (let i = sourceItemIndex + 1; i <= targetItemIndex; i++) { + const currentPrefix = parentPath ? `${parentPath},${i}` : `${i}`; + const keysToMove = allKeys.filter(key => String(key).startsWith(currentPrefix)); + + keysToMove.forEach(oldKey => { + const newKey = parentPath + ? String(oldKey).replace(new RegExp(`^${parentPath},${i}([,]|$)`), `${parentPath},${i - 1}$1`) + : String(oldKey).replace(new RegExp(`^${i}([,]|$)`), `${i - 1}$1`); + const value = checkedState.get(oldKey); + checkedState.set(newKey, value); + }); } - // checkedState[targetIndex] = sourceRecord; - checkedState.set(target.toString(), sourceRecord); } + + sourceValues.forEach(({ key, value }) => { + const newKey = parentPath + ? String(key).replace( + new RegExp(`^${parentPath},${sourceItemIndex}([,]|$)`), + `${parentPath},${targetItemIndex}$1` + ) + : String(key).replace(new RegExp(`^${sourceItemIndex}([,]|$)`), `${targetItemIndex}$1`); + checkedState.set(newKey, value); + }); + } else { + // 不同层级的移动暂不支持 + console.warn('不支持跨层级的移动'); + return; } } diff --git a/packages/vtable/src/state/state.ts b/packages/vtable/src/state/state.ts index 55f6e07538..a002a9770e 100644 --- a/packages/vtable/src/state/state.ts +++ b/packages/vtable/src/state/state.ts @@ -1769,10 +1769,13 @@ export class StateManager { return syncRadioState(col, row, field, radioType, indexInCell, isChecked, this); } - changeCheckboxAndRadioOrder(sourceIndex: number, targetIndex: number) { + changeCheckboxOrder(sourceRecordPath: number | number[], targetRecordPath: number | number[]) { if (this.checkedState.size) { - changeCheckboxOrder(sourceIndex, targetIndex, this); + changeCheckboxOrder(sourceRecordPath, targetRecordPath, this); } + } + + changeRadioOrder(sourceIndex: number, targetIndex: number) { if (this.radioState.length) { changeRadioOrder(sourceIndex, targetIndex, this); }