diff --git a/common/changes/@visactor/vtable/fix-fix-tree-checkbox-state-sync_2025-03-05-13-32.json b/common/changes/@visactor/vtable/fix-fix-tree-checkbox-state-sync_2025-03-05-13-32.json new file mode 100644 index 0000000000..828f82e842 --- /dev/null +++ b/common/changes/@visactor/vtable/fix-fix-tree-checkbox-state-sync_2025-03-05-13-32.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "fix: fix tree checkbox state update problem", + "type": "none" + } + ], + "packageName": "@visactor/vtable" +} \ No newline at end of file diff --git a/packages/vtable/examples/list/list-tree-checkbox.ts b/packages/vtable/examples/list/list-tree-checkbox.ts index b716398a47..67fba2e6fb 100644 --- a/packages/vtable/examples/list/list-tree-checkbox.ts +++ b/packages/vtable/examples/list/list-tree-checkbox.ts @@ -10,113 +10,317 @@ VTable.register.editor('input', input_editor); const titleColorPool = ['#3370ff', '#34c724', '#ff9f1a', '#ff4050', '#1f2329']; export function createTable() { - let tableInstance; - fetch('https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/North_American_Superstore_data.json') - .then(res => res.json()) - .then(data => { - const columns = [ - { - field: 'Order ID', - title: 'Order ID', - width: 'auto', - sort: true - }, - { - field: 'Customer ID', - title: 'Customer ID', - width: 'auto' - // cellType: 'checkbox' - }, - { - field: 'Product Name', - title: 'Product Name', - width: 'auto' - }, + // let tableInstance; + // fetch('https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/North_American_Superstore_data.json') + // .then(res => res.json()) + // .then(data => { + // const columns = [ + // { + // field: 'Order ID', + // title: 'Order ID', + // width: 'auto', + // sort: true + // }, + // { + // field: 'Customer ID', + // title: 'Customer ID', + // width: 'auto' + // // cellType: 'checkbox' + // }, + // { + // field: 'Product Name', + // title: 'Product Name', + // width: 'auto' + // }, + // { + // field: 'Category', + // title: 'Category', + // width: 'auto' + // }, + // { + // field: 'Sub-Category', + // title: 'Sub-Category', + // width: 'auto' + // }, + // { + // field: 'Region', + // title: 'Region', + // width: 'auto' + // }, + // { + // field: 'City', + // title: 'City', + // width: 'auto' + // }, + // { + // field: 'Order Date', + // title: 'Order Date', + // width: 'auto' + // }, + // { + // field: 'Quantity', + // title: 'Quantity', + // width: 'auto' + // }, + // { + // field: 'Sales', + // title: 'Sales', + // width: 'auto' + // }, + // { + // field: 'Profit', + // title: 'Profit', + // width: 'auto' + // } + // ]; + + // const option: VTable.ListTableConstructorOptions = { + // records: data.slice(0, 100), + // columns, + // widthMode: 'standard', + // groupBy: ['Category', 'Sub-Category', 'Region'], + // // hierarchyExpandLevel: 2, + // // theme: VTable.themes.DEFAULT.extends({ + // // groupTitleStyle: { + // // fontWeight: 'bold', + // // // bgColor: '#3370ff' + // // bgColor: args => { + // // const { col, row, table } = args; + // // const index = table.getGroupTitleLevel(col, row); + // // if (col > 0 && index !== undefined) { + // // return titleColorPool[index % titleColorPool.length]; + // // } + // // } + // // } + // // }), + // enableTreeStickCell: true, + // rowSeriesNumber: { + // // dragOrder: true, + // // title: '序号', + // width: 'auto', + // headerStyle: { + // color: 'black', + // bgColor: 'pink' + // }, + // format: () => { + // return ''; + // }, + // style: { + // color: 'red' + // }, + // cellType: 'checkbox', + // enableTreeCheckbox: true + // } + // }; + // tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID), option); + // window.tableInstance = tableInstance; + // bindDebugTool(tableInstance.scenegraph.stage, { customGrapicKeys: ['col', 'row'] }); + // }) + // .catch(e => { + // console.error(e); + // }); + + const data = [ + { + 类别: '办公用品', + 销售额: '129.696', + 数量: '2', + 利润: '60.704', + children: [ { - field: 'Category', - title: 'Category', - width: 'auto' + 类别: '信封', // 对应原子类别 + 销售额: '125.44', + 数量: '2', + 利润: '42.56', + children: [ + { + 类别: '黄色信封', + 销售额: '125.44', + 数量: '2', + 利润: '42.56' + }, + { + 类别: '白色信封', + 销售额: '1375.92', + 数量: '3', + 利润: '550.2' + } + ] }, { - field: 'Sub-Category', - title: 'Sub-Category', - width: 'auto' - }, + 类别: '器具', // 对应原子类别 + 销售额: '1375.92', + 数量: '3', + 利润: '550.2', + children: [ + { + 类别: '订书机', + 销售额: '125.44', + 数量: '2', + 利润: '42.56' + }, + { + 类别: '计算器', + 销售额: '1375.92', + 数量: '3', + 利润: '550.2' + } + ] + } + ] + }, + { + 类别: '技术', + 销售额: '229.696', + 数量: '20', + 利润: '90.704', + children: [ { - field: 'Region', - title: 'Region', - width: 'auto' + 类别: '设备', // 对应原子类别 + 销售额: '225.44', + 数量: '5', + 利润: '462.56' }, { - field: 'City', - title: 'City', - width: 'auto' + 类别: '配件', // 对应原子类别 + 销售额: '375.92', + 数量: '8', + 利润: '550.2' }, { - field: 'Order Date', - title: 'Order Date', - width: 'auto' + 类别: '复印机', // 对应原子类别 + 销售额: '425.44', + 数量: '7', + 利润: '34.56' }, { - field: 'Quantity', - title: 'Quantity', - width: 'auto' - }, + 类别: '电话', // 对应原子类别 + 销售额: '175.92', + 数量: '6', + 利润: '750.2' + } + ] + }, + { + 类别: '家具', + 销售额: '129.696', + 数量: '2', + 利润: '-60.704', + children: [ { - field: 'Sales', - title: 'Sales', - width: 'auto' + 类别: '桌子', // 对应原子类别 + 销售额: '125.44', + 数量: '2', + 利润: '42.56', + children: [ + { + 类别: '黄色桌子', + 销售额: '125.44', + 数量: '2', + 利润: '42.56' + }, + { + 类别: '白色桌子', + 销售额: '1375.92', + 数量: '3', + 利润: '550.2' + } + ] }, { - field: 'Profit', - title: 'Profit', - width: 'auto' + 类别: '椅子', // 对应原子类别 + 销售额: '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, - const option: VTable.ListTableConstructorOptions = { - records: data.slice(0, 100), - columns, - widthMode: 'standard', - groupBy: ['Category', 'Sub-Category', 'Region'], + hierarchyIndent: 20, + hierarchyExpandLevel: 2, - // theme: VTable.themes.DEFAULT.extends({ - // groupTitleStyle: { - // fontWeight: 'bold', - // // bgColor: '#3370ff' - // bgColor: args => { - // const { col, row, table } = args; - // const index = table.getGroupTitleLevel(col, row); - // if (col > 0 && index !== undefined) { - // return titleColorPool[index % titleColorPool.length]; - // } - // } - // } - // }), - enableTreeStickCell: true, - rowSeriesNumber: { - // dragOrder: true, - // title: '序号', - width: 'auto', - headerStyle: { - color: 'black', - bgColor: 'pink' - }, - format: () => { - return ''; - }, - style: { - color: 'red' - }, - cellType: 'checkbox', - enableTreeCheckbox: true - } - }; - tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID), option); - window.tableInstance = tableInstance; - bindDebugTool(tableInstance.scenegraph.stage, { customGrapicKeys: ['col', 'row'] }); - }) - .catch(e => { - console.error(e); - }); + // sortState: { + // field: '销售额', + // order: 'desc' + // }, + theme: VTable.themes.BRIGHT, + defaultRowHeight: 32, + select: { + disableDragSelect: true + }, + + rowSeriesNumber: { + // dragOrder: true, + // title: '序号', + width: 'auto', + headerStyle: { + color: 'black', + bgColor: 'pink' + }, + format: () => { + return ''; + }, + style: { + color: 'red' + }, + cellType: 'checkbox', + enableTreeCheckbox: true + } + }; + + const tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID), option); + window.tableInstance = tableInstance; + bindDebugTool(tableInstance.scenegraph.stage, { customGrapicKeys: ['col', 'row'] }); } diff --git a/packages/vtable/src/ListTable.ts b/packages/vtable/src/ListTable.ts index 9e16ef3f2c..b8e56836c4 100644 --- a/packages/vtable/src/ListTable.ts +++ b/packages/vtable/src/ListTable.ts @@ -1082,16 +1082,32 @@ export class ListTable extends BaseTable implements ListTableAPI { if (isValid(field)) { // let stateArr = this.stateManager.checkedState.values() as any; // map按照key(dataIndex)的升序输出value - const keys = Array.from(this.stateManager.checkedState.keys()).sort( - (a: string, b: string) => Number(a) - Number(b) - ); + // const keys = Array.from(this.stateManager.checkedState.keys()).sort( + // (a: string, b: string) => Number(a) - Number(b) + // ); + const keys = Array.from(this.stateManager.checkedState.keys()).sort((a: string, b: string) => { + // number or number[] + const aArr = (a as string).split(','); + const bArr = (b as string).split(','); + const maxLength = Math.max(aArr.length, bArr.length); + + // judge from first to last + for (let i = 0; i < maxLength; i++) { + const a = Number(aArr[i]) ?? 0; + const b = Number(bArr[i]) ?? 0; + if (a !== b) { + return a - b; + } + } + return 0; + }); let stateArr = keys.map(key => this.stateManager.checkedState.get(key)); if (this.options.groupBy) { stateArr = getGroupCheckboxState(this) as any; } return Array.from(stateArr, (state: any) => { - return state[field]; + return state && state[field]; }); } return new Array(...this.stateManager.checkedState.values()); diff --git a/packages/vtable/src/event/event.ts b/packages/vtable/src/event/event.ts index 9f2c200e47..bb1c473f43 100644 --- a/packages/vtable/src/event/event.ts +++ b/packages/vtable/src/event/event.ts @@ -26,7 +26,7 @@ import type { ListTable } from '../ListTable'; import { isValid } from '@visactor/vutils'; import { InertiaScroll } from './scroll'; import { isCellDisableSelect } from '../state/select/is-cell-select-highlight'; -import { bindGroupTitleCheckboxChange } from './self-event-listener/list-table/checkbox'; +import { bindGroupTitleCheckboxChange, bindHeaderCheckboxChange } from './self-event-listener/list-table/checkbox'; import { bindButtonClickEvent } from './component/button'; import { bindIconClickEvent } from './self-event-listener/base-table/icon'; import { bindDropdownMenuClickEvent } from './self-event-listener/base-table/dropdown-menu'; @@ -163,6 +163,8 @@ export class EventManager { // group title checkbox change bindGroupTitleCheckboxChange(this.table); + // header checkbox change + bindHeaderCheckboxChange(this.table); // button click bindButtonClickEvent(this.table); diff --git a/packages/vtable/src/event/self-event-listener/list-table/checkbox.ts b/packages/vtable/src/event/self-event-listener/list-table/checkbox.ts index a508a8e9d5..6a209c649a 100644 --- a/packages/vtable/src/event/self-event-listener/list-table/checkbox.ts +++ b/packages/vtable/src/event/self-event-listener/list-table/checkbox.ts @@ -2,14 +2,19 @@ import { isArray, isNumber } from '@visactor/vutils'; import type { BaseTableAPI } from '../../../ts-types/base-table'; import { setCellCheckboxStateByAttribute } from '../../../state/checkbox/checkbox'; import { HierarchyState } from '../../../ts-types'; +import type { CachedDataSource } from '../../../data'; export function bindGroupTitleCheckboxChange(table: BaseTableAPI) { table.on('checkbox_state_change', args => { - if (table.internalProps.rowSeriesNumber?.enableTreeCheckbox !== true) { + const { col, row, checked, field } = args; + + if (field !== '_vtable_rowSeries_number' || table.internalProps.rowSeriesNumber?.enableTreeCheckbox !== true) { return; } - const { col, row, checked } = args; + if (table.isHeader(col, row)) { + return; + } const record = table.getCellOriginRecord(col, row); const indexedData = (table.dataSource as any).currentPagerIndexedData as (number | number[])[]; const titleShowIndex = table.getRecordShowIndexByCell(col, row); @@ -18,12 +23,12 @@ export function bindGroupTitleCheckboxChange(table: BaseTableAPI) { titleIndex = [titleIndex]; } - if (record.vtableMerge) { + if (record.vtableMerge || record.children?.length) { // 1. group title if (checked) { // 1.1 group title check // 1.1.1 check all children - if (table.getHierarchyState(col, row) === HierarchyState.collapse) { + if (getHierarchyState(table, col, row) === HierarchyState.collapse) { updateChildrenCheckboxState(true, titleIndex, table); } else { setAllChildrenCheckboxState(true, titleShowIndex, titleIndex, indexedData, table); @@ -33,7 +38,7 @@ export function bindGroupTitleCheckboxChange(table: BaseTableAPI) { } else { // 1.2 group title uncheck // 1.2.1 uncheck all children - if (table.getHierarchyState(col, row) === HierarchyState.collapse) { + if (getHierarchyState(table, col, row) === HierarchyState.collapse) { updateChildrenCheckboxState(false, titleIndex, table); } else { setAllChildrenCheckboxState(false, titleShowIndex, titleIndex, indexedData, table); @@ -105,7 +110,28 @@ function updateParentCheckboxState(col: number, row: number, currentIndex: numbe const currentIndexLength = isArray(currentIndex) ? currentIndex.length : 1; let start = false; const result: (boolean | string)[] = []; - checkedState.forEach((value, index: string) => { + + const keys = Array.from(checkedState.keys()).sort((a: string, b: string) => { + // number or number[] + const aArr = (a as string).split(','); + const bArr = (b as string).split(','); + const maxLength = Math.max(aArr.length, bArr.length); + + // judge from first to last + for (let i = 0; i < maxLength; i++) { + const a = Number(aArr[i]) ?? 0; + const b = Number(bArr[i]) ?? 0; + if (a !== b) { + return a - b; + } + } + return 0; + }); + const stateArr = keys.map(key => checkedState.get(key)); + + stateArr.forEach((state, i) => { + const index = keys[i] as string; + const value = state; if (start) { const indexData = index.split(','); if (indexData.length === currentIndexLength) { @@ -139,19 +165,40 @@ function updateParentCheckboxState(col: number, row: number, currentIndex: numbe } // update invisible children checkbox state(collapsed) -function updateChildrenCheckboxState(state: boolean, currentIndex: number | number[], table: BaseTableAPI) { +function updateChildrenCheckboxState(parentState: boolean, currentIndex: number | number[], table: BaseTableAPI) { const { checkedState } = table.stateManager; const key = currentIndex.toString(); const currentIndexLength = isArray(currentIndex) ? currentIndex.length : 1; let start = false; - checkedState.forEach((value, index: string) => { + const keys = Array.from(checkedState.keys()).sort((a: string, b: string) => { + // number or number[] + const aArr = (a as string).split(','); + const bArr = (b as string).split(','); + const maxLength = Math.max(aArr.length, bArr.length); + + // judge from first to last + for (let i = 0; i < maxLength; i++) { + const a = Number(aArr[i]) ?? 0; + const b = Number(bArr[i]) ?? 0; + if (a !== b) { + return a - b; + } + } + return 0; + }); + const stateArr = keys.map(key => checkedState.get(key)); + + stateArr.forEach((state, i) => { + const index = keys[i] as string; + const value = state; + if (start) { const indexData = index.split(','); if (indexData.length === currentIndexLength) { start = false; } else { - value._vtable_rowSeries_number = state; + value._vtable_rowSeries_number = parentState; } } if (index === key) { @@ -159,3 +206,34 @@ function updateChildrenCheckboxState(state: boolean, currentIndex: number | numb } }); } + +export function bindHeaderCheckboxChange(table: BaseTableAPI) { + table.on('checkbox_state_change', args => { + const { col, row, checked, field } = args; + if (table.isHeader(col, row)) { + //点击的表头部分的checkbox 需要同时处理表头和body单元格的状态 + table.stateManager.setHeaderCheckedState(field as string | number, checked); + const cellType = table.getCellType(col, row); + if (cellType === 'checkbox') { + table.scenegraph.updateCheckboxCellState(col, row, checked); + } + } else { + //点击的是body单元格的checkbox 处理本单元格的状态维护 同时需要检查表头是否改变状态 + table.stateManager.setCheckedState(col, row, field as string | number, checked); + const cellType = table.getCellType(col, row); + if (cellType === 'checkbox') { + const oldHeaderCheckedState = table.stateManager.headerCheckedState[field as string | number]; + const newHeaderCheckedState = table.stateManager.updateHeaderCheckedState(field as string | number, col, row); + if (oldHeaderCheckedState !== newHeaderCheckedState) { + table.scenegraph.updateHeaderCheckboxCellState(col, row, newHeaderCheckedState); + } + } + } + }); +} + +// get hierarchy state by row +function getHierarchyState(table: BaseTableAPI, col: number, row: number) { + const index = table.getRecordShowIndexByCell(col, row); + return (table.dataSource as CachedDataSource).getHierarchyState(index); +} diff --git a/packages/vtable/src/state/checkbox/checkbox.ts b/packages/vtable/src/state/checkbox/checkbox.ts index 60bf8d7cfd..f3bad8fcc7 100644 --- a/packages/vtable/src/state/checkbox/checkbox.ts +++ b/packages/vtable/src/state/checkbox/checkbox.ts @@ -71,6 +71,18 @@ export function syncCheckedState( } if (state.checkedState.has(dataIndex)) { state.checkedState.get(dataIndex)[field] = checked; + } else if (dataIndex.includes(',')) { + // child record, sync parent record state + const parentDataIndex = dataIndex.split(',').slice(0, -1).join(','); // get latest parent data index + if (state.checkedState.has(parentDataIndex) && state.checkedState.get(parentDataIndex)[field] === true) { + state.checkedState.set(dataIndex, { + [field]: true + }); + } else { + state.checkedState.set(dataIndex, { + [field]: checked + }); + } } else { state.checkedState.set(dataIndex, { [field]: checked diff --git a/packages/vtable/src/ts-types/base-table.ts b/packages/vtable/src/ts-types/base-table.ts index 2ed220bea7..4c78edb138 100644 --- a/packages/vtable/src/ts-types/base-table.ts +++ b/packages/vtable/src/ts-types/base-table.ts @@ -557,10 +557,9 @@ export interface BaseTableConstructorOptions { // 图片资源请求时是否使用anonymous模式 imageAnonymous?: boolean; - // 滚动到边界是否继续触发滚动事件 scrollEventAlwaysTrigger?: boolean; - + // 开启透视结构缓存 enablePivotPathCache?: boolean; }; // 部分特殊配置,兼容xTable等作用