From 1a479e065f07c5ca3da950fbddea437f70b8d760 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Wed, 3 Dec 2025 17:01:58 +0800 Subject: [PATCH 01/34] feat: support custom styles. close#4720 --- ...-plugin-for-business_2025-12-03-08-59.json | 10 ++ packages/vtable-plugins/demo/filter/filter.ts | 108 +++++++++++++++++- .../src/filter/condition-filter.ts | 11 +- .../src/filter/filter-toolbar.ts | 11 +- packages/vtable-plugins/src/filter/filter.ts | 6 +- packages/vtable-plugins/src/filter/types.ts | 37 ++++++ .../vtable-plugins/src/filter/value-filter.ts | 12 +- 7 files changed, 183 insertions(+), 12 deletions(-) create mode 100644 common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-03-08-59.json diff --git a/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-03-08-59.json b/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-03-08-59.json new file mode 100644 index 0000000000..528d3230cc --- /dev/null +++ b/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-03-08-59.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "feat: support custom styles. close#4720", + "type": "none" + } + ], + "packageName": "@visactor/vtable" +} \ No newline at end of file diff --git a/packages/vtable-plugins/demo/filter/filter.ts b/packages/vtable-plugins/demo/filter/filter.ts index e1d247129e..f7d4ec7530 100644 --- a/packages/vtable-plugins/demo/filter/filter.ts +++ b/packages/vtable-plugins/demo/filter/filter.ts @@ -143,7 +143,113 @@ export function createTable() { } ]; - const filterPlugin = new FilterPlugin({}); + const filterPlugin = new FilterPlugin({ + styles: { + filterMenu: { + display: 'none', + position: 'absolute', + backgroundColor: '#0E1119', + border: '0px solid #272A30', + boxShadow: '0 4px 8px rgba(0,0,0,0.15)', + zIndex: '100', + borderRadius: '4px', + color: '#FFF', + fontFamily: 'SourceHanSansCN-Normal', + fontSize: '12px' + }, + filterPanel: { + padding: '10px', + display: 'block' + }, + searchContainer: { + padding: '5px' + }, + searchInput: { + width: '100%', + padding: '8px 10px', + border: '1px solid #272a30', + borderRadius: '4px', + fontSize: '14px', + boxSizing: 'border-box', + backgroundColor: '#0e1119', + color: 'red' + }, + optionsContainer: { + maxHeight: '200px', + overflowY: 'auto', + marginTop: '10px' + }, + optionItem: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '8px 5px', + color: 'red' + }, + optionLabel: { + display: 'flex', + alignItems: 'center', + cursor: 'pointer', + flexGrow: '1', + fontWeight: 'normal' + }, + checkbox: { + marginRight: '10px' + }, + countSpan: { + color: '#888', + fontSize: '12px' + }, + tabsContainer: { + display: 'flex', + justifyContent: 'space-around', + borderBottom: '0px solid #e0e0e0' + }, + footerContainer: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '10px 15px', + borderTop: '0px solid #e0e0e0', + backgroundColor: 'transparent' + }, + clearLink: { + color: '#006EFF', + textDecoration: 'none' + }, + conditionContainer: { + marginBottom: '15px', + padding: '10px', + backgroundColor: '#0E1119' + }, + formLabel: { + display: 'block', + marginBottom: '8px', + fontWeight: 'normal', + backgroundColor: 'transparent' + }, + operatorSelect: { + width: '100%', + padding: '8px', + marginBottom: '15px', + border: '1px solid #272a30', + borderRadius: '4px', + boxSizing: 'border-box', + backgroundColor: '#0e1119', + color: 'red' + }, + rangeInputContainer: { + display: 'flex', + alignItems: 'center', + gap: '8px', + backgroundColor: '#0E1119' + }, + addLabel: { + display: 'none', + padding: '0 5px' + } + } + }); (window as any).filterPlugin = filterPlugin; const option: VTable.ListTableConstructorOptions = { diff --git a/packages/vtable-plugins/src/filter/condition-filter.ts b/packages/vtable-plugins/src/filter/condition-filter.ts index 0955ae78ca..7665ff11d4 100644 --- a/packages/vtable-plugins/src/filter/condition-filter.ts +++ b/packages/vtable-plugins/src/filter/condition-filter.ts @@ -1,7 +1,7 @@ import type { ListTable, PivotTable } from '@visactor/vtable'; import type { FilterStateManager } from './filter-state-manager'; import { applyStyles, filterStyles, createElement } from './styles'; -import type { FilterOperator, OperatorOption } from './types'; +import type { FilterOperator, FilterOptions, OperatorOption } from './types'; import { FilterActionType, FilterOperatorCategory } from './types'; /** @@ -10,6 +10,10 @@ import { FilterActionType, FilterOperatorCategory } from './types'; export class ConditionFilter { private table: ListTable | PivotTable; private filterStateManager: FilterStateManager; + private pluginOptions: FilterOptions; + + private styles: Record; + private filterByConditionPanel: HTMLElement; private selectedField: string | number; private operatorSelect: HTMLSelectElement; @@ -67,9 +71,11 @@ export class ConditionFilter { { value: FilterOperatorCategory.RADIO, label: '单选框' } ]; - constructor(table: ListTable | PivotTable, filterStateManager: FilterStateManager) { + constructor(table: ListTable | PivotTable, filterStateManager: FilterStateManager, pluginOptions: FilterOptions) { this.table = table; this.filterStateManager = filterStateManager; + this.pluginOptions = pluginOptions; + this.styles = pluginOptions.styles || {}; } setSelectedField(fieldId: string | number): void { @@ -310,6 +316,7 @@ export class ConditionFilter { * 渲染条件筛选面板 */ render(container: HTMLElement): void { + const filterStyles = this.styles; // 按条件筛选面板 this.filterByConditionPanel = document.createElement('div'); applyStyles(this.filterByConditionPanel, filterStyles.filterPanel); diff --git a/packages/vtable-plugins/src/filter/filter-toolbar.ts b/packages/vtable-plugins/src/filter/filter-toolbar.ts index 851b05c116..065fcc77c3 100644 --- a/packages/vtable-plugins/src/filter/filter-toolbar.ts +++ b/packages/vtable-plugins/src/filter/filter-toolbar.ts @@ -3,7 +3,7 @@ import type { FilterStateManager } from './filter-state-manager'; import { ValueFilter } from './value-filter'; import { ConditionFilter } from './condition-filter'; import { applyStyles, filterStyles } from './styles'; -import type { FilterMode } from './types'; +import type { FilterMode, FilterOptions } from './types'; /** * 筛选工具栏,管理按值和按条件筛选组件 @@ -11,6 +11,7 @@ import type { FilterMode } from './types'; export class FilterToolbar { table: ListTable | PivotTable; filterStateManager: FilterStateManager; + pluginOptions: FilterOptions; valueFilter: ValueFilter | null = null; conditionFilter: ConditionFilter | null = null; activeTab: 'byValue' | 'byCondition' = 'byValue'; @@ -28,11 +29,12 @@ export class FilterToolbar { private cancelFilterButton: HTMLButtonElement; private applyFilterButton: HTMLButtonElement; - constructor(table: ListTable | PivotTable, filterStateManager: FilterStateManager) { + constructor(table: ListTable | PivotTable, filterStateManager: FilterStateManager, pluginOptions: FilterOptions) { this.table = table; this.filterStateManager = filterStateManager; - this.valueFilter = new ValueFilter(this.table, this.filterStateManager); - this.conditionFilter = new ConditionFilter(this.table, this.filterStateManager); + this.valueFilter = new ValueFilter(this.table, this.filterStateManager, pluginOptions); + this.conditionFilter = new ConditionFilter(this.table, this.filterStateManager, pluginOptions); + this.pluginOptions = pluginOptions; this.filterMenuWidth = 300; // 待优化,可能需要自适应内容的宽度 @@ -105,6 +107,7 @@ export class FilterToolbar { render(container: HTMLElement): void { // === 主容器 === this.filterMenu = document.createElement('div'); + this.filterMenu.classList.add('vtable-filter-menu'); applyStyles(this.filterMenu, filterStyles.filterMenu); this.filterMenu.style.width = `${this.filterMenuWidth}px`; diff --git a/packages/vtable-plugins/src/filter/filter.ts b/packages/vtable-plugins/src/filter/filter.ts index e08ca08ada..d15f586816 100644 --- a/packages/vtable-plugins/src/filter/filter.ts +++ b/packages/vtable-plugins/src/filter/filter.ts @@ -13,6 +13,8 @@ import type { ColumnDefine, ColumnsDefine } from '@visactor/vtable'; +import { merge } from 'lodash'; +import { filterStyles } from './styles'; /** * 筛选插件,负责初始化筛选引擎、状态管理器和工具栏 @@ -60,6 +62,8 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin { if (!this.pluginOptions.filterModes || !this.pluginOptions.filterModes.length) { this.pluginOptions.filterModes = ['byValue', 'byCondition']; } + + this.pluginOptions.styles = merge(filterStyles, this.pluginOptions.styles ?? {}); } run(...args: any[]) { @@ -71,7 +75,7 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin { if (runtime === TABLE_EVENT_TYPE.BEFORE_INIT) { this.filterEngine = new FilterEngine(); this.filterStateManager = new FilterStateManager(this.table, this.filterEngine); - this.filterToolbar = new FilterToolbar(this.table, this.filterStateManager); + this.filterToolbar = new FilterToolbar(this.table, this.filterStateManager, this.pluginOptions); this.columns = eventArgs.options.columns; this.filterToolbar.render(document.body); diff --git a/packages/vtable-plugins/src/filter/types.ts b/packages/vtable-plugins/src/filter/types.ts index 604c4c80c5..2269be30bf 100644 --- a/packages/vtable-plugins/src/filter/types.ts +++ b/packages/vtable-plugins/src/filter/types.ts @@ -13,6 +13,8 @@ export interface FilterOptions { defaultEnabled?: boolean; /** 是否展示按条件筛选,按值筛选 UI */ filterModes?: FilterMode[]; + /** 筛选器样式 */ + styles?: FilterStyles; } export type FilterMode = 'byValue' | 'byCondition'; @@ -92,3 +94,38 @@ export enum FilterOperatorCategory { } export type FilterListener = (state: FilterState, action?: FilterAction) => void; + +/** + * 筛选组件样式类型定义 + */ + +// 静态样式类型 +interface StaticStyles { + filterMenu?: Record; + filterPanel?: Record; + searchContainer?: Record; + searchInput?: Record; + optionsContainer?: Record; + optionItem?: Record; + optionLabel?: Record; + checkbox?: Record; + countSpan?: Record; + tabsContainer?: Record; + footerContainer?: Record; + clearLink?: Record; + conditionContainer?: Record; + formLabel?: Record; + operatorSelect?: Record; + rangeInputContainer?: Record; + addLabel?: Record; +} + +// 函数样式类型 +interface FunctionStyles { + tabStyle?: (isActive: boolean) => Record; + footerButton?: (isPrimary: boolean) => Record; + buttonStyle?: (isPrimary?: boolean) => Record; +} + +// 完整的筛选样式类型 +export type FilterStyles = StaticStyles & FunctionStyles; diff --git a/packages/vtable-plugins/src/filter/value-filter.ts b/packages/vtable-plugins/src/filter/value-filter.ts index 11dcf932e6..c03fe25155 100644 --- a/packages/vtable-plugins/src/filter/value-filter.ts +++ b/packages/vtable-plugins/src/filter/value-filter.ts @@ -1,13 +1,14 @@ -import { ListTable, PivotTable } from '@visactor/vtable'; +import type { ListTable, PivotTable } from '@visactor/vtable'; import { arrayEqual } from '@visactor/vutils'; -import type { FilterConfig, ValueFilterOptionDom, FilterState } from './types'; +import type { FilterConfig, ValueFilterOptionDom, FilterState, FilterOptions } from './types'; import { FilterActionType } from './types'; import type { FilterStateManager } from './filter-state-manager'; -import { applyStyles, filterStyles } from './styles'; +import { applyStyles } from './styles'; export class ValueFilter { private table: ListTable | PivotTable; private filterStateManager: FilterStateManager; + private styles: Record; private selectedField: string | number; private selectedKeys = new Map>(); // 存储 format 之前的原始数据 private candidateKeys = new Map>(); // 存储 format 后的数据 @@ -24,9 +25,10 @@ export class ValueFilter { private _onInputKeyUpHandler: (event: KeyboardEvent) => void; private _onCheckboxChangeHandler: (event: Event) => void; - constructor(table: ListTable | PivotTable, filterStateManager: FilterStateManager) { + constructor(table: ListTable | PivotTable, filterStateManager: FilterStateManager, pluginOptions: FilterOptions) { this.table = table; this.filterStateManager = filterStateManager; + this.styles = pluginOptions.styles || {}; } setSelectedField(fieldId: string | number): void { @@ -237,6 +239,7 @@ export class ValueFilter { } render(container: HTMLElement): void { + const filterStyles = this.styles; // === 按值筛选的菜单内容 === this.filterByValuePanel = document.createElement('div'); applyStyles(this.filterByValuePanel, filterStyles.filterPanel); @@ -285,6 +288,7 @@ export class ValueFilter { } private renderFilterOptions(field: string | number): void { + const filterStyles = this.styles; this.filterItemsContainer.innerHTML = ''; this.valueFilterOptionList.delete(field); this.valueFilterOptionList.set(field, []); From 5bb149c5be6b238010f2c88d3aeb2a48555a1dda Mon Sep 17 00:00:00 2001 From: skie1997 Date: Wed, 3 Dec 2025 19:45:41 +0800 Subject: [PATCH 02/34] feat: support custom conditionCategories. close#4781 --- ...-plugin-for-business_2025-12-03-11-45.json | 10 +++ packages/vtable-plugins/demo/filter/filter.ts | 12 +++- .../src/filter/condition-filter.ts | 69 ++++--------------- .../vtable-plugins/src/filter/constant.ts | 51 ++++++++++++++ packages/vtable-plugins/src/filter/filter.ts | 2 + packages/vtable-plugins/src/filter/types.ts | 7 ++ 6 files changed, 94 insertions(+), 57 deletions(-) create mode 100644 common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-03-11-45.json create mode 100644 packages/vtable-plugins/src/filter/constant.ts diff --git a/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-03-11-45.json b/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-03-11-45.json new file mode 100644 index 0000000000..a8e47d8ccc --- /dev/null +++ b/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-03-11-45.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "feat: support custom conditionCategories. close#4781", + "type": "none" + } + ], + "packageName": "@visactor/vtable" +} \ No newline at end of file diff --git a/packages/vtable-plugins/demo/filter/filter.ts b/packages/vtable-plugins/demo/filter/filter.ts index f7d4ec7530..1422b8267e 100644 --- a/packages/vtable-plugins/demo/filter/filter.ts +++ b/packages/vtable-plugins/demo/filter/filter.ts @@ -1,6 +1,6 @@ import * as VTable from '@visactor/vtable'; import { bindDebugTool } from '@visactor/vtable/es/scenegraph/debug-tool'; -import { FilterPlugin } from '../../src/filter'; +import { FilterOperatorCategory, FilterPlugin } from '../../src/filter'; const CONTAINER_ID = 'vTable'; /** @@ -248,7 +248,15 @@ export function createTable() { display: 'none', padding: '0 5px' } - } + }, + conditionCategories: [ + { value: FilterOperatorCategory.ALL, label: '全部' }, + { value: FilterOperatorCategory.TEXT, label: '文本' }, + { value: FilterOperatorCategory.NUMBER, label: '数值' } + // { value: FilterOperatorCategory.COLOR, label: '颜色' }, + // { value: FilterOperatorCategory.CHECKBOX, label: '复选框' }, + // { value: FilterOperatorCategory.RADIO, label: '单选框' } + ] }); (window as any).filterPlugin = filterPlugin; diff --git a/packages/vtable-plugins/src/filter/condition-filter.ts b/packages/vtable-plugins/src/filter/condition-filter.ts index 7665ff11d4..e5fecf6702 100644 --- a/packages/vtable-plugins/src/filter/condition-filter.ts +++ b/packages/vtable-plugins/src/filter/condition-filter.ts @@ -1,8 +1,9 @@ import type { ListTable, PivotTable } from '@visactor/vtable'; import type { FilterStateManager } from './filter-state-manager'; import { applyStyles, filterStyles, createElement } from './styles'; -import type { FilterOperator, FilterOptions, OperatorOption } from './types'; +import type { FilterOperator, FilterOperatorCategoryOption, FilterOptions, OperatorOption } from './types'; import { FilterActionType, FilterOperatorCategory } from './types'; +import { operators } from './constant'; /** * 按条件筛选组件 @@ -22,60 +23,16 @@ export class ConditionFilter { private categorySelect: HTMLSelectElement; private currentCategory: FilterOperatorCategory = FilterOperatorCategory.ALL; - // 按分类组织的操作符选项 - private operators: OperatorOption[] = [ - // 通用操作符 (全部分类中显示) - { value: 'equals', label: '等于', category: FilterOperatorCategory.ALL }, - { value: 'notEquals', label: '不等于', category: FilterOperatorCategory.ALL }, - - // 数值操作符 - { value: 'equals', label: '等于', category: FilterOperatorCategory.NUMBER }, - { value: 'notEquals', label: '不等于', category: FilterOperatorCategory.NUMBER }, - { value: 'greaterThan', label: '大于', category: FilterOperatorCategory.NUMBER }, - { value: 'lessThan', label: '小于', category: FilterOperatorCategory.NUMBER }, - { value: 'greaterThanOrEqual', label: '大于等于', category: FilterOperatorCategory.NUMBER }, - { value: 'lessThanOrEqual', label: '小于等于', category: FilterOperatorCategory.NUMBER }, - { value: 'between', label: '介于', category: FilterOperatorCategory.NUMBER }, - { value: 'notBetween', label: '不介于', category: FilterOperatorCategory.NUMBER }, - - // 文本操作符 - { value: 'equals', label: '等于', category: FilterOperatorCategory.TEXT }, - { value: 'notEquals', label: '不等于', category: FilterOperatorCategory.TEXT }, - { value: 'contains', label: '包含', category: FilterOperatorCategory.TEXT }, - { value: 'notContains', label: '不包含', category: FilterOperatorCategory.TEXT }, - { value: 'startsWith', label: '开头是', category: FilterOperatorCategory.TEXT }, - { value: 'notStartsWith', label: '开头不是', category: FilterOperatorCategory.TEXT }, - { value: 'endsWith', label: '结尾是', category: FilterOperatorCategory.TEXT }, - { value: 'notEndsWith', label: '结尾不是', category: FilterOperatorCategory.TEXT }, - - // 颜色操作符 - { value: 'equals', label: '等于', category: FilterOperatorCategory.COLOR }, - { value: 'notEquals', label: '不等于', category: FilterOperatorCategory.COLOR }, - - // 复选框操作符 - { value: 'isChecked', label: '已选中', category: FilterOperatorCategory.CHECKBOX }, - { value: 'isUnchecked', label: '未选中', category: FilterOperatorCategory.CHECKBOX }, - - // 单选框操作符 - { value: 'isChecked', label: '已选中', category: FilterOperatorCategory.RADIO }, - { value: 'isUnchecked', label: '未选中', category: FilterOperatorCategory.RADIO } - ]; - - // 分类下拉选项 - private categories = [ - { value: FilterOperatorCategory.ALL, label: '全部' }, - { value: FilterOperatorCategory.TEXT, label: '文本' }, - { value: FilterOperatorCategory.NUMBER, label: '数值' }, - { value: FilterOperatorCategory.COLOR, label: '颜色' }, - { value: FilterOperatorCategory.CHECKBOX, label: '复选框' }, - { value: FilterOperatorCategory.RADIO, label: '单选框' } - ]; + private categories: FilterOperatorCategoryOption[] = []; + protected operators: OperatorOption[] = []; constructor(table: ListTable | PivotTable, filterStateManager: FilterStateManager, pluginOptions: FilterOptions) { this.table = table; this.filterStateManager = filterStateManager; this.pluginOptions = pluginOptions; this.styles = pluginOptions.styles || {}; + this.categories = pluginOptions.conditionCategories; + this.operators = operators; } setSelectedField(fieldId: string | number): void { @@ -95,13 +52,15 @@ export class ConditionFilter { let filteredOperators: OperatorOption[]; if (this.currentCategory === FilterOperatorCategory.ALL) { - // 当选择"全部"时,收集所有不重复的操作符 + // 当选择"全部"时,收集所有配置的分类中, 不重复的操作符 const uniqueOperators = new Map(); - this.operators.forEach(op => { - if (!uniqueOperators.has(op.value)) { - uniqueOperators.set(op.value, op); - } - }); + this.operators + .filter(op => this.categories.map(cat => cat.value).includes(op.category)) + .forEach(op => { + if (!uniqueOperators.has(op.value)) { + uniqueOperators.set(op.value, op); + } + }); filteredOperators = Array.from(uniqueOperators.values()); } else { // 其他类别正常筛选 diff --git a/packages/vtable-plugins/src/filter/constant.ts b/packages/vtable-plugins/src/filter/constant.ts new file mode 100644 index 0000000000..d7e75c107d --- /dev/null +++ b/packages/vtable-plugins/src/filter/constant.ts @@ -0,0 +1,51 @@ +import type { OperatorOption } from './types'; +import { FilterOperatorCategory } from './types'; + +// 分类下拉选项 +export const categories = [ + { value: FilterOperatorCategory.ALL, label: '全部' }, + { value: FilterOperatorCategory.TEXT, label: '文本' }, + { value: FilterOperatorCategory.NUMBER, label: '数值' }, + { value: FilterOperatorCategory.COLOR, label: '颜色' }, + { value: FilterOperatorCategory.CHECKBOX, label: '复选框' }, + { value: FilterOperatorCategory.RADIO, label: '单选框' } +]; + +// 按分类组织的操作符选项 +export const operators: OperatorOption[] = [ + // 通用操作符 (全部分类中显示) + { value: 'equals', label: '等于', category: FilterOperatorCategory.ALL }, + { value: 'notEquals', label: '不等于', category: FilterOperatorCategory.ALL }, + + // 数值操作符 + { value: 'equals', label: '等于', category: FilterOperatorCategory.NUMBER }, + { value: 'notEquals', label: '不等于', category: FilterOperatorCategory.NUMBER }, + { value: 'greaterThan', label: '大于', category: FilterOperatorCategory.NUMBER }, + { value: 'lessThan', label: '小于', category: FilterOperatorCategory.NUMBER }, + { value: 'greaterThanOrEqual', label: '大于等于', category: FilterOperatorCategory.NUMBER }, + { value: 'lessThanOrEqual', label: '小于等于', category: FilterOperatorCategory.NUMBER }, + { value: 'between', label: '介于', category: FilterOperatorCategory.NUMBER }, + { value: 'notBetween', label: '不介于', category: FilterOperatorCategory.NUMBER }, + + // 文本操作符 + { value: 'equals', label: '等于', category: FilterOperatorCategory.TEXT }, + { value: 'notEquals', label: '不等于', category: FilterOperatorCategory.TEXT }, + { value: 'contains', label: '包含', category: FilterOperatorCategory.TEXT }, + { value: 'notContains', label: '不包含', category: FilterOperatorCategory.TEXT }, + { value: 'startsWith', label: '开头是', category: FilterOperatorCategory.TEXT }, + { value: 'notStartsWith', label: '开头不是', category: FilterOperatorCategory.TEXT }, + { value: 'endsWith', label: '结尾是', category: FilterOperatorCategory.TEXT }, + { value: 'notEndsWith', label: '结尾不是', category: FilterOperatorCategory.TEXT }, + + // 颜色操作符 + { value: 'equals', label: '等于', category: FilterOperatorCategory.COLOR }, + { value: 'notEquals', label: '不等于', category: FilterOperatorCategory.COLOR }, + + // 复选框操作符 + { value: 'isChecked', label: '已选中', category: FilterOperatorCategory.CHECKBOX }, + { value: 'isUnchecked', label: '未选中', category: FilterOperatorCategory.CHECKBOX }, + + // 单选框操作符 + { value: 'isChecked', label: '已选中', category: FilterOperatorCategory.RADIO }, + { value: 'isUnchecked', label: '未选中', category: FilterOperatorCategory.RADIO } +]; diff --git a/packages/vtable-plugins/src/filter/filter.ts b/packages/vtable-plugins/src/filter/filter.ts index d15f586816..a127824404 100644 --- a/packages/vtable-plugins/src/filter/filter.ts +++ b/packages/vtable-plugins/src/filter/filter.ts @@ -15,6 +15,7 @@ import type { } from '@visactor/vtable'; import { merge } from 'lodash'; import { filterStyles } from './styles'; +import { categories } from './constant'; /** * 筛选插件,负责初始化筛选引擎、状态管理器和工具栏 @@ -64,6 +65,7 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin { } this.pluginOptions.styles = merge(filterStyles, this.pluginOptions.styles ?? {}); + this.pluginOptions.conditionCategories = pluginOptions.conditionCategories ?? categories; } run(...args: any[]) { diff --git a/packages/vtable-plugins/src/filter/types.ts b/packages/vtable-plugins/src/filter/types.ts index 2269be30bf..ed27f568d0 100644 --- a/packages/vtable-plugins/src/filter/types.ts +++ b/packages/vtable-plugins/src/filter/types.ts @@ -15,6 +15,13 @@ export interface FilterOptions { filterModes?: FilterMode[]; /** 筛选器样式 */ styles?: FilterStyles; + /** 自定义筛选分类 */ + conditionCategories?: FilterOperatorCategoryOption[]; +} + +export interface FilterOperatorCategoryOption { + value: FilterOperatorCategory; + label: string; } export type FilterMode = 'byValue' | 'byCondition'; From c3becac51eeef25b48d044bf8fb4b8c2394ce952 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Wed, 3 Dec 2025 20:03:41 +0800 Subject: [PATCH 03/34] fix: filter swtich enable erroe. fix#4783 --- ...-plugin-for-business_2025-12-03-12-00.json | 10 ++++ packages/vtable-plugins/demo/filter/filter.ts | 9 +++- packages/vtable-plugins/src/filter/filter.ts | 46 +++++++++++-------- packages/vtable/src/ListTable.ts | 27 ++++++----- packages/vtable/src/plugins/plugin-manager.ts | 8 +++- 5 files changed, 69 insertions(+), 31 deletions(-) create mode 100644 common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-03-12-00.json diff --git a/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-03-12-00.json b/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-03-12-00.json new file mode 100644 index 0000000000..fff83e6fe2 --- /dev/null +++ b/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-03-12-00.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "fix: filter swtich enable erroe. fix#4783", + "type": "none" + } + ], + "packageName": "@visactor/vtable" +} \ No newline at end of file diff --git a/packages/vtable-plugins/demo/filter/filter.ts b/packages/vtable-plugins/demo/filter/filter.ts index 1422b8267e..3b462398c6 100644 --- a/packages/vtable-plugins/demo/filter/filter.ts +++ b/packages/vtable-plugins/demo/filter/filter.ts @@ -265,11 +265,18 @@ export function createTable() { records, columns, padding: 10, - plugins: [filterPlugin] + plugins: [] }; const tableInstance = new VTable.ListTable(option); (window as any).tableInstance = tableInstance; + setTimeout(() => { + tableInstance.updateOption({ + ...option, + plugins: [filterPlugin] + }); + }, 2000); + bindDebugTool(tableInstance.scenegraph.stage, { customGrapicKeys: ['col', 'row'] }); diff --git a/packages/vtable-plugins/src/filter/filter.ts b/packages/vtable-plugins/src/filter/filter.ts index a127824404..b9d10b7f00 100644 --- a/packages/vtable-plugins/src/filter/filter.ts +++ b/packages/vtable-plugins/src/filter/filter.ts @@ -68,6 +68,26 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin { this.pluginOptions.conditionCategories = pluginOptions.conditionCategories ?? categories; } + initFilterPlugin(eventArgs: any) { + this.filterEngine = new FilterEngine(); + this.filterStateManager = new FilterStateManager(this.table, this.filterEngine); + this.filterToolbar = new FilterToolbar(this.table, this.filterStateManager, this.pluginOptions); + this.columns = eventArgs.options.columns; + + this.filterToolbar.render(document.body); + this.updateFilterIcons(this.columns); + this.filterStateManager.subscribe((_: FilterState, action?: FilterAction) => { + // 新增筛选配置时,不需要更新筛选图标以及表格 + if (action?.type === FilterActionType.ADD_FILTER) { + return; + } + this.updateFilterIcons(this.columns); + (this.table as ListTable).updateColumns(this.columns, { + clearRowHeightCache: false + }); + }); + } + run(...args: any[]) { const eventArgs = args[0]; const runtime = args[1]; @@ -75,27 +95,14 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin { this.table = table as ListTable | PivotTable; if (runtime === TABLE_EVENT_TYPE.BEFORE_INIT) { - this.filterEngine = new FilterEngine(); - this.filterStateManager = new FilterStateManager(this.table, this.filterEngine); - this.filterToolbar = new FilterToolbar(this.table, this.filterStateManager, this.pluginOptions); - this.columns = eventArgs.options.columns; - - this.filterToolbar.render(document.body); - this.updateFilterIcons(this.columns); - this.filterStateManager.subscribe((_: FilterState, action?: FilterAction) => { - // 新增筛选配置时,不需要更新筛选图标以及表格 - if (action?.type === FilterActionType.ADD_FILTER) { - return; - } - this.updateFilterIcons(this.columns); - (this.table as ListTable).updateColumns(this.columns, { - clearRowHeightCache: false - }); - }); + this.initFilterPlugin(eventArgs); } else if (runtime === TABLE_EVENT_TYPE.BEFORE_UPDATE_OPTION) { + if (!this.filterEngine || !this.filterStateManager || !this.filterToolbar) { + this.initFilterPlugin(eventArgs); + } this.pluginOptions = { ...this.pluginOptions, - ...(eventArgs.options.plugins as FilterPlugin[]).find(plugin => plugin.id === this.id).pluginOptions + ...(eventArgs.options.plugins as FilterPlugin[])?.find(plugin => plugin.id === this.id)?.pluginOptions }; this.columns = eventArgs.options.columns; this.handleOptionUpdate(eventArgs.options); @@ -319,6 +326,9 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin { } release() { + this.columns.forEach(column => { + column.headerIcon = undefined; + }); this.table = null; this.filterEngine = null; this.filterStateManager = null; diff --git a/packages/vtable/src/ListTable.ts b/packages/vtable/src/ListTable.ts index 8538d12e19..4a0f7d16d2 100644 --- a/packages/vtable/src/ListTable.ts +++ b/packages/vtable/src/ListTable.ts @@ -126,8 +126,8 @@ export class ListTable extends BaseTable implements ListTableAPI { internalProps.columns = options.columns ? cloneDeepSpec(options.columns, ['children']) // children for react : options.header - ? cloneDeepSpec(options.header, ['children']) - : []; + ? cloneDeepSpec(options.header, ['children']) + : []; generateAggregationForColumn(this); // options.columns?.forEach((colDefine, index) => { // //如果editor 是一个IEditor的实例 需要这样重新赋值 否则clone后变质了 @@ -636,6 +636,7 @@ export class ListTable extends BaseTable implements ListTableAPI { } ) { const internalProps = this.internalProps; + this.pluginManager.updatePlugins(options.plugins); super.updateOption(options, updateConfig); internalProps.frozenColDragHeaderMode = options.dragOrder?.frozenColDragHeaderMode ?? options.frozenColDragHeaderMode; @@ -651,8 +652,8 @@ export class ListTable extends BaseTable implements ListTableAPI { internalProps.columns = options.columns ? cloneDeepSpec(options.columns, ['children']) : options.header - ? cloneDeepSpec(options.header, ['children']) - : []; + ? cloneDeepSpec(options.header, ['children']) + : []; generateAggregationForColumn(this); // options.columns.forEach((colDefine, index) => { // if (colDefine.editor) { @@ -717,7 +718,7 @@ export class ListTable extends BaseTable implements ListTableAPI { this.internalProps.emptyTip?.resetVisible(); } } - this.pluginManager.updatePlugins(options.plugins); + // this.pluginManager.updatePlugins(options.plugins); setTimeout(() => { this.fireListeners(TABLE_EVENT_TYPE.UPDATED, null); }, 0); @@ -1158,6 +1159,7 @@ export class ListTable extends BaseTable implements ListTableAPI { this.clearCellStyleCache(); this.internalProps.layoutMap.clearCellRangeMap(); this.internalProps.useOneRowHeightFillAll = false; + // ts-ignore // this.scenegraph.updateHierarchyIcon(col, row);// 添加了updateCells:[{ col, row }] 就不需要单独更新图标了(只更新图标针对有自定义元素的情况 会有更新不到问题)' // const updateCells = [{ col, row }]; // // 如果需要移出的节点超过了当前加载部分最后一行 则转变成更新对应的行 @@ -1356,7 +1358,7 @@ export class ListTable extends BaseTable implements ListTableAPI { return state && state[field]; }); } - return new Array(...this.stateManager.checkedState.values()); + return [...this.stateManager.checkedState.values()]; } /** 获取某个单元格checkbox的状态 */ getCellCheckboxState(col: number, row: number) { @@ -1529,8 +1531,8 @@ export class ListTable extends BaseTable implements ListTableAPI { getEditor(col: number, row: number) { const define = this.getBodyColumnDefine(col, row); let editorDefine = this.isHeader(col, row) - ? (define as ColumnDefine)?.headerEditor ?? this.options.headerEditor - : (define as ColumnDefine)?.editor ?? this.options.editor; + ? ((define as ColumnDefine)?.headerEditor ?? this.options.headerEditor) + : ((define as ColumnDefine)?.editor ?? this.options.editor); if (typeof editorDefine === 'function') { const arg = { @@ -1551,8 +1553,8 @@ export class ListTable extends BaseTable implements ListTableAPI { isHasEditorDefine(col: number, row: number) { const define = this.getBodyColumnDefine(col, row); let editorDefine = this.isHeader(col, row) - ? (define as ColumnDefine)?.headerEditor ?? this.options.headerEditor - : (define as ColumnDefine)?.editor ?? this.options.editor; + ? ((define as ColumnDefine)?.headerEditor ?? this.options.headerEditor) + : ((define as ColumnDefine)?.editor ?? this.options.editor); if (typeof editorDefine === 'function') { const arg = { @@ -1845,7 +1847,10 @@ export class ListTable extends BaseTable implements ListTableAPI { }); } } - /** 合并单元格 对外接口 。会自动刷新渲染节点 + /** 获取某个单元格checkbox的状态 */ + + /** + * 合并单元格 对外接口 。会自动刷新渲染节点 * 注意:如果之前options有customMergeCell的函数配置,将失效重置为空数组 */ mergeCells(startCol: number, startRow: number, endCol: number, endRow: number) { diff --git a/packages/vtable/src/plugins/plugin-manager.ts b/packages/vtable/src/plugins/plugin-manager.ts index b1447ee6f3..8f07f0721e 100644 --- a/packages/vtable/src/plugins/plugin-manager.ts +++ b/packages/vtable/src/plugins/plugin-manager.ts @@ -6,6 +6,8 @@ export class PluginManager { private plugins: Map = new Map(); private table: BaseTableAPI; + private pluginEventMap: Map = new Map(); + constructor(table: BaseTableAPI, options: BaseTableConstructorOptions) { this.table = table; options.plugins?.map(plugin => { @@ -34,9 +36,10 @@ export class PluginManager { _bindTableEventForPlugin(plugin: IVTablePlugin) { plugin.runTime?.forEach(runTime => { - this.table.on(runTime, (...args) => { + const id = this.table.on(runTime, (...args) => { plugin.run?.(...args, runTime, this.table); }); + this.pluginEventMap.set(plugin.id, [...(this.pluginEventMap.get(plugin.id) || []), id]); }); } @@ -45,6 +48,9 @@ export class PluginManager { // 先找到plugins中没有,但this.plugins中有,也就是已经被移除的插件 const removedPlugins = Array.from(this.plugins.values()).filter(plugin => !plugins?.some(p => p.id === plugin.id)); removedPlugins.forEach(plugin => { + this.pluginEventMap.get(plugin.id)?.forEach(id => { + this.table.off(id); + }); this.release(); this.plugins.delete(plugin.id); }); From d2d1b5a75dd626c2d05d2b4e9ca29c39c6e34f9c Mon Sep 17 00:00:00 2001 From: skie1997 Date: Wed, 3 Dec 2025 20:31:57 +0800 Subject: [PATCH 04/34] feat: emit event when filter menu hide or show. close#4784 --- ...-plugin-for-business_2025-12-03-12-31.json | 10 ++++++++++ packages/vtable-plugins/demo/filter/filter.ts | 4 ++++ .../src/filter/filter-toolbar.ts | 20 +++++++++++++------ packages/vtable-plugins/src/filter/filter.ts | 6 +++++- packages/vtable/src/core/TABLE_EVENT_TYPE.ts | 13 +++++++++++- packages/vtable/src/ts-types/events.ts | 3 +++ 6 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-03-12-31.json diff --git a/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-03-12-31.json b/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-03-12-31.json new file mode 100644 index 0000000000..65435cc87c --- /dev/null +++ b/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-03-12-31.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "feat: emit event when filter menu hide or show. close#4784", + "type": "none" + } + ], + "packageName": "@visactor/vtable" +} \ No newline at end of file diff --git a/packages/vtable-plugins/demo/filter/filter.ts b/packages/vtable-plugins/demo/filter/filter.ts index 3b462398c6..fa7b10cff4 100644 --- a/packages/vtable-plugins/demo/filter/filter.ts +++ b/packages/vtable-plugins/demo/filter/filter.ts @@ -270,6 +270,10 @@ export function createTable() { const tableInstance = new VTable.ListTable(option); (window as any).tableInstance = tableInstance; + tableInstance.on(VTable.ListTable.EVENT_TYPE.FILTER_MENU_SHOW, (...args) => { + console.log('filter_menu_show', args); + }); + setTimeout(() => { tableInstance.updateOption({ ...option, diff --git a/packages/vtable-plugins/src/filter/filter-toolbar.ts b/packages/vtable-plugins/src/filter/filter-toolbar.ts index 065fcc77c3..6d9e10469b 100644 --- a/packages/vtable-plugins/src/filter/filter-toolbar.ts +++ b/packages/vtable-plugins/src/filter/filter-toolbar.ts @@ -1,4 +1,4 @@ -import type { ListTable, PivotTable } from '@visactor/vtable'; +import { TABLE_EVENT_TYPE, type ListTable, type PivotTable } from '@visactor/vtable'; import type { FilterStateManager } from './filter-state-manager'; import { ValueFilter } from './value-filter'; import { ConditionFilter } from './condition-filter'; @@ -78,7 +78,7 @@ export class FilterToolbar { } else if (this.activeTab === 'byCondition') { this.conditionFilter.applyFilter(field); } - this.hide(); + this.hide(this.currentCol, this.currentRow); } private clearFilter(field: string | number): void { @@ -88,7 +88,7 @@ export class FilterToolbar { if (this.conditionFilter) { this.conditionFilter.clearFilter(field); } - this.hide(); + this.hide(this.currentCol, this.currentRow); } /** @@ -170,7 +170,7 @@ export class FilterToolbar { this.onTabSwitch('byCondition'); }); - this.cancelFilterButton.addEventListener('click', () => this.hide()); + this.cancelFilterButton.addEventListener('click', () => this.hide(this.currentCol, this.currentRow)); this.clearFilterOptionLink.addEventListener('click', e => { e.preventDefault(); @@ -184,7 +184,7 @@ export class FilterToolbar { // 点击空白处整个筛选菜单可消失 document.addEventListener('click', () => { if (this.isVisible) { - this.hide(); + this.hide(this.currentCol, this.currentRow); } }); @@ -269,11 +269,19 @@ export class FilterToolbar { // 确保在事件冒泡完成后才设置 isVisible 为 true setTimeout(() => { this.isVisible = true; + this.table.fireListeners(TABLE_EVENT_TYPE.FILTER_MENU_SHOW, { + col: col, + row: row + }); }, 0); } - hide(): void { + hide(currentCol: number | null, currentRow: number | null): void { this.filterMenu.style.display = 'none'; this.isVisible = false; + this.table.fireListeners(TABLE_EVENT_TYPE.FILTER_MENU_HIDE, { + col: currentCol, + row: currentRow + }); } } diff --git a/packages/vtable-plugins/src/filter/filter.ts b/packages/vtable-plugins/src/filter/filter.ts index b9d10b7f00..d0ed0119af 100644 --- a/packages/vtable-plugins/src/filter/filter.ts +++ b/packages/vtable-plugins/src/filter/filter.ts @@ -120,9 +120,13 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin { const col = eventArgs.col; const row = eventArgs.row; if (this.filterToolbar.isVisible) { - this.filterToolbar.hide(); + this.filterToolbar.hide(eventArgs.col, eventArgs.row); } else { this.filterToolbar.show(col, row, this.pluginOptions.filterModes); + this.table.fireListeners(TABLE_EVENT_TYPE.FILTER_MENU_SHOW, { + col: eventArgs.col, + row: eventArgs.row + }); } } else if (runtime === TABLE_EVENT_TYPE.SCROLL) { if (eventArgs.scrollDirection === 'horizontal') { diff --git a/packages/vtable/src/core/TABLE_EVENT_TYPE.ts b/packages/vtable/src/core/TABLE_EVENT_TYPE.ts index d816a3a268..b105d7b826 100644 --- a/packages/vtable/src/core/TABLE_EVENT_TYPE.ts +++ b/packages/vtable/src/core/TABLE_EVENT_TYPE.ts @@ -247,6 +247,14 @@ export interface TableEvents { * 删除列事件 */ DELETE_COLUMN: 'delete_column'; + /** + * 筛选菜单显示事件 + */ + FILTER_MENU_SHOW: 'filter_menu_show'; + /** + * 筛选菜单隐藏事件 + */ + FILTER_MENU_HIDE: 'filter_menu_hide'; } /** * Table event types @@ -338,5 +346,8 @@ export const TABLE_EVENT_TYPE: TableEvents = { DELETE_RECORD: 'delete_record', UPDATE_RECORD: 'update_record', ADD_COLUMN: 'add_column', - DELETE_COLUMN: 'delete_column' + DELETE_COLUMN: 'delete_column', + + FILTER_MENU_SHOW: 'filter_menu_show', + FILTER_MENU_HIDE: 'filter_menu_hide' } as TableEvents; diff --git a/packages/vtable/src/ts-types/events.ts b/packages/vtable/src/ts-types/events.ts index fd1890bd30..eca14ff897 100644 --- a/packages/vtable/src/ts-types/events.ts +++ b/packages/vtable/src/ts-types/events.ts @@ -447,4 +447,7 @@ export interface TableEventHandlersReturnMap { update_record: void; add_column: void; delete_column: void; + + filter_menu_show: { col: number; row: number }; + filter_menu_hide: { col: number; row: number }; } From dec6523f5f1f47fdc0b1a5f425ccd968e8e86448 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Wed, 3 Dec 2025 20:44:41 +0800 Subject: [PATCH 05/34] fix: apply filter after update table data. fix#4785 --- ...-plugin-for-business_2025-12-03-12-44.json | 10 ++++ packages/vtable-plugins/demo/filter/filter.ts | 59 ++++++++++++++++--- packages/vtable/src/ListTable.ts | 5 +- packages/vtable/src/plugins/plugin-manager.ts | 20 ++++--- 4 files changed, 75 insertions(+), 19 deletions(-) create mode 100644 common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-03-12-44.json diff --git a/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-03-12-44.json b/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-03-12-44.json new file mode 100644 index 0000000000..838d05be18 --- /dev/null +++ b/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-03-12-44.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "fix: apply filter after update table data. fix#4785", + "type": "none" + } + ], + "packageName": "@visactor/vtable" +} \ No newline at end of file diff --git a/packages/vtable-plugins/demo/filter/filter.ts b/packages/vtable-plugins/demo/filter/filter.ts index fa7b10cff4..7c2a03d0b3 100644 --- a/packages/vtable-plugins/demo/filter/filter.ts +++ b/packages/vtable-plugins/demo/filter/filter.ts @@ -1,13 +1,14 @@ import * as VTable from '@visactor/vtable'; import { bindDebugTool } from '@visactor/vtable/es/scenegraph/debug-tool'; import { FilterOperatorCategory, FilterPlugin } from '../../src/filter'; +import { ListTable } from '@visactor/vtable'; const CONTAINER_ID = 'vTable'; /** * 生成展示筛选功能的演示数据 * 包含各种类型的数据:文本、数值、日期、布尔值、颜色等 */ -const generateDemoData = (count: number) => { +const generateDemoData = (count: number, prefix: string) => { const colors = ['#f5a623', '#7ed321', '#bd10e0', '#4a90e2', '#50e3c2', '#ff5a5f', '#000000']; const departments = ['研发部', '市场部', '销售部', '人事部', '财务部', '设计部', '客服部', '运营部']; @@ -19,7 +20,7 @@ const generateDemoData = (count: number) => { return { id: i + 1, - name: `员工${i + 1}`, + name: `${prefix}_员工${i + 1}`, gender: i % 2 === 0 ? '男' : '女', salary, sales, @@ -39,7 +40,7 @@ const generateDemoData = (count: number) => { }; export function createTable() { - const records = generateDemoData(50); + const records = generateDemoData(50, '第一次'); const columns: VTable.ColumnsDefine = [ { field: 'id', @@ -257,31 +258,71 @@ export function createTable() { // { value: FilterOperatorCategory.CHECKBOX, label: '复选框' }, // { value: FilterOperatorCategory.RADIO, label: '单选框' } ] + // syncCheckboxCheckedState: false }); (window as any).filterPlugin = filterPlugin; const option: VTable.ListTableConstructorOptions = { container: document.getElementById(CONTAINER_ID), - records, + records: [...records], columns, padding: 10, - plugins: [] + plugins: [filterPlugin] }; const tableInstance = new VTable.ListTable(option); (window as any).tableInstance = tableInstance; - - tableInstance.on(VTable.ListTable.EVENT_TYPE.FILTER_MENU_SHOW, (...args) => { + tableInstance.on(ListTable.EVENT_TYPE.FILTER_MENU_SHOW, (...args) => { console.log('filter_menu_show', args); + tableInstance.arrangeCustomCellStyle({ col: 1, row: 0 }, 'header_highlight'); + }); + + tableInstance.on(ListTable.EVENT_TYPE.FILTER_MENU_HIDE, (...args) => { + console.log('filter_menu_hide', args); + tableInstance.arrangeCustomCellStyle({ col: 1, row: 0 }, 'header_highlight'); + }); + + tableInstance.registerCustomCellStyle('header_highlight', { + bgColor: 'red' }); setTimeout(() => { + // filterPlugin.updatePluginOptions({ + // styles: { + // optionItem: { + // display: 'flex', + // justifyContent: 'space-between', + // alignItems: 'center', + // padding: '8px 5px', + // color: 'blue' + // } + // } + // }); + console.log('update'); tableInstance.updateOption({ ...option, - plugins: [filterPlugin] + plugins: [filterPlugin], + records: generateDemoData(50, '第二次') }); - }, 2000); + }, 5000); + + // setTimeout(() => { + // console.log('update'); + // tableInstance.updateOption({ + // ...option, + // plugins: [filterPlugin] + // }); + // }, 8000); bindDebugTool(tableInstance.scenegraph.stage, { customGrapicKeys: ['col', 'row'] }); + + tableInstance.on('click_cell', (...args) => { + console.log('click_cell', args); + }); + tableInstance.on('icon_click', (...args) => { + args[0].event.stopPropagation(); + args[0].event.preventDefault(); + console.log('icon_click'); + }); } diff --git a/packages/vtable/src/ListTable.ts b/packages/vtable/src/ListTable.ts index 4a0f7d16d2..a5de25d11d 100644 --- a/packages/vtable/src/ListTable.ts +++ b/packages/vtable/src/ListTable.ts @@ -636,7 +636,8 @@ export class ListTable extends BaseTable implements ListTableAPI { } ) { const internalProps = this.internalProps; - this.pluginManager.updatePlugins(options.plugins); + + this.pluginManager.removeOrAddPlugins(options.plugins); super.updateOption(options, updateConfig); internalProps.frozenColDragHeaderMode = options.dragOrder?.frozenColDragHeaderMode ?? options.frozenColDragHeaderMode; @@ -718,7 +719,7 @@ export class ListTable extends BaseTable implements ListTableAPI { this.internalProps.emptyTip?.resetVisible(); } } - // this.pluginManager.updatePlugins(options.plugins); + this.pluginManager.updatePlugins(options.plugins); setTimeout(() => { this.fireListeners(TABLE_EVENT_TYPE.UPDATED, null); }, 0); diff --git a/packages/vtable/src/plugins/plugin-manager.ts b/packages/vtable/src/plugins/plugin-manager.ts index 8f07f0721e..3fa669c082 100644 --- a/packages/vtable/src/plugins/plugin-manager.ts +++ b/packages/vtable/src/plugins/plugin-manager.ts @@ -43,8 +43,8 @@ export class PluginManager { }); } - // 更新所有插件 - updatePlugins(plugins?: IVTablePlugin[]): void { + // 移除或添加插件 + removeOrAddPlugins(plugins?: IVTablePlugin[]): void { // 先找到plugins中没有,但this.plugins中有,也就是已经被移除的插件 const removedPlugins = Array.from(this.plugins.values()).filter(plugin => !plugins?.some(p => p.id === plugin.id)); removedPlugins.forEach(plugin => { @@ -54,12 +54,7 @@ export class PluginManager { this.release(); this.plugins.delete(plugin.id); }); - // 更新插件 - this.plugins.forEach(plugin => { - if (plugin.update) { - plugin.update(); - } - }); + // 添加新插件 const addedPlugins = plugins?.filter(plugin => !this.plugins.has(plugin.id)); addedPlugins?.forEach(plugin => { @@ -67,6 +62,15 @@ export class PluginManager { this._bindTableEventForPlugin(plugin); }); } + + // 更新插件 + updatePlugins(plugins?: IVTablePlugin[]): void { + plugins?.forEach(plugin => { + if (plugin.update) { + plugin.update(); + } + }); + } release() { this.plugins.forEach(plugin => { plugin.release?.(this.table); From 6429ffb63b6888194f22952c0e3b84510493f40a Mon Sep 17 00:00:00 2001 From: skie1997 Date: Wed, 3 Dec 2025 21:02:06 +0800 Subject: [PATCH 06/34] feat: add option to format display value. close#4786 --- ...at-filter-plugin-for-business_2025-12-03-13-00.json | 10 ++++++++++ packages/vtable-plugins/demo/filter/filter.ts | 6 ++++-- packages/vtable-plugins/src/filter/types.ts | 2 ++ packages/vtable-plugins/src/filter/value-filter.ts | 4 +++- 4 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-03-13-00.json diff --git a/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-03-13-00.json b/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-03-13-00.json new file mode 100644 index 0000000000..4a09adec33 --- /dev/null +++ b/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-03-13-00.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "feat: add option to format display value. close#4786", + "type": "none" + } + ], + "packageName": "@visactor/vtable" +} \ No newline at end of file diff --git a/packages/vtable-plugins/demo/filter/filter.ts b/packages/vtable-plugins/demo/filter/filter.ts index 7c2a03d0b3..78c95cf183 100644 --- a/packages/vtable-plugins/demo/filter/filter.ts +++ b/packages/vtable-plugins/demo/filter/filter.ts @@ -257,8 +257,10 @@ export function createTable() { // { value: FilterOperatorCategory.COLOR, label: '颜色' }, // { value: FilterOperatorCategory.CHECKBOX, label: '复选框' }, // { value: FilterOperatorCategory.RADIO, label: '单选框' } - ] - // syncCheckboxCheckedState: false + ], + checkboxItemFormat: (formatValue, rawValue) => { + return formatValue; + } }); (window as any).filterPlugin = filterPlugin; diff --git a/packages/vtable-plugins/src/filter/types.ts b/packages/vtable-plugins/src/filter/types.ts index ed27f568d0..f0e5ec669f 100644 --- a/packages/vtable-plugins/src/filter/types.ts +++ b/packages/vtable-plugins/src/filter/types.ts @@ -17,6 +17,8 @@ export interface FilterOptions { styles?: FilterStyles; /** 自定义筛选分类 */ conditionCategories?: FilterOperatorCategoryOption[]; + /** 筛选选项是否展示为原始值 */ + checkboxItemFormat?: (rawValue: any, formatValue: any) => any; } export interface FilterOperatorCategoryOption { diff --git a/packages/vtable-plugins/src/filter/value-filter.ts b/packages/vtable-plugins/src/filter/value-filter.ts index c03fe25155..130d06d68f 100644 --- a/packages/vtable-plugins/src/filter/value-filter.ts +++ b/packages/vtable-plugins/src/filter/value-filter.ts @@ -8,6 +8,7 @@ import { applyStyles } from './styles'; export class ValueFilter { private table: ListTable | PivotTable; private filterStateManager: FilterStateManager; + private pluginOptions: FilterOptions; private styles: Record; private selectedField: string | number; private selectedKeys = new Map>(); // 存储 format 之前的原始数据 @@ -28,6 +29,7 @@ export class ValueFilter { constructor(table: ListTable | PivotTable, filterStateManager: FilterStateManager, pluginOptions: FilterOptions) { this.table = table; this.filterStateManager = filterStateManager; + this.pluginOptions = pluginOptions; this.styles = pluginOptions.styles || {}; } @@ -337,7 +339,7 @@ export class ValueFilter { countSpan.textContent = String(count); applyStyles(countSpan, filterStyles.countSpan); - label.append(checkbox, ` ${val}`); // UI显示格式化值 + label.append(checkbox, ` ${this.pluginOptions.checkboxItemFormat?.(val, unformattedArr) || val}`); // UI显示格式化值 或 用户二次加工的值 itemDiv.append(label, countSpan); this.filterItemsContainer.appendChild(itemDiv); From a9b390bf53ef6de258d226cc32bcf3c399072a6a Mon Sep 17 00:00:00 2001 From: skie1997 Date: Wed, 3 Dec 2025 21:03:14 +0800 Subject: [PATCH 07/34] fix: update error in focus highlight plugin --- packages/vtable-plugins/src/focus-highlight.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/vtable-plugins/src/focus-highlight.ts b/packages/vtable-plugins/src/focus-highlight.ts index 13dad2da00..797a2b8139 100644 --- a/packages/vtable-plugins/src/focus-highlight.ts +++ b/packages/vtable-plugins/src/focus-highlight.ts @@ -155,6 +155,8 @@ export class FocusHighlightPlugin implements pluginsDefinition.IVTablePlugin { }); } update() { - this.setFocusHighlightRange(this.range, true); + if (this.table) { + this.setFocusHighlightRange(this.range, true); + } } } From 8b934e83f2d5fce61115b558c58f0a1afe563c5d Mon Sep 17 00:00:00 2001 From: skie1997 Date: Thu, 4 Dec 2025 15:03:03 +0800 Subject: [PATCH 08/34] fix: update filter state and keys when update data. fix#4787 --- ...-plugin-for-business_2025-12-04-07-02.json | 10 +++++++ packages/vtable-plugins/demo/filter/filter.ts | 2 +- packages/vtable-plugins/src/filter/filter.ts | 1 + .../vtable-plugins/src/filter/value-filter.ts | 26 +++++++++++++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-04-07-02.json diff --git a/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-04-07-02.json b/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-04-07-02.json new file mode 100644 index 0000000000..6551a912d4 --- /dev/null +++ b/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-04-07-02.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "fix: update filter state and keys when update data. fix#4787", + "type": "none" + } + ], + "packageName": "@visactor/vtable" +} \ No newline at end of file diff --git a/packages/vtable-plugins/demo/filter/filter.ts b/packages/vtable-plugins/demo/filter/filter.ts index 78c95cf183..d2b8c0bb93 100644 --- a/packages/vtable-plugins/demo/filter/filter.ts +++ b/packages/vtable-plugins/demo/filter/filter.ts @@ -305,7 +305,7 @@ export function createTable() { plugins: [filterPlugin], records: generateDemoData(50, '第二次') }); - }, 5000); + }, 8000); // setTimeout(() => { // console.log('update'); diff --git a/packages/vtable-plugins/src/filter/filter.ts b/packages/vtable-plugins/src/filter/filter.ts index d0ed0119af..1d8c11b361 100644 --- a/packages/vtable-plugins/src/filter/filter.ts +++ b/packages/vtable-plugins/src/filter/filter.ts @@ -136,6 +136,7 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin { } update() { + this.filterToolbar.valueFilter?.update(); if (this.filterStateManager) { this.reapplyActiveFilters(); } diff --git a/packages/vtable-plugins/src/filter/value-filter.ts b/packages/vtable-plugins/src/filter/value-filter.ts index 130d06d68f..06b9b20351 100644 --- a/packages/vtable-plugins/src/filter/value-filter.ts +++ b/packages/vtable-plugins/src/filter/value-filter.ts @@ -85,6 +85,32 @@ export class ValueFilter { this.toUnformattedCache.set(fieldId, toUnformatted); } + update() { + // 表格更新时, 可能会插入新数据, 此时需要更新筛选结果和候选值: + // 1. 更新筛选结果 + // - 出现了之前没有出现过的选项 + // - 筛选器出于被筛选状态(有值) + // - 则将该选项添加到筛选器中 + // 2. 更新候选值 + const currentRecords = this.table.internalProps.dataSource.records; // 当前数据 + const filteredFields = this.filterStateManager.getActiveFilterFields(); + currentRecords.forEach(record => { + filteredFields.forEach(candidateField => { + const formatFn = this.getFormatFnCache(candidateField); + const originalValue = record[candidateField]; + const formattedValue = formatFn(record); + const lastToUnformatted = this.toUnformattedCache.get(candidateField) || new Map(); + if ( + !lastToUnformatted.has(formattedValue) && + this.filterStateManager.getFilterState(candidateField)?.values?.length > 0 + ) { + this.filterStateManager.getFilterState(candidateField).values.push(originalValue); + this.selectedKeys.get(candidateField).add(originalValue); + } + }); + }); + } + /** * 为已应用筛选的列,收集候选值集合 */ From cc15f0d32435ebd8ea6191520fe2f835bb1f8fa2 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Tue, 2 Dec 2025 20:32:32 +0800 Subject: [PATCH 09/34] feat: add update styles api for filter plugin.close#4790 --- ...-plugin-custom-style_2025-12-02-12-32.json | 10 +++ packages/vtable-plugins/demo/filter/filter.ts | 26 ++++--- .../src/filter/condition-filter.ts | 77 +++++++++++++------ .../src/filter/filter-toolbar.ts | 33 +++++--- packages/vtable-plugins/src/filter/filter.ts | 8 ++ .../vtable-plugins/src/filter/value-filter.ts | 46 +++++++---- 6 files changed, 140 insertions(+), 60 deletions(-) create mode 100644 common/changes/@visactor/vtable/feat-filter-plugin-custom-style_2025-12-02-12-32.json diff --git a/common/changes/@visactor/vtable/feat-filter-plugin-custom-style_2025-12-02-12-32.json b/common/changes/@visactor/vtable/feat-filter-plugin-custom-style_2025-12-02-12-32.json new file mode 100644 index 0000000000..8e091649cb --- /dev/null +++ b/common/changes/@visactor/vtable/feat-filter-plugin-custom-style_2025-12-02-12-32.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "feat: add update styles api for filter plugin.close#4790", + "type": "none" + } + ], + "packageName": "@visactor/vtable" +} \ No newline at end of file diff --git a/packages/vtable-plugins/demo/filter/filter.ts b/packages/vtable-plugins/demo/filter/filter.ts index d2b8c0bb93..348ee85b58 100644 --- a/packages/vtable-plugins/demo/filter/filter.ts +++ b/packages/vtable-plugins/demo/filter/filter.ts @@ -288,17 +288,21 @@ export function createTable() { }); setTimeout(() => { - // filterPlugin.updatePluginOptions({ - // styles: { - // optionItem: { - // display: 'flex', - // justifyContent: 'space-between', - // alignItems: 'center', - // padding: '8px 5px', - // color: 'blue' - // } - // } - // }); + filterPlugin.updatePluginOptions({ + styles: { + searchInput: { + placeholder: 'xxx', + color: 'blue' + }, + optionItem: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '8px 5px', + color: 'blue' + } + } + }); console.log('update'); tableInstance.updateOption({ ...option, diff --git a/packages/vtable-plugins/src/filter/condition-filter.ts b/packages/vtable-plugins/src/filter/condition-filter.ts index e5fecf6702..a106138fb5 100644 --- a/packages/vtable-plugins/src/filter/condition-filter.ts +++ b/packages/vtable-plugins/src/filter/condition-filter.ts @@ -1,7 +1,13 @@ import type { ListTable, PivotTable } from '@visactor/vtable'; import type { FilterStateManager } from './filter-state-manager'; import { applyStyles, filterStyles, createElement } from './styles'; -import type { FilterOperator, FilterOperatorCategoryOption, FilterOptions, OperatorOption } from './types'; +import type { + FilterOperator, + FilterOperatorCategoryOption, + FilterOptions, + FilterStyles, + OperatorOption +} from './types'; import { FilterActionType, FilterOperatorCategory } from './types'; import { operators } from './constant'; @@ -16,11 +22,18 @@ export class ConditionFilter { private styles: Record; private filterByConditionPanel: HTMLElement; + private conditionContainer: HTMLElement; + private categoryLabel: HTMLElement; private selectedField: string | number; private operatorSelect: HTMLSelectElement; private valueInput: HTMLInputElement; + private andLabel: HTMLElement; private valueInputMax: HTMLInputElement; private categorySelect: HTMLSelectElement; + private operatorLabel: HTMLElement; + private rangeInputContainer: HTMLElement; + private valueLabel: HTMLElement; + private currentCategory: FilterOperatorCategory = FilterOperatorCategory.ALL; private categories: FilterOperatorCategoryOption[] = []; @@ -281,12 +294,12 @@ export class ConditionFilter { applyStyles(this.filterByConditionPanel, filterStyles.filterPanel); // 条件选择区域 - const conditionContainer = document.createElement('div'); - applyStyles(conditionContainer, filterStyles.conditionContainer); + this.conditionContainer = document.createElement('div'); + applyStyles(this.conditionContainer, this.styles.conditionContainer); // 分类选择下拉框 - const categoryLabel = createElement('label', {}, ['筛选类型:']); - applyStyles(categoryLabel, filterStyles.formLabel); + this.categoryLabel = createElement('label', {}, ['筛选类型:']); + applyStyles(this.categoryLabel, this.styles.formLabel); this.categorySelect = createElement('select') as HTMLSelectElement; applyStyles(this.categorySelect, filterStyles.operatorSelect); @@ -300,19 +313,19 @@ export class ConditionFilter { }); // 操作符选择下拉框 - const operatorLabel = createElement('label', {}, ['筛选条件:']); - applyStyles(operatorLabel, filterStyles.formLabel); + this.operatorLabel = createElement('label', {}, ['筛选条件:']); + applyStyles(this.operatorLabel, this.styles.formLabel); this.operatorSelect = createElement('select') as HTMLSelectElement; applyStyles(this.operatorSelect, filterStyles.operatorSelect); // 条件值输入框 - const valueLabel = createElement('label', {}, ['筛选值:']); - applyStyles(valueLabel, filterStyles.formLabel); + this.valueLabel = createElement('label', {}, ['筛选值:']); + applyStyles(this.valueLabel, this.styles.formLabel); // 一个容器来包装两个输入框和"和"字 - const rangeInputContainer = createElement('div'); - applyStyles(rangeInputContainer, filterStyles.rangeInputContainer); + this.rangeInputContainer = createElement('div'); + applyStyles(this.rangeInputContainer, this.styles.rangeInputContainer); this.valueInput = createElement('input', { type: 'text', @@ -321,9 +334,9 @@ export class ConditionFilter { applyStyles(this.valueInput, filterStyles.searchInput); // "和"字标签 - const andLabel = createElement('span', {}, ['和']); - applyStyles(andLabel, filterStyles.addLabel); - andLabel.style.display = 'none'; // 默认隐藏 + this.andLabel = createElement('span', {}, ['和']); + applyStyles(this.andLabel, this.styles.addLabel); + this.andLabel.style.display = 'none'; // 默认隐藏 // 范围筛选的最大值输入框 this.valueInputMax = createElement('input', { @@ -334,19 +347,19 @@ export class ConditionFilter { this.valueInputMax.style.display = 'none'; // 默认隐藏 // 将输入框和"和"字添加到容器中 - rangeInputContainer.appendChild(this.valueInput); - rangeInputContainer.appendChild(andLabel); - rangeInputContainer.appendChild(this.valueInputMax); + this.rangeInputContainer.appendChild(this.valueInput); + this.rangeInputContainer.appendChild(this.andLabel); + this.rangeInputContainer.appendChild(this.valueInputMax); // 将元素添加到容器中 - conditionContainer.appendChild(categoryLabel); - conditionContainer.appendChild(this.categorySelect); - conditionContainer.appendChild(operatorLabel); - conditionContainer.appendChild(this.operatorSelect); - conditionContainer.appendChild(valueLabel); - conditionContainer.appendChild(rangeInputContainer); - - this.filterByConditionPanel.appendChild(conditionContainer); + this.conditionContainer.appendChild(this.categoryLabel); + this.conditionContainer.appendChild(this.categorySelect); + this.conditionContainer.appendChild(this.operatorLabel); + this.conditionContainer.appendChild(this.operatorSelect); + this.conditionContainer.appendChild(this.valueLabel); + this.conditionContainer.appendChild(this.rangeInputContainer); + + this.filterByConditionPanel.appendChild(this.conditionContainer); container.appendChild(this.filterByConditionPanel); // 默认隐藏 @@ -357,6 +370,20 @@ export class ConditionFilter { this.bindEvents(); } + updateStyles(styles: FilterStyles) { + applyStyles(this.filterByConditionPanel, styles.filterPanel); + applyStyles(this.conditionContainer, styles.conditionContainer); + applyStyles(this.categoryLabel, styles.formLabel); + applyStyles(this.categorySelect, styles.operatorSelect); + applyStyles(this.operatorLabel, styles.formLabel); + applyStyles(this.operatorSelect, styles.operatorSelect); + applyStyles(this.valueLabel, styles.formLabel); + applyStyles(this.rangeInputContainer, styles.rangeInputContainer); + applyStyles(this.valueInput, styles.searchInput); + applyStyles(this.andLabel, styles.addLabel); + applyStyles(this.valueInputMax, styles.searchInput); + } + /** * 绑定事件 */ diff --git a/packages/vtable-plugins/src/filter/filter-toolbar.ts b/packages/vtable-plugins/src/filter/filter-toolbar.ts index 6d9e10469b..d146be0a64 100644 --- a/packages/vtable-plugins/src/filter/filter-toolbar.ts +++ b/packages/vtable-plugins/src/filter/filter-toolbar.ts @@ -3,7 +3,7 @@ import type { FilterStateManager } from './filter-state-manager'; import { ValueFilter } from './value-filter'; import { ConditionFilter } from './condition-filter'; import { applyStyles, filterStyles } from './styles'; -import type { FilterMode, FilterOptions } from './types'; +import type { FilterMode, FilterOptions, FilterStyles } from './types'; /** * 筛选工具栏,管理按值和按条件筛选组件 @@ -20,11 +20,13 @@ export class FilterToolbar { filterModes: FilterMode[] = []; private filterMenu: HTMLElement; + private filterTabsContainer: HTMLElement; private filterMenuWidth: number; private currentCol?: number | null; private currentRow?: number | null; private filterTabByValue: HTMLButtonElement; private filterTabByCondition: HTMLButtonElement; + private footerContainer: HTMLElement; private clearFilterOptionLink: HTMLAnchorElement; private cancelFilterButton: HTMLButtonElement; private applyFilterButton: HTMLButtonElement; @@ -105,6 +107,7 @@ export class FilterToolbar { } render(container: HTMLElement): void { + const filterStyles = this.pluginOptions.styles || {}; // === 主容器 === this.filterMenu = document.createElement('div'); this.filterMenu.classList.add('vtable-filter-menu'); @@ -112,8 +115,8 @@ export class FilterToolbar { this.filterMenu.style.width = `${this.filterMenuWidth}px`; // === 筛选 Tab === - const filterTabsContainer = document.createElement('div'); - applyStyles(filterTabsContainer, filterStyles.tabsContainer); + this.filterTabsContainer = document.createElement('div'); + applyStyles(this.filterTabsContainer, filterStyles.tabsContainer); this.filterTabByValue = document.createElement('button'); this.filterTabByValue.innerText = '按值筛选'; @@ -123,11 +126,11 @@ export class FilterToolbar { this.filterTabByCondition.innerText = '按条件筛选'; applyStyles(this.filterTabByCondition, filterStyles.tabStyle(false)); - filterTabsContainer.append(this.filterTabByValue, this.filterTabByCondition); + this.filterTabsContainer.append(this.filterTabByValue, this.filterTabByCondition); // === 页脚(清除、取消、确定 筛选按钮) === - const footerContainer = document.createElement('div'); - applyStyles(footerContainer, filterStyles.footerContainer); + this.footerContainer = document.createElement('div'); + applyStyles(this.footerContainer, filterStyles.footerContainer); this.clearFilterOptionLink = document.createElement('a'); this.clearFilterOptionLink.href = '#'; @@ -144,22 +147,34 @@ export class FilterToolbar { applyStyles(this.applyFilterButton, filterStyles.footerButton(true)); footerButtons.append(this.cancelFilterButton, this.applyFilterButton); - footerContainer.append(this.clearFilterOptionLink, footerButtons); + this.footerContainer.append(this.clearFilterOptionLink, footerButtons); // --- 筛选器头部 Tab --- - this.filterMenu.append(filterTabsContainer); + this.filterMenu.append(this.filterTabsContainer); // --- 筛选器内容 --- this.valueFilter.render(this.filterMenu); this.conditionFilter.render(this.filterMenu); // --- 筛选器页脚 --- - this.filterMenu.append(footerContainer); + this.filterMenu.append(this.footerContainer); container.appendChild(this.filterMenu); // 将筛选器添加到 DOM 中 this.attachEventListeners(); } + updateStyles(styles: FilterStyles) { + applyStyles(this.filterMenu, styles.filterMenu); + applyStyles(this.filterTabsContainer, styles.tabsContainer); + applyStyles(this.filterTabByValue, styles.tabStyle(true)); + applyStyles(this.footerContainer, styles.footerContainer); + applyStyles(this.clearFilterOptionLink, styles.clearLink); + applyStyles(this.cancelFilterButton, styles.footerButton(false)); + applyStyles(this.applyFilterButton, styles.footerButton(true)); + this.valueFilter.updateStyles(styles); + this.conditionFilter.updateStyles(styles); + } + attachEventListeners() { // 按值筛选/按条件筛选的事件监听 this.filterTabByValue.addEventListener('click', () => { diff --git a/packages/vtable-plugins/src/filter/filter.ts b/packages/vtable-plugins/src/filter/filter.ts index 1d8c11b361..fc494dfa44 100644 --- a/packages/vtable-plugins/src/filter/filter.ts +++ b/packages/vtable-plugins/src/filter/filter.ts @@ -135,8 +135,16 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin { } } + updatePluginOptions(pluginOptions: FilterOptions) { + this.pluginOptions = merge(this.pluginOptions, pluginOptions); + // 更新筛选器UI样式 + this.filterToolbar.updateStyles(this.pluginOptions.styles); + } + update() { + // 更新筛选状态 this.filterToolbar.valueFilter?.update(); + if (this.filterStateManager) { this.reapplyActiveFilters(); } diff --git a/packages/vtable-plugins/src/filter/value-filter.ts b/packages/vtable-plugins/src/filter/value-filter.ts index 06b9b20351..beda325cdc 100644 --- a/packages/vtable-plugins/src/filter/value-filter.ts +++ b/packages/vtable-plugins/src/filter/value-filter.ts @@ -1,6 +1,6 @@ import type { ListTable, PivotTable } from '@visactor/vtable'; import { arrayEqual } from '@visactor/vutils'; -import type { FilterConfig, ValueFilterOptionDom, FilterState, FilterOptions } from './types'; +import type { FilterConfig, ValueFilterOptionDom, FilterState, FilterOptions, FilterStyles } from './types'; import { FilterActionType } from './types'; import type { FilterStateManager } from './filter-state-manager'; import { applyStyles } from './styles'; @@ -17,7 +17,12 @@ export class ValueFilter { private toUnformattedCache = new Map>>(); private valueFilterOptionList: Map = new Map(); + private filterByValuePanel: HTMLElement; + private searchContainer: HTMLElement; + private optionsContainer: HTMLElement; + private selectAllItemDiv: HTMLElement; + private selectAllLabel: HTMLElement; private filterByValueSearchInput: HTMLInputElement; private selectAllCheckbox: HTMLInputElement; private totalCountSpan: HTMLSpanElement; @@ -273,25 +278,25 @@ export class ValueFilter { applyStyles(this.filterByValuePanel, filterStyles.filterPanel); // -- 搜索栏 --- - const searchContainer = document.createElement('div'); - applyStyles(searchContainer, filterStyles.searchContainer); + this.searchContainer = document.createElement('div'); + applyStyles(this.searchContainer, this.styles.searchContainer); this.filterByValueSearchInput = document.createElement('input'); this.filterByValueSearchInput.type = 'text'; - this.filterByValueSearchInput.placeholder = '可使用空格分隔多个关键词'; + this.filterByValueSearchInput.placeholder = filterStyles.searchInput?.placeholder || '可使用空格分隔多个关键词'; applyStyles(this.filterByValueSearchInput, filterStyles.searchInput); - searchContainer.appendChild(this.filterByValueSearchInput); + this.searchContainer.appendChild(this.filterByValueSearchInput); // --- 筛选选项 --- - const optionsContainer = document.createElement('div'); - applyStyles(optionsContainer, filterStyles.optionsContainer); + this.optionsContainer = document.createElement('div'); + applyStyles(this.optionsContainer, this.styles.optionsContainer); - const selectAllItemDiv = document.createElement('div'); - applyStyles(selectAllItemDiv, filterStyles.optionItem); + this.selectAllItemDiv = document.createElement('div'); + applyStyles(this.selectAllItemDiv, this.styles.optionItem); - const selectAllLabel = document.createElement('label'); - applyStyles(selectAllLabel, filterStyles.optionLabel); + this.selectAllLabel = document.createElement('label'); + applyStyles(this.selectAllLabel, this.styles.optionLabel); this.selectAllCheckbox = document.createElement('input'); this.selectAllCheckbox.type = 'checkbox'; @@ -302,19 +307,30 @@ export class ValueFilter { this.totalCountSpan.textContent = ''; applyStyles(this.totalCountSpan, filterStyles.countSpan); - selectAllLabel.append(this.selectAllCheckbox, ' 全选'); - selectAllItemDiv.append(selectAllLabel, this.totalCountSpan); + this.selectAllLabel.append(this.selectAllCheckbox, ' 全选'); + this.selectAllItemDiv.appendChild(this.selectAllLabel); this.filterItemsContainer = document.createElement('div'); // 筛选条目的容器,后续应动态 appendChild - optionsContainer.append(selectAllItemDiv, this.filterItemsContainer); - this.filterByValuePanel.append(searchContainer, optionsContainer); + this.optionsContainer.append(this.selectAllItemDiv, this.filterItemsContainer); + this.filterByValuePanel.append(this.searchContainer, this.optionsContainer); container.appendChild(this.filterByValuePanel); this.bindEventForFilterByValue(); } + updateStyles(styles: FilterStyles): void { + applyStyles(this.filterByValuePanel, styles.filterPanel); + applyStyles(this.searchContainer, styles.searchContainer); + this.filterByValueSearchInput.placeholder = styles.searchInput?.placeholder || '可使用空格分隔多个关键词'; + applyStyles(this.filterByValueSearchInput, styles.searchInput); + applyStyles(this.optionsContainer, styles.optionsContainer); + applyStyles(this.selectAllItemDiv, styles.optionItem); + applyStyles(this.selectAllLabel, styles.optionLabel); + applyStyles(this.selectAllCheckbox, styles.checkbox); + } + private renderFilterOptions(field: string | number): void { const filterStyles = this.styles; this.filterItemsContainer.innerHTML = ''; From 2b55c68b8b024ad664cb187084987948ea2d46e3 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Mon, 1 Dec 2025 15:29:14 +0800 Subject: [PATCH 10/34] fix: scroll bug when update option --- ...-plugin-custom-style_2025-12-01-07-29.json | 10 +++++++ packages/vtable/src/core/BaseTable.ts | 30 +++++++++---------- 2 files changed, 25 insertions(+), 15 deletions(-) create mode 100644 common/changes/@visactor/vtable/feat-filter-plugin-custom-style_2025-12-01-07-29.json diff --git a/common/changes/@visactor/vtable/feat-filter-plugin-custom-style_2025-12-01-07-29.json b/common/changes/@visactor/vtable/feat-filter-plugin-custom-style_2025-12-01-07-29.json new file mode 100644 index 0000000000..c1ee271a0c --- /dev/null +++ b/common/changes/@visactor/vtable/feat-filter-plugin-custom-style_2025-12-01-07-29.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "fix: scroll bug when update option", + "type": "none" + } + ], + "packageName": "@visactor/vtable" +} \ No newline at end of file diff --git a/packages/vtable/src/core/BaseTable.ts b/packages/vtable/src/core/BaseTable.ts index 6d31ccd782..810238d364 100644 --- a/packages/vtable/src/core/BaseTable.ts +++ b/packages/vtable/src/core/BaseTable.ts @@ -502,16 +502,16 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { ? typeof limitMinWidth === 'number' ? limitMinWidth : limitMinWidth - ? 10 - : 0 + ? 10 + : 0 : 10; internalProps.limitMinHeight = limitMinHeight !== null && limitMinHeight !== undefined ? typeof limitMinHeight === 'number' ? limitMinHeight : limitMinHeight - ? 10 - : 0 + ? 10 + : 0 : 10; // 生成scenegraph // this._vDataSet = new DataSet(); @@ -1487,12 +1487,12 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { // : this.defaultColWidth; if (this.isRowHeader(col, 0) || this.isCornerHeader(col, 0)) { return Array.isArray(this.defaultHeaderColWidth) - ? this.defaultHeaderColWidth[col] ?? this.defaultColWidth + ? (this.defaultHeaderColWidth[col] ?? this.defaultColWidth) : this.defaultHeaderColWidth; } else if (this.isRightFrozenColumn(col, this.columnHeaderLevelCount)) { if (this.isPivotTable()) { return Array.isArray(this.defaultHeaderColWidth) - ? this.defaultHeaderColWidth[this.rowHeaderLevelCount - this.rightFrozenColCount] ?? this.defaultColWidth + ? (this.defaultHeaderColWidth[this.rowHeaderLevelCount - this.rightFrozenColCount] ?? this.defaultColWidth) : this.defaultHeaderColWidth; } return this.defaultColWidth; @@ -1503,15 +1503,15 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { getDefaultRowHeight(row: number) { if (this.isColumnHeader(0, row) || this.isCornerHeader(0, row) || this.isSeriesNumberInHeader(0, row)) { return Array.isArray(this.defaultHeaderRowHeight) - ? this.defaultHeaderRowHeight[row] ?? this.internalProps.defaultRowHeight + ? (this.defaultHeaderRowHeight[row] ?? this.internalProps.defaultRowHeight) : this.defaultHeaderRowHeight; } if (this.isBottomFrozenRow(row)) { //底部冻结行默认取用了表头的行高 但针对非表头数据冻结的情况这里可能不妥 return Array.isArray(this.defaultHeaderRowHeight) - ? this.defaultHeaderRowHeight[ + ? (this.defaultHeaderRowHeight[ this.columnHeaderLevelCount > 0 ? this.columnHeaderLevelCount - this.bottomFrozenRowCount : 0 - ] ?? this.internalProps.defaultRowHeight + ] ?? this.internalProps.defaultRowHeight) : this.defaultHeaderRowHeight; } return this.internalProps.defaultRowHeight; @@ -1883,7 +1883,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { * @returns */ getAllRowsHeight(): number { - if (this.internalProps.rowCount <= 0) { + if (!this.internalProps?.rowCount || this.internalProps.rowCount <= 0) { return 0; } const h = this.getRowsHeight(0, this.internalProps.rowCount - 1); @@ -1894,7 +1894,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { * @returns */ getAllColsWidth(): number { - if (this.internalProps.colCount <= 0) { + if (!this.internalProps?.colCount || this.internalProps.colCount <= 0) { return 0; } const w = this.getColsWidth(0, this.internalProps.colCount - 1); @@ -2796,16 +2796,16 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { ? typeof limitMinWidth === 'number' ? limitMinWidth : limitMinWidth - ? 10 - : 0 + ? 10 + : 0 : 10; internalProps.limitMinHeight = limitMinHeight !== null && limitMinHeight !== undefined ? typeof limitMinHeight === 'number' ? limitMinHeight : limitMinHeight - ? 10 - : 0 + ? 10 + : 0 : 10; // 生成scenegraph // this._vDataSet = new DataSet(); From e3ac252647473693b2c2a398cd0fcb5369c8a532 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Thu, 4 Dec 2025 15:30:10 +0800 Subject: [PATCH 11/34] feat: menu limit to body range. close#4791 --- ...at-filter-plugin-for-business_2025-12-04-07-30.json | 10 ++++++++++ packages/vtable-plugins/src/filter/filter-toolbar.ts | 7 +++++++ 2 files changed, 17 insertions(+) create mode 100644 common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-04-07-30.json diff --git a/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-04-07-30.json b/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-04-07-30.json new file mode 100644 index 0000000000..4be05f6552 --- /dev/null +++ b/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-04-07-30.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "feat: menu limit to body range. close#4791", + "type": "none" + } + ], + "packageName": "@visactor/vtable" +} \ No newline at end of file diff --git a/packages/vtable-plugins/src/filter/filter-toolbar.ts b/packages/vtable-plugins/src/filter/filter-toolbar.ts index d146be0a64..1e5cb6b506 100644 --- a/packages/vtable-plugins/src/filter/filter-toolbar.ts +++ b/packages/vtable-plugins/src/filter/filter-toolbar.ts @@ -238,6 +238,9 @@ export class FilterToolbar { const canvasBounds = this.table.canvas.getBoundingClientRect(); const cell = this.table.getCellRelativeRect(effectiveCol, effectiveRow); + const filterMenuWidth = this.filterMenuWidth; + const filterMenuHeight = 380; // 最高高度预估值 + if (cell.right < this.filterMenuWidth) { // 无法把筛选菜单完整地显示在左侧,那么显示在右侧 left = cell.left + canvasBounds.left; @@ -248,6 +251,10 @@ export class FilterToolbar { top = cell.bottom + canvasBounds.top; } + // 确保筛选菜单不会超出窗口边界 + left = Math.max(0, Math.min(window.innerWidth - filterMenuWidth, left)); + top = Math.max(0, Math.min(window.innerHeight - filterMenuHeight, top)); + this.filterMenu.style.display = this.isVisible ? 'block' : 'none'; this.filterMenu.style.left = `${left}px`; this.filterMenu.style.top = `${top}px`; From 7627860b75af0f32298ba9a68eaf4acafb360499 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Thu, 4 Dec 2025 15:34:26 +0800 Subject: [PATCH 12/34] fix: select none not effect. fix#4792 --- ...at-filter-plugin-for-business_2025-12-04-07-34.json | 10 ++++++++++ packages/vtable-plugins/src/filter/value-filter.ts | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-04-07-34.json diff --git a/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-04-07-34.json b/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-04-07-34.json new file mode 100644 index 0000000000..7e5b5ce497 --- /dev/null +++ b/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-04-07-34.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "fix: select none not effect. fix#4792", + "type": "none" + } + ], + "packageName": "@visactor/vtable" +} \ No newline at end of file diff --git a/packages/vtable-plugins/src/filter/value-filter.ts b/packages/vtable-plugins/src/filter/value-filter.ts index beda325cdc..8d010aee01 100644 --- a/packages/vtable-plugins/src/filter/value-filter.ts +++ b/packages/vtable-plugins/src/filter/value-filter.ts @@ -240,7 +240,7 @@ export class ValueFilter { this.selectedKeys.set(fieldId, new Set(selections)); - if (selections.length > 0 && selections.length < this.valueFilterOptionList.get(fieldId).length) { + if (selections.length >= 0 && selections.length < this.valueFilterOptionList.get(fieldId).length) { this.filterStateManager.dispatch({ type: FilterActionType.APPLY_FILTERS, payload: { From ad68d8007b0650beb11a315116308bc3ff082e75 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Thu, 4 Dec 2025 16:19:31 +0800 Subject: [PATCH 13/34] feat: add config to disable sync multiple filter state. close#4793 --- ...-plugin-for-business_2025-12-04-08-19.json | 10 +++++++ packages/vtable-plugins/src/filter/types.ts | 4 +++ .../vtable-plugins/src/filter/value-filter.ts | 30 ++++++++++++++----- 3 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-04-08-19.json diff --git a/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-04-08-19.json b/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-04-08-19.json new file mode 100644 index 0000000000..c1ea5c239b --- /dev/null +++ b/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-04-08-19.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "feat: add config to disable sync multiple filter state. close#4793", + "type": "none" + } + ], + "packageName": "@visactor/vtable" +} \ No newline at end of file diff --git a/packages/vtable-plugins/src/filter/types.ts b/packages/vtable-plugins/src/filter/types.ts index f0e5ec669f..361c56abdd 100644 --- a/packages/vtable-plugins/src/filter/types.ts +++ b/packages/vtable-plugins/src/filter/types.ts @@ -19,6 +19,10 @@ export interface FilterOptions { conditionCategories?: FilterOperatorCategoryOption[]; /** 筛选选项是否展示为原始值 */ checkboxItemFormat?: (rawValue: any, formatValue: any) => any; + /** 多个筛选器之间是否联动 + * @default true + */ + syncCheckboxCheckedState?: boolean; } export interface FilterOperatorCategoryOption { diff --git a/packages/vtable-plugins/src/filter/value-filter.ts b/packages/vtable-plugins/src/filter/value-filter.ts index 8d010aee01..c3383d7396 100644 --- a/packages/vtable-plugins/src/filter/value-filter.ts +++ b/packages/vtable-plugins/src/filter/value-filter.ts @@ -66,8 +66,15 @@ export class ValueFilter { * 为未应用筛选的列,收集候选值集合 */ private collectCandidateKeysForUnfilteredColumn(fieldId: string | number): void { + const syncCheckboxCheckedState = this.pluginOptions?.syncCheckboxCheckedState ?? true; const countMap = new Map(); // 计算每个候选值的计数 - const records = this.table.internalProps.dataSource.records; // 未筛选:使用当前表格数据 + let records = []; + // 如果各个筛选器之间不联动, 则永远从原数据中获取候选值 + if (!syncCheckboxCheckedState) { + records = this.table.internalProps.records; + } else { + records = this.table.internalProps.dataSource.records; // 未筛选:使用当前表格数据 + } const formatFn = this.getFormatFnCache(fieldId); const toUnformatted = new Map(); @@ -120,18 +127,25 @@ export class ValueFilter { * 为已应用筛选的列,收集候选值集合 */ private collectCandidateKeysForFilteredColumn(candidateField: string | number): void { + const syncCheckboxCheckedState = this.pluginOptions?.syncCheckboxCheckedState ?? true; const filteredFields = this.filterStateManager.getActiveFilterFields().filter(field => field !== candidateField); const toUnformatted = new Map(); const formatFn = this.getFormatFnCache(candidateField); const countMap = new Map(); // 计算每个候选值的计数 - const recordsList = this.table.internalProps.records; // 已筛选:使用原始表格数据 - const records = recordsList.filter(record => - filteredFields.every(field => { - const set = this.selectedKeys.get(field); - return set.has(record[field]); - }) - ); + let records = []; + // 如果各个筛选器之间不联动, 则永远从原数据中获取候选值 + if (!syncCheckboxCheckedState) { + records = this.table.internalProps.records; + } else { + const recordsList = this.table.internalProps.records; // 已筛选:使用原始表格数据 + records = recordsList.filter(record => + filteredFields.every(field => { + const set = this.selectedKeys.get(field); + return set.has(record[field]); + }) + ); + } records.forEach(record => { const originalValue = record[candidateField]; From 65fc51f3fc6db28bcdbf4e258c1d70e9f02ab598 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Thu, 4 Dec 2025 16:27:31 +0800 Subject: [PATCH 14/34] fix: empty line bug --- ...-plugin-for-business_2025-12-04-08-27.json | 10 ++++ packages/vtable-plugins/demo/filter/filter.ts | 48 +++++++++---------- .../vtable-plugins/src/filter/value-filter.ts | 27 ++++++----- 3 files changed, 49 insertions(+), 36 deletions(-) create mode 100644 common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-04-08-27.json diff --git a/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-04-08-27.json b/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-04-08-27.json new file mode 100644 index 0000000000..7814b5890b --- /dev/null +++ b/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-04-08-27.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "fix: empty line bug", + "type": "none" + } + ], + "packageName": "@visactor/vtable" +} \ No newline at end of file diff --git a/packages/vtable-plugins/demo/filter/filter.ts b/packages/vtable-plugins/demo/filter/filter.ts index 348ee85b58..f4ee20d6df 100644 --- a/packages/vtable-plugins/demo/filter/filter.ts +++ b/packages/vtable-plugins/demo/filter/filter.ts @@ -266,7 +266,7 @@ export function createTable() { const option: VTable.ListTableConstructorOptions = { container: document.getElementById(CONTAINER_ID), - records: [...records], + records: [...records, null, undefined], columns, padding: 10, plugins: [filterPlugin] @@ -287,29 +287,29 @@ export function createTable() { bgColor: 'red' }); - setTimeout(() => { - filterPlugin.updatePluginOptions({ - styles: { - searchInput: { - placeholder: 'xxx', - color: 'blue' - }, - optionItem: { - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - padding: '8px 5px', - color: 'blue' - } - } - }); - console.log('update'); - tableInstance.updateOption({ - ...option, - plugins: [filterPlugin], - records: generateDemoData(50, '第二次') - }); - }, 8000); + // setTimeout(() => { + // filterPlugin.updatePluginOptions({ + // styles: { + // searchInput: { + // placeholder: 'xxx', + // color: 'blue' + // }, + // optionItem: { + // display: 'flex', + // justifyContent: 'space-between', + // alignItems: 'center', + // padding: '8px 5px', + // color: 'blue' + // } + // } + // }); + // console.log('update'); + // tableInstance.updateOption({ + // ...option, + // plugins: [filterPlugin], + // records: generateDemoData(50, '第二次') + // }); + // }, 8000); // setTimeout(() => { // console.log('update'); diff --git a/packages/vtable-plugins/src/filter/value-filter.ts b/packages/vtable-plugins/src/filter/value-filter.ts index c3383d7396..e8c85771e6 100644 --- a/packages/vtable-plugins/src/filter/value-filter.ts +++ b/packages/vtable-plugins/src/filter/value-filter.ts @@ -1,6 +1,6 @@ import type { ListTable, PivotTable } from '@visactor/vtable'; -import { arrayEqual } from '@visactor/vutils'; -import type { FilterConfig, ValueFilterOptionDom, FilterState, FilterOptions, FilterStyles } from './types'; +import { arrayEqual, isValid } from '@visactor/vutils'; +import type { ValueFilterOptionDom, FilterState, FilterOptions, FilterStyles } from './types'; import { FilterActionType } from './types'; import type { FilterStateManager } from './filter-state-manager'; import { applyStyles } from './styles'; @@ -79,16 +79,19 @@ export class ValueFilter { const toUnformatted = new Map(); records.forEach(record => { - const originalValue = record[fieldId]; - const formattedValue = formatFn(record); - if (formattedValue !== undefined && formattedValue !== null) { - countMap.set(formattedValue, (countMap.get(formattedValue) || 0) + 1); - - const unformattedSet = toUnformatted.get(formattedValue); - if (unformattedSet !== undefined && unformattedSet !== null) { - unformattedSet.add(originalValue); - } else { - toUnformatted.set(formattedValue, new Set([originalValue])); + // 空行不做处理 + if (isValid(record)) { + const originalValue = record[fieldId]; + const formattedValue = formatFn(record); + if (formattedValue !== undefined && formattedValue !== null) { + countMap.set(formattedValue, (countMap.get(formattedValue) || 0) + 1); + + const unformattedSet = toUnformatted.get(formattedValue); + if (unformattedSet !== undefined && unformattedSet !== null) { + unformattedSet.add(originalValue); + } else { + toUnformatted.set(formattedValue, new Set([originalValue])); + } } } }); From 27dcb45fba585c944c0b03898f3831ef8f23ef0d Mon Sep 17 00:00:00 2001 From: skie1997 Date: Thu, 4 Dec 2025 16:34:52 +0800 Subject: [PATCH 15/34] chore: lint error --- packages/vtable/src/plugins/plugin-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vtable/src/plugins/plugin-manager.ts b/packages/vtable/src/plugins/plugin-manager.ts index 3fa669c082..e984eac6df 100644 --- a/packages/vtable/src/plugins/plugin-manager.ts +++ b/packages/vtable/src/plugins/plugin-manager.ts @@ -35,7 +35,7 @@ export class PluginManager { } _bindTableEventForPlugin(plugin: IVTablePlugin) { - plugin.runTime?.forEach(runTime => { + plugin.runTime?.forEach((runTime: any) => { const id = this.table.on(runTime, (...args) => { plugin.run?.(...args, runTime, this.table); }); From f5286df3379c2ec5b266a5f58dc8ab12150beffe Mon Sep 17 00:00:00 2001 From: skie1997 Date: Thu, 4 Dec 2025 19:29:59 +0800 Subject: [PATCH 16/34] fix: update checkbox state after update data. fix#4795 --- ...-plugin-for-business_2025-12-04-11-29.json | 10 +++ packages/vtable-plugins/demo/filter/filter.ts | 49 +++++------ .../src/filter/filter-toolbar.ts | 9 +++ packages/vtable-plugins/src/filter/filter.ts | 7 +- .../vtable-plugins/src/filter/value-filter.ts | 81 +++++++++++++------ 5 files changed, 107 insertions(+), 49 deletions(-) create mode 100644 common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-04-11-29.json diff --git a/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-04-11-29.json b/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-04-11-29.json new file mode 100644 index 0000000000..84d6a2027c --- /dev/null +++ b/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-04-11-29.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "fix: update checkbox state after update data. fix#4795", + "type": "none" + } + ], + "packageName": "@visactor/vtable" +} \ No newline at end of file diff --git a/packages/vtable-plugins/demo/filter/filter.ts b/packages/vtable-plugins/demo/filter/filter.ts index f4ee20d6df..3fd0426a58 100644 --- a/packages/vtable-plugins/demo/filter/filter.ts +++ b/packages/vtable-plugins/demo/filter/filter.ts @@ -260,7 +260,8 @@ export function createTable() { ], checkboxItemFormat: (formatValue, rawValue) => { return formatValue; - } + }, + syncCheckboxCheckedState: false }); (window as any).filterPlugin = filterPlugin; @@ -287,29 +288,29 @@ export function createTable() { bgColor: 'red' }); - // setTimeout(() => { - // filterPlugin.updatePluginOptions({ - // styles: { - // searchInput: { - // placeholder: 'xxx', - // color: 'blue' - // }, - // optionItem: { - // display: 'flex', - // justifyContent: 'space-between', - // alignItems: 'center', - // padding: '8px 5px', - // color: 'blue' - // } - // } - // }); - // console.log('update'); - // tableInstance.updateOption({ - // ...option, - // plugins: [filterPlugin], - // records: generateDemoData(50, '第二次') - // }); - // }, 8000); + setTimeout(() => { + filterPlugin.updatePluginOptions({ + styles: { + searchInput: { + placeholder: 'xxx', + color: 'blue' + }, + optionItem: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '8px 5px', + color: 'blue' + } + } + }); + console.log('update'); + tableInstance.updateOption({ + ...option, + plugins: [filterPlugin], + records: [...generateDemoData(50, '第二次'), null, undefined] + }); + }, 8000); // setTimeout(() => { // console.log('update'); diff --git a/packages/vtable-plugins/src/filter/filter-toolbar.ts b/packages/vtable-plugins/src/filter/filter-toolbar.ts index 1e5cb6b506..76ae6635dc 100644 --- a/packages/vtable-plugins/src/filter/filter-toolbar.ts +++ b/packages/vtable-plugins/src/filter/filter-toolbar.ts @@ -173,6 +173,15 @@ export class FilterToolbar { applyStyles(this.applyFilterButton, styles.footerButton(true)); this.valueFilter.updateStyles(styles); this.conditionFilter.updateStyles(styles); + + // 面板处于显示状态, 更新了样式, 则需要手动控制tab显隐 + // 面板显示按值筛选或按条件筛选 + const currentFilter = this.filterStateManager.getFilterState(this.selectedField); + if (currentFilter && currentFilter.type === 'byCondition') { + this.onTabSwitch('byCondition'); + } else { + this.onTabSwitch('byValue'); + } } attachEventListeners() { diff --git a/packages/vtable-plugins/src/filter/filter.ts b/packages/vtable-plugins/src/filter/filter.ts index fc494dfa44..ae4df5c945 100644 --- a/packages/vtable-plugins/src/filter/filter.ts +++ b/packages/vtable-plugins/src/filter/filter.ts @@ -143,11 +143,14 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin { update() { // 更新筛选状态 - this.filterToolbar.valueFilter?.update(); - + // 如果处于按值筛选状态, 则需要更新候选值 + this.filterToolbar.valueFilter?.updateBeforeFilter(); if (this.filterStateManager) { this.reapplyActiveFilters(); } + // 更新筛选状态 + // 如果处于按条件筛选, 则需要执行筛选后, 更新值面板中checkbox的状态 + this.filterToolbar.valueFilter?.updateAfterFilter(); } /** diff --git a/packages/vtable-plugins/src/filter/value-filter.ts b/packages/vtable-plugins/src/filter/value-filter.ts index e8c85771e6..3743b920d0 100644 --- a/packages/vtable-plugins/src/filter/value-filter.ts +++ b/packages/vtable-plugins/src/filter/value-filter.ts @@ -100,27 +100,53 @@ export class ValueFilter { this.toUnformattedCache.set(fieldId, toUnformatted); } - update() { - // 表格更新时, 可能会插入新数据, 此时需要更新筛选结果和候选值: + updateBeforeFilter() { + // 处于值筛选状态, 表格更新时: + // 可能会插入新数据, 此时需要更新筛选结果和候选值: // 1. 更新筛选结果 // - 出现了之前没有出现过的选项 // - 筛选器出于被筛选状态(有值) // - 则将该选项添加到筛选器中 // 2. 更新候选值 - const currentRecords = this.table.internalProps.dataSource.records; // 当前数据 + const currentRecords = this.table.internalProps.dataSource.records; // 此时还没做筛选, 当前数据 = 原始表格数据 const filteredFields = this.filterStateManager.getActiveFilterFields(); currentRecords.forEach(record => { filteredFields.forEach(candidateField => { const formatFn = this.getFormatFnCache(candidateField); - const originalValue = record[candidateField]; - const formattedValue = formatFn(record); - const lastToUnformatted = this.toUnformattedCache.get(candidateField) || new Map(); - if ( - !lastToUnformatted.has(formattedValue) && - this.filterStateManager.getFilterState(candidateField)?.values?.length > 0 - ) { - this.filterStateManager.getFilterState(candidateField).values.push(originalValue); - this.selectedKeys.get(candidateField).add(originalValue); + + // 空行不做处理 + if (isValid(record)) { + const originalValue = record[candidateField]; + const formattedValue = formatFn(record); + const lastToUnformatted = this.toUnformattedCache.get(candidateField) || new Map(); + if ( + !lastToUnformatted.has(formattedValue) && + this.filterStateManager.getFilterState(candidateField)?.values?.length > 0 + ) { + this.filterStateManager.getFilterState(candidateField).values.push(originalValue); + this.selectedKeys.get(candidateField).add(originalValue); + } + } + }); + }); + } + + updateAfterFilter() { + // 处于条件筛选状态, 表格更新时: + // 值筛选面板需要同步筛选结果 + const currentRecords = this.table.internalProps.dataSource.records; // 此时还没做筛选, 当前数据 = 原始表格数据 + const filteredFields = this.filterStateManager.getActiveFilterFields(); + currentRecords.forEach(record => { + filteredFields.forEach(candidateField => { + // 空行不做处理 + if (isValid(record)) { + const originalValue = record[candidateField]; + if (this.filterStateManager.getFilterState(candidateField)?.type === 'byCondition') { + if (!this.selectedKeys.get(candidateField)) { + this.selectedKeys.set(candidateField, new Set()); + } + this.selectedKeys.get(candidateField).add(originalValue); + } } }); }); @@ -151,15 +177,18 @@ export class ValueFilter { } records.forEach(record => { - const originalValue = record[candidateField]; - const formattedValue = formatFn(record); - countMap.set(formattedValue, (countMap.get(formattedValue) || 0) + 1); - if (formattedValue !== undefined && formattedValue !== null) { - const unformattedSet = toUnformatted.get(formattedValue); - if (unformattedSet !== undefined && unformattedSet !== null) { - unformattedSet.add(originalValue); - } else { - toUnformatted.set(formattedValue, new Set([originalValue])); + // 空行不做处理 + if (isValid(record)) { + const originalValue = record[candidateField]; + const formattedValue = formatFn(record); + countMap.set(formattedValue, (countMap.get(formattedValue) || 0) + 1); + if (formattedValue !== undefined && formattedValue !== null) { + const unformattedSet = toUnformatted.get(formattedValue); + if (unformattedSet !== undefined && unformattedSet !== null) { + unformattedSet.add(originalValue); + } else { + toUnformatted.set(formattedValue, new Set([originalValue])); + } } } }); @@ -215,12 +244,18 @@ export class ValueFilter { const currentRecords = this.table.internalProps.dataSource.records; // 当前数据 currentRecords.forEach(record => { - selectedValues.add(record[fieldId]); + // 空行不做处理 + if (isValid(record)) { + selectedValues.add(record[fieldId]); + } }); const originalRecords = this.table.internalProps.records; // 原始数据 originalRecords.forEach(record => { - originalValues.add(record[fieldId]); + // 空行不做处理 + if (isValid(record)) { + originalValues.add(record[fieldId]); + } }); const hasFiltered = !arrayEqual(Array.from(originalValues), Array.from(selectedValues)); From 18bf4ee41c1434f7a69887833927d4bde14e158b Mon Sep 17 00:00:00 2001 From: skie1997 Date: Mon, 8 Dec 2025 17:32:05 +0800 Subject: [PATCH 17/34] chore: add detail of filter --- packages/vtable-plugins/demo/filter/filter.ts | 2 +- packages/vtable-plugins/src/filter/filter-toolbar.ts | 4 +++- packages/vtable-plugins/src/filter/filter.ts | 7 ++++--- packages/vtable-plugins/src/filter/styles.ts | 2 +- packages/vtable-plugins/src/filter/types.ts | 2 +- packages/vtable-plugins/src/filter/value-filter.ts | 12 ++++++------ 6 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/vtable-plugins/demo/filter/filter.ts b/packages/vtable-plugins/demo/filter/filter.ts index 3fd0426a58..e909d70bc6 100644 --- a/packages/vtable-plugins/demo/filter/filter.ts +++ b/packages/vtable-plugins/demo/filter/filter.ts @@ -261,7 +261,7 @@ export function createTable() { checkboxItemFormat: (formatValue, rawValue) => { return formatValue; }, - syncCheckboxCheckedState: false + syncFilterItemsState: false }); (window as any).filterPlugin = filterPlugin; diff --git a/packages/vtable-plugins/src/filter/filter-toolbar.ts b/packages/vtable-plugins/src/filter/filter-toolbar.ts index 76ae6635dc..2afa41a63e 100644 --- a/packages/vtable-plugins/src/filter/filter-toolbar.ts +++ b/packages/vtable-plugins/src/filter/filter-toolbar.ts @@ -248,7 +248,9 @@ export class FilterToolbar { const cell = this.table.getCellRelativeRect(effectiveCol, effectiveRow); const filterMenuWidth = this.filterMenuWidth; - const filterMenuHeight = 380; // 最高高度预估值 + // 最高高度预估值 + // TODO: 需要获取精确高度 + const filterMenuHeight = 380; if (cell.right < this.filterMenuWidth) { // 无法把筛选菜单完整地显示在左侧,那么显示在右侧 diff --git a/packages/vtable-plugins/src/filter/filter.ts b/packages/vtable-plugins/src/filter/filter.ts index ae4df5c945..be6e1c7f55 100644 --- a/packages/vtable-plugins/src/filter/filter.ts +++ b/packages/vtable-plugins/src/filter/filter.ts @@ -136,6 +136,7 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin { } updatePluginOptions(pluginOptions: FilterOptions) { + // TODO: 目前额外只处理了styles,其他的后续再处理 this.pluginOptions = merge(this.pluginOptions, pluginOptions); // 更新筛选器UI样式 this.filterToolbar.updateStyles(this.pluginOptions.styles); @@ -143,14 +144,14 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin { update() { // 更新筛选状态 - // 如果处于按值筛选状态, 则需要更新候选值 - this.filterToolbar.valueFilter?.updateBeforeFilter(); + // 如果处于按值筛选状态, 则需要更新筛选条件 和 候选值 + this.filterToolbar.valueFilter?.syncRulesAndCandidateKeys(); if (this.filterStateManager) { this.reapplyActiveFilters(); } // 更新筛选状态 // 如果处于按条件筛选, 则需要执行筛选后, 更新值面板中checkbox的状态 - this.filterToolbar.valueFilter?.updateAfterFilter(); + this.filterToolbar.valueFilter?.syncSelectedKeys(); } /** diff --git a/packages/vtable-plugins/src/filter/styles.ts b/packages/vtable-plugins/src/filter/styles.ts index d974d75448..7060cce26b 100644 --- a/packages/vtable-plugins/src/filter/styles.ts +++ b/packages/vtable-plugins/src/filter/styles.ts @@ -9,7 +9,7 @@ export const filterStyles = { backgroundColor: 'white', border: '1px solid #ccc', boxShadow: '0 4px 8px rgba(0,0,0,0.15)', - zIndex: '100' + zIndex: '99999' }, // 筛选面板 diff --git a/packages/vtable-plugins/src/filter/types.ts b/packages/vtable-plugins/src/filter/types.ts index 361c56abdd..7b9f062f1c 100644 --- a/packages/vtable-plugins/src/filter/types.ts +++ b/packages/vtable-plugins/src/filter/types.ts @@ -22,7 +22,7 @@ export interface FilterOptions { /** 多个筛选器之间是否联动 * @default true */ - syncCheckboxCheckedState?: boolean; + syncFilterItemsState?: boolean; } export interface FilterOperatorCategoryOption { diff --git a/packages/vtable-plugins/src/filter/value-filter.ts b/packages/vtable-plugins/src/filter/value-filter.ts index 3743b920d0..d1f4bdbde5 100644 --- a/packages/vtable-plugins/src/filter/value-filter.ts +++ b/packages/vtable-plugins/src/filter/value-filter.ts @@ -66,11 +66,11 @@ export class ValueFilter { * 为未应用筛选的列,收集候选值集合 */ private collectCandidateKeysForUnfilteredColumn(fieldId: string | number): void { - const syncCheckboxCheckedState = this.pluginOptions?.syncCheckboxCheckedState ?? true; + const syncFilterItemsState = this.pluginOptions?.syncFilterItemsState ?? true; const countMap = new Map(); // 计算每个候选值的计数 let records = []; // 如果各个筛选器之间不联动, 则永远从原数据中获取候选值 - if (!syncCheckboxCheckedState) { + if (!syncFilterItemsState) { records = this.table.internalProps.records; } else { records = this.table.internalProps.dataSource.records; // 未筛选:使用当前表格数据 @@ -100,7 +100,7 @@ export class ValueFilter { this.toUnformattedCache.set(fieldId, toUnformatted); } - updateBeforeFilter() { + syncRulesAndCandidateKeys() { // 处于值筛选状态, 表格更新时: // 可能会插入新数据, 此时需要更新筛选结果和候选值: // 1. 更新筛选结果 @@ -131,7 +131,7 @@ export class ValueFilter { }); } - updateAfterFilter() { + syncSelectedKeys() { // 处于条件筛选状态, 表格更新时: // 值筛选面板需要同步筛选结果 const currentRecords = this.table.internalProps.dataSource.records; // 此时还没做筛选, 当前数据 = 原始表格数据 @@ -156,7 +156,7 @@ export class ValueFilter { * 为已应用筛选的列,收集候选值集合 */ private collectCandidateKeysForFilteredColumn(candidateField: string | number): void { - const syncCheckboxCheckedState = this.pluginOptions?.syncCheckboxCheckedState ?? true; + const syncFilterItemsState = this.pluginOptions?.syncFilterItemsState ?? true; const filteredFields = this.filterStateManager.getActiveFilterFields().filter(field => field !== candidateField); const toUnformatted = new Map(); const formatFn = this.getFormatFnCache(candidateField); @@ -164,7 +164,7 @@ export class ValueFilter { const countMap = new Map(); // 计算每个候选值的计数 let records = []; // 如果各个筛选器之间不联动, 则永远从原数据中获取候选值 - if (!syncCheckboxCheckedState) { + if (!syncFilterItemsState) { records = this.table.internalProps.records; } else { const recordsList = this.table.internalProps.records; // 已筛选:使用原始表格数据 From db79f5694491895d63c164410109741ff294a61a Mon Sep 17 00:00:00 2001 From: skie1997 Date: Mon, 8 Dec 2025 17:33:58 +0800 Subject: [PATCH 18/34] fix: bug of scroll top in vscreen --- packages/vtable/src/state/state.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/vtable/src/state/state.ts b/packages/vtable/src/state/state.ts index b16f0b0705..e359179ad5 100644 --- a/packages/vtable/src/state/state.ts +++ b/packages/vtable/src/state/state.ts @@ -471,8 +471,8 @@ export class StateManager { const cornerHeaderSelectMode = this.table.options.select?.cornerHeaderSelectMode ? this.table.options.select?.cornerHeaderSelectMode : this.table.options.select?.headerSelectMode === 'body' - ? this.table.options.select?.headerSelectMode - : 'all'; + ? this.table.options.select?.headerSelectMode + : 'all'; // if (enableRowHighlight && enableColumnHighlight) { // this.select.highlightScope = HighlightScope.cross; @@ -537,7 +537,7 @@ export class StateManager { function flatten(cols: any, parentStartIndex = 0) { cols.forEach((col: any) => { const startIndex = col.startInTotal - ? col.startInTotal + state.table.internalProps.layoutMap.leftRowSeriesNumberColumnCount ?? 0 + ? (col.startInTotal + state.table.internalProps.layoutMap.leftRowSeriesNumberColumnCount ?? 0) : parentStartIndex; if (col.columns) { flatten(col.columns, startIndex); @@ -1194,10 +1194,10 @@ export class StateManager { // (由于小数在取数时被省略) // 这里加入tolerance,避免出现无用滚动 const sizeTolerance = this.table.options.customConfig?._disableColumnAndRowSizeRound ? 1 : 0; - top = Math.max(0, Math.min(top, totalHeight - this.table.scenegraph.height - sizeTolerance)); + top = Math.max(0, Math.min(top, totalHeight - (this.table.scenegraph?.height ?? 0) - sizeTolerance)); top = Math.ceil(top); const oldVerticalBarPos = this.scroll.verticalBarPos; - const yRatio = top / (totalHeight - this.table.scenegraph.height); + const yRatio = top / (totalHeight - (this.table.scenegraph?.height ?? 0)); if ( (oldVerticalBarPos !== top || this.table.options?.customConfig?.scrollEventAlwaysTrigger === true) && @@ -1224,7 +1224,7 @@ export class StateManager { if (canScroll.some(value => value === false)) { // reset scrollbar pos - const yRatio = this.scroll.verticalBarPos / (totalHeight - this.table.scenegraph.height); + const yRatio = this.scroll.verticalBarPos / (totalHeight - (this.table.scenegraph?.height ?? 0)); this.table.scenegraph.component.updateVerticalScrollBarPos(yRatio); return; } @@ -1643,8 +1643,8 @@ export class StateManager { this.sort[index]?.order === 'asc' ? 'sort_downward' : this.sort[index]?.order === 'desc' - ? 'sort_upward' - : 'sort_normal'; + ? 'sort_upward' + : 'sort_normal'; this.setSortState(sortState.slice(0, index + 1)); // 获取sort对应的行列位置 const cellAddress = this.table.internalProps.layoutMap.getHeaderCellAddressByField( From 94f0c43868b298ad533a68186580d953d053046a Mon Sep 17 00:00:00 2001 From: skie1997 Date: Mon, 8 Dec 2025 18:19:15 +0800 Subject: [PATCH 19/34] fix: filter empty tips. fix#4804 --- packages/vtable-plugins/demo/filter/filter.ts | 5 ++++- packages/vtable/src/ListTable.ts | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/vtable-plugins/demo/filter/filter.ts b/packages/vtable-plugins/demo/filter/filter.ts index e909d70bc6..c41f337e86 100644 --- a/packages/vtable-plugins/demo/filter/filter.ts +++ b/packages/vtable-plugins/demo/filter/filter.ts @@ -270,7 +270,10 @@ export function createTable() { records: [...records, null, undefined], columns, padding: 10, - plugins: [filterPlugin] + plugins: [filterPlugin], + emptyTip: { + text: 'no data' + } }; const tableInstance = new VTable.ListTable(option); (window as any).tableInstance = tableInstance; diff --git a/packages/vtable/src/ListTable.ts b/packages/vtable/src/ListTable.ts index a5de25d11d..b90cd3d0d5 100644 --- a/packages/vtable/src/ListTable.ts +++ b/packages/vtable/src/ListTable.ts @@ -1317,6 +1317,7 @@ export class ListTable extends BaseTable implements ListTableAPI { this.refreshRowColCount(); this.stateManager.initCheckedState(this.records); this.scenegraph.createSceneGraph(!!!options?.clearRowHeightCache); + this.internalProps.emptyTip?.resetVisible(); this.resize(); } /** 获取过滤后的数据 */ From d7d2cd41c9c5a8425036f746648a3bb7f7a5baa9 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Mon, 8 Dec 2025 18:22:47 +0800 Subject: [PATCH 20/34] fix: remove menu dom after release instance. fix#4805 --- packages/vtable-plugins/demo/filter/filter.ts | 53 +++++++++++-------- .../src/filter/filter-toolbar.ts | 5 ++ packages/vtable-plugins/src/filter/filter.ts | 2 +- 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/packages/vtable-plugins/demo/filter/filter.ts b/packages/vtable-plugins/demo/filter/filter.ts index c41f337e86..1efb0b9055 100644 --- a/packages/vtable-plugins/demo/filter/filter.ts +++ b/packages/vtable-plugins/demo/filter/filter.ts @@ -291,30 +291,32 @@ export function createTable() { bgColor: 'red' }); - setTimeout(() => { - filterPlugin.updatePluginOptions({ - styles: { - searchInput: { - placeholder: 'xxx', - color: 'blue' - }, - optionItem: { - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - padding: '8px 5px', - color: 'blue' - } - } - }); - console.log('update'); - tableInstance.updateOption({ - ...option, - plugins: [filterPlugin], - records: [...generateDemoData(50, '第二次'), null, undefined] - }); - }, 8000); + // 数据更新 + // setTimeout(() => { + // filterPlugin.updatePluginOptions({ + // styles: { + // searchInput: { + // placeholder: 'xxx', + // color: 'blue' + // }, + // optionItem: { + // display: 'flex', + // justifyContent: 'space-between', + // alignItems: 'center', + // padding: '8px 5px', + // color: 'blue' + // } + // } + // }); + // console.log('update'); + // tableInstance.updateOption({ + // ...option, + // plugins: [filterPlugin], + // records: [...generateDemoData(50, '第二次'), null, undefined] + // }); + // }, 8000); + // 插件更新 // setTimeout(() => { // console.log('update'); // tableInstance.updateOption({ @@ -323,6 +325,11 @@ export function createTable() { // }); // }, 8000); + // 实例释放 + setTimeout(() => { + tableInstance.release(); + }, 3000); + bindDebugTool(tableInstance.scenegraph.stage, { customGrapicKeys: ['col', 'row'] }); diff --git a/packages/vtable-plugins/src/filter/filter-toolbar.ts b/packages/vtable-plugins/src/filter/filter-toolbar.ts index 2afa41a63e..943f3cdd5c 100644 --- a/packages/vtable-plugins/src/filter/filter-toolbar.ts +++ b/packages/vtable-plugins/src/filter/filter-toolbar.ts @@ -317,4 +317,9 @@ export class FilterToolbar { row: currentRow }); } + + destroy() { + this.valueFilter.destroy(); + this.filterMenu.remove(); + } } diff --git a/packages/vtable-plugins/src/filter/filter.ts b/packages/vtable-plugins/src/filter/filter.ts index be6e1c7f55..26d9f75fa8 100644 --- a/packages/vtable-plugins/src/filter/filter.ts +++ b/packages/vtable-plugins/src/filter/filter.ts @@ -349,7 +349,7 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin { this.table = null; this.filterEngine = null; this.filterStateManager = null; - this.filterToolbar.valueFilter.destroy(); + this.filterToolbar.destroy(); this.filterToolbar = null; } } From b27f7d829df37c371226daa94bfdba67c779f982 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Tue, 9 Dec 2025 22:51:46 +0800 Subject: [PATCH 21/34] =?UTF-8?q?fix:=20=E9=85=8D=E7=BD=AE=E4=B8=8D?= =?UTF-8?q?=E8=81=94=E5=8A=A8=E5=90=8E=EF=BC=8C=20=E5=90=8C=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E7=AD=9B=E9=80=89=E5=99=A8=E7=9A=84=E6=8C=89=E5=80=BC?= =?UTF-8?q?=E7=AD=9B=E9=80=89=E5=92=8C=E6=8C=89=E6=9D=A1=E4=BB=B6=E7=AD=9B?= =?UTF-8?q?=E9=80=89=E4=BF=9D=E6=8C=81=E7=8B=AC=E7=AB=8B=EF=BC=8C=E4=B8=8D?= =?UTF-8?q?=E8=81=94=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/filter/condition-filter.ts | 3 +- packages/vtable-plugins/src/filter/filter.ts | 6 ++-- .../vtable-plugins/src/filter/value-filter.ts | 30 +++++++++++-------- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/packages/vtable-plugins/src/filter/condition-filter.ts b/packages/vtable-plugins/src/filter/condition-filter.ts index a106138fb5..edffc70d59 100644 --- a/packages/vtable-plugins/src/filter/condition-filter.ts +++ b/packages/vtable-plugins/src/filter/condition-filter.ts @@ -106,8 +106,9 @@ export class ConditionFilter { */ private loadCurrentFilterState(): void { const filter = this.filterStateManager.getFilterState(this.selectedField); + const syncFilterItemsState = this.pluginOptions?.syncFilterItemsState ?? true; - if (filter && filter.type === 'byCondition') { + if (filter && (filter.type === 'byCondition' || !syncFilterItemsState)) { // 设置操作符 if (filter.operator && this.operatorSelect) { this.operatorSelect.value = filter.operator; diff --git a/packages/vtable-plugins/src/filter/filter.ts b/packages/vtable-plugins/src/filter/filter.ts index 26d9f75fa8..a97786aeb0 100644 --- a/packages/vtable-plugins/src/filter/filter.ts +++ b/packages/vtable-plugins/src/filter/filter.ts @@ -69,7 +69,7 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin { } initFilterPlugin(eventArgs: any) { - this.filterEngine = new FilterEngine(); + this.filterEngine = new FilterEngine(this.pluginOptions); this.filterStateManager = new FilterStateManager(this.table, this.filterEngine); this.filterToolbar = new FilterToolbar(this.table, this.filterStateManager, this.pluginOptions); this.columns = eventArgs.options.columns; @@ -145,13 +145,13 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin { update() { // 更新筛选状态 // 如果处于按值筛选状态, 则需要更新筛选条件 和 候选值 - this.filterToolbar.valueFilter?.syncRulesAndCandidateKeys(); + // this.filterToolbar.valueFilter?.syncRulesAndCandidateKeys(); if (this.filterStateManager) { this.reapplyActiveFilters(); } // 更新筛选状态 // 如果处于按条件筛选, 则需要执行筛选后, 更新值面板中checkbox的状态 - this.filterToolbar.valueFilter?.syncSelectedKeys(); + // this.filterToolbar.valueFilter?.syncSelectedKeys(); } /** diff --git a/packages/vtable-plugins/src/filter/value-filter.ts b/packages/vtable-plugins/src/filter/value-filter.ts index d1f4bdbde5..a5dc2fc09a 100644 --- a/packages/vtable-plugins/src/filter/value-filter.ts +++ b/packages/vtable-plugins/src/filter/value-filter.ts @@ -235,21 +235,13 @@ export class ValueFilter { */ private initFilterStateFromTableData(fieldId: string | number): void { const isHasFilteredState = this.filterStateManager.getActiveFilterFields(); - if (isHasFilteredState) { + const isValueFilter = this.filterStateManager.getFilterState(fieldId)?.type === 'byValue'; + if (isHasFilteredState && isValueFilter) { return; } - const selectedValues = new Set(); + let selectedValues = new Set(); const originalValues = new Set(); - - const currentRecords = this.table.internalProps.dataSource.records; // 当前数据 - currentRecords.forEach(record => { - // 空行不做处理 - if (isValid(record)) { - selectedValues.add(record[fieldId]); - } - }); - const originalRecords = this.table.internalProps.records; // 原始数据 originalRecords.forEach(record => { // 空行不做处理 @@ -258,10 +250,22 @@ export class ValueFilter { } }); + const syncFilterItemsState = this.pluginOptions?.syncFilterItemsState ?? true; + if (syncFilterItemsState) { + const currentRecords = this.table.internalProps.dataSource.records; // 当前数据 + currentRecords.forEach(record => { + // 空行不做处理 + if (isValid(record)) { + selectedValues.add(record[fieldId]); + } + }); + } else { + const selectedFromRules = this.filterStateManager.getFilterState(fieldId)?.values || originalValues; // 如果按值筛选没有状态, 则默认选中所有值 + selectedValues = new Set(selectedFromRules); + } + this.selectedKeys.set(fieldId, selectedValues); const hasFiltered = !arrayEqual(Array.from(originalValues), Array.from(selectedValues)); if (hasFiltered) { - this.selectedKeys.set(fieldId, selectedValues); - this.filterStateManager.dispatch({ type: FilterActionType.ADD_FILTER, payload: { From 4591b8421ba7e697eb512e87d238e7fac68495e9 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Tue, 9 Dec 2025 22:52:49 +0800 Subject: [PATCH 22/34] =?UTF-8?q?fix:=20=E5=9B=BE=E8=A1=A8=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E5=90=8E,=20=E7=AD=9B=E9=80=89=E9=9D=A2=E6=9D=BF?= =?UTF-8?q?=E4=B8=8D=E8=87=AA=E5=8A=A8=E6=B6=88=E5=A4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/vtable-plugins/src/filter/filter-toolbar.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/vtable-plugins/src/filter/filter-toolbar.ts b/packages/vtable-plugins/src/filter/filter-toolbar.ts index 943f3cdd5c..d0d24d88e2 100644 --- a/packages/vtable-plugins/src/filter/filter-toolbar.ts +++ b/packages/vtable-plugins/src/filter/filter-toolbar.ts @@ -164,7 +164,8 @@ export class FilterToolbar { } updateStyles(styles: FilterStyles) { - applyStyles(this.filterMenu, styles.filterMenu); + const realDisplay = (this.filterMenu.style.display ?? styles.filterMenu.display) || 'none'; + applyStyles(this.filterMenu, { ...styles.filterMenu, display: realDisplay }); applyStyles(this.filterTabsContainer, styles.tabsContainer); applyStyles(this.filterTabByValue, styles.tabStyle(true)); applyStyles(this.footerContainer, styles.footerContainer); From afd6daffcc2c9440e90e60d9f418b8d0ce963a6e Mon Sep 17 00:00:00 2001 From: skie1997 Date: Tue, 9 Dec 2025 22:55:13 +0800 Subject: [PATCH 23/34] feat: add config to control filter result --- ...-plugin-for-business_2025-12-09-14-53.json | 10 +++ packages/vtable-plugins/demo/filter/filter.ts | 65 +++++++++++-------- .../src/filter/filter-engine.ts | 11 +++- packages/vtable-plugins/src/filter/types.ts | 2 + packages/vtable/src/ListTable.ts | 3 +- packages/vtable/src/data/DataSource.ts | 7 +- 6 files changed, 66 insertions(+), 32 deletions(-) create mode 100644 common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-09-14-53.json diff --git a/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-09-14-53.json b/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-09-14-53.json new file mode 100644 index 0000000000..202c096cb3 --- /dev/null +++ b/common/changes/@visactor/vtable/feat-filter-plugin-for-business_2025-12-09-14-53.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "add config to control filter result", + "type": "none" + } + ], + "packageName": "@visactor/vtable" +} \ No newline at end of file diff --git a/packages/vtable-plugins/demo/filter/filter.ts b/packages/vtable-plugins/demo/filter/filter.ts index 1efb0b9055..d4514809ae 100644 --- a/packages/vtable-plugins/demo/filter/filter.ts +++ b/packages/vtable-plugins/demo/filter/filter.ts @@ -261,7 +261,11 @@ export function createTable() { checkboxItemFormat: (formatValue, rawValue) => { return formatValue; }, - syncFilterItemsState: false + syncFilterItemsState: false, + onFilterRecordsEnd: records => { + console.log('onFilterRecordsEnd'); + return records; + } }); (window as any).filterPlugin = filterPlugin; @@ -292,29 +296,36 @@ export function createTable() { }); // 数据更新 - // setTimeout(() => { - // filterPlugin.updatePluginOptions({ - // styles: { - // searchInput: { - // placeholder: 'xxx', - // color: 'blue' - // }, - // optionItem: { - // display: 'flex', - // justifyContent: 'space-between', - // alignItems: 'center', - // padding: '8px 5px', - // color: 'blue' - // } - // } - // }); - // console.log('update'); - // tableInstance.updateOption({ - // ...option, - // plugins: [filterPlugin], - // records: [...generateDemoData(50, '第二次'), null, undefined] - // }); - // }, 8000); + setTimeout(() => { + filterPlugin.updatePluginOptions({ + styles: { + searchInput: { + placeholder: 'xxx', + color: 'blue' + }, + optionItem: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '8px 5px', + color: 'blue' + }, + filterMenu: { + color: 'red' + } + }, + onFilterRecordsEnd: records => { + console.log('onFilterRecordsEnd-2'); + return records; + } + }); + console.log('update'); + tableInstance.updateOption({ + ...option, + plugins: [filterPlugin], + records: [...generateDemoData(50, '第二次'), null, undefined] + }); + }, 8000); // 插件更新 // setTimeout(() => { @@ -326,9 +337,9 @@ export function createTable() { // }, 8000); // 实例释放 - setTimeout(() => { - tableInstance.release(); - }, 3000); + // setTimeout(() => { + // tableInstance.release(); + // }, 3000); bindDebugTool(tableInstance.scenegraph.stage, { customGrapicKeys: ['col', 'row'] diff --git a/packages/vtable-plugins/src/filter/filter-engine.ts b/packages/vtable-plugins/src/filter/filter-engine.ts index 4136b8e80f..5742fd1646 100644 --- a/packages/vtable-plugins/src/filter/filter-engine.ts +++ b/packages/vtable-plugins/src/filter/filter-engine.ts @@ -1,5 +1,5 @@ import type { ListTable, PivotTable, TYPES } from '@visactor/vtable'; -import type { FilterState, FilterOperator, FilterConfig } from './types'; +import type { FilterState, FilterConfig, FilterOptions } from './types'; /** * 筛选引擎,用于进行实际的筛选操作 @@ -8,6 +8,12 @@ export class FilterEngine { filterFuncRule: (TYPES.FilterFuncRule & { fieldId?: string })[] = []; filterValueRule: TYPES.FilterValueRule[] = []; + private pluginOptions: FilterOptions; + + constructor(filterPluginOptions: FilterOptions) { + this.pluginOptions = filterPluginOptions; + } + applyFilter(state: FilterState, table: ListTable | PivotTable) { const { filters } = state; this.filterFuncRule = []; @@ -34,7 +40,8 @@ export class FilterEngine { }); table.updateFilterRules([...this.filterFuncRule, ...this.filterValueRule], { - clearRowHeightCache: false + clearRowHeightCache: false, + onFilterRecordsEnd: this.pluginOptions?.onFilterRecordsEnd }); } diff --git a/packages/vtable-plugins/src/filter/types.ts b/packages/vtable-plugins/src/filter/types.ts index 7b9f062f1c..17afe667f7 100644 --- a/packages/vtable-plugins/src/filter/types.ts +++ b/packages/vtable-plugins/src/filter/types.ts @@ -23,6 +23,8 @@ export interface FilterOptions { * @default true */ syncFilterItemsState?: boolean; + /** 筛选记录结束回调 */ + onFilterRecordsEnd?: (records: any[]) => any[]; } export interface FilterOperatorCategoryOption { diff --git a/packages/vtable/src/ListTable.ts b/packages/vtable/src/ListTable.ts index b90cd3d0d5..a741accc0c 100644 --- a/packages/vtable/src/ListTable.ts +++ b/packages/vtable/src/ListTable.ts @@ -1305,6 +1305,7 @@ export class ListTable extends BaseTable implements ListTableAPI { filterRules: FilterRules, options: { clearRowHeightCache?: boolean; + onFilterRecordsEnd?: (records: any[]) => any[]; } = { clearRowHeightCache: true } ) { this.scenegraph.clearCells(); @@ -1312,7 +1313,7 @@ export class ListTable extends BaseTable implements ListTableAPI { this.dataSource.updateFilterRulesForSorted(filterRules); sortRecords(this); } else { - this.dataSource.updateFilterRules(filterRules); + this.dataSource.updateFilterRules(filterRules, options?.onFilterRecordsEnd); } this.refreshRowColCount(); this.stateManager.initCheckedState(this.records); diff --git a/packages/vtable/src/data/DataSource.ts b/packages/vtable/src/data/DataSource.ts index 84cc8837fa..8757117994 100644 --- a/packages/vtable/src/data/DataSource.ts +++ b/packages/vtable/src/data/DataSource.ts @@ -457,7 +457,7 @@ export class DataSource extends EventTarget implements DataSourceAPI { // return 0; // } let childTotalLength = 0; - const nodeLength = nodeData.filteredChildren ? nodeData.filteredChildren.length : nodeData.children?.length ?? 0; + const nodeLength = nodeData.filteredChildren ? nodeData.filteredChildren.length : (nodeData.children?.length ?? 0); for (let j = 0; j < nodeLength; j++) { if (currentLevel <= hierarchyExpandLevel || nodeData.hierarchyState === HierarchyState.expand) { childTotalLength += 1; @@ -1225,10 +1225,13 @@ export class DataSource extends EventTarget implements DataSourceAPI { } } - updateFilterRules(filterRules?: FilterRules): void { + updateFilterRules(filterRules?: FilterRules, onFilterRecordsEnd?: (records: any[]) => any[]): void { this.lastFilterRules = this.dataConfig.filterRules; this.dataConfig.filterRules = filterRules; this._source = this.processRecords(this.dataSourceObj?.records ?? this.dataSourceObj); + if (onFilterRecordsEnd) { + this._source = onFilterRecordsEnd(this._source as any[]); + } this._sourceLength = this._source?.length || 0; // 初始化currentIndexedData 正常未排序。设置其状态 this.currentIndexedData = Array.from({ length: this._sourceLength }, (_, i) => i); From 0070b71e84b45d64789843577bcbd6e905878767 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Wed, 10 Dec 2025 15:47:32 +0800 Subject: [PATCH 24/34] fix: null error when set scroll top --- packages/vtable/src/state/state.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/vtable/src/state/state.ts b/packages/vtable/src/state/state.ts index e359179ad5..fd19c7abb6 100644 --- a/packages/vtable/src/state/state.ts +++ b/packages/vtable/src/state/state.ts @@ -1187,6 +1187,9 @@ export class StateManager { } } setScrollTop(top: number, event?: FederatedWheelEvent, triggerEvent: boolean = true) { + if (!this.table) { + return; + } // 矫正top值范围 const totalHeight = this.table.getAllRowsHeight(); // _disableColumnAndRowSizeRound环境中,可能出现 From f4ae575a9457d1085cf51ee1fba350edaefea55c Mon Sep 17 00:00:00 2001 From: skie1997 Date: Wed, 10 Dec 2025 15:50:37 +0800 Subject: [PATCH 25/34] feat: no effect for different filter mode. feat#4793 --- packages/vtable-plugins/demo/filter/filter.ts | 20 ++++ .../src/filter/condition-filter.ts | 18 +-- .../src/filter/filter-state-manager.ts | 18 ++- packages/vtable-plugins/src/filter/filter.ts | 6 - .../vtable-plugins/src/filter/value-filter.ts | 112 +++++++----------- 5 files changed, 88 insertions(+), 86 deletions(-) diff --git a/packages/vtable-plugins/demo/filter/filter.ts b/packages/vtable-plugins/demo/filter/filter.ts index d4514809ae..7748fb31ca 100644 --- a/packages/vtable-plugins/demo/filter/filter.ts +++ b/packages/vtable-plugins/demo/filter/filter.ts @@ -145,6 +145,26 @@ export function createTable() { ]; const filterPlugin = new FilterPlugin({ + filterIcon: { + name: 'filter-icon', + type: 'svg', + width: 20, + height: 20, + positionType: VTable.TYPES.IconPosition.right, + cursor: 'pointer', + marginRight: 4, + svg: `` + }, + filteringIcon: { + name: 'filtering-icon', + type: 'svg', + width: 20, + height: 20, + positionType: VTable.TYPES.IconPosition.right, + cursor: 'pointer', + marginRight: 4, + svg: `` + }, styles: { filterMenu: { display: 'none', diff --git a/packages/vtable-plugins/src/filter/condition-filter.ts b/packages/vtable-plugins/src/filter/condition-filter.ts index edffc70d59..981c28bee6 100644 --- a/packages/vtable-plugins/src/filter/condition-filter.ts +++ b/packages/vtable-plugins/src/filter/condition-filter.ts @@ -108,21 +108,22 @@ export class ConditionFilter { const filter = this.filterStateManager.getFilterState(this.selectedField); const syncFilterItemsState = this.pluginOptions?.syncFilterItemsState ?? true; - if (filter && (filter.type === 'byCondition' || !syncFilterItemsState)) { + // 不联动的场景下, 用户的配置始终会被展示出来 + if ((filter && filter.type === 'byCondition') || !syncFilterItemsState) { // 设置操作符 - if (filter.operator && this.operatorSelect) { - this.operatorSelect.value = filter.operator; + if (this.operatorSelect) { + this.operatorSelect.value = filter?.operator ?? operators[0].value; } // 设置条件值 - if (filter.condition !== undefined && this.valueInput) { - if (Array.isArray(filter.condition)) { + if (this.valueInput) { + if (Array.isArray(filter?.condition)) { this.valueInput.value = String(filter.condition[0]); if (this.valueInputMax) { this.valueInputMax.value = String(filter.condition[1]); } } else { - this.valueInput.value = String(filter.condition); + this.valueInput.value = String(filter?.condition ?? ''); if (this.valueInputMax) { this.valueInputMax.value = ''; } @@ -256,7 +257,7 @@ export class ConditionFilter { } // TODO:处理单元格颜色和字体颜色的筛选 - + const syncFilterItemsState = this.pluginOptions?.syncFilterItemsState ?? true; this.filterStateManager.dispatch({ type: FilterActionType.APPLY_FILTERS, payload: { @@ -264,7 +265,8 @@ export class ConditionFilter { type: 'byCondition', operator, condition: conditionValue, - enable: true + enable: true, + shouldKeepUnrelatedState: !syncFilterItemsState } }); diff --git a/packages/vtable-plugins/src/filter/filter-state-manager.ts b/packages/vtable-plugins/src/filter/filter-state-manager.ts index 1af4422370..507c0098e1 100644 --- a/packages/vtable-plugins/src/filter/filter-state-manager.ts +++ b/packages/vtable-plugins/src/filter/filter-state-manager.ts @@ -80,10 +80,24 @@ export class FilterStateManager { const newFilter = new Map(state.filters); switch (type) { case FilterActionType.ADD_FILTER: - newFilter.set(payload.field, payload); + if (payload.shouldKeepUnrelatedState) { + newFilter.set(payload.field, { ...newFilter.get(payload.field), ...payload }); + } else { + newFilter.set(payload.field, payload); + } break; case FilterActionType.REMOVE_FILTER: - newFilter.delete(payload.field); + if (payload.shouldKeepUnrelatedState && payload.type === 'byValue') { + delete newFilter.get(payload.field).values; + newFilter.set(payload.field, { ...newFilter.get(payload.field), enable: false }); + } else if (payload.shouldKeepUnrelatedState && payload.type === 'byCondition') { + delete newFilter.get(payload.field).condition; + delete newFilter.get(payload.field).operator; + newFilter.set(payload.field, { ...newFilter.get(payload.field), enable: false }); + } else { + newFilter.delete(payload.field); + } + break; case FilterActionType.UPDATE_FILTER: newFilter.set(payload.field, { ...newFilter.get(payload.field), ...payload }); diff --git a/packages/vtable-plugins/src/filter/filter.ts b/packages/vtable-plugins/src/filter/filter.ts index a97786aeb0..7cfd7e65dd 100644 --- a/packages/vtable-plugins/src/filter/filter.ts +++ b/packages/vtable-plugins/src/filter/filter.ts @@ -143,15 +143,9 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin { } update() { - // 更新筛选状态 - // 如果处于按值筛选状态, 则需要更新筛选条件 和 候选值 - // this.filterToolbar.valueFilter?.syncRulesAndCandidateKeys(); if (this.filterStateManager) { this.reapplyActiveFilters(); } - // 更新筛选状态 - // 如果处于按条件筛选, 则需要执行筛选后, 更新值面板中checkbox的状态 - // this.filterToolbar.valueFilter?.syncSelectedKeys(); } /** diff --git a/packages/vtable-plugins/src/filter/value-filter.ts b/packages/vtable-plugins/src/filter/value-filter.ts index a5dc2fc09a..bf1589497c 100644 --- a/packages/vtable-plugins/src/filter/value-filter.ts +++ b/packages/vtable-plugins/src/filter/value-filter.ts @@ -100,58 +100,6 @@ export class ValueFilter { this.toUnformattedCache.set(fieldId, toUnformatted); } - syncRulesAndCandidateKeys() { - // 处于值筛选状态, 表格更新时: - // 可能会插入新数据, 此时需要更新筛选结果和候选值: - // 1. 更新筛选结果 - // - 出现了之前没有出现过的选项 - // - 筛选器出于被筛选状态(有值) - // - 则将该选项添加到筛选器中 - // 2. 更新候选值 - const currentRecords = this.table.internalProps.dataSource.records; // 此时还没做筛选, 当前数据 = 原始表格数据 - const filteredFields = this.filterStateManager.getActiveFilterFields(); - currentRecords.forEach(record => { - filteredFields.forEach(candidateField => { - const formatFn = this.getFormatFnCache(candidateField); - - // 空行不做处理 - if (isValid(record)) { - const originalValue = record[candidateField]; - const formattedValue = formatFn(record); - const lastToUnformatted = this.toUnformattedCache.get(candidateField) || new Map(); - if ( - !lastToUnformatted.has(formattedValue) && - this.filterStateManager.getFilterState(candidateField)?.values?.length > 0 - ) { - this.filterStateManager.getFilterState(candidateField).values.push(originalValue); - this.selectedKeys.get(candidateField).add(originalValue); - } - } - }); - }); - } - - syncSelectedKeys() { - // 处于条件筛选状态, 表格更新时: - // 值筛选面板需要同步筛选结果 - const currentRecords = this.table.internalProps.dataSource.records; // 此时还没做筛选, 当前数据 = 原始表格数据 - const filteredFields = this.filterStateManager.getActiveFilterFields(); - currentRecords.forEach(record => { - filteredFields.forEach(candidateField => { - // 空行不做处理 - if (isValid(record)) { - const originalValue = record[candidateField]; - if (this.filterStateManager.getFilterState(candidateField)?.type === 'byCondition') { - if (!this.selectedKeys.get(candidateField)) { - this.selectedKeys.set(candidateField, new Set()); - } - this.selectedKeys.get(candidateField).add(originalValue); - } - } - }); - }); - } - /** * 为已应用筛选的列,收集候选值集合 */ @@ -240,7 +188,7 @@ export class ValueFilter { return; } - let selectedValues = new Set(); + const selectedValues = new Set(); const originalValues = new Set(); const originalRecords = this.table.internalProps.records; // 原始数据 originalRecords.forEach(record => { @@ -259,22 +207,39 @@ export class ValueFilter { selectedValues.add(record[fieldId]); } }); + const hasFiltered = !arrayEqual(Array.from(originalValues), Array.from(selectedValues)); + if (hasFiltered) { + this.selectedKeys.set(fieldId, selectedValues); + this.filterStateManager.dispatch({ + type: FilterActionType.ADD_FILTER, + payload: { + field: fieldId, + type: 'byValue', + values: Array.from(selectedValues), + enable: true + } + }); + } } else { - const selectedFromRules = this.filterStateManager.getFilterState(fieldId)?.values || originalValues; // 如果按值筛选没有状态, 则默认选中所有值 - selectedValues = new Set(selectedFromRules); - } - this.selectedKeys.set(fieldId, selectedValues); - const hasFiltered = !arrayEqual(Array.from(originalValues), Array.from(selectedValues)); - if (hasFiltered) { - this.filterStateManager.dispatch({ - type: FilterActionType.ADD_FILTER, - payload: { - field: fieldId, - type: 'byValue', - values: Array.from(selectedValues), - enable: true + const selectedRules = this.filterStateManager.getFilterState(fieldId)?.values; // 如果按值筛选没有状态, 则默认选中所有值 + if (selectedRules) { + const hasFiltered = !arrayEqual(Array.from(originalValues), selectedRules); + if (hasFiltered) { + this.selectedKeys.set(fieldId, new Set(selectedRules)); + this.filterStateManager.dispatch({ + type: FilterActionType.ADD_FILTER, + payload: { + field: fieldId, + type: 'byValue', + values: selectedRules, + enable: true, + shouldKeepUnrelatedState: true + } + }); } - }); + } else { + this.selectedKeys.set(fieldId, originalValues); + } } } @@ -296,21 +261,28 @@ export class ValueFilter { this.selectedKeys.set(fieldId, new Set(selections)); - if (selections.length >= 0 && selections.length < this.valueFilterOptionList.get(fieldId).length) { + const syncFilterItemsState = this.pluginOptions?.syncFilterItemsState ?? true; + + if ( + (selections.length >= 0 && selections.length < this.valueFilterOptionList.get(fieldId).length) || + !syncFilterItemsState + ) { this.filterStateManager.dispatch({ type: FilterActionType.APPLY_FILTERS, payload: { field: fieldId, type: 'byValue', values: selections, - enable: true + enable: true, + shouldKeepUnrelatedState: !syncFilterItemsState } }); } else { this.filterStateManager.dispatch({ type: FilterActionType.REMOVE_FILTER, payload: { - field: fieldId + field: fieldId, + type: 'byValue' } }); } From 985e5c88859d97bb6b0c88c08f648ab0b4f72b0f Mon Sep 17 00:00:00 2001 From: skie1997 Date: Wed, 10 Dec 2025 19:49:28 +0800 Subject: [PATCH 26/34] fix: panel hide when press enter. fix#4813 --- ...elease-1.22.7-alpha.8_2025-12-10-11-49.json | 10 ++++++++++ packages/vtable-plugins/demo/filter/filter.ts | 2 +- .../src/filter/condition-filter.ts | 13 ++++++++++--- .../src/filter/filter-toolbar.ts | 18 +++++++++--------- packages/vtable/src/state/state.ts | 2 +- 5 files changed, 31 insertions(+), 14 deletions(-) create mode 100644 common/changes/@visactor/vtable/pre-release-1.22.7-alpha.8_2025-12-10-11-49.json diff --git a/common/changes/@visactor/vtable/pre-release-1.22.7-alpha.8_2025-12-10-11-49.json b/common/changes/@visactor/vtable/pre-release-1.22.7-alpha.8_2025-12-10-11-49.json new file mode 100644 index 0000000000..33642e2f80 --- /dev/null +++ b/common/changes/@visactor/vtable/pre-release-1.22.7-alpha.8_2025-12-10-11-49.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "fix: panel hide when press enter. fix#4813", + "type": "none" + } + ], + "packageName": "@visactor/vtable" +} \ No newline at end of file diff --git a/packages/vtable-plugins/demo/filter/filter.ts b/packages/vtable-plugins/demo/filter/filter.ts index 7748fb31ca..d80c1fbfe3 100644 --- a/packages/vtable-plugins/demo/filter/filter.ts +++ b/packages/vtable-plugins/demo/filter/filter.ts @@ -284,7 +284,7 @@ export function createTable() { syncFilterItemsState: false, onFilterRecordsEnd: records => { console.log('onFilterRecordsEnd'); - return records; + return [...records, null, undefined]; } }); (window as any).filterPlugin = filterPlugin; diff --git a/packages/vtable-plugins/src/filter/condition-filter.ts b/packages/vtable-plugins/src/filter/condition-filter.ts index 981c28bee6..9129534549 100644 --- a/packages/vtable-plugins/src/filter/condition-filter.ts +++ b/packages/vtable-plugins/src/filter/condition-filter.ts @@ -18,6 +18,7 @@ export class ConditionFilter { private table: ListTable | PivotTable; private filterStateManager: FilterStateManager; private pluginOptions: FilterOptions; + private filterToolBarHide: () => void; private styles: Record; @@ -39,13 +40,19 @@ export class ConditionFilter { private categories: FilterOperatorCategoryOption[] = []; protected operators: OperatorOption[] = []; - constructor(table: ListTable | PivotTable, filterStateManager: FilterStateManager, pluginOptions: FilterOptions) { + constructor( + table: ListTable | PivotTable, + filterStateManager: FilterStateManager, + pluginOptions: FilterOptions, + filterToolBarHide: () => void + ) { this.table = table; this.filterStateManager = filterStateManager; this.pluginOptions = pluginOptions; this.styles = pluginOptions.styles || {}; this.categories = pluginOptions.conditionCategories; this.operators = operators; + this.filterToolBarHide = filterToolBarHide; } setSelectedField(fieldId: string | number): void { @@ -269,8 +276,6 @@ export class ConditionFilter { shouldKeepUnrelatedState: !syncFilterItemsState } }); - - this.hide(); } /** @@ -395,6 +400,7 @@ export class ConditionFilter { this.valueInput.addEventListener('keypress', event => { if (event.key === 'Enter') { this.applyFilter(); + this.filterToolBarHide(); } }); @@ -402,6 +408,7 @@ export class ConditionFilter { this.valueInputMax.addEventListener('keypress', event => { if (event.key === 'Enter') { this.applyFilter(); + this.filterToolBarHide(); } }); diff --git a/packages/vtable-plugins/src/filter/filter-toolbar.ts b/packages/vtable-plugins/src/filter/filter-toolbar.ts index d0d24d88e2..4734830d02 100644 --- a/packages/vtable-plugins/src/filter/filter-toolbar.ts +++ b/packages/vtable-plugins/src/filter/filter-toolbar.ts @@ -35,7 +35,7 @@ export class FilterToolbar { this.table = table; this.filterStateManager = filterStateManager; this.valueFilter = new ValueFilter(this.table, this.filterStateManager, pluginOptions); - this.conditionFilter = new ConditionFilter(this.table, this.filterStateManager, pluginOptions); + this.conditionFilter = new ConditionFilter(this.table, this.filterStateManager, pluginOptions, this.hide); this.pluginOptions = pluginOptions; this.filterMenuWidth = 300; // 待优化,可能需要自适应内容的宽度 @@ -80,7 +80,7 @@ export class FilterToolbar { } else if (this.activeTab === 'byCondition') { this.conditionFilter.applyFilter(field); } - this.hide(this.currentCol, this.currentRow); + this.hide(); } private clearFilter(field: string | number): void { @@ -90,7 +90,7 @@ export class FilterToolbar { if (this.conditionFilter) { this.conditionFilter.clearFilter(field); } - this.hide(this.currentCol, this.currentRow); + this.hide(); } /** @@ -195,7 +195,7 @@ export class FilterToolbar { this.onTabSwitch('byCondition'); }); - this.cancelFilterButton.addEventListener('click', () => this.hide(this.currentCol, this.currentRow)); + this.cancelFilterButton.addEventListener('click', () => this.hide()); this.clearFilterOptionLink.addEventListener('click', e => { e.preventDefault(); @@ -209,7 +209,7 @@ export class FilterToolbar { // 点击空白处整个筛选菜单可消失 document.addEventListener('click', () => { if (this.isVisible) { - this.hide(this.currentCol, this.currentRow); + this.hide(); } }); @@ -310,14 +310,14 @@ export class FilterToolbar { }, 0); } - hide(currentCol: number | null, currentRow: number | null): void { + hide = (currentCol?: number, currentRow?: number): void => { this.filterMenu.style.display = 'none'; this.isVisible = false; this.table.fireListeners(TABLE_EVENT_TYPE.FILTER_MENU_HIDE, { - col: currentCol, - row: currentRow + col: currentCol ?? this.currentCol, + row: currentRow ?? this.currentRow }); - } + }; destroy() { this.valueFilter.destroy(); diff --git a/packages/vtable/src/state/state.ts b/packages/vtable/src/state/state.ts index fd19c7abb6..63f9ecac02 100644 --- a/packages/vtable/src/state/state.ts +++ b/packages/vtable/src/state/state.ts @@ -1187,7 +1187,7 @@ export class StateManager { } } setScrollTop(top: number, event?: FederatedWheelEvent, triggerEvent: boolean = true) { - if (!this.table) { + if (!this.table || !this.table.scenegraph) { return; } // 矫正top值范围 From 4ef1c45e7790991b23cd904c7d8bab0bfc0e8345 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Wed, 10 Dec 2025 21:01:35 +0800 Subject: [PATCH 27/34] chore: add filter docs --- docs/assets/guide/en/plugin/filter.md | 462 ++++++++++++++++++++++++- docs/assets/guide/zh/plugin/filter.md | 474 +++++++++++++++++++++++++- 2 files changed, 909 insertions(+), 27 deletions(-) diff --git a/docs/assets/guide/en/plugin/filter.md b/docs/assets/guide/en/plugin/filter.md index d206efaf37..7d863b65fc 100644 --- a/docs/assets/guide/en/plugin/filter.md +++ b/docs/assets/guide/en/plugin/filter.md @@ -29,29 +29,51 @@ export interface FilterOptions { defaultEnabled?: boolean; /** Filter modes: value-based filtering, condition-based filtering */ filterModes?: FilterMode[]; + /** + * Filter styles + * If style configuration is updated, you need to additionally call `filterPlugin.updateStyles` before updating the chart + */ + styles?: FilterStyles; + /** Custom filter categories */ + conditionCategories?: FilterOperatorCategoryOption[]; + /** Custom filter option display format */ + checkboxItemFormat?: (rawValue: any, formatValue: any) => any; + /** Whether multiple filters are linked + * @default true + */ + syncFilterItemsState?: boolean; + /** Filter records end callback */ + onFilterRecordsEnd?: (records: any[]) => any[]; } ``` ### Configuration Parameters -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `id` | string | `filter-${Date.now()}` | Plugin instance unique identifier | -| `filterIcon` | ColumnIconOption | Default filter icon | Filter icon for inactive state | -| `filteringIcon` | ColumnIconOption | Default active icon | Filter icon for active state | -| `enableFilter` | function | - | Custom column filter enable logic | -| `defaultEnabled` | boolean | true | Default filter enabled state | -| `filterModes` | FilterMode[] | ['byValue', 'byCondition'] | Supported filter modes | +| Parameter | Type | Default | Description | +| ---------------------- | ---------------------------------------- | -------------------------- | ----------------------------------- | +| `id` | string | `filter-${Date.now()}` | Plugin instance unique identifier | +| `filterIcon` | ColumnIconOption | Default filter icon | Filter icon for inactive state | +| `filteringIcon` | ColumnIconOption | Default active icon | Filter icon for active state | +| `enableFilter` | function | - | Custom column filter enable logic | +| `defaultEnabled` | boolean | true | Default filter enabled state | +| `filterModes` | FilterMode[] | ['byValue', 'byCondition'] | Supported filter modes | +| `styles` | FilterStyles | - | Custom filter styles | +| `conditionCategories` | FilterOperatorCategoryOption[] | - | Custom filter categories | +| `checkboxItemFormat` | (rawValue: any, formatValue: any) => any | - | Custom filter option display format | +| `syncFilterItemsState` | boolean | true | Whether multiple filters are linked | +| `onFilterRecordsEnd` | (records: any[]) => any[] | - | Filter records end callback | ### Filter Operators The plugin supports the following filter operators: **General Operators** + - `equals` - Equals - `notEquals` - Not equals **Numeric Operators** + - `greaterThan` - Greater than - `lessThan` - Less than - `greaterThanOrEqual` - Greater than or equal @@ -60,6 +82,7 @@ The plugin supports the following filter operators: - `notBetween` - Not between **Text Operators** + - `contains` - Contains - `notContains` - Does not contain - `startsWith` - Starts with @@ -68,6 +91,7 @@ The plugin supports the following filter operators: - `notEndsWith` - Does not end with **Boolean Operators** + - `isChecked` - Is checked - `isUnchecked` - Is unchecked @@ -129,7 +153,7 @@ const filterPlugin = new FilterPlugin({ const columns = [ { field: 'name', title: 'Name', width: 120 }, // Default enable filtering { field: 'age', title: 'Age', width: 100, filter: false }, // Disable filtering - { field: 'department', title: 'Department', width: 150 }, // Default enable filtering + { field: 'department', title: 'Department', width: 150 } // Default enable filtering ]; ``` @@ -158,7 +182,7 @@ if (savedState) { // Normal usage: const filterPlugin = new FilterPlugin({}); // In the website editor, VTable.plugins is renamed to VTablePlugins -const generateDemoData = (count) => { +const generateDemoData = count => { const departments = ['Engineering', 'Marketing', 'Sales', 'HR', 'Finance']; const statuses = ['Active', 'On Leave', 'Inactive']; @@ -184,8 +208,7 @@ const option = { { field: 'name', title: 'Name', width: 120 }, { field: 'age', title: 'Age', width: 100 }, { field: 'department', title: 'Department', width: 120 }, - { field: 'salary', title: 'Salary', width: 120, - fieldFormat: (record) => '$' + record.salary }, + { field: 'salary', title: 'Salary', width: 120, fieldFormat: record => '$' + record.salary }, { field: 'status', title: 'Status', width: 100 }, { field: 'isFullTime', title: 'Full Time', width: 80, cellType: 'checkbox' } ], @@ -196,6 +219,416 @@ const tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID) window.tableInstance = tableInstance; ``` +## Large Screen Business Scenario Examples + +```javascript livedemo template=vtable +// import * as VTable from '@visactor/vtable'; +// When using, you need to import the plugin package @visactor/vtable-plugins +// import { FilterPlugin } from '@visactor/vtable-plugins'; +// Normal usage: const filterPlugin = new FilterPlugin({}); +// In the website editor, VTable.plugins is renamed to VTablePlugins +const columnStyle = { + textAlign: 'center', + borderColor: ['rgba(63,63,86,0)', null, null, null], + borderLineWidth: [1, 0, 0, 0], + borderLineDash: [null, null, null, null], + padding: [0, 0, 0, 0], + hover: { + cellBgColor: 'rgba(186, 215, 255, 0.7)', + inlineRowBgColor: 'rgba(186, 215, 255, 0.3)', + inlineColumnBgColor: 'rgba(186, 215, 255, 0.3)' + }, + fontFamily: 'D-DIN', + fontSize: 12, + fontStyle: 'normal', + fontWeight: 'normal', + fontVariant: 'normal', + color: 'rgba(255,255,255,1)', + lineHeight: 18, + underline: false +}; +const headerStyle = { + textAlign: 'center', + borderColor: [null, null, null, null], + borderLineWidth: [null, 0, 0, 0], + borderLineDash: [null, null, null, null], + padding: [0, 0, 0, 0], + hover: { + cellBgColor: 'rgba(0, 100, 250, 0.16)', + inlineRowBgColor: 'rgba(255, 255, 255, 0)', + inlineColumnBgColor: 'rgba(255, 255, 255, 0)' + }, + frameStyle: { + borderColor: [null, null, null, null], + borderLineWidth: 2 + }, + fontFamily: 'SourceHanSansCN-Normal', + fontSize: 12, + fontVariant: 'normal', + fontStyle: 'normal', + fontWeight: 'bold', + color: '#FFFFFF', + bgColor: '#0e305c', + lineHeight: 18, + underline: false +}; +// Special filter configuration: +// 1. syncFilterItemsState: +// - When set to false, it means: +// - The filter panel does not sync with data, the filter displays whatever conditions the user configures, and the filter result is the combined effect of multiple filters +// - For value filtering, after configuring and applying filters, when data is updated, new data will not be automatically added to the checked configuration, i.e., will not be selected +// 2. styles: Custom styles +// 3. conditionCategories: Filter types for condition-based filtering +// 4. checkboxItemFormat: Ensure filter options display as original values through callback +const getTableFilterPluginAttrFromProps = () => { + const filterProps = { + visible: false, + fillColor: '#0E1119', + strokeColor: '#272A30', + strokeWidth: 0, + borderRadius: 4, + highlightColor: '#006EFF', + defaultColor: '#FFF', + textStyle: { + color: '#FFF', + fontFamily: 'SourceHanSansCN-Normal', + fontSize: 12 + } + }; + const { + fillColor, + strokeColor, + strokeWidth, + borderRadius, + highlightColor, + defaultColor, + textStyle: { color: textColor, fontFamily: textFontFamily, fontSize: textFontSize, fontWeight: fontFontWeight } + } = filterProps; + return { + syncFilterItemsState: false, + filterModes: ['byValue', 'byCondition'], + filterIcon: { + name: 'filter-icon', + type: 'svg', + width: 12, + height: 12, + positionType: 'right', + cursor: 'pointer', + marginRight: 4, + svg: `` + }, + filteringIcon: { + name: 'filtering-icon', + type: 'svg', + width: 12, + height: 12, + positionType: 'right', + cursor: 'pointer', + marginRight: 4, + svg: `` + }, + styles: { + filterMenu: { + position: 'absolute', + backgroundColor: fillColor, + border: `${strokeWidth}px solid ${strokeColor}`, + boxShadow: '0 4px 8px rgba(0,0,0,0.15)', + zIndex: '100', + borderRadius: `${borderRadius}px`, + color: textColor, + fontFamily: textFontFamily, + fontSize: `${textFontSize}px` + }, + searchInput: { + width: '100%', + padding: '8px 10px', + border: '1px solid #272a30', + borderRadius: '4px', + backgroundColor: '#0e1119', + boxSizing: 'border-box', + placeholder: '请输入关键字搜索', + color: textColor + }, + tabsContainer: { + borderBottom: '0px solid #e0e0e0' + }, + + tabStyle: isActive => ({ + backgroundColor: 'transparent', + border: 'none', + flex: '1', + padding: '10px 15px', + cursor: 'pointer', + fontWeight: isActive ? 'bold' : 'normal', + color: isActive ? highlightColor : defaultColor, + borderBottom: isActive ? `3px solid ${highlightColor}` : '2px solid transparent' + }), + + countSpan: { + color: 'transparent' + }, + + footerContainer: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '10px 15px', + borderTop: '0px solid #e0e0e0', + backgroundColor: 'transparent' + }, + + footerButton: isPrimary => ({ + padding: '6px 12px', + border: '1px solid #ccc', + borderRadius: '4px', + cursor: 'pointer', + marginLeft: '5px', + backgroundColor: isPrimary ? highlightColor : 'transparent', + color: isPrimary ? defaultColor : highlightColor, + borderColor: isPrimary ? highlightColor : 'transparent' + }), + + clearLink: { + color: highlightColor, + textDecoration: 'none' + }, + + buttonStyle: (isPrimary = false) => ({ + padding: '6px 12px', + border: '1px solid #ccc', + borderRadius: `${borderRadius}px`, + cursor: 'pointer', + marginLeft: '5px', + backgroundColor: isPrimary ? highlightColor : 'white', + color: isPrimary ? 'white' : textColor, + borderColor: isPrimary ? highlightColor : '#ccc' + }), + + conditionContainer: { + marginBottom: '15px', + padding: '10px', + backgroundColor: fillColor, + color: textColor + }, + + formLabel: { + display: 'block', + marginBottom: '8px', + fontWeight: 'normal', + backgroundColor: 'transparent', + color: textColor + }, + + operatorSelect: { + width: '100%', + padding: '8px', + marginBottom: '15px', + border: '1px solid #272a30', + borderRadius: '4px', + backgroundColor: '#0e1119', + color: textColor + }, + + rangeInputContainer: { + display: 'flex', + alignItems: 'center', + gap: '8px', + backgroundColor: fillColor, + color: textColor + } + }, + conditionCategories: [ + { value: 'all', label: '全部' }, + { value: 'text', label: '文本' }, + { value: 'number', label: '数值' } + // { value: FilterOperatorCategory.COLOR, label: '颜色' }, + // { value: FilterOperatorCategory.CHECKBOX, label: '复选框' }, + // { value: FilterOperatorCategory.RADIO, label: '单选框' } + ], + checkboxItemFormat: (rawValue, formatValue) => rawValue + }; +}; +const filterPlugin = new VTablePlugins.FilterPlugin(getTableFilterPluginAttrFromProps()); +const option = { + columns: [ + { + field: '0#LINE_NUMBER_DIM_ID_STR', + title: '序号', + width: '12%', + style: columnStyle + }, + { + field: 'PZwFghHcsvwt', + title: 'From Province', + showSort: false, + style: columnStyle, + headerStyle, + width: '29.333333333333332%' + }, + { + field: 'CKQPX3dQXKYk', + title: 'To Province', + showSort: false, + style: columnStyle, + headerStyle, + width: '29.333333333333332%' + }, + { + firstRow: 0.8, + field: 'RMLcpglcOTHo', + title: 'Profit', + showSort: false, + style: columnStyle, + headerStyle, + width: '29.333333333333332%', + fieldFormat: datum => { + return '¥' + datum['RMLcpglcOTHo']; + } + } + ], + records: [ + { + PZwFghHcsvwt: '河北', + CKQPX3dQXKYk: '河南', + RMLcpglcOTHo: 0.8, + '0#LINE_NUMBER_DIM_ID_STR': 1, + '0#LINE_NUMBER_ICON': + '' + }, + { + PZwFghHcsvwt: '山西', + CKQPX3dQXKYk: '湖北', + RMLcpglcOTHo: 15, + '0#LINE_NUMBER_DIM_ID_STR': 2, + '0#LINE_NUMBER_ICON': + '' + }, + { + PZwFghHcsvwt: '内蒙古', + CKQPX3dQXKYk: '湖南', + RMLcpglcOTHo: 50, + '0#LINE_NUMBER_DIM_ID_STR': 3, + '0#LINE_NUMBER_ICON': + '' + }, + { + PZwFghHcsvwt: '辽宁', + CKQPX3dQXKYk: '广东', + RMLcpglcOTHo: 15, + '0#LINE_NUMBER_DIM_ID_STR': 4, + '0#LINE_NUMBER_ICON': + '' + }, + { + PZwFghHcsvwt: '吉林', + CKQPX3dQXKYk: '广西', + RMLcpglcOTHo: 57, + '0#LINE_NUMBER_DIM_ID_STR': 5, + '0#LINE_NUMBER_ICON': + '' + }, + { + PZwFghHcsvwt: '江西', + CKQPX3dQXKYk: '湖南', + RMLcpglcOTHo: 44, + '0#LINE_NUMBER_DIM_ID_STR': 6, + '0#LINE_NUMBER_ICON': + '' + }, + { + PZwFghHcsvwt: '山东', + CKQPX3dQXKYk: '福建', + RMLcpglcOTHo: 20, + '0#LINE_NUMBER_DIM_ID_STR': 7, + '0#LINE_NUMBER_ICON': + '' + }, + { + PZwFghHcsvwt: '河南', + CKQPX3dQXKYk: '广东', + RMLcpglcOTHo: 65, + '0#LINE_NUMBER_DIM_ID_STR': 8, + '0#LINE_NUMBER_ICON': + '' + }, + { + PZwFghHcsvwt: '湖北', + CKQPX3dQXKYk: '江西', + RMLcpglcOTHo: 40, + '0#LINE_NUMBER_DIM_ID_STR': 9, + '0#LINE_NUMBER_ICON': + '' + }, + { + PZwFghHcsvwt: '湖南', + CKQPX3dQXKYk: '湖北', + RMLcpglcOTHo: 35, + '0#LINE_NUMBER_DIM_ID_STR': 10, + '0#LINE_NUMBER_ICON': + '' + } + ], + theme: { + underlayBackgroundColor: 'rgba(255,255,255,0)', + headerStyle, + bodyStyle: { + bgColor: ({ row, col }) => { + return row % 2 === 0 ? 'rgba(16,34,58,1)' : 'rgba(12,9,41,1)'; + } + } + }, + transpose: false, + widthMode: 'adaptive', + columnResizeMode: 'all', + heightMode: 'standard', + heightAdaptiveMode: 'all', + autoFillHeight: true, + autoWrapText: true, + maxCharactersNumber: 256, + defaultHeaderColWidth: 'auto', + keyboardOptions: { + selectAllOnCtrlA: true, + copySelected: false + }, + menu: { + renderMode: 'html' + }, + disableScroll: true, + customConfig: { + _disableColumnAndRowSizeRound: true, + imageMargin: 4, + multilinesForXTable: true, + shrinkSparklineFirst: true, + limitContentHeight: false + }, + frozenColCount: 0, + showHeader: true, + hover: { + disableHover: true + }, + select: { + highlightMode: 'row', + headerSelectMode: 'cell', + blankAreaClickDeselect: true, + disableSelect: true + }, + autoHeightInAdaptiveMode: false, + defaultRowHeight: 61.25, + animationAppear: { + duration: 100, + delay: 0, + type: 'one-by-one', + direction: 'row' + }, + hash: '0fa7dcedd7d638eff65f3a5bd3906361', + width: 400, + height: 245, + plugins: [filterPlugin] +}; +const tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID), option); +window.tableInstance = tableInstance; +``` + ## Usage Instructions 1. **Click filter icon**: Click the filter icon on the right side of the column header to open the filter panel @@ -208,9 +641,10 @@ window.tableInstance = tableInstance; - The filter plugin currently only supports `ListTable`, not `PivotTable` - When using column-level filter control, you need to add the `filter` property to column definitions -- Filter states are automatically synchronized when table configuration is updated +- `syncFilterItemsState` defaults to true, in which case the filter state will automatically sync when the table configuration is updated - It is recommended to enable filtering for large data tables to improve user experience +- If the style configuration is updated, you need to additionally call `filterPlugin.updateStyles` before updating the chart -# This plugin was contributed by +# This plugin was contributed by [PoorShawn](https://github.com/PoorShawn) diff --git a/docs/assets/guide/zh/plugin/filter.md b/docs/assets/guide/zh/plugin/filter.md index 394791bd6b..386f66200b 100644 --- a/docs/assets/guide/zh/plugin/filter.md +++ b/docs/assets/guide/zh/plugin/filter.md @@ -29,29 +29,51 @@ export interface FilterOptions { defaultEnabled?: boolean; /** 筛选模式:按值筛选、按条件筛选 */ filterModes?: FilterMode[]; + /** + * 筛选器样式 + * 如果样式配置更新, 需在更新图表之前额外调用 `filterPlugin.updateStyles` + * */ + styles?: FilterStyles; + /** 自定义筛选分类 */ + conditionCategories?: FilterOperatorCategoryOption[]; + /** 自定义筛选选项展示格式 */ + checkboxItemFormat?: (rawValue: any, formatValue: any) => any; + /** 多个筛选器之间是否联动 + * @default true + */ + syncFilterItemsState?: boolean; + /** 筛选记录结束回调 */ + onFilterRecordsEnd?: (records: any[]) => any[]; } ``` ### 配置参数说明 -| 参数名 | 类型 | 默认值 | 说明 | -|--------|------|--------|------| -| `id` | string | `filter-${Date.now()}` | 插件实例唯一标识符 | -| `filterIcon` | ColumnIconOption | 默认筛选图标 | 未激活状态的筛选图标 | -| `filteringIcon` | ColumnIconOption | 默认激活图标 | 激活状态的筛选图标 | -| `enableFilter` | function | - | 自定义列筛选启用逻辑 | -| `defaultEnabled` | boolean | true | 默认是否启用筛选 | -| `filterModes` | FilterMode[] | ['byValue', 'byCondition'] | 支持的筛选模式 | +| 参数名 | 类型 | 默认值 | 说明 | +| ---------------------- | ---------------------------------------- | -------------------------- | ---------------------- | +| `id` | string | `filter-${Date.now()}` | 插件实例唯一标识符 | +| `filterIcon` | ColumnIconOption | 默认筛选图标 | 未激活状态的筛选图标 | +| `filteringIcon` | ColumnIconOption | 默认激活图标 | 激活状态的筛选图标 | +| `enableFilter` | function | - | 自定义列筛选启用逻辑 | +| `defaultEnabled` | boolean | true | 默认是否启用筛选 | +| `filterModes` | FilterMode[] | ['byValue', 'byCondition'] | 支持的筛选模式 | +| `styles` | FilterStyles | - | 自定义筛选器样式 | +| `conditionCategories` | FilterOperatorCategoryOption[] | - | 自定义筛选分类 | +| `checkboxItemFormat` | (rawValue: any, formatValue: any) => any | - | 自定义筛选选项展示格式 | +| `syncFilterItemsState` | boolean | true | 多个筛选器之间是否联动 | +| `onFilterRecordsEnd` | (records: any[]) => any[] | - | 筛选记录结束回调 | ### 筛选操作符 插件支持以下筛选操作符: **通用操作符** + - `equals` - 等于 - `notEquals` - 不等于 **数值操作符** + - `greaterThan` - 大于 - `lessThan` - 小于 - `greaterThanOrEqual` - 大于等于 @@ -60,6 +82,7 @@ export interface FilterOptions { - `notBetween` - 不介于 **文本操作符** + - `contains` - 包含 - `notContains` - 不包含 - `startsWith` - 开始于 @@ -68,6 +91,7 @@ export interface FilterOptions { - `notEndsWith` - 不结束于 **布尔操作符** + - `isChecked` - 已选中 - `isUnchecked` - 未选中 @@ -129,7 +153,7 @@ const filterPlugin = new FilterPlugin({ const columns = [ { field: 'name', title: '姓名', width: 120 }, // 默认启用筛选 { field: 'age', title: '年龄', width: 100, filter: false }, // 禁用筛选 - { field: 'department', title: '部门', width: 150 }, // 默认启用筛选 + { field: 'department', title: '部门', width: 150 } // 默认启用筛选 ]; ``` @@ -158,7 +182,7 @@ if (savedState) { // 正常使用方式 const filterPlugin = new FilterPlugin({}); // 官网编辑器中将 VTable.plugins 重命名成了 VTablePlugins -const generateDemoData = (count) => { +const generateDemoData = count => { const departments = ['研发部', '市场部', '销售部', '人事部', '财务部']; const statuses = ['在职', '请假', '离职']; @@ -184,8 +208,7 @@ const option = { { field: 'name', title: '姓名', width: 120 }, { field: 'age', title: '年龄', width: 100 }, { field: 'department', title: '部门', width: 120 }, - { field: 'salary', title: '薪资', width: 120, - fieldFormat: (record) => '¥' + record.salary }, + { field: 'salary', title: '薪资', width: 120, fieldFormat: record => '¥' + record.salary }, { field: 'status', title: '状态', width: 100 }, { field: 'isFullTime', title: '全职', width: 80, cellType: 'checkbox' } ], @@ -196,6 +219,430 @@ const tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID) window.tableInstance = tableInstance; ``` +## 大屏业务场景案例 + +```javascript livedemo template=vtable +// import * as VTable from '@visactor/vtable'; +// 使用时需要引入插件包 @visactor/vtable-plugins +// import { FilterPlugin } from '@visactor/vtable-plugins'; +// 正常使用方式 const filterPlugin = new FilterPlugin({}); +// 官网编辑器中将 VTable.plugins 重命名成了 VTablePlugins + +const columnStyle = { + textAlign: 'center', + borderColor: ['rgba(63,63,86,0)', null, null, null], + borderLineWidth: [1, 0, 0, 0], + borderLineDash: [null, null, null, null], + padding: [0, 0, 0, 0], + hover: { + cellBgColor: 'rgba(186, 215, 255, 0.7)', + inlineRowBgColor: 'rgba(186, 215, 255, 0.3)', + inlineColumnBgColor: 'rgba(186, 215, 255, 0.3)' + }, + fontFamily: 'D-DIN', + fontSize: 12, + fontStyle: 'normal', + fontWeight: 'normal', + fontVariant: 'normal', + color: 'rgba(255,255,255,1)', + lineHeight: 18, + underline: false +}; +const headerStyle = { + textAlign: 'center', + borderColor: [null, null, null, null], + borderLineWidth: [null, 0, 0, 0], + borderLineDash: [null, null, null, null], + padding: [0, 0, 0, 0], + hover: { + cellBgColor: 'rgba(0, 100, 250, 0.16)', + inlineRowBgColor: 'rgba(255, 255, 255, 0)', + inlineColumnBgColor: 'rgba(255, 255, 255, 0)' + }, + frameStyle: { + borderColor: [null, null, null, null], + borderLineWidth: 2 + }, + fontFamily: 'SourceHanSansCN-Normal', + fontSize: 12, + fontVariant: 'normal', + fontStyle: 'normal', + fontWeight: 'bold', + color: '#FFFFFF', + bgColor: '#0e305c', + lineHeight: 18, + underline: false +}; +// filter特殊配置: +// 1. syncFilterItemsState: +// - 配置为false, 表示: +// - 筛选面板不同步数据, 用户配置什么条件筛选器就回显什么条件, 筛选结果为多个筛选器共同作用的结果 +// - 对于值筛选而言, 配置筛选且生效后, 更新数据, 则新数据不会被自动加到已勾选配置中, 即不被选中 +// 2. styles: 自定义样式 +// 3. conditionCategories: 条件筛选的筛选类型 +// 4. checkboxItemFormat: 通过回调保证筛选项展示为原始值 +const getTableFilterPluginAttrFromProps = () => { + const filterProps = { + visible: false, + fillColor: '#0E1119', + strokeColor: '#272A30', + strokeWidth: 0, + borderRadius: 4, + highlightColor: '#006EFF', + defaultColor: '#FFF', + textStyle: { + color: '#FFF', + fontFamily: 'SourceHanSansCN-Normal', + fontSize: 12 + } + }; + const { + fillColor, + strokeColor, + strokeWidth, + borderRadius, + highlightColor, + defaultColor, + textStyle: { color: textColor, fontFamily: textFontFamily, fontSize: textFontSize, fontWeight: fontFontWeight } + } = filterProps; + return { + syncFilterItemsState: false, + filterModes: ['byValue', 'byCondition'], + filterIcon: { + name: 'filter-icon', + type: 'svg', + width: 12, + height: 12, + positionType: 'right', + cursor: 'pointer', + marginRight: 4, + svg: `` + }, + filteringIcon: { + name: 'filtering-icon', + type: 'svg', + width: 12, + height: 12, + positionType: 'right', + cursor: 'pointer', + marginRight: 4, + svg: `` + }, + styles: { + // 筛选菜单 + filterMenu: { + position: 'absolute', + backgroundColor: fillColor, + border: `${strokeWidth}px solid ${strokeColor}`, + boxShadow: '0 4px 8px rgba(0,0,0,0.15)', + zIndex: '100', + borderRadius: `${borderRadius}px`, + color: textColor, + fontFamily: textFontFamily, + fontSize: `${textFontSize}px` + }, + + // 搜索输入框 + searchInput: { + width: '100%', + padding: '8px 10px', + border: '1px solid #272a30', + borderRadius: '4px', + backgroundColor: '#0e1119', + boxSizing: 'border-box', + // vtable内部没有做国际化, 所以这里也不做国际化 + placeholder: '请输入关键字搜索', + color: textColor + }, + tabsContainer: { + borderBottom: '0px solid #e0e0e0' + }, + + // 标签样式 + tabStyle: isActive => ({ + backgroundColor: 'transparent', + border: 'none', + flex: '1', + padding: '10px 15px', + cursor: 'pointer', + fontWeight: isActive ? 'bold' : 'normal', + color: isActive ? highlightColor : defaultColor, + borderBottom: isActive ? `3px solid ${highlightColor}` : '2px solid transparent' + }), + + countSpan: { + color: 'transparent' + }, + + // 页脚容器 + footerContainer: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '10px 15px', + borderTop: '0px solid #e0e0e0', + backgroundColor: 'transparent' + }, + + footerButton: isPrimary => ({ + padding: '6px 12px', + border: '1px solid #ccc', + borderRadius: '4px', + cursor: 'pointer', + marginLeft: '5px', + backgroundColor: isPrimary ? highlightColor : 'transparent', + color: isPrimary ? defaultColor : highlightColor, + borderColor: isPrimary ? highlightColor : 'transparent' + }), + + // 清除链接 + clearLink: { + color: highlightColor, + textDecoration: 'none' + }, + + // 按钮样式 + buttonStyle: (isPrimary = false) => ({ + padding: '6px 12px', + border: '1px solid #ccc', + borderRadius: `${borderRadius}px`, + cursor: 'pointer', + marginLeft: '5px', + backgroundColor: isPrimary ? highlightColor : 'white', + color: isPrimary ? 'white' : textColor, + borderColor: isPrimary ? highlightColor : '#ccc' + }), + + // === 条件筛选相关样式 === + + // 条件筛选容器 + conditionContainer: { + marginBottom: '15px', + padding: '10px', + backgroundColor: fillColor, + color: textColor + }, + + // 表单标签样式 + formLabel: { + display: 'block', + marginBottom: '8px', + fontWeight: 'normal', + backgroundColor: 'transparent', + color: textColor + }, + + // 操作符选择框样式 + operatorSelect: { + width: '100%', + padding: '8px', + marginBottom: '15px', + border: '1px solid #272a30', + borderRadius: '4px', + backgroundColor: '#0e1119', + color: textColor + }, + + rangeInputContainer: { + display: 'flex', + alignItems: 'center', + gap: '8px', + backgroundColor: fillColor, + color: textColor + } + }, + conditionCategories: [ + { value: 'all', label: '全部' }, + { value: 'text', label: '文本' }, + { value: 'number', label: '数值' } + // { value: FilterOperatorCategory.COLOR, label: '颜色' }, + // { value: FilterOperatorCategory.CHECKBOX, label: '复选框' }, + // { value: FilterOperatorCategory.RADIO, label: '单选框' } + ], + checkboxItemFormat: (rawValue, formatValue) => rawValue + }; +}; +const filterPlugin = new VTablePlugins.FilterPlugin(getTableFilterPluginAttrFromProps()); +const option = { + columns: [ + { + field: '0#LINE_NUMBER_DIM_ID_STR', + title: '序号', + width: '12%', + style: columnStyle + }, + { + field: 'PZwFghHcsvwt', + title: 'From Province', + showSort: false, + style: columnStyle, + headerStyle, + width: '29.333333333333332%' + }, + { + field: 'CKQPX3dQXKYk', + title: 'To Province', + showSort: false, + style: columnStyle, + headerStyle, + width: '29.333333333333332%' + }, + { + firstRow: 0.8, + field: 'RMLcpglcOTHo', + title: 'Profit', + showSort: false, + style: columnStyle, + headerStyle, + width: '29.333333333333332%', + fieldFormat: datum => { + return '¥' + datum['RMLcpglcOTHo']; + } + } + ], + records: [ + { + PZwFghHcsvwt: '河北', + CKQPX3dQXKYk: '河南', + RMLcpglcOTHo: 0.8, + '0#LINE_NUMBER_DIM_ID_STR': 1, + '0#LINE_NUMBER_ICON': + '' + }, + { + PZwFghHcsvwt: '山西', + CKQPX3dQXKYk: '湖北', + RMLcpglcOTHo: 15, + '0#LINE_NUMBER_DIM_ID_STR': 2, + '0#LINE_NUMBER_ICON': + '' + }, + { + PZwFghHcsvwt: '内蒙古', + CKQPX3dQXKYk: '湖南', + RMLcpglcOTHo: 50, + '0#LINE_NUMBER_DIM_ID_STR': 3, + '0#LINE_NUMBER_ICON': + '' + }, + { + PZwFghHcsvwt: '辽宁', + CKQPX3dQXKYk: '广东', + RMLcpglcOTHo: 15, + '0#LINE_NUMBER_DIM_ID_STR': 4, + '0#LINE_NUMBER_ICON': + '' + }, + { + PZwFghHcsvwt: '吉林', + CKQPX3dQXKYk: '广西', + RMLcpglcOTHo: 57, + '0#LINE_NUMBER_DIM_ID_STR': 5, + '0#LINE_NUMBER_ICON': + '' + }, + { + PZwFghHcsvwt: '江西', + CKQPX3dQXKYk: '湖南', + RMLcpglcOTHo: 44, + '0#LINE_NUMBER_DIM_ID_STR': 6, + '0#LINE_NUMBER_ICON': + '' + }, + { + PZwFghHcsvwt: '山东', + CKQPX3dQXKYk: '福建', + RMLcpglcOTHo: 20, + '0#LINE_NUMBER_DIM_ID_STR': 7, + '0#LINE_NUMBER_ICON': + '' + }, + { + PZwFghHcsvwt: '河南', + CKQPX3dQXKYk: '广东', + RMLcpglcOTHo: 65, + '0#LINE_NUMBER_DIM_ID_STR': 8, + '0#LINE_NUMBER_ICON': + '' + }, + { + PZwFghHcsvwt: '湖北', + CKQPX3dQXKYk: '江西', + RMLcpglcOTHo: 40, + '0#LINE_NUMBER_DIM_ID_STR': 9, + '0#LINE_NUMBER_ICON': + '' + }, + { + PZwFghHcsvwt: '湖南', + CKQPX3dQXKYk: '湖北', + RMLcpglcOTHo: 35, + '0#LINE_NUMBER_DIM_ID_STR': 10, + '0#LINE_NUMBER_ICON': + '' + } + ], + theme: { + underlayBackgroundColor: 'rgba(255,255,255,0)', + headerStyle, + bodyStyle: { + bgColor: ({ row, col }) => { + return row % 2 === 0 ? 'rgba(16,34,58,1)' : 'rgba(12,9,41,1)'; + } + } + }, + transpose: false, + widthMode: 'adaptive', + columnResizeMode: 'all', + heightMode: 'standard', + heightAdaptiveMode: 'all', + autoFillHeight: true, + autoWrapText: true, + maxCharactersNumber: 256, + defaultHeaderColWidth: 'auto', + keyboardOptions: { + selectAllOnCtrlA: true, + copySelected: false + }, + menu: { + renderMode: 'html' + }, + disableScroll: true, + customConfig: { + _disableColumnAndRowSizeRound: true, + imageMargin: 4, + multilinesForXTable: true, + shrinkSparklineFirst: true, + limitContentHeight: false + }, + frozenColCount: 0, + showHeader: true, + hover: { + disableHover: true + }, + select: { + highlightMode: 'row', + headerSelectMode: 'cell', + blankAreaClickDeselect: true, + disableSelect: true + }, + autoHeightInAdaptiveMode: false, + defaultRowHeight: 61.25, + animationAppear: { + duration: 100, + delay: 0, + type: 'one-by-one', + direction: 'row' + }, + hash: '0fa7dcedd7d638eff65f3a5bd3906361', + width: 400, + height: 245, + plugins: [filterPlugin] +}; +const tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID), option); +window.tableInstance = tableInstance; +``` + ## 使用说明 1. **点击筛选图标**:点击列标题右侧的筛选图标打开筛选面板 @@ -208,8 +655,9 @@ window.tableInstance = tableInstance; - 筛选插件目前仅支持 `ListTable`,不支持 `PivotTable` - 使用列级别筛选控制时,需要在列定义中添加 `filter` 属性 -- 筛选状态会在表格配置更新时自动同步 +- `syncFilterItemsState`默认为true, 此时筛选状态会在表格配置更新时自动同步 - 建议为大数据量表格启用筛选功能以提升用户体验 +- 如果样式配置更新, 需在更新图表之前额外调用 `filterPlugin.updateStyles` # 本插件贡献者及文档作者 From 5db56881026d7eba356a0ce3c9d08eeabe06cc2d Mon Sep 17 00:00:00 2001 From: skie1997 Date: Wed, 10 Dec 2025 21:23:14 +0800 Subject: [PATCH 28/34] fix: props null error --- packages/vtable/src/state/state.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/vtable/src/state/state.ts b/packages/vtable/src/state/state.ts index 63f9ecac02..a8dcdf3e73 100644 --- a/packages/vtable/src/state/state.ts +++ b/packages/vtable/src/state/state.ts @@ -1267,6 +1267,9 @@ export class StateManager { } } setScrollLeft(left: number, event?: FederatedWheelEvent, triggerEvent: boolean = true) { + if (!this.table || !this.table.scenegraph) { + return; + } const oldScrollLeft = this.table.scrollLeft; // 矫正left值范围 const totalWidth = this.table.getAllColsWidth(); From a638519ef2d36e94003ea43753d7747c483cb06d Mon Sep 17 00:00:00 2001 From: skie1997 Date: Wed, 10 Dec 2025 21:37:27 +0800 Subject: [PATCH 29/34] =?UTF-8?q?fix:=20=E9=9D=A2=E6=9D=BF=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E7=8A=B6=E6=80=81=E4=B8=8B,=20=E5=88=87=E6=8D=A2tab,?= =?UTF-8?q?=20=E6=9B=B4=E6=96=B0styles,=20tab=E7=8A=B6=E6=80=81=E4=BF=9D?= =?UTF-8?q?=E6=8C=81=E4=B8=8D=E5=8F=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/vtable-plugins/demo/filter/filter.ts | 2 +- packages/vtable-plugins/src/filter/filter-toolbar.ts | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/vtable-plugins/demo/filter/filter.ts b/packages/vtable-plugins/demo/filter/filter.ts index d80c1fbfe3..183fa0843b 100644 --- a/packages/vtable-plugins/demo/filter/filter.ts +++ b/packages/vtable-plugins/demo/filter/filter.ts @@ -345,7 +345,7 @@ export function createTable() { plugins: [filterPlugin], records: [...generateDemoData(50, '第二次'), null, undefined] }); - }, 8000); + }, 2000); // 插件更新 // setTimeout(() => { diff --git a/packages/vtable-plugins/src/filter/filter-toolbar.ts b/packages/vtable-plugins/src/filter/filter-toolbar.ts index 4734830d02..2fb8d486c1 100644 --- a/packages/vtable-plugins/src/filter/filter-toolbar.ts +++ b/packages/vtable-plugins/src/filter/filter-toolbar.ts @@ -31,6 +31,8 @@ export class FilterToolbar { private cancelFilterButton: HTMLButtonElement; private applyFilterButton: HTMLButtonElement; + private activeType: 'byValue' | 'byCondition' = 'byValue'; + constructor(table: ListTable | PivotTable, filterStateManager: FilterStateManager, pluginOptions: FilterOptions) { this.table = table; this.filterStateManager = filterStateManager; @@ -177,8 +179,7 @@ export class FilterToolbar { // 面板处于显示状态, 更新了样式, 则需要手动控制tab显隐 // 面板显示按值筛选或按条件筛选 - const currentFilter = this.filterStateManager.getFilterState(this.selectedField); - if (currentFilter && currentFilter.type === 'byCondition') { + if (this.activeType === 'byCondition') { this.onTabSwitch('byCondition'); } else { this.onTabSwitch('byValue'); @@ -188,10 +189,12 @@ export class FilterToolbar { attachEventListeners() { // 按值筛选/按条件筛选的事件监听 this.filterTabByValue.addEventListener('click', () => { + this.activeType = 'byValue'; this.onTabSwitch('byValue'); }); this.filterTabByCondition.addEventListener('click', () => { + this.activeType = 'byCondition'; this.onTabSwitch('byCondition'); }); From 31e021821f17ee3f905b60c2bacf8ff890debd7f Mon Sep 17 00:00:00 2001 From: skie1997 Date: Fri, 12 Dec 2025 14:42:10 +0800 Subject: [PATCH 30/34] chore: eslint problem --- packages/vtable/src/data/DataSource.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vtable/src/data/DataSource.ts b/packages/vtable/src/data/DataSource.ts index 8757117994..33e6323d61 100644 --- a/packages/vtable/src/data/DataSource.ts +++ b/packages/vtable/src/data/DataSource.ts @@ -1229,6 +1229,7 @@ export class DataSource extends EventTarget implements DataSourceAPI { this.lastFilterRules = this.dataConfig.filterRules; this.dataConfig.filterRules = filterRules; this._source = this.processRecords(this.dataSourceObj?.records ?? this.dataSourceObj); + // 如果配置了筛选回调, 则用户可自定义处理筛选后的数据 if (onFilterRecordsEnd) { this._source = onFilterRecordsEnd(this._source as any[]); } From 47ca88acbe9b6e78a19e9a2edbe27fcced33fef1 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Fri, 12 Dec 2025 14:46:11 +0800 Subject: [PATCH 31/34] fix: should not publish api of onFilterRecordsEnd --- docs/assets/guide/en/plugin/filter.md | 3 --- docs/assets/guide/zh/plugin/filter.md | 3 --- 2 files changed, 6 deletions(-) diff --git a/docs/assets/guide/en/plugin/filter.md b/docs/assets/guide/en/plugin/filter.md index 7d863b65fc..f8cf20ce7a 100644 --- a/docs/assets/guide/en/plugin/filter.md +++ b/docs/assets/guide/en/plugin/filter.md @@ -42,8 +42,6 @@ export interface FilterOptions { * @default true */ syncFilterItemsState?: boolean; - /** Filter records end callback */ - onFilterRecordsEnd?: (records: any[]) => any[]; } ``` @@ -61,7 +59,6 @@ export interface FilterOptions { | `conditionCategories` | FilterOperatorCategoryOption[] | - | Custom filter categories | | `checkboxItemFormat` | (rawValue: any, formatValue: any) => any | - | Custom filter option display format | | `syncFilterItemsState` | boolean | true | Whether multiple filters are linked | -| `onFilterRecordsEnd` | (records: any[]) => any[] | - | Filter records end callback | ### Filter Operators diff --git a/docs/assets/guide/zh/plugin/filter.md b/docs/assets/guide/zh/plugin/filter.md index 386f66200b..40ba55b021 100644 --- a/docs/assets/guide/zh/plugin/filter.md +++ b/docs/assets/guide/zh/plugin/filter.md @@ -42,8 +42,6 @@ export interface FilterOptions { * @default true */ syncFilterItemsState?: boolean; - /** 筛选记录结束回调 */ - onFilterRecordsEnd?: (records: any[]) => any[]; } ``` @@ -61,7 +59,6 @@ export interface FilterOptions { | `conditionCategories` | FilterOperatorCategoryOption[] | - | 自定义筛选分类 | | `checkboxItemFormat` | (rawValue: any, formatValue: any) => any | - | 自定义筛选选项展示格式 | | `syncFilterItemsState` | boolean | true | 多个筛选器之间是否联动 | -| `onFilterRecordsEnd` | (records: any[]) => any[] | - | 筛选记录结束回调 | ### 筛选操作符 From f6db40d0cdd6b0f22537edc1c118e5fd0585d771 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Fri, 12 Dec 2025 14:54:40 +0800 Subject: [PATCH 32/34] feat: change onFilterRecordsEnd to a void return api --- docs/assets/guide/en/plugin/filter.md | 3 +++ docs/assets/guide/zh/plugin/filter.md | 3 +++ packages/vtable-plugins/demo/filter/filter.ts | 2 +- packages/vtable-plugins/src/filter/types.ts | 2 +- packages/vtable/src/data/DataSource.ts | 2 +- 5 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/assets/guide/en/plugin/filter.md b/docs/assets/guide/en/plugin/filter.md index f8cf20ce7a..a847f271ac 100644 --- a/docs/assets/guide/en/plugin/filter.md +++ b/docs/assets/guide/en/plugin/filter.md @@ -42,6 +42,8 @@ export interface FilterOptions { * @default true */ syncFilterItemsState?: boolean; + /** Filter records end callback */ + onFilterRecordsEnd?: (records: any[]) => void; } ``` @@ -59,6 +61,7 @@ export interface FilterOptions { | `conditionCategories` | FilterOperatorCategoryOption[] | - | Custom filter categories | | `checkboxItemFormat` | (rawValue: any, formatValue: any) => any | - | Custom filter option display format | | `syncFilterItemsState` | boolean | true | Whether multiple filters are linked | +| `onFilterRecordsEnd` | (records: any[]) => void | - | Filter records end callback | ### Filter Operators diff --git a/docs/assets/guide/zh/plugin/filter.md b/docs/assets/guide/zh/plugin/filter.md index 40ba55b021..d054792784 100644 --- a/docs/assets/guide/zh/plugin/filter.md +++ b/docs/assets/guide/zh/plugin/filter.md @@ -42,6 +42,8 @@ export interface FilterOptions { * @default true */ syncFilterItemsState?: boolean; + /** 筛选记录结束回调 */ + onFilterRecordsEnd?: (records: any[]) => void; } ``` @@ -59,6 +61,7 @@ export interface FilterOptions { | `conditionCategories` | FilterOperatorCategoryOption[] | - | 自定义筛选分类 | | `checkboxItemFormat` | (rawValue: any, formatValue: any) => any | - | 自定义筛选选项展示格式 | | `syncFilterItemsState` | boolean | true | 多个筛选器之间是否联动 | +| `onFilterRecordsEnd` | (records: any[]) => void | - | 筛选记录结束回调 | ### 筛选操作符 diff --git a/packages/vtable-plugins/demo/filter/filter.ts b/packages/vtable-plugins/demo/filter/filter.ts index 183fa0843b..567bfc39ff 100644 --- a/packages/vtable-plugins/demo/filter/filter.ts +++ b/packages/vtable-plugins/demo/filter/filter.ts @@ -336,7 +336,7 @@ export function createTable() { }, onFilterRecordsEnd: records => { console.log('onFilterRecordsEnd-2'); - return records; + records.push(null, undefined); } }); console.log('update'); diff --git a/packages/vtable-plugins/src/filter/types.ts b/packages/vtable-plugins/src/filter/types.ts index 17afe667f7..42774bac74 100644 --- a/packages/vtable-plugins/src/filter/types.ts +++ b/packages/vtable-plugins/src/filter/types.ts @@ -24,7 +24,7 @@ export interface FilterOptions { */ syncFilterItemsState?: boolean; /** 筛选记录结束回调 */ - onFilterRecordsEnd?: (records: any[]) => any[]; + onFilterRecordsEnd?: (records: any[]) => void; } export interface FilterOperatorCategoryOption { diff --git a/packages/vtable/src/data/DataSource.ts b/packages/vtable/src/data/DataSource.ts index 33e6323d61..a8ef6459ca 100644 --- a/packages/vtable/src/data/DataSource.ts +++ b/packages/vtable/src/data/DataSource.ts @@ -1231,7 +1231,7 @@ export class DataSource extends EventTarget implements DataSourceAPI { this._source = this.processRecords(this.dataSourceObj?.records ?? this.dataSourceObj); // 如果配置了筛选回调, 则用户可自定义处理筛选后的数据 if (onFilterRecordsEnd) { - this._source = onFilterRecordsEnd(this._source as any[]); + onFilterRecordsEnd(this._source as any[]); } this._sourceLength = this._source?.length || 0; // 初始化currentIndexedData 正常未排序。设置其状态 From cf8b1cd8a34e22c2302956f799ade810fb13223b Mon Sep 17 00:00:00 2001 From: skie1997 Date: Fri, 12 Dec 2025 15:43:02 +0800 Subject: [PATCH 33/34] chore: format code --- packages/vtable-plugins/demo/filter/filter.ts | 2 +- packages/vtable/src/ListTable.ts | 16 ++++++------ packages/vtable/src/core/BaseTable.ts | 26 +++++++++---------- packages/vtable/src/data/DataSource.ts | 2 +- packages/vtable/src/state/state.ts | 10 +++---- 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/packages/vtable-plugins/demo/filter/filter.ts b/packages/vtable-plugins/demo/filter/filter.ts index 567bfc39ff..e3dea746d6 100644 --- a/packages/vtable-plugins/demo/filter/filter.ts +++ b/packages/vtable-plugins/demo/filter/filter.ts @@ -172,7 +172,7 @@ export function createTable() { backgroundColor: '#0E1119', border: '0px solid #272A30', boxShadow: '0 4px 8px rgba(0,0,0,0.15)', - zIndex: '100', + zIndex: '99999', borderRadius: '4px', color: '#FFF', fontFamily: 'SourceHanSansCN-Normal', diff --git a/packages/vtable/src/ListTable.ts b/packages/vtable/src/ListTable.ts index a741accc0c..4224a65291 100644 --- a/packages/vtable/src/ListTable.ts +++ b/packages/vtable/src/ListTable.ts @@ -126,8 +126,8 @@ export class ListTable extends BaseTable implements ListTableAPI { internalProps.columns = options.columns ? cloneDeepSpec(options.columns, ['children']) // children for react : options.header - ? cloneDeepSpec(options.header, ['children']) - : []; + ? cloneDeepSpec(options.header, ['children']) + : []; generateAggregationForColumn(this); // options.columns?.forEach((colDefine, index) => { // //如果editor 是一个IEditor的实例 需要这样重新赋值 否则clone后变质了 @@ -653,8 +653,8 @@ export class ListTable extends BaseTable implements ListTableAPI { internalProps.columns = options.columns ? cloneDeepSpec(options.columns, ['children']) : options.header - ? cloneDeepSpec(options.header, ['children']) - : []; + ? cloneDeepSpec(options.header, ['children']) + : []; generateAggregationForColumn(this); // options.columns.forEach((colDefine, index) => { // if (colDefine.editor) { @@ -1534,8 +1534,8 @@ export class ListTable extends BaseTable implements ListTableAPI { getEditor(col: number, row: number) { const define = this.getBodyColumnDefine(col, row); let editorDefine = this.isHeader(col, row) - ? ((define as ColumnDefine)?.headerEditor ?? this.options.headerEditor) - : ((define as ColumnDefine)?.editor ?? this.options.editor); + ? (define as ColumnDefine)?.headerEditor ?? this.options.headerEditor + : (define as ColumnDefine)?.editor ?? this.options.editor; if (typeof editorDefine === 'function') { const arg = { @@ -1556,8 +1556,8 @@ export class ListTable extends BaseTable implements ListTableAPI { isHasEditorDefine(col: number, row: number) { const define = this.getBodyColumnDefine(col, row); let editorDefine = this.isHeader(col, row) - ? ((define as ColumnDefine)?.headerEditor ?? this.options.headerEditor) - : ((define as ColumnDefine)?.editor ?? this.options.editor); + ? (define as ColumnDefine)?.headerEditor ?? this.options.headerEditor + : (define as ColumnDefine)?.editor ?? this.options.editor; if (typeof editorDefine === 'function') { const arg = { diff --git a/packages/vtable/src/core/BaseTable.ts b/packages/vtable/src/core/BaseTable.ts index 810238d364..6c4e3e6c78 100644 --- a/packages/vtable/src/core/BaseTable.ts +++ b/packages/vtable/src/core/BaseTable.ts @@ -502,16 +502,16 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { ? typeof limitMinWidth === 'number' ? limitMinWidth : limitMinWidth - ? 10 - : 0 + ? 10 + : 0 : 10; internalProps.limitMinHeight = limitMinHeight !== null && limitMinHeight !== undefined ? typeof limitMinHeight === 'number' ? limitMinHeight : limitMinHeight - ? 10 - : 0 + ? 10 + : 0 : 10; // 生成scenegraph // this._vDataSet = new DataSet(); @@ -1487,12 +1487,12 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { // : this.defaultColWidth; if (this.isRowHeader(col, 0) || this.isCornerHeader(col, 0)) { return Array.isArray(this.defaultHeaderColWidth) - ? (this.defaultHeaderColWidth[col] ?? this.defaultColWidth) + ? this.defaultHeaderColWidth[col] ?? this.defaultColWidth : this.defaultHeaderColWidth; } else if (this.isRightFrozenColumn(col, this.columnHeaderLevelCount)) { if (this.isPivotTable()) { return Array.isArray(this.defaultHeaderColWidth) - ? (this.defaultHeaderColWidth[this.rowHeaderLevelCount - this.rightFrozenColCount] ?? this.defaultColWidth) + ? this.defaultHeaderColWidth[this.rowHeaderLevelCount - this.rightFrozenColCount] ?? this.defaultColWidth : this.defaultHeaderColWidth; } return this.defaultColWidth; @@ -1503,15 +1503,15 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { getDefaultRowHeight(row: number) { if (this.isColumnHeader(0, row) || this.isCornerHeader(0, row) || this.isSeriesNumberInHeader(0, row)) { return Array.isArray(this.defaultHeaderRowHeight) - ? (this.defaultHeaderRowHeight[row] ?? this.internalProps.defaultRowHeight) + ? this.defaultHeaderRowHeight[row] ?? this.internalProps.defaultRowHeight : this.defaultHeaderRowHeight; } if (this.isBottomFrozenRow(row)) { //底部冻结行默认取用了表头的行高 但针对非表头数据冻结的情况这里可能不妥 return Array.isArray(this.defaultHeaderRowHeight) - ? (this.defaultHeaderRowHeight[ + ? this.defaultHeaderRowHeight[ this.columnHeaderLevelCount > 0 ? this.columnHeaderLevelCount - this.bottomFrozenRowCount : 0 - ] ?? this.internalProps.defaultRowHeight) + ] ?? this.internalProps.defaultRowHeight : this.defaultHeaderRowHeight; } return this.internalProps.defaultRowHeight; @@ -2796,16 +2796,16 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { ? typeof limitMinWidth === 'number' ? limitMinWidth : limitMinWidth - ? 10 - : 0 + ? 10 + : 0 : 10; internalProps.limitMinHeight = limitMinHeight !== null && limitMinHeight !== undefined ? typeof limitMinHeight === 'number' ? limitMinHeight : limitMinHeight - ? 10 - : 0 + ? 10 + : 0 : 10; // 生成scenegraph // this._vDataSet = new DataSet(); diff --git a/packages/vtable/src/data/DataSource.ts b/packages/vtable/src/data/DataSource.ts index a8ef6459ca..f79882ede4 100644 --- a/packages/vtable/src/data/DataSource.ts +++ b/packages/vtable/src/data/DataSource.ts @@ -457,7 +457,7 @@ export class DataSource extends EventTarget implements DataSourceAPI { // return 0; // } let childTotalLength = 0; - const nodeLength = nodeData.filteredChildren ? nodeData.filteredChildren.length : (nodeData.children?.length ?? 0); + const nodeLength = nodeData.filteredChildren ? nodeData.filteredChildren.length : nodeData.children?.length ?? 0; for (let j = 0; j < nodeLength; j++) { if (currentLevel <= hierarchyExpandLevel || nodeData.hierarchyState === HierarchyState.expand) { childTotalLength += 1; diff --git a/packages/vtable/src/state/state.ts b/packages/vtable/src/state/state.ts index a8dcdf3e73..e16c0fa61a 100644 --- a/packages/vtable/src/state/state.ts +++ b/packages/vtable/src/state/state.ts @@ -471,8 +471,8 @@ export class StateManager { const cornerHeaderSelectMode = this.table.options.select?.cornerHeaderSelectMode ? this.table.options.select?.cornerHeaderSelectMode : this.table.options.select?.headerSelectMode === 'body' - ? this.table.options.select?.headerSelectMode - : 'all'; + ? this.table.options.select?.headerSelectMode + : 'all'; // if (enableRowHighlight && enableColumnHighlight) { // this.select.highlightScope = HighlightScope.cross; @@ -537,7 +537,7 @@ export class StateManager { function flatten(cols: any, parentStartIndex = 0) { cols.forEach((col: any) => { const startIndex = col.startInTotal - ? (col.startInTotal + state.table.internalProps.layoutMap.leftRowSeriesNumberColumnCount ?? 0) + ? col.startInTotal + state.table.internalProps.layoutMap.leftRowSeriesNumberColumnCount ?? 0 : parentStartIndex; if (col.columns) { flatten(col.columns, startIndex); @@ -1649,8 +1649,8 @@ export class StateManager { this.sort[index]?.order === 'asc' ? 'sort_downward' : this.sort[index]?.order === 'desc' - ? 'sort_upward' - : 'sort_normal'; + ? 'sort_upward' + : 'sort_normal'; this.setSortState(sortState.slice(0, index + 1)); // 获取sort对应的行列位置 const cellAddress = this.table.internalProps.layoutMap.getHeaderCellAddressByField( From 6890322facf9f624919f41bd2183b2c0e3ef691e Mon Sep 17 00:00:00 2001 From: skie1997 Date: Wed, 17 Dec 2025 10:36:59 +0800 Subject: [PATCH 34/34] fix: compare values about number --- packages/vtable-plugins/src/filter/filter-engine.ts | 6 ++++++ packages/vtable-plugins/src/filter/filter.ts | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/vtable-plugins/src/filter/filter-engine.ts b/packages/vtable-plugins/src/filter/filter-engine.ts index 5742fd1646..8e09ab2819 100644 --- a/packages/vtable-plugins/src/filter/filter-engine.ts +++ b/packages/vtable-plugins/src/filter/filter-engine.ts @@ -124,8 +124,14 @@ export class FilterEngine { return value === condition ? 0 : -1; } + // 进入字符串比较 const valueStr = String(value).toLowerCase(); const conditionStr = String(condition).toLowerCase(); + + // 如果两个字符串都能正确转换成数字, 则仍然按数字比较 + if (!isNaN(Number(valueStr)) && !isNaN(Number(conditionStr))) { + return Number(valueStr) === Number(conditionStr) ? 0 : Number(valueStr) > Number(conditionStr) ? 1 : -1; + } return valueStr === conditionStr ? 0 : valueStr > conditionStr ? 1 : -1; } diff --git a/packages/vtable-plugins/src/filter/filter.ts b/packages/vtable-plugins/src/filter/filter.ts index 47fc7def4a..21bb737cdc 100644 --- a/packages/vtable-plugins/src/filter/filter.ts +++ b/packages/vtable-plugins/src/filter/filter.ts @@ -13,7 +13,7 @@ import type { ColumnDefine, ColumnsDefine } from '@visactor/vtable'; -import { merge } from 'lodash'; +import { cloneDeep, merge } from 'lodash'; import { filterStyles } from './styles'; import { categories } from './constant'; @@ -45,7 +45,7 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin { constructor(pluginOptions: FilterOptions) { this.id = pluginOptions?.id ?? this.id; - this.pluginOptions = pluginOptions; + this.pluginOptions = cloneDeep(pluginOptions); // 不污染用户的配置, 以便上层业务做diff的时候使用 this.pluginOptions.filterIcon = pluginOptions.filterIcon ?? { name: 'filter-icon', type: 'svg',