diff --git a/common/changes/@visactor/vtable/feat-listtable-columntree_2025-04-03-10-14.json b/common/changes/@visactor/vtable/feat-listtable-columntree_2025-04-03-10-14.json new file mode 100644 index 0000000000..e83ceffab6 --- /dev/null +++ b/common/changes/@visactor/vtable/feat-listtable-columntree_2025-04-03-10-14.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "feat: list table header support hierarchy \n\n", + "type": "none", + "packageName": "@visactor/vtable" + } + ], + "packageName": "@visactor/vtable", + "email": "892739385@qq.com" +} \ No newline at end of file diff --git a/docs/assets/demo/en/basic-functionality/list-table-header-group-collapse.md b/docs/assets/demo/en/basic-functionality/list-table-header-group-collapse.md new file mode 100644 index 0000000000..782e9b8312 --- /dev/null +++ b/docs/assets/demo/en/basic-functionality/list-table-header-group-collapse.md @@ -0,0 +1,115 @@ +--- +category: examples +group: Basic Features +title: List Table - Header Group Collapse +cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/preview/list-table-header-group.png +link: table-type/list-table +option: ListTable-columns-text#columns +--- + +# List Table - Header Group Collapse + +Configure columns as a nested multi-layer structure to achieve multi-layer header grouping effects. Enable tree-style expansion and collapse functionality through `headerHierarchyType: 'grid-tree'`, and set the default expansion level with `headerExpandLevel`. + +## Key Configurations + +- columns +- `headerHierarchyType` Set hierarchy display to `grid-tree` to enable tree-style expand/collapse +- `headerExpandLevel` Configure default expansion level (defaults to 1) + +## Code demo + +```javascript livedemo template=vtable +let tableInstance; +const records = [ + { + id: 1, + name: 'name.1', + name_1: 'name_1.1', + name_2: 'name_2.1', + name_2_1: 'name_2_1.1', + name_2_2: 'name_2_2.1' + }, + { + id: 2, + name: 'name.2', + name_1: 'name_1.2', + name_2: 'name_2.2', + name_2_1: 'name_2_1.2', + name_2_2: 'name_2_2.2' + }, + { + id: 3, + name: 'name.3', + name_1: 'name_1.3', + name_2: 'name_2.3', + name_2_1: 'name_2_1.3', + name_2_2: 'name_2_2.3' + }, + { + id: 4, + name: 'name.4', + name_1: 'name_1.4', + name_2: 'name_2.4', + name_2_1: 'name_2_1.4', + name_2_2: 'name_2_2.4' + }, + { + id: 5, + name: 'name.5', + name_1: 'name_1.5', + name_2: 'name_2.5', + name_2_1: 'name_2_1.5', + name_2_2: 'name_2_2.5' + } +]; + +const columns = [ + { + field: 'id', + title: 'ID', + width: 100 + }, + { + field: 'name', + title: 'Name', + columns: [ + { + field: 'name_1', + title: 'Name_1', + width: 120 + }, + { + field: 'name_2', + title: 'Name_2', + width: 150, + columns: [ + { + field: 'name_2_1', + title: 'Name_2_1', + width: 150 + }, + { + field: 'name_2_2', + title: 'Name_2_2', + width: 150 + } + ] + } + ] + } +]; + +const option = { + records, + columns, + headerHierarchyType: 'grid-tree', + headerExpandLevel: 3, + widthMode: 'standard', + autoWrapText: true, + autoRowHeight: true, + defaultColWidth: 150 +}; +tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID), option); +window['tableInstance'] = tableInstance; +``` diff --git a/docs/assets/demo/menu.json b/docs/assets/demo/menu.json index 51b1716f84..465824021a 100644 --- a/docs/assets/demo/menu.json +++ b/docs/assets/demo/menu.json @@ -520,6 +520,13 @@ "en": "List Table - Header Group" } }, + { + "path": "list-table-header-group-collapse", + "title": { + "zh": "基本表格表头分组与折叠", + "en": "List Table - Header Group Collapse" + } + }, { "path": "auto-wrap-text", "title": { diff --git a/docs/assets/demo/zh/basic-functionality/list-table-header-group-collapse.md b/docs/assets/demo/zh/basic-functionality/list-table-header-group-collapse.md new file mode 100644 index 0000000000..bf8500cf6a --- /dev/null +++ b/docs/assets/demo/zh/basic-functionality/list-table-header-group-collapse.md @@ -0,0 +1,115 @@ +--- +category: examples +group: Basic Features +title: 基本表格表头分组与折叠 +cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/preview/list-table-header-group.png +link: table-type/list-table +option: ListTable-columns-text#columns +--- + +# 基本表格表头分组与折叠 + +将 columns 配置为嵌套多层结构来实现多层表头分组效果,可通过配置 `headerHierarchyType: 'grid-tree'` 开启树形的展开和折叠,并通过 `headerExpandLevel` 来设置默认展开层级。 + +## 关键配置 + +- columns +- `headerHierarchyType` 将层级展示设置为 `grid-tree`,开启树形的展开和折叠功能 +- `headerExpandLevel` 设置默认展开层级,默认为`1` + +## 代码演示 + +```javascript livedemo template=vtable +let tableInstance; +const records = [ + { + id: 1, + name: 'name.1', + name_1: 'name_1.1', + name_2: 'name_2.1', + name_2_1: 'name_2_1.1', + name_2_2: 'name_2_2.1' + }, + { + id: 2, + name: 'name.2', + name_1: 'name_1.2', + name_2: 'name_2.2', + name_2_1: 'name_2_1.2', + name_2_2: 'name_2_2.2' + }, + { + id: 3, + name: 'name.3', + name_1: 'name_1.3', + name_2: 'name_2.3', + name_2_1: 'name_2_1.3', + name_2_2: 'name_2_2.3' + }, + { + id: 4, + name: 'name.4', + name_1: 'name_1.4', + name_2: 'name_2.4', + name_2_1: 'name_2_1.4', + name_2_2: 'name_2_2.4' + }, + { + id: 5, + name: 'name.5', + name_1: 'name_1.5', + name_2: 'name_2.5', + name_2_1: 'name_2_1.5', + name_2_2: 'name_2_2.5' + } +]; + +const columns = [ + { + field: 'id', + title: 'ID', + width: 100 + }, + { + field: 'name', + title: 'Name', + columns: [ + { + field: 'name_1', + title: 'Name_1', + width: 120 + }, + { + field: 'name_2', + title: 'Name_2', + width: 150, + columns: [ + { + field: 'name_2_1', + title: 'Name_2_1', + width: 150 + }, + { + field: 'name_2_2', + title: 'Name_2_2', + width: 150 + } + ] + } + ] + } +]; + +const option = { + records, + columns, + headerHierarchyType: 'grid-tree', + headerExpandLevel: 3, + widthMode: 'standard', + autoWrapText: true, + autoRowHeight: true, + defaultColWidth: 150 +}; +tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID), option); +window['tableInstance'] = tableInstance; +``` diff --git a/docs/assets/guide/en/table_type/List_table/columns_tree.md b/docs/assets/guide/en/table_type/List_table/columns_tree.md new file mode 100644 index 0000000000..ecbb3f6b3e --- /dev/null +++ b/docs/assets/guide/en/table_type/List_table/columns_tree.md @@ -0,0 +1,114 @@ +## Basic Table Header Grouping and Collapsing + +In this tutorial, we will learn how to implement multi-level header grouping and collapsing in VTable using a tree structure to visualize complex header hierarchies. + +--- + +## Use Cases + +Header grouping and collapsing are suitable for the following scenarios: + +- **Multidimensional Data Analysis**: Combine related fields into logical groups (e.g., "Sales Data" includes sub-columns like "Revenue" and "Profit"). +- **Complex Data Structures**: Tables with explicit hierarchical relationships (e.g., "Region-Province-City" three-level structure). +- **Space Optimization**: Improve readability by collapsing non-critical columns. +- **Dynamic Interaction**: Allow users to expand/collapse specific groups on demand for flexible data exploration. + +--- + +## Implementation Steps + +### 1. Configure Multi-Level Header Structure + +Define header hierarchies using nested structures in the `columns` configuration. Each group adds sub-columns via the `columns` field to form a tree relationship. + +### 2. Enable Tree-Style Collapsing + +Set `headerHierarchyType: 'grid-tree'` to enable interactive tree-style collapsing for headers. + +### 3. Set Default Expansion Level + +Specify the initial expansion level using `headerExpandLevel` (default: `1`, showing only the first-level groups). + +--- + +## Example + +```javascript livedemo template=vtable +const records = [ + { region: 'North', province: 'Province A', city: 'City 1', revenue: 1000, cost: 600 }, + { region: 'North', province: 'Province A', city: 'City 2', revenue: 1500, cost: 800 }, + { region: 'South', province: 'Province B', city: 'City 3', revenue: 2000, cost: 1100 } +]; + +const columns = [ + { + title: 'Region', + field: 'region', + width: 150, + columns: [ + { + title: 'Province', + field: 'province', + width: 150, + columns: [ + { + title: 'City', + field: 'city', + width: 150 + } + ] + } + ] + }, + { + title: 'Financial Metrics', + field: 'metrics', + width: 180, + columns: [ + { title: 'Revenue', field: 'revenue', width: 150 }, + { title: 'Cost', field: 'cost', width: 150 } + ] + } +]; + +const option = { + records, + columns, + headerHierarchyType: 'grid-tree', // Enable tree-style collapsing + headerExpandLevel: 2, // Expand to the second level by default + widthMode: 'standard', + defaultRowHeight: 40 +}; + +const tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID), option); +window.tableInstance = tableInstance; +``` + +--- + +## Advanced Configuration + +### Listen to Collapse Events + +Capture user interactions and execute custom logic: + +```javascript +tableInstance.on(VTable.ListTable.EVENT_TYPE.TREE_HIERARCHY_STATE_CHANGE, args => { + if (args.cellLocation === 'columnHeader') { + console.log('Header state changed:', args); + } +}); +``` + +### Dynamically Update Header States + +Programmatically control header expansion/collapse: + +```javascript +// Toggle the expansion state of a specific column +tableInstance.toggleHierarchyState(col, row); +``` + +--- + +With these configurations, you can quickly implement structured multi-level headers with dynamic interactions, ideal for complex data analysis scenarios. diff --git a/docs/assets/guide/menu.json b/docs/assets/guide/menu.json index c4e7535d66..0577c37e5d 100644 --- a/docs/assets/guide/menu.json +++ b/docs/assets/guide/menu.json @@ -87,6 +87,13 @@ "en": "tree list" } }, + { + "path": "columns_tree", + "title": { + "zh": "表头分组及折叠", + "en": "Columns tree" + } + }, { "path": "group_list", "title": { diff --git a/docs/assets/guide/zh/table_type/List_table/columns_tree.md b/docs/assets/guide/zh/table_type/List_table/columns_tree.md new file mode 100644 index 0000000000..5f32092b7e --- /dev/null +++ b/docs/assets/guide/zh/table_type/List_table/columns_tree.md @@ -0,0 +1,108 @@ +## 基本表格表头分组及折叠 + +在本教程中,我们将学习如何使用 VTable 实现多层表头分组及折叠功能,通过树形结构展示复杂表头层级。 + +## 使用场景 + +表头分组及折叠功能适用于以下场景: + +- **多维数据分析**:需要将多个关联字段合并为逻辑分组(如“销售数据”包含“销售额”“利润”等子列)。 +- **复杂数据结构**:数据表字段具有明确的层级关系(如“地区-省份-城市”三级结构)。 +- **空间优化**:通过折叠功能隐藏非关键列,提升表格可读性。 +- **动态交互**:允许用户按需展开/收起特定分组,灵活查看数据。 + +## 使用方式 + +### 1. 配置多层表头结构 + +在 `columns` 配置中使用嵌套结构定义表头层级。每个分组通过 `columns` 字段添加子列,形成树形关系。 + +### 2. 启用树形折叠功能 + +设置 `headerHierarchyType: 'grid-tree'` 开启表头树形折叠交互。 + +### 3. 设置默认展开层级 + +通过 `headerExpandLevel` 指定初始展开层级(默认值为 `1`,即仅展示第一级分组)。 + +## 示例 + +```javascript livedemo template=vtable +const records = [ + { region: 'North', province: 'A', city: 'City1', revenue: 1000, cost: 600 }, + { region: 'North', province: 'A', city: 'City2', revenue: 1500, cost: 800 }, + { region: 'South', province: 'B', city: 'City3', revenue: 2000, cost: 1100 } +]; + +const columns = [ + { + title: 'Region', + field: 'region', + width: 150, + columns: [ + { + title: 'Province', + field: 'province', + width: 150, + columns: [ + { + title: 'City', + field: 'city', + width: 150 + } + ] + } + ] + }, + { + title: 'Financial Metrics', + field: 'metrics', + width: 180, + columns: [ + { title: 'Revenue', field: 'revenue', width: 150 }, + { title: 'Cost', field: 'cost', width: 150 } + ] + } +]; + +const option = { + records, + columns, + headerHierarchyType: 'grid-tree', // 启用树形折叠 + headerExpandLevel: 2, // 默认展开至第二级 + widthMode: 'standard', + defaultRowHeight: 40 +}; + +const tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID), option); +window.tableInstance = tableInstance; +``` + +--- + +## 高级配置 + +### 监听折叠事件 + +获取用户交互行为并执行自定义逻辑: + +```javascript +tableInstance.on(VTable.ListTable.EVENT_TYPE.TREE_HIERARCHY_STATE_CHANGE, args => { + if (args.cellLocation === 'columnHeader') { + console.log('表头状态变化:', args); + } +}); +``` + +### 动态更新表头状态 + +手动控制表头展开/收起: + +```javascript +// 切换指定列的展开状态 +tableInstance.toggleHierarchyState(col, row); +``` + +--- + +通过上述配置,可快速实现多层表头的结构化展示与动态交互,适用于复杂数据场景下的灵活分析需求。 diff --git a/docs/assets/option/en/table/listTable.md b/docs/assets/option/en/table/listTable.md index 50c73f0741..3ff595bb3d 100644 --- a/docs/assets/option/en/table/listTable.md +++ b/docs/assets/option/en/table/listTable.md @@ -93,6 +93,13 @@ When displayed as a tree structure, the number of levels is expanded by default. Whether nodes at the same level are aligned by text, such as nodes without collapsed expansion icons and nodes with icons. Default is false +## headerHierarchyType('grid-tree') + +Defines the hierarchy display mode for headers. When set to 'grid-tree', it enables tree-style expand/collapse functionality in the header structure. + +## headerExpandLevel(number) + +Sets the initial expansion level of headers. Defaults to 1. ## aggregation(Aggregation|CustomAggregation|Array|Function) diff --git a/docs/assets/option/zh/table/listTable.md b/docs/assets/option/zh/table/listTable.md index 46912dd2f7..42b7aa478b 100644 --- a/docs/assets/option/zh/table/listTable.md +++ b/docs/assets/option/zh/table/listTable.md @@ -90,6 +90,13 @@ SortState { 同层级的结点是否按文字对齐 如没有收起展开图标的节点和有图标的节点文字对齐 默认 false +## headerHierarchyType('grid-tree') + +表头中层级维度结构显示形式,设置为 'grid-tree' 时开启树形结构的展开折叠功能。 + +## headerExpandLevel(number) + +表头初始化展开层数,默认是 1。 ## aggregation(Aggregation|CustomAggregation|Array|Function) diff --git a/packages/vtable/src/ListTable.ts b/packages/vtable/src/ListTable.ts index fc45ffa453..5635ad8a85 100644 --- a/packages/vtable/src/ListTable.ts +++ b/packages/vtable/src/ListTable.ts @@ -36,7 +36,7 @@ import { computeColWidth } from './scenegraph/layout/compute-col-width'; import { computeRowHeight } from './scenegraph/layout/compute-row-height'; import { defaultOrderFn } from './tools/util'; import type { IEditor } from '@visactor/vtable-editors'; -import type { ColumnData, ColumnDefine } from './ts-types/list-table/layout-map/api'; +import type { ColumnData, ColumnDefine, HeaderData } from './ts-types/list-table/layout-map/api'; import { getCellRadioState, setCellRadioState } from './state/radio/radio'; import { cloneDeepSpec } from '@visactor/vutils-extension'; import { getGroupCheckboxState, setCellCheckboxState } from './state/checkbox/checkbox'; @@ -839,6 +839,9 @@ export class ListTable extends BaseTable implements ListTableAPI { * @returns */ getHierarchyState(col: number, row: number) { + if (this.isHeader(col, row)) { + return (this._getHeaderLayoutMap(col, row) as HeaderData)?.hierarchyState; + } if (!this.options.groupBy || (isArray(this.options.groupBy) && this.options.groupBy.length === 0)) { const define = this.getBodyColumnDefine(col, row) as ColumnDefine; if (!define.tree) { @@ -857,6 +860,34 @@ export class ListTable extends BaseTable implements ListTableAPI { toggleHierarchyState(col: number, row: number, recalculateColWidths: boolean = true) { this.stateManager.updateHoverIcon(col, row, undefined, undefined); const hierarchyState = this.getHierarchyState(col, row); + if (this.isHeader(col, row)) { + // 表头的展开和收起 + const headerTreeNode = this.internalProps.layoutMap.getHeader(col, row) as any; + const { hierarchyState: rawHierarchyState, define: columnDefine } = headerTreeNode; + if (![HierarchyState.collapse, HierarchyState.expand].includes(rawHierarchyState) || !columnDefine) { + return; + } + const children = columnDefine.columns; + // 有子节点才需要自动展开和折叠 + if (!!Array.isArray(children) && children.length > 0) { + const hierarchyState = + rawHierarchyState === HierarchyState.expand ? HierarchyState.collapse : HierarchyState.expand; + headerTreeNode.hierarchyState = hierarchyState; + headerTreeNode.define.hierarchyState = hierarchyState; + // 全量更新 + this.updateColumns(this.internalProps.columns); + } + + this.fireListeners(TABLE_EVENT_TYPE.TREE_HIERARCHY_STATE_CHANGE, { + col, + row, + hierarchyState, + originData: headerTreeNode, + cellLocation: this.getCellLocation(col, row) + }); + return; + } + if (hierarchyState === HierarchyState.expand) { this._refreshHierarchyState(col, row, recalculateColWidths); this.fireListeners(TABLE_EVENT_TYPE.TREE_HIERARCHY_STATE_CHANGE, { diff --git a/packages/vtable/src/layout/simple-header-layout.ts b/packages/vtable/src/layout/simple-header-layout.ts index 99c8f05217..40c529e298 100644 --- a/packages/vtable/src/layout/simple-header-layout.ts +++ b/packages/vtable/src/layout/simple-header-layout.ts @@ -2,15 +2,15 @@ import { isValid, merge } from '@visactor/vutils'; import type { ListTable } from '../ListTable'; import { DefaultSparklineSpec } from '../tools/global'; -import type { - CellAddress, - CellRange, - CellLocation, - IListTableCellHeaderPaths, - LayoutObjectId, - AggregationType, - Aggregation, - IRowSeriesNumber +import { + type CellAddress, + type CellRange, + type CellLocation, + type IListTableCellHeaderPaths, + type LayoutObjectId, + type Aggregation, + type IRowSeriesNumber, + HierarchyState } from '../ts-types'; import type { ChartColumnDefine, ColumnsDefine } from '../ts-types/list-table/define'; import type { @@ -66,6 +66,10 @@ export class SimpleHeaderLayoutMap implements LayoutMapAPI { _hasAggregationOnBottomCount: number = 0; /**层级维度结构显示形式 */ rowHierarchyType?: 'grid' | 'tree'; + /** 列表头树形展示模式 */ + columnHierarchyType?: 'grid-tree'; + /** 列表头默认展开层级 */ + columnExpandLevel?: number; // 缓存行号列号对应的cellRange 需要注意当表头位置拖拽后 这个缓存的行列号已不准确 进行重置 _cellRangeMap: Map; //存储单元格的行列号范围 针对解决是否为合并单元格情况 constructor(table: ListTable, columns: ColumnsDefine, showHeader: boolean, hierarchyIndent: number) { @@ -77,7 +81,14 @@ export class SimpleHeaderLayoutMap implements LayoutMapAPI { this._headerCellIds = []; this.hierarchyIndent = hierarchyIndent ?? 20; this.hierarchyTextStartAlignment = table.options.hierarchyTextStartAlignment; - this.columnTree = new DimensionTree(columns as any, { seqId: 0 }, null); //seqId这里没有利用上 所有顺便传了0 + this.columnHierarchyType = table.options.headerHierarchyType; + this.columnExpandLevel = table.options.headerExpandLevel ?? 1; + this.columnTree = new DimensionTree( + columns as any, + { seqId: 0 }, + this.columnHierarchyType ?? null, + this.columnHierarchyType === 'grid-tree' ? this.columnExpandLevel : undefined + ); //seqId这里没有利用上 所有顺便传了0 this._headerObjectsIncludeHided = this._addHeaders(0, columns, []); // this._headerObjectMapIncludeHided = this._headerObjectsIncludeHided.reduce((o, e) => { // o[e.id as number] = e; @@ -989,6 +1000,8 @@ export class SimpleHeaderLayoutMap implements LayoutMapAPI { headerType: hd.headerType ?? 'text', dropDownMenu: hd.dropDownMenu, define: hd, + // 展开/折叠状态 + hierarchyState: (hd as HeaderData).hierarchyState, columnWidthComputeMode: hd.columnWidthComputeMode // iconPositionList:[] }; @@ -1002,7 +1015,9 @@ export class SimpleHeaderLayoutMap implements LayoutMapAPI { } else if (this._headerCellIds[row - 1]) { rowCells[col] = this._headerCellIds[row - 1][col]; } - if (hd.columns) { + // 当前节点是展开状态才需要添加子节点 + const expand = !(hd as HeaderData).hierarchyState || (hd as HeaderData).hierarchyState === HierarchyState.expand; + if (!!hd.columns && !!expand) { const isAllHided = hd.columns.every((c: any) => c.hide); !isAllHided && this._addHeaders( diff --git a/packages/vtable/src/ts-types/events.ts b/packages/vtable/src/ts-types/events.ts index 49509d8caf..acab71eb7f 100644 --- a/packages/vtable/src/ts-types/events.ts +++ b/packages/vtable/src/ts-types/events.ts @@ -203,6 +203,7 @@ export interface TableEventHandlersEventArgumentMap { dimensionInfo?: IDimensionInfo[]; /**整条数据-原始数据 */ originData?: any; + cellLocation?: CellLocation; }; vchart_event_type: { eventName: string; diff --git a/packages/vtable/src/ts-types/table-engine.ts b/packages/vtable/src/ts-types/table-engine.ts index b2da3755c0..ce2a7fdddd 100644 --- a/packages/vtable/src/ts-types/table-engine.ts +++ b/packages/vtable/src/ts-types/table-engine.ts @@ -239,6 +239,10 @@ export interface ListTableConstructorOptions extends BaseTableConstructorOptions hierarchyExpandLevel?: number; /** 同层级的结点是否按文字对齐 如没有收起展开图标的节点和有图标的节点文字对齐 默认false */ hierarchyTextStartAlignment?: boolean; + /** 表头树形展示模式(设置成 'grid-tree' 则支持展开和折叠) */ + headerHierarchyType?: 'grid-tree'; + /** 表头默认展开层级(headerHierarchyType 为 'grid-tree' 时有效) */ + headerExpandLevel?: number; /** 分页配置 */ pagination?: IPagination;