diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7cfa4ed63..5f9146229 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@
- 修复 Switch 的 width 属性无效的问题
- Table 增加 rowClassName 属性
- TableColumn 增加 fixed 属性,可选值:true, false, left, right
+- TableColumn 增加属性:filters、filterMultiple、filterMethod、filteredValue
- TableColumn[type="selection"] 增加 selectable 属性
- 修复 Input textarea 在动态赋值时 autosize 没有触发的问题
- 修复 Input Number min max 属性设置后点击加减出现的崩溃的bug
diff --git a/examples/docs/zh-cn/table.md b/examples/docs/zh-cn/table.md
index e8bdd8fe4..a7e222b91 100644
--- a/examples/docs/zh-cn/table.md
+++ b/examples/docs/zh-cn/table.md
@@ -9,28 +9,32 @@
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1518 弄',
- zip: 200333
+ zip: 200333,
+ tag: '家'
}, {
date: '2016-05-02',
name: '王小虎',
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1518 弄',
- zip: 200333
+ zip: 200333,
+ tag: '公司'
}, {
date: '2016-05-04',
name: '王小虎',
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1518 弄',
- zip: 200333
+ zip: 200333,
+ tag: '家'
}, {
date: '2016-05-01',
name: '王小虎',
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1518 弄',
- zip: 200333
+ zip: 200333,
+ tag: '公司'
}],
tableData2: [{
date: '2016-05-02',
@@ -119,6 +123,10 @@
return row.address;
},
+ filterTag(value, row) {
+ return row.tag === value;
+ },
+
tableRowClassName(row, index) {
if (index === 1) {
return 'info-row';
@@ -810,6 +818,85 @@
```
:::
+### 筛选
+
+对表格进行筛选,可快速查找到自己想看的数据。
+
+:::demo 在列中设置`filters``filter-method`属性即可开启该列的筛选,filters 是一个数组,`filter-method`是一个方法,它用于决定某些数据是否显示,会传入两个参数:`value`和`row`。
+```html
+
+
+
+
+
+
+
+
+
+ {{row.tag}}
+
+
+
+
+
+```
+:::
+
### Table Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|---------- |-------------- |---------- |-------------------------------- |-------- |
@@ -852,4 +939,8 @@
| inline-template | 指定该属性后可以自定义 column 模板,参考多选的时间列,通过 row 获取行信息,JSX 里通过 _self 获取当前上下文。此时不需要配置 prop 属性 | — | — |
| align | 对齐方式 | String | left, center, right | left |
| selectable | 仅对 type=selection 的列有效,类型为 Function,Function 的返回值用来决定这一行的 CheckBox 是否可以勾选 | Function(row, index) | - | - |
-| reserve-selection | 仅对 type=selection 的列有效,类型为 Boolean,为 true 则代表会保留之前数据的选项,需要配合 Table 的 clearSelection 方法使用。 | Boolean | - | false |
\ No newline at end of file
+| reserve-selection | 仅对 type=selection 的列有效,类型为 Boolean,为 true 则代表会保留之前数据的选项,需要配合 Table 的 clearSelection 方法使用。 | Boolean | - | false |
+| filters | 数据过滤的选项,数组格式,数组中的元素需要有 text 和 value 属性。 | Array[{ text, value }] | — | — |
+| filter-multiple | 数据过滤的选项是否多选 | Boolean | — | true |
+| filter-method | 数据过滤使用的方法,如果是多选的筛选项,对每一条数据会执行多次,任意一次返回 true 就会显示。 | Function(value, row) | — | — |
+| filteredValue | 选中的数据过滤项,如果需要自定义表头过滤的渲染方式,可能会需要此属性。 | Array | — | — |
\ No newline at end of file
diff --git a/packages/table/src/dropdown.js b/packages/table/src/dropdown.js
new file mode 100644
index 000000000..dd2bec9df
--- /dev/null
+++ b/packages/table/src/dropdown.js
@@ -0,0 +1,27 @@
+var dropdowns = [];
+
+document.addEventListener('click', function(event) {
+ dropdowns.forEach(function(dropdown) {
+ var target = event.target;
+ if (!dropdown || !dropdown.$el) return;
+ if (target === dropdown.$el || dropdown.$el.contains(target)) {
+ return;
+ }
+ dropdown.handleOutsideClick && dropdown.handleOutsideClick(event);
+ });
+});
+
+export default {
+ open(instance) {
+ if (instance) {
+ dropdowns.push(instance);
+ }
+ },
+
+ close(instance) {
+ var index = dropdowns.indexOf(instance);
+ if (index !== -1) {
+ dropdowns.splice(instance, 1);
+ }
+ }
+};
diff --git a/packages/table/src/filter-panel.vue b/packages/table/src/filter-panel.vue
new file mode 100644
index 000000000..820d82e2e
--- /dev/null
+++ b/packages/table/src/filter-panel.vue
@@ -0,0 +1,180 @@
+
+
+
+
+
+ {{ filter.text }}
+
+
+
+
+
+
+
+
+
+ - {{ $t('el.table.clearFilter') }}
+ - {{ filter.text }}
+
+
+
+
+
+
diff --git a/packages/table/src/table-body.js b/packages/table/src/table-body.js
index d9079bd51..36e5919c2 100644
--- a/packages/table/src/table-body.js
+++ b/packages/table/src/table-body.js
@@ -1,22 +1,4 @@
-import { getValueByPath, getCell } from './util';
-
-const getColumnById = function(table, columnId) {
- let column = null;
- table.columns.forEach(function(item) {
- if (item.id === columnId) {
- column = item;
- }
- });
- return column;
-};
-
-const getColumnByCell = function(table, cell) {
- const matches = (cell.className || '').match(/el-table_[^\s]+/gm);
- if (matches) {
- return getColumnById(table, matches[0]);
- }
- return null;
-};
+import { getValueByPath, getCell, getColumnById, getColumnByCell } from './util';
export default {
props: {
diff --git a/packages/table/src/table-column.js b/packages/table/src/table-column.js
index d2a3567b9..7ebc2792e 100644
--- a/packages/table/src/table-column.js
+++ b/packages/table/src/table-column.js
@@ -19,21 +19,16 @@ const defaults = {
minWidth: 48,
realWidth: 48,
direction: ''
- },
- filter: {
- headerTemplate: function(h) { return filter header; },
- direction: ''
}
};
const forced = {
selection: {
headerTemplate: function(h) {
- return
{ this.$emit('allselectedchange', value); } } />
-
;
+ on-input={ (value) => { this.$emit('allselectedchange', value); } } />;
},
template: function(h, { row, column, store, $index }) {
return #; },
headerTemplate: function(h, label) {
- return { label || '#' }
;
+ return label || '#';
},
template: function(h, { $index }) {
return { $index + 1 }
;
@@ -56,7 +51,7 @@ const forced = {
},
filter: {
headerTemplate: function(h) {
- return #
;
+ return '#';
},
template: function(h, { row, column }) {
return { row[column.property] };
@@ -118,7 +113,13 @@ export default {
fixed: [Boolean, String],
formatter: Function,
selectable: Function,
- reserveSelection: Boolean
+ reserveSelection: Boolean,
+ filterMethod: Function,
+ filters: Array,
+ filterMultiple: {
+ type: Boolean,
+ default: true
+ }
},
render() {},
@@ -205,7 +206,13 @@ export default {
formatter: this.formatter,
selectable: this.selectable,
reserveSelection: this.reserveSelection,
- fixed: this.fixed
+ fixed: this.fixed,
+ filterMethod: this.filterMethod,
+ filters: this.filters,
+ filterable: this.filters || this.filterMethod,
+ filterMultiple: this.filterMultiple,
+ filterOpened: false,
+ filteredValue: []
});
objectAssign(column, forced[type] || {});
diff --git a/packages/table/src/table-header.js b/packages/table/src/table-header.js
index bc13dc1c0..d827de61e 100644
--- a/packages/table/src/table-header.js
+++ b/packages/table/src/table-header.js
@@ -1,5 +1,7 @@
import ElCheckbox from 'element-ui/packages/checkbox';
import ElTag from 'element-ui/packages/tag';
+import Vue from 'vue';
+import FilterPanel from './filter-panel.vue';
export default {
name: 'el-table-header',
@@ -31,21 +33,27 @@ export default {
on-mousemove={ ($event) => this.handleMouseMove($event, column) }
on-mouseout={ this.handleMouseOut }
on-mousedown={ ($event) => this.handleMouseDown($event, column) }
- on-click={ ($event) => this.handleHeaderClick($event, column) }
class={ [column.id, column.direction, column.align, this.isCellHidden(cellIndex) ? 'hidden' : ''] }>
+ 0 ? 'highlight' : ''] }>
{
- [
- column.headerTemplate
- ? column.headerTemplate.call(this._renderProxy, h, column.label)
- :
{ column.label }
,
- column.sortable
- ?
-
-
-
- : ''
- ]
+ column.headerTemplate
+ ? column.headerTemplate.call(this._renderProxy, h, column.label)
+ : column.label
}
+ {
+ column.sortable
+ ?
this.handleHeaderClick($event, column) }>
+
+
+
+ : ''
+ }
+ {
+ column.filterable
+ ?
this.handleFilterClick($event, column) }>
+ : ''
+ }
+
)
}
@@ -61,7 +69,6 @@ export default {
},
props: {
- columns: {},
fixed: String,
store: {
required: true
@@ -99,6 +106,19 @@ export default {
}
},
+ created() {
+ this.filterPanels = {};
+ },
+
+ beforeDestroy() {
+ const panels = this.filterPanels;
+ for (let prop in panels) {
+ if (panels.hasOwnProperty(prop) && panels[prop]) {
+ panels[prop].$destroy(true);
+ }
+ }
+ },
+
methods: {
isCellHidden(index) {
if (this.fixed === true || this.fixed === 'left') {
@@ -114,6 +134,34 @@ export default {
this.store.commit('toggleAllSelection');
},
+ handleFilterClick(event, column) {
+ event.stopPropagation();
+ const target = event.target;
+ const cell = target.parentNode;
+ const table = this.$parent;
+
+ let filterPanel = this.filterPanels[column.id];
+
+ if (filterPanel && column.filterOpened) {
+ filterPanel.showPopper = false;
+ return;
+ }
+
+ if (!filterPanel) {
+ filterPanel = new Vue(FilterPanel);
+ this.filterPanels[column.id] = filterPanel;
+
+ filterPanel.table = table;
+ filterPanel.cell = cell;
+ filterPanel.column = column;
+ filterPanel.$mount(document.createElement('div'));
+ }
+
+ setTimeout(() => {
+ filterPanel.showPopper = true;
+ }, 16);
+ },
+
handleMouseDown(event, column) {
if (this.draggingColumn && this.border) {
this.dragging = true;
@@ -180,7 +228,10 @@ export default {
},
handleMouseMove(event, column) {
- const target = event.target;
+ let target = event.target;
+ while (target && target.tagName !== 'TH') {
+ target = target.parentNode;
+ }
if (!column || !column.resizable) return;
@@ -194,7 +245,6 @@ export default {
} else if (!this.dragging) {
bodyStyle.cursor = '';
this.draggingColumn = null;
- if (column.sortable) bodyStyle.cursor = 'pointer';
}
}
},
diff --git a/packages/table/src/table-store.js b/packages/table/src/table-store.js
index 9db6e6fa8..e844b0478 100644
--- a/packages/table/src/table-store.js
+++ b/packages/table/src/table-store.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import debounce from 'throttle-debounce/debounce';
-import { orderBy } from './util';
+import { orderBy, getColumnById } from './util';
const getRowIdentity = (row, rowKey) => {
if (!row) throw new Error('row is required when get row identity');
@@ -24,6 +24,7 @@ const TableStore = function(table, initialState = {}) {
fixedColumns: [],
rightFixedColumns: [],
_data: null,
+ filteredData: null,
data: null,
sortCondition: {
column: null,
@@ -34,7 +35,8 @@ const TableStore = function(table, initialState = {}) {
selection: [],
reserveSelection: false,
selectable: null,
- hoverRow: null
+ hoverRow: null,
+ filters: {}
};
for (let prop in initialState) {
@@ -80,7 +82,38 @@ TableStore.prototype.mutations = {
},
changeSortCondition(states) {
- states.data = orderBy((states._data || []), states.sortCondition.property, states.sortCondition.direction);
+ states.data = orderBy((states.filteredData || states._data || []), states.sortCondition.property, states.sortCondition.direction);
+
+ Vue.nextTick(() => this.table.updateScrollY());
+ },
+
+ filterChange(states, options) {
+ let { column, values } = options;
+ if (values && !Array.isArray(values)) {
+ values = [values];
+ }
+
+ const prop = column.property;
+ if (prop) {
+ states.filters[column.id] = values;
+ }
+
+ let data = states._data;
+ const filters = states.filters;
+
+ Object.keys(filters).forEach((columnId) => {
+ const values = filters[columnId];
+ if (!values || values.length === 0) return;
+ const column = getColumnById(this.states, columnId);
+ if (column && column.filterMethod) {
+ data = data.filter((row) => {
+ return values.some(value => column.filterMethod.call(null, value, row));
+ });
+ }
+ });
+
+ states.filteredData = data;
+ states.data = orderBy(data, states.sortCondition.property, states.sortCondition.direction);
Vue.nextTick(() => this.table.updateScrollY());
},
diff --git a/packages/table/src/table.vue b/packages/table/src/table.vue
index f53c976a9..825903d1d 100644
--- a/packages/table/src/table.vue
+++ b/packages/table/src/table.vue
@@ -91,6 +91,7 @@
import throttle from 'throttle-debounce/throttle';
import debounce from 'throttle-debounce/debounce';
import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event';
+ import { $t } from 'element-ui/src/locale';
import TableStore from './table-store';
import TableLayout from './table-layout';
import TableBody from './table-body';
@@ -130,7 +131,7 @@
emptyText: {
type: String,
- default: '暂无数据'
+ default: $t('el.table.emptyText')
}
},
diff --git a/packages/table/src/util.js b/packages/table/src/util.js
index 896f6c67c..d7e3e1c2d 100644
--- a/packages/table/src/util.js
+++ b/packages/table/src/util.js
@@ -75,3 +75,21 @@ export const orderBy = function(array, sortKey, reverse) {
return a === b ? 0 : a > b ? order : -order;
});
};
+
+export const getColumnById = function(table, columnId) {
+ let column = null;
+ table.columns.forEach(function(item) {
+ if (item.id === columnId) {
+ column = item;
+ }
+ });
+ return column;
+};
+
+export const getColumnByCell = function(table, cell) {
+ const matches = (cell.className || '').match(/el-table_[^\s]+/gm);
+ if (matches) {
+ return getColumnById(table, matches[0]);
+ }
+ return null;
+};
diff --git a/packages/theme-default/src/index.css b/packages/theme-default/src/index.css
index a2c8792b2..df2aac229 100644
--- a/packages/theme-default/src/index.css
+++ b/packages/theme-default/src/index.css
@@ -13,6 +13,7 @@
@import "./loading.css";
@import "./dialog.css";
@import "./table.css";
+@import "./table-column.css";
@import "./pagination.css";
@import "./popover.css";
@import "./tooltip.css";
diff --git a/packages/theme-default/src/table-column.css b/packages/theme-default/src/table-column.css
index e69de29bb..f367c8f3e 100644
--- a/packages/theme-default/src/table-column.css
+++ b/packages/theme-default/src/table-column.css
@@ -0,0 +1,85 @@
+@charset "UTF-8";
+@import "./checkbox.css";
+@import "./tag.css";
+@import "./common/var.css";
+
+@component-namespace el {
+ @b table-filter {
+ border: solid 1px #d3dce6;
+ border-radius: 2px;
+ background-color: #fff;
+ box-shadow: var(--dropdown-menu-box-shadow);
+ box-sizing: border-box;
+ margin: 2px 0;
+
+ /** used for dropdown mode */
+ @e list {
+ padding: 5px 0;
+ margin: 0;
+ list-style: none;
+ min-width: 100px;
+ }
+
+ @e list-item {
+ line-height: 36px;
+ padding: 0 10px;
+ cursor: pointer;
+ font-size: var(--font-size-base);
+
+ &:hover {
+ background-color: var(--dropdown-menuItem-hover-fill);
+ color: var(--dropdown-menuItem-hover-color);
+ }
+
+ @when active {
+ background-color: #20a0ff;
+ color: #fff;
+ }
+ }
+
+ @e content {
+ min-width: 100px;
+ }
+
+ @e bottom {
+ border-top: 1px solid #d3dce6;
+ padding: 8px;
+
+ button {
+ background: transparent;
+ border: none;
+ color: #8492a6;
+ cursor: pointer;
+ font-size: var(--font-size-base);
+ padding: 0 3px;
+
+ &:hover {
+ color: #20a0ff;
+ }
+
+ &:focus {
+ outline: none;
+ }
+
+ &.is-disabled {
+ color: #c0ccda;
+ cursor: not-allowed;
+ }
+ }
+ }
+
+ @e checkbox-group {
+ padding: 10px;
+
+ .el-checkbox {
+ display: block;
+ margin-bottom: 8px;
+ margin-left: 5px;
+ }
+
+ .el-checkbox:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/theme-default/src/table.css b/packages/theme-default/src/table.css
index 9c1d4e9e9..ba995d47e 100644
--- a/packages/theme-default/src/table.css
+++ b/packages/theme-default/src/table.css
@@ -213,16 +213,21 @@
vertical-align: middle;
width: 100%;
box-sizing: border-box;
+
+ &.highlight {
+ color: #20a0ff;
+ }
}
- & div.caret-wrapper {
- position: absolute;
- top: 50%;
- transform: translateY(-50%);
- right: 10px;
- width: 10px;
- height: 12px;
- padding: 0;
+ & .caret-wrapper {
+ position: relative;
+ cursor: pointer;
+ display: inline-block;
+ vertical-align: middle;
+ margin-left: 5px;
+ margin-top: -2px;
+ width: 16px;
+ height: 34px;
overflow: initial;
}
@@ -233,19 +238,20 @@
border: 0;
content: "";
position: absolute;
+ left: 3px;
z-index: 2;
&.ascending {
- top: 0;
+ top: 11px;
border-top: none;
border-right: 5px solid transparent;
- border-bottom: 5px solid #99A9BF;
+ border-bottom: 5px solid #99a9bf;
border-left: 5px solid transparent;
}
&.descending {
- bottom: 0;
- border-top: 5px solid #99A9BF;
+ bottom: 11px;
+ border-top: 5px solid #99a9bf;
border-right: 5px solid transparent;
border-bottom: none;
border-left: 5px solid transparent;
@@ -333,7 +339,12 @@
z-index: -1;
}
- @e column-filter-label {
+ @e column-filter-trigger {
+ display: inline-block;
+ line-height: 34px;
+ margin-left: 5px;
+ cursor: pointer;
+
& i {
color: #99a9bf;
}
diff --git a/src/locale/lang/en.js b/src/locale/lang/en.js
index 6e75278e3..2e1d78e91 100644
--- a/src/locale/lang/en.js
+++ b/src/locale/lang/en.js
@@ -68,6 +68,12 @@ export default {
delete: 'Delete',
preview: 'Preview',
continue: 'Continue'
+ },
+ table: {
+ emptyText: 'No Data',
+ confirmFilter: 'Confirm',
+ resetFilter: 'Reset',
+ clearFilter: 'All'
}
}
};
diff --git a/src/locale/lang/zh-cn.js b/src/locale/lang/zh-cn.js
index 7b4b5cbf3..9b4dafb8b 100644
--- a/src/locale/lang/zh-cn.js
+++ b/src/locale/lang/zh-cn.js
@@ -68,6 +68,12 @@ export default {
delete: '删除',
preview: '查看图片',
continue: '继续上传'
+ },
+ table: {
+ emptyText: '暂无数据',
+ confirmFilter: '筛选',
+ resetFilter: '重置',
+ clearFilter: '全部'
}
}
};
diff --git a/test/unit/specs/table.spec.js b/test/unit/specs/table.spec.js
index 470e2887d..0f4d304c7 100644
--- a/test/unit/specs/table.spec.js
+++ b/test/unit/specs/table.spec.js
@@ -54,7 +54,7 @@ describe('Table', () => {
});
it('row data', () => {
- const cells = toArray(vm.$el.querySelectorAll('.cell'))
+ const cells = toArray(vm.$el.querySelectorAll('td .cell'))
.map(node => node.textContent);
expect(cells).to.eql(testDataArr);
@@ -591,7 +591,7 @@ describe('Table', () => {
it('ascending', done => {
const elm = vm.$el.querySelector('.caret-wrapper');
- elm.parentNode.click();
+ elm.click();
setTimeout(_ => {
const lastCells = vm.$el.querySelectorAll('.el-table__body-wrapper tbody tr td:last-child');
expect(toArray(lastCells).map(node => node.textContent))
@@ -603,7 +603,7 @@ describe('Table', () => {
it('descending', done => {
const elm = vm.$el.querySelector('.caret-wrapper');
- elm.parentNode.click();
+ elm.click();
setTimeout(_ => {
const lastCells = vm.$el.querySelectorAll('.el-table__body-wrapper tbody tr td:last-child');
expect(toArray(lastCells).map(node => node.textContent))