From eecff830489abdadf173fee8e90a09cb98bbb920 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 28 Jul 2026 17:00:46 +0800 Subject: [PATCH 1/3] fix: support context menu on canvas blank area --- ...-context-menu-canvas_2026-07-28-00-00.json | 11 +++++ ...sue-5215-plugin-init_2026-07-28-00-00.json | 11 +++++ .../context-menu/handle-menu-helper.test.ts | 27 ++++++++++++ .../issue-5215-context-menu-canvas.ts | 44 +++++++++++++++++++ packages/vtable-plugins/demo/menu.ts | 4 ++ packages/vtable-plugins/src/context-menu.ts | 31 ++++++++++++- packages/vtable/src/plugins/interface.ts | 6 ++- packages/vtable/src/plugins/plugin-manager.ts | 2 + 8 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 common/changes/@visactor/vtable-plugins/fix-issue-5215-context-menu-canvas_2026-07-28-00-00.json create mode 100644 common/changes/@visactor/vtable/fix-issue-5215-plugin-init_2026-07-28-00-00.json create mode 100644 packages/vtable-plugins/demo/context-menu/issue-5215-context-menu-canvas.ts diff --git a/common/changes/@visactor/vtable-plugins/fix-issue-5215-context-menu-canvas_2026-07-28-00-00.json b/common/changes/@visactor/vtable-plugins/fix-issue-5215-context-menu-canvas_2026-07-28-00-00.json new file mode 100644 index 0000000000..54808331df --- /dev/null +++ b/common/changes/@visactor/vtable-plugins/fix-issue-5215-context-menu-canvas_2026-07-28-00-00.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: support context menu plugin on canvas blank area", + "type": "patch", + "packageName": "@visactor/vtable-plugins" + } + ], + "packageName": "@visactor/vtable-plugins", + "email": "github@visactor.io" +} diff --git a/common/changes/@visactor/vtable/fix-issue-5215-plugin-init_2026-07-28-00-00.json b/common/changes/@visactor/vtable/fix-issue-5215-plugin-init_2026-07-28-00-00.json new file mode 100644 index 0000000000..674f58f075 --- /dev/null +++ b/common/changes/@visactor/vtable/fix-issue-5215-plugin-init_2026-07-28-00-00.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: allow plugins to initialize before first render", + "type": "patch", + "packageName": "@visactor/vtable" + } + ], + "packageName": "@visactor/vtable", + "email": "github@visactor.io" +} diff --git a/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts b/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts index 1c777b352a..9ef542b030 100644 --- a/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts +++ b/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts @@ -1,6 +1,7 @@ // @ts-nocheck import { ListTable } from '@visactor/vtable'; import { createDiv } from '../../../vtable/__tests__/dom'; +import { ContextMenuPlugin } from '../../src/context-menu'; import { MenuHandler } from '../../src/contextmenu/handle-menu-helper'; import { TableSeriesNumber } from '../../src/table-series-number'; @@ -63,3 +64,29 @@ describe('Context menu row deletion', () => { expect(table.records.map(record => record.id)).toEqual([0, 4]); }); }); + +describe('Context menu canvas option', () => { + let table: ListTable; + + afterEach(() => { + table?.release(); + document.body.innerHTML = ''; + }); + + test('ContextMenuPlugin enables canvas context menu through contextMenuWorkOnlyCell', () => { + const container = createDiv(); + const contextMenuPlugin = new ContextMenuPlugin({ + contextMenuWorkOnlyCell: false + }); + + table = new ListTable({ + container, + columns: [{ field: 'id', title: 'ID' }], + records: [{ id: 1 }], + plugins: [contextMenuPlugin] + }); + + expect(table.options.menu.contextMenuWorkOnlyCell).toBe(false); + expect(contextMenuPlugin.runTime).toContain(ListTable.EVENT_TYPE.CONTEXTMENU_CANVAS); + }); +}); diff --git a/packages/vtable-plugins/demo/context-menu/issue-5215-context-menu-canvas.ts b/packages/vtable-plugins/demo/context-menu/issue-5215-context-menu-canvas.ts new file mode 100644 index 0000000000..9b14e1f8d1 --- /dev/null +++ b/packages/vtable-plugins/demo/context-menu/issue-5215-context-menu-canvas.ts @@ -0,0 +1,44 @@ +import * as VTable from '@visactor/vtable'; +import { ContextMenuPlugin } from '../../src/context-menu'; + +const CONTAINER_ID = 'vTable'; + +export function createTableInstance() { + const records = [ + { id: 1, name: 'alpha', value: 12 }, + { id: 2, name: 'beta', value: 34 } + ]; + + const plugin = new ContextMenuPlugin({ + contextMenuWorkOnlyCell: false, + bodyCellMenuItems: [ + { text: 'Copy from plugin', menuKey: 'copy' }, + { text: 'Canvas menu item', menuKey: 'canvas_menu_item' } + ], + beforeShowAdjustMenuItems: (menuItems, _table, col, row) => { + if (col === -1 && row === -1) { + return [{ text: 'Canvas blank area', menuKey: 'canvas_blank_area' }, ...menuItems]; + } + return menuItems; + } + }); + + const option: VTable.ListTableConstructorOptions = { + container: document.getElementById(CONTAINER_ID), + records, + columns: [ + { field: 'id', title: 'ID', width: 100 }, + { field: 'name', title: 'Name', width: 160 }, + { field: 'value', title: 'Value', width: 120 } + ], + defaultRowHeight: 40, + defaultHeaderRowHeight: 40, + widthMode: 'standard', + heightMode: 'standard', + plugins: [plugin] + }; + + const tableInstance = new VTable.ListTable(option); + window.tableInstance = tableInstance; + return tableInstance; +} diff --git a/packages/vtable-plugins/demo/menu.ts b/packages/vtable-plugins/demo/menu.ts index 655f873fee..b252dd55ad 100644 --- a/packages/vtable-plugins/demo/menu.ts +++ b/packages/vtable-plugins/demo/menu.ts @@ -152,6 +152,10 @@ export const menus = [ path: 'context-menu', name: 'context-menu' }, + { + path: 'context-menu', + name: 'issue-5215-context-menu-canvas' + }, { path: 'context-menu', name: 'issue-5214-reverse-selected-row-delete' diff --git a/packages/vtable-plugins/src/context-menu.ts b/packages/vtable-plugins/src/context-menu.ts index 6ecc46b94f..9dcbd8abfd 100644 --- a/packages/vtable-plugins/src/context-menu.ts +++ b/packages/vtable-plugins/src/context-menu.ts @@ -29,6 +29,8 @@ export interface ContextMenuOptions { headerCellMenuItems?: MenuItemOrSeparator[]; /** 表体菜单项 */ bodyCellMenuItems?: MenuItemOrSeparator[]; + /** 右键菜单是否只工作在单元格上。默认 true;配置 false 时空白画布区域也弹出菜单。 */ + contextMenuWorkOnlyCell?: boolean; /** 自定义菜单样式 */ customMenuAttributions?: MenuAttributions; /** 菜单点击回调。如果设置是函数,则忽略内部默认的菜单项处理逻辑。如果这里配置的是个对象(对象的key为menuKey),则有匹配的menuKey时忽略内部默认的菜单项处理逻辑, @@ -56,7 +58,7 @@ export type MenuClickCallback = (args: MenuClickEventArgs, table: ListTable) => export class ContextMenuPlugin implements pluginsDefinition.IVTablePlugin { id = `context-menu`; name = 'Context Menu'; - runTime = [TABLE_EVENT_TYPE.CONTEXTMENU_CELL, TABLE_EVENT_TYPE.PLUGIN_EVENT]; + runTime = [TABLE_EVENT_TYPE.CONTEXTMENU_CELL, TABLE_EVENT_TYPE.CONTEXTMENU_CANVAS, TABLE_EVENT_TYPE.PLUGIN_EVENT]; pluginOptions: ContextMenuOptions; table: ListTable; /** 菜单管理器 */ @@ -156,6 +158,22 @@ export class ContextMenuPlugin implements pluginsDefinition.IVTablePlugin { } }; + /** + * 处理空白画布右键菜单事件 + */ + private handleContextMenuCanvas = (eventArgs: any, table: BaseTableAPI): void => { + let menuItems = this.pluginOptions.bodyCellMenuItems || []; + const { col = -1, row = -1 } = eventArgs; + + if (this.pluginOptions.beforeShowAdjustMenuItems) { + menuItems = this.pluginOptions.beforeShowAdjustMenuItems(menuItems, table as ListTable, col, row); + } + + if (menuItems.length > 0) { + this.showContextMenu(menuItems, eventArgs.event.clientX, eventArgs.event.clientY, col, row); + } + }; + /** * 处理插件事件 */ @@ -191,6 +209,15 @@ export class ContextMenuPlugin implements pluginsDefinition.IVTablePlugin { /** * 运行插件 */ + init(_table: BaseTableAPI, options: BaseTableAPI['options']) { + if (this.pluginOptions.contextMenuWorkOnlyCell === false) { + options.menu = { + ...options.menu, + contextMenuWorkOnlyCell: false + }; + } + } + run(...args: any[]) { const eventArgs = args[0]; const runTime = args[1]; @@ -203,6 +230,8 @@ export class ContextMenuPlugin implements pluginsDefinition.IVTablePlugin { // 根据事件类型处理不同的右键菜单 if (runTime === TABLE_EVENT_TYPE.CONTEXTMENU_CELL) { this.handleContextMenuCell(eventArgs, table); + } else if (runTime === TABLE_EVENT_TYPE.CONTEXTMENU_CANVAS) { + this.handleContextMenuCanvas(eventArgs, table); } else if (runTime === TABLE_EVENT_TYPE.PLUGIN_EVENT) { this.handlePluginEvent(eventArgs, table); } diff --git a/packages/vtable/src/plugins/interface.ts b/packages/vtable/src/plugins/interface.ts index 8119b3dfb7..2f7adb9998 100644 --- a/packages/vtable/src/plugins/interface.ts +++ b/packages/vtable/src/plugins/interface.ts @@ -1,5 +1,5 @@ import type { TableEvents } from '../core/TABLE_EVENT_TYPE'; -import type { BaseTableAPI } from '../ts-types/base-table'; +import type { BaseTableAPI, BaseTableConstructorOptions } from '../ts-types/base-table'; // 插件生命周期接口 export interface IVTablePlugin { @@ -16,7 +16,9 @@ export interface IVTablePlugin { runTime: TableEvents[keyof TableEvents][]; // // 插件依赖 // dependencies?: string[]; - // 初始化方法,在VTable实例创建后、首次渲染前调用 + // 初始化方法,在 VTable 实例创建后、首次渲染前调用 + init?: (table: BaseTableAPI, options: BaseTableConstructorOptions) => void; + // 运行方法,在订阅的表格事件触发时调用 run: (...args: any[]) => void; // 更新方法,当表格数据或配置更新时调用 update?: () => void; diff --git a/packages/vtable/src/plugins/plugin-manager.ts b/packages/vtable/src/plugins/plugin-manager.ts index e984eac6df..4c2838ad93 100644 --- a/packages/vtable/src/plugins/plugin-manager.ts +++ b/packages/vtable/src/plugins/plugin-manager.ts @@ -12,6 +12,7 @@ export class PluginManager { this.table = table; options.plugins?.map(plugin => { this.register(plugin); + plugin.init?.(this.table, options); this._bindTableEventForPlugin(plugin); }); } @@ -59,6 +60,7 @@ export class PluginManager { const addedPlugins = plugins?.filter(plugin => !this.plugins.has(plugin.id)); addedPlugins?.forEach(plugin => { this.register(plugin); + plugin.init?.(this.table, this.table.options); this._bindTableEventForPlugin(plugin); }); } From c5f2114dc050d6d56b3cbf419c666f5f50db8106 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 28 Jul 2026 17:01:19 +0800 Subject: [PATCH 2/3] fix: support checkbox state by record index --- ...heckbox-record-index_2026-07-28-00-00.json | 11 ++ docs/assets/api/en/methods.md | 22 +++ docs/assets/api/zh/methods.md | 22 +++ .../listTable-checkbox-record-index.test.ts | 72 +++++++++ .../issue-5211-tree-checkbox-record-index.ts | 79 ++++++++++ packages/vtable/examples/menu.ts | 4 + packages/vtable/src/ListTable.ts | 20 ++- .../vtable/src/state/checkbox/checkbox.ts | 137 +++++++++++++++++- packages/vtable/src/ts-types/table-engine.ts | 19 +++ 9 files changed, 384 insertions(+), 2 deletions(-) create mode 100644 common/changes/@visactor/vtable/fix-issues-5211-checkbox-record-index_2026-07-28-00-00.json create mode 100644 packages/vtable/__tests__/listTable-checkbox-record-index.test.ts create mode 100644 packages/vtable/examples/list/issue-5211-tree-checkbox-record-index.ts diff --git a/common/changes/@visactor/vtable/fix-issues-5211-checkbox-record-index_2026-07-28-00-00.json b/common/changes/@visactor/vtable/fix-issues-5211-checkbox-record-index_2026-07-28-00-00.json new file mode 100644 index 0000000000..bf7b04563f --- /dev/null +++ b/common/changes/@visactor/vtable/fix-issues-5211-checkbox-record-index_2026-07-28-00-00.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: support updating checkbox state by record index", + "type": "patch", + "packageName": "@visactor/vtable" + } + ], + "packageName": "@visactor/vtable", + "email": "github@visactor.io" +} diff --git a/docs/assets/api/en/methods.md b/docs/assets/api/en/methods.md index f41c4a192c..2baced55dc 100644 --- a/docs/assets/api/en/methods.md +++ b/docs/assets/api/en/methods.md @@ -2143,6 +2143,28 @@ setCellCheckboxState(col: number, row: number, checked: boolean) => void - row: Row number - checked: Whether selected +## setCellCheckboxStateByRecordIndex(Function) + +Set the checkbox state by source records index and field. For tree tables, pass a children path such as `[0, 1]` for the second child of the first root record. The state is updated even when the target node is collapsed and not currently visible. + +``` +setCellCheckboxStateByRecordIndex(recordIndex: number | number[], field: string | number, checked: boolean | 'indeterminate') => void +``` + +- recordIndex: Source data index; number for normal tables, number[] for tree tables +- field: Field of the checkbox column +- checked: Checkbox state, including `'indeterminate'` + +## clearCheckboxState(Function) + +Clear all checkbox checked states under the specified field. `clearAllCheckboxState(field)` is an alias of this method. + +``` +clearCheckboxState(field: string | number) => void +``` + +- field: Field of the checkbox column + ## setCellRadioState(Function) Set the radio state of the cell to selected state diff --git a/docs/assets/api/zh/methods.md b/docs/assets/api/zh/methods.md index e10c0b8330..f7557c9c82 100644 --- a/docs/assets/api/zh/methods.md +++ b/docs/assets/api/zh/methods.md @@ -2142,6 +2142,28 @@ setCellCheckboxState(col: number, row: number, checked: boolean) => void - row: 行号 - checked: 是否选中 +## setCellCheckboxStateByRecordIndex(Function) + +根据源数据 records 的 index 和 field 设置 checkbox 状态。树形表格可传入 children 路径,例如 `[0, 1]` 表示第 1 条根节点下第 2 条子节点;即使该节点当前处于折叠不可见状态,也会更新其 checkbox 状态。 + +``` +setCellCheckboxStateByRecordIndex(recordIndex: number | number[], field: string | number, checked: boolean | 'indeterminate') => void +``` + +- recordIndex: 源数据索引;普通表格为 number,树形表格为 number[] +- field: checkbox 所在字段 +- checked: 是否选中,支持半选状态 `'indeterminate'` + +## clearCheckboxState(Function) + +清除指定 field 下所有 checkbox 的选中状态。`clearAllCheckboxState(field)` 是该方法的别名。 + +``` +clearCheckboxState(field: string | number) => void +``` + +- field: checkbox 所在字段 + ## setCellRadioState(Function) 将单元格的 radio 状态设置为选中状态 diff --git a/packages/vtable/__tests__/listTable-checkbox-record-index.test.ts b/packages/vtable/__tests__/listTable-checkbox-record-index.test.ts new file mode 100644 index 0000000000..594cd6042d --- /dev/null +++ b/packages/vtable/__tests__/listTable-checkbox-record-index.test.ts @@ -0,0 +1,72 @@ +// @ts-nocheck +import { ListTable, TYPES } from '../src'; +import { createDiv } from './dom'; + +global.__VERSION__ = 'none'; + +describe('ListTable checkbox record index api', () => { + let table: ListTable; + + afterEach(() => { + table?.release(); + document.body.innerHTML = ''; + }); + + test('sets collapsed tree child checkbox state by record index path', () => { + table = new ListTable({ + container: createDiv(), + columns: [ + { + field: 'task', + title: 'Task', + tree: true, + cellType: 'checkbox', + headerType: 'checkbox' + } + ], + records: [ + { + task: { text: 'parent', checked: true }, + hierarchyState: TYPES.HierarchyState.collapse, + children: [{ task: { text: 'child', checked: true } }] + } + ], + enableCheckboxCascade: false + }); + + table.setCellCheckboxStateByRecordIndex([0, 0], 'task', false); + + expect(table.stateManager.checkedState.get('0,0').task).toBe(false); + }); + + test('clears all checkbox states for a field', () => { + table = new ListTable({ + container: createDiv(), + columns: [ + { + field: 'task', + title: 'Task', + tree: true, + cellType: 'checkbox', + headerType: 'checkbox' + } + ], + records: [ + { + task: { text: 'parent', checked: true }, + hierarchyState: TYPES.HierarchyState.collapse, + children: [{ task: { text: 'child', checked: true } }] + }, + { task: { text: 'sibling', checked: true } } + ], + enableCheckboxCascade: false + }); + + table.clearAllCheckboxState('task'); + + expect(table.stateManager.checkedState.get('0').task).toBe(false); + expect(table.stateManager.checkedState.get('0,0').task).toBe(false); + expect(table.stateManager.checkedState.get('1').task).toBe(false); + expect(table.stateManager.headerCheckedState.task).toBe(false); + }); +}); diff --git a/packages/vtable/examples/list/issue-5211-tree-checkbox-record-index.ts b/packages/vtable/examples/list/issue-5211-tree-checkbox-record-index.ts new file mode 100644 index 0000000000..5c8de24968 --- /dev/null +++ b/packages/vtable/examples/list/issue-5211-tree-checkbox-record-index.ts @@ -0,0 +1,79 @@ +import * as VTable from '../../src'; + +const CONTAINER_ID = 'vTable'; + +export function createTable() { + const records = [ + { + task: { text: 'Project A', checked: true }, + owner: 'Alice', + status: 'collapsed parent', + hierarchyState: VTable.TYPES.HierarchyState.collapse, + children: [ + { + task: { text: 'Hidden task A-1', checked: true }, + owner: 'Bob', + status: 'hidden child' + }, + { + task: { text: 'Hidden task A-2', checked: true }, + owner: 'Cindy', + status: 'hidden child' + } + ] + }, + { + task: { text: 'Project B', checked: true }, + owner: 'David', + status: 'root' + } + ]; + + const option: VTable.ListTableConstructorOptions = { + container: document.getElementById(CONTAINER_ID), + records, + columns: [ + { + field: 'task', + title: 'Task', + tree: true, + cellType: 'checkbox', + headerType: 'checkbox', + width: 260 + }, + { field: 'owner', title: 'Owner', width: 120 }, + { field: 'status', title: 'Status', width: 180 } + ], + defaultRowHeight: 38, + hierarchyIndent: 20, + enableCheckboxCascade: false, + enableHeaderCheckboxCascade: false, + theme: VTable.themes.BRIGHT + }; + + const tableInstance = new VTable.ListTable(option); + window.tableInstance = tableInstance; + + const toolbar = document.createElement('div'); + toolbar.style.cssText = 'position:absolute;top:8px;left:8px;z-index:10;display:flex;gap:8px;'; + + const setHiddenChildButton = document.createElement('button'); + setHiddenChildButton.textContent = 'Uncheck hidden child [0,0]'; + setHiddenChildButton.onclick = () => { + tableInstance.setCellCheckboxStateByRecordIndex([0, 0], 'task', false); + console.log('checkbox state after set hidden child:', tableInstance.getCheckboxState('task')); + }; + + const clearAllButton = document.createElement('button'); + clearAllButton.textContent = 'Clear task checkbox field'; + clearAllButton.onclick = () => { + tableInstance.clearAllCheckboxState('task'); + console.log('checkbox state after clear:', tableInstance.getCheckboxState('task')); + }; + + toolbar.appendChild(setHiddenChildButton); + toolbar.appendChild(clearAllButton); + document.body.appendChild(toolbar); + + return tableInstance; +} diff --git a/packages/vtable/examples/menu.ts b/packages/vtable/examples/menu.ts index 13b2c16107..34dbb96a68 100644 --- a/packages/vtable/examples/menu.ts +++ b/packages/vtable/examples/menu.ts @@ -123,6 +123,10 @@ export const menus = [ path: 'list', name: 'list-checkbox-tree' }, + { + path: 'list', + name: 'issue-5211-tree-checkbox-record-index' + }, { path: 'list', name: 'list-tree-20000' diff --git a/packages/vtable/src/ListTable.ts b/packages/vtable/src/ListTable.ts index 8366170c65..0dda560261 100644 --- a/packages/vtable/src/ListTable.ts +++ b/packages/vtable/src/ListTable.ts @@ -39,7 +39,12 @@ import type { IEditor } from '@visactor/vtable-editors'; 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'; +import { + clearCheckboxState, + getGroupCheckboxState, + setCellCheckboxState, + setCheckboxStateByRecordIndex +} from './state/checkbox/checkbox'; import type { IEmptyTipComponent } from './components/empty-tip/empty-tip'; import { Factory } from './core/factory'; import { getGroupByDataConfig } from './core/group-helper'; @@ -1568,6 +1573,19 @@ export class ListTable extends BaseTable implements ListTableAPI { setCellCheckboxState(col: number, row: number, checked: boolean | 'indeterminate') { setCellCheckboxState(col, row, checked, this); } + setCellCheckboxStateByRecordIndex( + recordIndex: number | number[], + field: FieldDef, + checked: boolean | 'indeterminate' + ) { + setCheckboxStateByRecordIndex(recordIndex, field, checked, this); + } + clearCheckboxState(field: FieldDef) { + clearCheckboxState(field, this); + } + clearAllCheckboxState(field: FieldDef) { + this.clearCheckboxState(field); + } setCellRadioState(col: number, row: number, index?: number) { setCellRadioState(col, row, index, this); } diff --git a/packages/vtable/src/state/checkbox/checkbox.ts b/packages/vtable/src/state/checkbox/checkbox.ts index 9ac8d7eef0..93b5301a82 100644 --- a/packages/vtable/src/state/checkbox/checkbox.ts +++ b/packages/vtable/src/state/checkbox/checkbox.ts @@ -1,6 +1,6 @@ import { isArray, isFunction, isNumber, isObject, isValid } from '@visactor/vutils'; import type { StateManager } from '../state'; -import type { CheckboxColumnDefine, ListTableAPI } from '../../ts-types'; +import type { CheckboxColumnDefine, FieldDef, ListTableAPI } from '../../ts-types'; import { getOrApply } from '../../tools/helper'; import type { BaseTableAPI } from '../../ts-types/base-table'; import type { CachedDataSource } from '../../data'; @@ -304,6 +304,34 @@ export function setCellCheckboxState( } } +export function setCheckboxStateByRecordIndex( + recordIndex: number | number[], + field: FieldDef, + checked: boolean | 'indeterminate', + table: BaseTableAPI +) { + const dataIndex = normalizeRecordIndex(recordIndex).toString(); + if (table.stateManager.checkedState.get(dataIndex)) { + table.stateManager.checkedState.get(dataIndex)[field as string | number] = checked; + } else { + table.stateManager.checkedState.set(dataIndex, { + [field as string | number]: checked + }); + } + + updateVisibleCheckboxCellByRecordIndex(recordIndex, field, checked, table); + updateHeaderCheckboxStateByField(field, table); +} + +export function clearCheckboxState(field: FieldDef, table: BaseTableAPI) { + if (!isValid(field)) { + return; + } + updateAllRecordCheckboxState((table as any).records, field, false, table.stateManager.checkedState); + updateVisibleCheckboxCellsByField(field, false, table); + updateHeaderCheckboxStateByField(field, table); +} + export function setCellCheckboxStateByAttribute( col: number, row: number, @@ -404,6 +432,113 @@ export function getGroupCheckboxState(table: BaseTableAPI) { return result; } +function normalizeRecordIndex(recordIndex: number | number[]): number | number[] { + if (isArray(recordIndex) && recordIndex.length === 1) { + return recordIndex[0]; + } + return recordIndex; +} + +function updateAllRecordCheckboxState( + records: any[], + field: FieldDef, + checked: boolean | 'indeterminate', + checkedState: Map, + parentIndex: number[] = [] +) { + if (!isArray(records)) { + return; + } + + records.forEach((record, index) => { + const recordIndex = parentIndex.length ? parentIndex.concat(index) : [index]; + const dataIndex = normalizeRecordIndex(recordIndex).toString(); + if (checkedState.get(dataIndex)) { + checkedState.get(dataIndex)[field as string | number] = checked; + } else { + checkedState.set(dataIndex, { + [field as string | number]: checked + }); + } + + if (isArray(record?.children)) { + updateAllRecordCheckboxState(record.children, field, checked, checkedState, recordIndex); + } + }); +} + +function updateVisibleCheckboxCellByRecordIndex( + recordIndex: number | number[], + field: FieldDef, + checked: boolean | 'indeterminate', + table: BaseTableAPI +) { + const bodyRowIndex = (table as ListTableAPI).getBodyRowIndexByRecordIndex?.(recordIndex); + if (!isNumber(bodyRowIndex) || bodyRowIndex < 0) { + return; + } + + const row = bodyRowIndex + table.columnHeaderLevelCount; + const cols = getCheckboxColsByField(field, row, table); + cols.forEach(col => { + setCellCheckboxStateByAttribute(col, row, checked, table); + }); +} + +function updateVisibleCheckboxCellsByField(field: FieldDef, checked: boolean | 'indeterminate', table: BaseTableAPI) { + for (let row = table.columnHeaderLevelCount; row < table.rowCount; row++) { + getCheckboxColsByField(field, row, table).forEach(col => { + setCellCheckboxStateByAttribute(col, row, checked, table); + }); + } +} + +function updateHeaderCheckboxStateByField(field: FieldDef, table: BaseTableAPI) { + const fieldKey = field as string | number; + let hasChecked = false; + let hasUnchecked = false; + + table.stateManager.checkedState.forEach(checkState => { + if (checkState?.[fieldKey] === true) { + hasChecked = true; + } else { + hasUnchecked = true; + } + }); + + const checked = hasChecked && !hasUnchecked ? true : hasChecked ? 'indeterminate' : false; + table.stateManager.headerCheckedState[fieldKey] = checked; + updateVisibleHeaderCheckboxCellByField(field, checked, table); +} + +function updateVisibleHeaderCheckboxCellByField( + field: FieldDef, + checked: boolean | 'indeterminate', + table: BaseTableAPI +) { + for (let row = 0; row < table.columnHeaderLevelCount; row++) { + for (let col = 0; col < table.colCount; col++) { + if (table.getHeaderField(col, row) === field && table.getCellType(col, row) === 'checkbox') { + table.scenegraph.updateHeaderCheckboxCellState(col, row, checked); + } + } + } +} + +function getCheckboxColsByField(field: FieldDef, row: number, table: BaseTableAPI): number[] { + const cols: number[] = []; + for (let col = 0; col < table.colCount; col++) { + if (table.isHeader(col, row)) { + continue; + } + const define = table.getBodyColumnDefine(col, row); + if (define?.field === field && table.getCellType(col, row) === 'checkbox') { + cols.push(col); + } + } + return cols; +} + function initRecordCheckState(records: any[], state: StateManager) { const table = state.table; const start = table.isPivotTable() diff --git a/packages/vtable/src/ts-types/table-engine.ts b/packages/vtable/src/ts-types/table-engine.ts index 5655c12ad8..00f45593ec 100644 --- a/packages/vtable/src/ts-types/table-engine.ts +++ b/packages/vtable/src/ts-types/table-engine.ts @@ -458,6 +458,25 @@ export interface ListTableAPI extends BaseTableAPI { reapplySort?: boolean; clearRowHeightCache?: boolean; }) => void; + /** 获取某个字段下 checkbox 全部数据的选中状态,顺序对应原始 records。 */ + getCheckboxState: (field?: string | number) => any[]; + /** 获取某个单元格 checkbox 的状态。 */ + getCellCheckboxState: (col: number, row: number) => boolean | 'indeterminate' | undefined; + /** 设置某个可见单元格 checkbox 的状态。 */ + setCellCheckboxState: (col: number, row: number, checked: boolean | 'indeterminate') => void; + /** + * 根据源数据 records 的 index + field 设置 checkbox 状态。 + * recordIndex 为源数据中的索引:普通表格为 number;树形表格为 number[](children 路径)。 + */ + setCellCheckboxStateByRecordIndex: ( + recordIndex: number | number[], + field: FieldDef, + checked: boolean | 'indeterminate' + ) => void; + /** 清除指定 field 的全部 checkbox 选中状态。 */ + clearCheckboxState: (field: FieldDef) => void; + /** clearCheckboxState 的别名,兼容 issue 中提出的 API 命名。 */ + clearAllCheckboxState: (field: FieldDef) => void; getFieldData: (field: FieldDef | FieldFormat | undefined, col: number, row: number) => FieldData; //#region 编辑器相关demo /** 获取单元格配置的编辑器 */ From 1c6ffcac993b6b8ff66b8cbac1205e587173bc72 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 28 Jul 2026 17:01:56 +0800 Subject: [PATCH 3/3] fix: refresh vue custom layout after sort --- ...e-custom-layout-sort_2026-07-28-00-00.json | 11 +++ packages/vue-vtable/demo/src/App.vue | 2 + .../composition/Issue5150CustomLayoutSort.vue | 69 +++++++++++++++++++ .../custom/vtable-vue-attribute-plugin.ts | 5 +- 4 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 common/changes/@visactor/vue-vtable/fix-issue-5150-vue-custom-layout-sort_2026-07-28-00-00.json create mode 100644 packages/vue-vtable/demo/src/table/gramatical/composition/Issue5150CustomLayoutSort.vue diff --git a/common/changes/@visactor/vue-vtable/fix-issue-5150-vue-custom-layout-sort_2026-07-28-00-00.json b/common/changes/@visactor/vue-vtable/fix-issue-5150-vue-custom-layout-sort_2026-07-28-00-00.json new file mode 100644 index 0000000000..e3bf74b7f8 --- /dev/null +++ b/common/changes/@visactor/vue-vtable/fix-issue-5150-vue-custom-layout-sort_2026-07-28-00-00.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: refresh vue custom layout dom content after sort", + "type": "patch", + "packageName": "@visactor/vue-vtable" + } + ], + "packageName": "@visactor/vue-vtable", + "email": "github@visactor.io" +} diff --git a/packages/vue-vtable/demo/src/App.vue b/packages/vue-vtable/demo/src/App.vue index a54581281b..e49cb43abc 100644 --- a/packages/vue-vtable/demo/src/App.vue +++ b/packages/vue-vtable/demo/src/App.vue @@ -19,6 +19,7 @@ import ListTableEditorRender from './table/gramatical/composition/ListTable-edit import ListTableDes from './table/gramatical/composition/ListTable-destruction.vue'; import ListTableCustom from './table/gramatical/composition/ListTable-custom.vue'; import ListTableCustomHover from './table/gramatical/composition/ListTable-custom-hover.vue'; +import Issue5150CustomLayoutSort from './table/gramatical/composition/Issue5150CustomLayoutSort.vue'; import ListTableVFor from './table/gramatical/options/ListTable-v-for.vue'; import PivotTable from './table/gramatical/options/PivotTable.vue'; @@ -51,6 +52,7 @@ import singleRadio from './table/single/single-radio.vue'; + diff --git a/packages/vue-vtable/demo/src/table/gramatical/composition/Issue5150CustomLayoutSort.vue b/packages/vue-vtable/demo/src/table/gramatical/composition/Issue5150CustomLayoutSort.vue new file mode 100644 index 0000000000..4f9d35e6ef --- /dev/null +++ b/packages/vue-vtable/demo/src/table/gramatical/composition/Issue5150CustomLayoutSort.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts b/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts index 23656f05f9..579dd52474 100644 --- a/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts +++ b/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts @@ -158,10 +158,6 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl wrapContainer.setAttribute('data-vue-renderId', dataRenderId); // 先隐藏 wrapContainer.style.display = 'none'; - if (!reuse) { - // 仅在非复用时需要重新渲染 - render(element, wrapContainer); - } targetMap = { wrapContainer, nativeContainer, @@ -181,6 +177,7 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl targetMap.renderId = this.renderId; targetMap.graphic = graphic; targetMap.lastAccessed = Date.now(); + render(element, targetMap.wrapContainer); this.updateAccessQueue(id); this.updateStyleOfWrapContainer(graphic, stage, targetMap.wrapContainer, targetMap.nativeContainer); }