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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/docs/assets/guide/en/plugin/filter.md b/docs/assets/guide/en/plugin/filter.md
index d206efaf37..a847f271ac 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[]) => void;
}
```
### 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[]) => void | - | 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..d054792784 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[]) => void;
}
```
### 配置参数说明
-| 参数名 | 类型 | 默认值 | 说明 |
-|--------|------|--------|------|
-| `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[]) => void | - | 筛选记录结束回调 |
### 筛选操作符
插件支持以下筛选操作符:
**通用操作符**
+
- `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`
# 本插件贡献者及文档作者
diff --git a/packages/vtable-plugins/demo/filter/filter.ts b/packages/vtable-plugins/demo/filter/filter.ts
index e1d247129e..e3dea746d6 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 { FilterPlugin } from '../../src/filter';
+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',
@@ -143,20 +144,233 @@ export function createTable() {
}
];
- const filterPlugin = new FilterPlugin({});
+ 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',
+ position: 'absolute',
+ backgroundColor: '#0E1119',
+ border: '0px solid #272A30',
+ boxShadow: '0 4px 8px rgba(0,0,0,0.15)',
+ zIndex: '99999',
+ 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'
+ }
+ },
+ 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: '单选框' }
+ ],
+ checkboxItemFormat: (formatValue, rawValue) => {
+ return formatValue;
+ },
+ syncFilterItemsState: false,
+ onFilterRecordsEnd: records => {
+ console.log('onFilterRecordsEnd');
+ return [...records, null, undefined];
+ }
+ });
(window as any).filterPlugin = filterPlugin;
const option: VTable.ListTableConstructorOptions = {
container: document.getElementById(CONTAINER_ID),
- records,
+ 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;
+ 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: {
+ 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');
+ records.push(null, undefined);
+ }
+ });
+ console.log('update');
+ tableInstance.updateOption({
+ ...option,
+ plugins: [filterPlugin],
+ records: [...generateDemoData(50, '第二次'), null, undefined]
+ });
+ }, 2000);
+
+ // 插件更新
+ // setTimeout(() => {
+ // console.log('update');
+ // tableInstance.updateOption({
+ // ...option,
+ // plugins: [filterPlugin]
+ // });
+ // }, 8000);
+
+ // 实例释放
+ // setTimeout(() => {
+ // tableInstance.release();
+ // }, 3000);
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-plugins/src/filter/condition-filter.ts b/packages/vtable-plugins/src/filter/condition-filter.ts
index 0955ae78ca..9129534549 100644
--- a/packages/vtable-plugins/src/filter/condition-filter.ts
+++ b/packages/vtable-plugins/src/filter/condition-filter.ts
@@ -1,8 +1,15 @@
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,
+ FilterOperatorCategoryOption,
+ FilterOptions,
+ FilterStyles,
+ OperatorOption
+} from './types';
import { FilterActionType, FilterOperatorCategory } from './types';
+import { operators } from './constant';
/**
* 按条件筛选组件
@@ -10,66 +17,42 @@ import { FilterActionType, FilterOperatorCategory } from './types';
export class ConditionFilter {
private table: ListTable | PivotTable;
private filterStateManager: FilterStateManager;
+ private pluginOptions: FilterOptions;
+ private filterToolBarHide: () => void;
+
+ 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 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: '单选框' }
- ];
-
- constructor(table: ListTable | PivotTable, filterStateManager: FilterStateManager) {
+ private categories: FilterOperatorCategoryOption[] = [];
+ protected operators: OperatorOption[] = [];
+
+ 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 {
@@ -89,13 +72,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 {
// 其他类别正常筛选
@@ -128,22 +113,24 @@ 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;
+ 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 = '';
}
@@ -277,7 +264,7 @@ export class ConditionFilter {
}
// TODO:处理单元格颜色和字体颜色的筛选
-
+ const syncFilterItemsState = this.pluginOptions?.syncFilterItemsState ?? true;
this.filterStateManager.dispatch({
type: FilterActionType.APPLY_FILTERS,
payload: {
@@ -285,11 +272,10 @@ export class ConditionFilter {
type: 'byCondition',
operator,
condition: conditionValue,
- enable: true
+ enable: true,
+ shouldKeepUnrelatedState: !syncFilterItemsState
}
});
-
- this.hide();
}
/**
@@ -310,17 +296,18 @@ export class ConditionFilter {
* 渲染条件筛选面板
*/
render(container: HTMLElement): void {
+ const filterStyles = this.styles;
// 按条件筛选面板
this.filterByConditionPanel = document.createElement('div');
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);
@@ -334,19 +321,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',
@@ -355,9 +342,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', {
@@ -368,19 +355,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);
// 默认隐藏
@@ -391,6 +378,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);
+ }
+
/**
* 绑定事件
*/
@@ -399,6 +400,7 @@ export class ConditionFilter {
this.valueInput.addEventListener('keypress', event => {
if (event.key === 'Enter') {
this.applyFilter();
+ this.filterToolBarHide();
}
});
@@ -406,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/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-engine.ts b/packages/vtable-plugins/src/filter/filter-engine.ts
index 4136b8e80f..8e09ab2819 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
});
}
@@ -117,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-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-toolbar.ts b/packages/vtable-plugins/src/filter/filter-toolbar.ts
index 851b05c116..2fb8d486c1 100644
--- a/packages/vtable-plugins/src/filter/filter-toolbar.ts
+++ b/packages/vtable-plugins/src/filter/filter-toolbar.ts
@@ -1,9 +1,9 @@
-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';
import { applyStyles, filterStyles } from './styles';
-import type { FilterMode } from './types';
+import type { FilterMode, FilterOptions, FilterStyles } 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';
@@ -19,20 +20,25 @@ 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;
- constructor(table: ListTable | PivotTable, filterStateManager: FilterStateManager) {
+ private activeType: 'byValue' | 'byCondition' = 'byValue';
+
+ 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.hide);
+ this.pluginOptions = pluginOptions;
this.filterMenuWidth = 300; // 待优化,可能需要自适应内容的宽度
@@ -103,14 +109,16 @@ export class FilterToolbar {
}
render(container: HTMLElement): void {
+ const filterStyles = this.pluginOptions.styles || {};
// === 主容器 ===
this.filterMenu = document.createElement('div');
+ this.filterMenu.classList.add('vtable-filter-menu');
applyStyles(this.filterMenu, filterStyles.filterMenu);
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 = '按值筛选';
@@ -120,11 +128,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 = '#';
@@ -141,29 +149,52 @@ 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) {
+ 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);
+ 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);
+
+ // 面板处于显示状态, 更新了样式, 则需要手动控制tab显隐
+ // 面板显示按值筛选或按条件筛选
+ if (this.activeType === 'byCondition') {
+ this.onTabSwitch('byCondition');
+ } else {
+ this.onTabSwitch('byValue');
+ }
+ }
+
attachEventListeners() {
// 按值筛选/按条件筛选的事件监听
this.filterTabByValue.addEventListener('click', () => {
+ this.activeType = 'byValue';
this.onTabSwitch('byValue');
});
this.filterTabByCondition.addEventListener('click', () => {
+ this.activeType = 'byCondition';
this.onTabSwitch('byCondition');
});
@@ -220,6 +251,11 @@ export class FilterToolbar {
const canvasBounds = this.table.canvas.getBoundingClientRect();
const cell = this.table.getCellRelativeRect(effectiveCol, effectiveRow);
+ const filterMenuWidth = this.filterMenuWidth;
+ // 最高高度预估值
+ // TODO: 需要获取精确高度
+ const filterMenuHeight = 380;
+
if (cell.right < this.filterMenuWidth) {
// 无法把筛选菜单完整地显示在左侧,那么显示在右侧
left = cell.left + canvasBounds.left;
@@ -230,6 +266,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`;
@@ -266,11 +306,24 @@ 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, currentRow?: number): void => {
this.filterMenu.style.display = 'none';
this.isVisible = false;
+ this.table.fireListeners(TABLE_EVENT_TYPE.FILTER_MENU_HIDE, {
+ col: currentCol ?? this.currentCol,
+ row: currentRow ?? this.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 121a049f38..21bb737cdc 100644
--- a/packages/vtable-plugins/src/filter/filter.ts
+++ b/packages/vtable-plugins/src/filter/filter.ts
@@ -13,6 +13,9 @@ import type {
ColumnDefine,
ColumnsDefine
} from '@visactor/vtable';
+import { cloneDeep, merge } from 'lodash';
+import { filterStyles } from './styles';
+import { categories } from './constant';
/**
* 筛选插件,负责初始化筛选引擎、状态管理器和工具栏
@@ -42,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',
@@ -64,6 +67,29 @@ 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 ?? {});
+ this.pluginOptions.conditionCategories = pluginOptions.conditionCategories ?? categories;
+ }
+
+ initFilterPlugin(eventArgs: any) {
+ 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;
+
+ 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[]) {
@@ -73,27 +99,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.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);
@@ -111,9 +124,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') {
@@ -131,6 +148,13 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin {
}
}
+ updatePluginOptions(pluginOptions: FilterOptions) {
+ // TODO: 目前额外只处理了styles,其他的后续再处理
+ this.pluginOptions = merge(this.pluginOptions, pluginOptions);
+ // 更新筛选器UI样式
+ this.filterToolbar.updateStyles(this.pluginOptions.styles);
+ }
+
// 当用户的配置项更新时调用
update() {
if (this.filterStateManager) {
@@ -342,10 +366,13 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin {
}
release() {
+ this.columns.forEach(column => {
+ column.headerIcon = undefined;
+ });
this.table = null;
this.filterEngine = null;
this.filterStateManager = null;
- this.filterToolbar.valueFilter.destroy();
+ this.filterToolbar.destroy();
this.filterToolbar = null;
}
}
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 604c4c80c5..42774bac74 100644
--- a/packages/vtable-plugins/src/filter/types.ts
+++ b/packages/vtable-plugins/src/filter/types.ts
@@ -13,6 +13,23 @@ export interface FilterOptions {
defaultEnabled?: boolean;
/** 是否展示按条件筛选,按值筛选 UI */
filterModes?: FilterMode[];
+ /** 筛选器样式 */
+ styles?: FilterStyles;
+ /** 自定义筛选分类 */
+ conditionCategories?: FilterOperatorCategoryOption[];
+ /** 筛选选项是否展示为原始值 */
+ checkboxItemFormat?: (rawValue: any, formatValue: any) => any;
+ /** 多个筛选器之间是否联动
+ * @default true
+ */
+ syncFilterItemsState?: boolean;
+ /** 筛选记录结束回调 */
+ onFilterRecordsEnd?: (records: any[]) => void;
+}
+
+export interface FilterOperatorCategoryOption {
+ value: FilterOperatorCategory;
+ label: string;
}
export type FilterMode = 'byValue' | 'byCondition';
@@ -92,3 +109,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 f7158586cb..270ebf1af8 100644
--- a/packages/vtable-plugins/src/filter/value-filter.ts
+++ b/packages/vtable-plugins/src/filter/value-filter.ts
@@ -1,13 +1,15 @@
import type { ListTable, PivotTable } from '@visactor/vtable';
-import { arrayEqual } from '@visactor/vutils';
-import type { FilterConfig, ValueFilterOptionDom, FilterState } 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, filterStyles } from './styles';
+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 之前的原始数据
private candidateKeys = new Map>(); // 存储 format 后的数据
@@ -15,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;
@@ -24,9 +31,11 @@ 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.pluginOptions = pluginOptions;
+ this.styles = pluginOptions.styles || {};
}
setSelectedField(fieldId: string | number): void {
@@ -79,22 +88,33 @@ export class ValueFilter {
* 为未应用筛选的列,收集候选值集合
*/
private collectCandidateKeysForUnfilteredColumn(fieldId: string | number): void {
+ const syncFilterItemsState = this.pluginOptions?.syncFilterItemsState ?? true;
const countMap = new Map(); // 计算每个候选值的计数
- const records = this.getRecords(this.table, false); // 未筛选:使用当前表格数据
+ let records = [];
+ // 如果各个筛选器之间不联动, 则永远从原数据中获取候选值
+ if (!syncFilterItemsState) {
+ records = this.table.internalProps.records;
+ } else {
+ records = this.getRecords(this.table, false); // 未筛选:使用当前表格数据
+ }
+
const formatFn = this.getFormatFnCache(fieldId);
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]));
+ }
}
}
});
@@ -107,33 +127,43 @@ export class ValueFilter {
* 为已应用筛选的列,收集候选值集合
*/
private collectCandidateKeysForFilteredColumn(candidateField: string | number): void {
+ const syncFilterItemsState = this.pluginOptions?.syncFilterItemsState ?? 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.getRecords(this.table, true); // 已筛选:使用原始表格数据
- const records = recordsList.filter(record =>
- filteredFields.every(field => {
- const filterType = this.filterStateManager.getFilterState(field)?.type;
- if (filterType !== 'byValue' && filterType !== null && filterType !== undefined) {
- this.syncSingleStateFromTableData(field);
- }
- const set = this.selectedKeys.get(field);
- return set.has(record[field]);
- })
- );
+ let records = [];
+ // 如果各个筛选器之间不联动, 则永远从原数据中获取候选值
+ if (!syncFilterItemsState) {
+ records = this.table.internalProps.records;
+ } else {
+ const recordsList = this.getRecords(this.table, true); // 已筛选:使用原始表格数据
+ const records = recordsList.filter(record =>
+ filteredFields.every(field => {
+ const filterType = this.filterStateManager.getFilterState(field)?.type;
+ if (filterType !== 'byValue' && filterType !== null && filterType !== undefined) {
+ this.syncSingleStateFromTableData(field);
+ }
+ const set = this.selectedKeys.get(field);
+ return set.has(record[field]);
+ })
+ );
+ }
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]));
+ }
}
}
});
@@ -182,29 +212,62 @@ export class ValueFilter {
syncSingleStateFromTableData(fieldId: string | number): void {
const selectedValues = new Set();
const originalValues = new Set();
-
- const currentRecords = this.table.internalProps.dataSource.records; // 当前数据
- currentRecords.forEach(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 syncFilterItemsState = this.pluginOptions?.syncFilterItemsState ?? true;
const hasFiltered = !arrayEqual(Array.from(originalValues), Array.from(selectedValues));
- if (hasFiltered) {
- this.selectedKeys.set(fieldId, selectedValues);
-
- this.filterStateManager.dispatch({
- type: FilterActionType.UPDATE_FILTER,
- payload: {
- field: fieldId,
- values: Array.from(selectedValues),
- enable: true
+ if (syncFilterItemsState) {
+ if (hasFiltered) {
+ this.selectedKeys.set(fieldId, selectedValues);
+
+ this.filterStateManager.dispatch({
+ type: FilterActionType.UPDATE_FILTER,
+ payload: {
+ field: fieldId,
+ values: Array.from(selectedValues),
+ enable: true
+ }
+ });
+ 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 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);
+ }
}
}
@@ -226,21 +289,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'
}
});
}
@@ -258,30 +328,31 @@ export class ValueFilter {
}
render(container: HTMLElement): void {
+ const filterStyles = this.styles;
// === 按值筛选的菜单内容 ===
this.filterByValuePanel = document.createElement('div');
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';
@@ -292,20 +363,32 @@ 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 = '';
this.valueFilterOptionList.delete(field);
this.valueFilterOptionList.set(field, []);
@@ -354,7 +437,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);
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);
+ }
}
}
diff --git a/packages/vtable-sheet/src/formula/formula-engine.ts b/packages/vtable-sheet/src/formula/formula-engine.ts
index bae7260d73..f75dfb61eb 100644
--- a/packages/vtable-sheet/src/formula/formula-engine.ts
+++ b/packages/vtable-sheet/src/formula/formula-engine.ts
@@ -3252,5 +3252,8 @@ export class FormulaEngine {
}
class FormulaError {
- constructor(public message: string, public type: 'REF' | 'VALUE' | 'DIV0' | 'NAME' | 'NA' = 'VALUE') {}
+ constructor(
+ public message: string,
+ public type: 'REF' | 'VALUE' | 'DIV0' | 'NAME' | 'NA' = 'VALUE'
+ ) {}
}
diff --git a/packages/vtable/src/ListTable.ts b/packages/vtable/src/ListTable.ts
index 8538d12e19..4224a65291 100644
--- a/packages/vtable/src/ListTable.ts
+++ b/packages/vtable/src/ListTable.ts
@@ -636,6 +636,8 @@ export class ListTable extends BaseTable implements ListTableAPI {
}
) {
const internalProps = this.internalProps;
+
+ this.pluginManager.removeOrAddPlugins(options.plugins);
super.updateOption(options, updateConfig);
internalProps.frozenColDragHeaderMode =
options.dragOrder?.frozenColDragHeaderMode ?? options.frozenColDragHeaderMode;
@@ -1158,6 +1160,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 }];
// // 如果需要移出的节点超过了当前加载部分最后一行 则转变成更新对应的行
@@ -1302,6 +1305,7 @@ export class ListTable extends BaseTable implements ListTableAPI {
filterRules: FilterRules,
options: {
clearRowHeightCache?: boolean;
+ onFilterRecordsEnd?: (records: any[]) => any[];
} = { clearRowHeightCache: true }
) {
this.scenegraph.clearCells();
@@ -1309,11 +1313,12 @@ 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);
this.scenegraph.createSceneGraph(!!!options?.clearRowHeightCache);
+ this.internalProps.emptyTip?.resetVisible();
this.resize();
}
/** 获取过滤后的数据 */
@@ -1356,7 +1361,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) {
@@ -1845,7 +1850,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/core/BaseTable.ts b/packages/vtable/src/core/BaseTable.ts
index 24b3e93a85..5447dcf27f 100644
--- a/packages/vtable/src/core/BaseTable.ts
+++ b/packages/vtable/src/core/BaseTable.ts
@@ -503,16 +503,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();
@@ -1488,12 +1488,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;
@@ -1504,15 +1504,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;
@@ -1884,7 +1884,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);
@@ -1895,7 +1895,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);
@@ -2808,16 +2808,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/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/data/DataSource.ts b/packages/vtable/src/data/DataSource.ts
index 84cc8837fa..f79882ede4 100644
--- a/packages/vtable/src/data/DataSource.ts
+++ b/packages/vtable/src/data/DataSource.ts
@@ -1225,10 +1225,14 @@ 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) {
+ onFilterRecordsEnd(this._source as any[]);
+ }
this._sourceLength = this._source?.length || 0;
// 初始化currentIndexedData 正常未排序。设置其状态
this.currentIndexedData = Array.from({ length: this._sourceLength }, (_, i) => i);
diff --git a/packages/vtable/src/event/event.ts b/packages/vtable/src/event/event.ts
index 465f962853..13126cca1d 100644
--- a/packages/vtable/src/event/event.ts
+++ b/packages/vtable/src/event/event.ts
@@ -284,7 +284,7 @@ export class EventManager {
eventArgs.event.shiftKey && shiftMultiSelect,
(eventArgs.event.ctrlKey || eventArgs.event.metaKey) && ctrlMultiSelect,
false,
- isSelectMoving ? false : this.table.options.select?.makeSelectCellVisible ?? true
+ isSelectMoving ? false : (this.table.options.select?.makeSelectCellVisible ?? true)
);
return true;
diff --git a/packages/vtable/src/plugins/plugin-manager.ts b/packages/vtable/src/plugins/plugin-manager.ts
index b1447ee6f3..e984eac6df 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 => {
@@ -33,27 +35,26 @@ export class PluginManager {
}
_bindTableEventForPlugin(plugin: IVTablePlugin) {
- plugin.runTime?.forEach(runTime => {
- this.table.on(runTime, (...args) => {
+ plugin.runTime?.forEach((runTime: any) => {
+ const id = this.table.on(runTime, (...args) => {
plugin.run?.(...args, runTime, this.table);
});
+ this.pluginEventMap.set(plugin.id, [...(this.pluginEventMap.get(plugin.id) || []), id]);
});
}
- // 更新所有插件
- 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 => {
+ this.pluginEventMap.get(plugin.id)?.forEach(id => {
+ this.table.off(id);
+ });
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 => {
@@ -61,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);
diff --git a/packages/vtable/src/state/state.ts b/packages/vtable/src/state/state.ts
index b16f0b0705..e16c0fa61a 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 || !this.table.scenegraph) {
+ return;
+ }
// 矫正top值范围
const totalHeight = this.table.getAllRowsHeight();
// _disableColumnAndRowSizeRound环境中,可能出现
@@ -1194,10 +1197,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 +1227,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;
}
@@ -1264,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();
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 };
}