Ver Fonte

Table: add filter feature. (#684)

FuryBean há 8 anos atrás
pai
commit
14495f6189

+ 1 - 0
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

+ 96 - 5
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
+<template>
+  <el-table
+    :data="tableData"
+    border
+    style="width: 100%">
+    <el-table-column
+      prop="date"
+      label="日期"
+      sortable
+      width="180">
+    </el-table-column>
+    <el-table-column
+      prop="name"
+      label="姓名"
+      width="180">
+    </el-table-column>
+    <el-table-column
+      prop="address"
+      label="地址"
+      :formatter="formatter">
+    </el-table-column>
+    <el-table-column
+      prop="tag"
+      label="标签"
+      width="100"
+      :filters="[{ text: '家', value: '家' }, { text: '公司', value: '公司' }]"
+      :filter-method="filterTag"
+      inline-template>
+      <el-tag :type="row.tag === '家' ? 'primary' : 'success'" close-transition>{{row.tag}}</el-tag>
+    </el-table-column>
+  </el-table>
+</template>
+
+<script>
+  export default {
+    data() {
+      return {
+        tableData: [{
+          date: '2016-05-02',
+          name: '王小虎',
+          address: '上海市普陀区金沙江路 1518 弄',
+          tag: '家'
+        }, {
+          date: '2016-05-04',
+          name: '王小虎',
+          address: '上海市普陀区金沙江路 1517 弄',
+          tag: '公司'
+        }, {
+          date: '2016-05-01',
+          name: '王小虎',
+          address: '上海市普陀区金沙江路 1519 弄',
+          tag: '家'
+        }, {
+          date: '2016-05-03',
+          name: '王小虎',
+          address: '上海市普陀区金沙江路 1516 弄',
+          tag: '公司'
+        }]
+      }
+    },
+    methods: {
+      formatter(row, column) {
+        return row.address;
+      },
+      filterTag(value, row) {
+        return row.tag === value;
+      }
+    }
+  }
+</script>
+```
+:::
+
 ### 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 |
+| 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 | — | — |

+ 27 - 0
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);
+    }
+  }
+};

+ 180 - 0
packages/table/src/filter-panel.vue

@@ -0,0 +1,180 @@
+<template>
+  <transition name="md-fade-bottom">
+    <div class="el-table-filter" v-if="multiple" v-show="showPopper">
+      <div class="el-table-filter__content">
+        <el-checkbox-group class="el-table-filter__checkbox-group" v-model="filteredValue">
+          <el-checkbox
+            v-for="filter in filters"
+            :label="filter.value">{{ filter.text }}</el-checkbox>
+        </el-checkbox-group>
+      </div>
+      <div class="el-table-filter__bottom">
+        <button @click="handleConfirm"
+          :class="{ 'is-disabled': filteredValue.length === 0 }"
+          :disabled="filteredValue.length === 0">{{ $t('el.table.confirmFilter') }}</button>
+        <button @click="handleReset">{{ $t('el.table.resetFilter') }}</button>
+      </div>
+    </div>
+    <div class="el-table-filter" v-else v-show="showPopper">
+      <ul class="el-table-filter__list">
+        <li class="el-table-filter__list-item"
+            :class="{ 'is-active': !filterValue }"
+            @click="handleSelect(null)">{{ $t('el.table.clearFilter') }}</li>
+        <li class="el-table-filter__list-item"
+            v-for="filter in filters"
+            :label="filter.value"
+            :class="{ 'is-active': isActive(filter) }"
+            @click="handleSelect(filter.value)" >{{ filter.text }}</li>
+      </ul>
+    </div>
+  </transition>
+</template>
+
+<script type="text/jsx">
+  import Popper from 'element-ui/src/utils/vue-popper';
+  import Locale from 'element-ui/src/mixins/locale';
+  import Clickoutside from 'element-ui/src/utils/clickoutside';
+  import Dropdown from './dropdown';
+  import ElCheckbox from 'element-ui/packages/checkbox';
+  import ElCheckboxGroup from 'element-ui/packages/checkbox-group';
+
+  export default {
+    name: 'el-table-filter-panel',
+
+    mixins: [Popper, Locale],
+
+    directives: {
+      Clickoutside
+    },
+
+    components: {
+      ElCheckbox,
+      ElCheckboxGroup
+    },
+
+    props: {
+      placement: {
+        type: String,
+        default: 'bottom-end'
+      }
+    },
+
+    customRender(h) {
+      return (<div class="el-table-filter">
+        <div class="el-table-filter__content">
+        </div>
+        <div class="el-table-filter__bottom">
+          <button on-click={ this.handleConfirm }>{ this.$t('el.table.confirmFilter') }</button>
+          <button on-click={ this.handleReset }>{ this.$t('el.table.resetFilter') }</button>
+        </div>
+      </div>);
+    },
+
+    methods: {
+      isActive(filter) {
+        return filter.value === this.filterValue;
+      },
+
+      handleOutsideClick() {
+        this.showPopper = false;
+      },
+
+      handleConfirm() {
+        this.confirmFilter(this.filteredValue);
+        this.handleOutsideClick();
+      },
+
+      handleReset() {
+        this.filteredValue = [];
+        this.confirmFilter(this.filteredValue);
+        this.handleOutsideClick();
+      },
+
+      handleSelect(filterValue) {
+        this.filterValue = filterValue;
+
+        if (filterValue) {
+          this.confirmFilter(this.filteredValue);
+        } else {
+          this.confirmFilter([]);
+        }
+
+        this.handleOutsideClick();
+      },
+
+      confirmFilter(filteredValue) {
+        this.table.store.commit('filterChange', {
+          column: this.column,
+          values: filteredValue
+        });
+      }
+    },
+
+    data() {
+      return {
+        table: null,
+        cell: null,
+        column: null
+      };
+    },
+
+    computed: {
+      filters() {
+        return this.column && this.column.filters;
+      },
+
+      filterValue: {
+        get() {
+          return (this.column.filteredValue || [])[0];
+        },
+        set(value) {
+          if (this.filteredValue) {
+            if (value) {
+              this.filteredValue.splice(0, 1, value);
+            } else {
+              this.filteredValue.splice(0, 1);
+            }
+          }
+        }
+      },
+
+      filteredValue: {
+        get() {
+          if (this.column) {
+            return this.column.filteredValue || [];
+          }
+          return [];
+        },
+        set(value) {
+          if (this.column) {
+            this.column.filteredValue = value;
+          }
+        }
+      },
+
+      multiple() {
+        if (this.column) {
+          return this.column.filterMultiple;
+        }
+        return true;
+      }
+    },
+
+    mounted() {
+      this.popperElm = this.$el;
+      this.referenceElm = this.cell;
+      this.table.$refs.bodyWrapper.addEventListener('scroll', () => {
+        this.updatePopper();
+      });
+
+      this.$watch('showPopper', (value) => {
+        if (this.column) this.column.filterOpened = value;
+        if (value) {
+          Dropdown.open(this);
+        } else {
+          Dropdown.close(this);
+        }
+      });
+    }
+  };
+</script>

+ 1 - 19
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: {

+ 18 - 11
packages/table/src/table-column.js

@@ -19,21 +19,16 @@ const defaults = {
     minWidth: 48,
     realWidth: 48,
     direction: ''
-  },
-  filter: {
-    headerTemplate: function(h) { return <span>filter header</span>; },
-    direction: ''
   }
 };
 
 const forced = {
   selection: {
     headerTemplate: function(h) {
-      return <div><el-checkbox
+      return <el-checkbox
         nativeOn-click={ this.toggleAllSelection }
         domProps-value={ this.isAllSelected }
-        on-input={ (value) => { this.$emit('allselectedchange', value); } } />
-      </div>;
+        on-input={ (value) => { this.$emit('allselectedchange', value); } } />;
     },
     template: function(h, { row, column, store, $index }) {
       return <el-checkbox
@@ -47,7 +42,7 @@ const forced = {
   index: {
     // headerTemplate: function(h) { return <div>#</div>; },
     headerTemplate: function(h, label) {
-      return <div>{ label || '#' }</div>;
+      return label || '#';
     },
     template: function(h, { $index }) {
       return <div>{ $index + 1 }</div>;
@@ -56,7 +51,7 @@ const forced = {
   },
   filter: {
     headerTemplate: function(h) {
-      return <div>#</div>;
+      return '#';
     },
     template: function(h, { row, column }) {
       return <el-tag type="primary" style="height: 16px; line-height: 16px; min-width: 40px; text-align: center">{ row[column.property] }</el-tag>;
@@ -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] || {});

+ 65 - 15
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' : ''] }>
+                  <div class={ ['cell', column.filteredValue && column.filteredValue.length > 0 ? 'highlight' : ''] }>
                   {
-                    [
-                      column.headerTemplate
-                        ? column.headerTemplate.call(this._renderProxy, h, column.label)
-                        : <div>{ column.label }</div>,
-                      column.sortable
-                        ? <div class="caret-wrapper">
-                            <i class="sort-caret ascending"></i>
-                            <i class="sort-caret descending"></i>
-                          </div>
-                        : ''
-                    ]
+                    column.headerTemplate
+                      ? column.headerTemplate.call(this._renderProxy, h, column.label)
+                      : column.label
                   }
+                  {
+                    column.sortable
+                      ? <span class="caret-wrapper" on-click={ ($event) => this.handleHeaderClick($event, column) }>
+                          <i class="sort-caret ascending"></i>
+                          <i class="sort-caret descending"></i>
+                        </span>
+                      : ''
+                  }
+                  {
+                    column.filterable
+                      ? <span class="el-table__column-filter-trigger" on-click={ ($event) => this.handleFilterClick($event, column) }><i class={ ['el-icon-arrow-down', column.filterOpened ? 'el-icon-arrow-up' : ''] }></i></span>
+                      : ''
+                  }
+                  </div>
                 </th>
               )
             }
@@ -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';
         }
       }
     },

+ 36 - 3
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());
   },

+ 2 - 1
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')
       }
     },
 

+ 18 - 0
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;
+};

+ 1 - 0
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";

+ 85 - 0
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;
+      }
+    }
+  }
+}

+ 24 - 13
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;
       }

+ 6 - 0
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'
     }
   }
 };

+ 6 - 0
src/locale/lang/zh-cn.js

@@ -68,6 +68,12 @@ export default {
       delete: '删除',
       preview: '查看图片',
       continue: '继续上传'
+    },
+    table: {
+      emptyText: '暂无数据',
+      confirmFilter: '筛选',
+      resetFilter: '重置',
+      clearFilter: '全部'
     }
   }
 };

+ 3 - 3
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))