Browse Source

Table: add header group feature. (#1312)

FuryBean 8 years ago
parent
commit
2f3f5eabc1

+ 109 - 0
examples/docs/en-US/table.md

@@ -683,6 +683,115 @@ When you have huge chunks of data to put in a table, you can fix the header and
 ```
 :::
 
+### Grouping table head
+
+When the data structure is complex, you can use group header to show the data hierarchy.
+
+:::demo Only need to place el-table-column inside a el-table-column, you can achieve group header.
+```html
+<template>
+  <el-table
+    :data="tableData3"
+    border
+    style="width: 100%">
+    <el-table-column
+      prop="date"
+      label="Date"
+      width="150">
+    </el-table-column>
+    <el-table-column label="Delivery Info">
+      <el-table-column
+        prop="name"
+        label="Name"
+        width="120">
+      </el-table-column>
+      <el-table-column label="Address Info">
+        <el-table-column
+          prop="state"
+          label="State"
+          width="120">
+        </el-table-column>
+        <el-table-column
+          prop="city"
+          label="City"
+          width="120">
+        </el-table-column>
+        <el-table-column
+          prop="address"
+          label="Address"
+          width="300">
+        </el-table-column>
+        <el-table-column
+          prop="zip"
+          label="Zip"
+          width="120">
+        </el-table-column>
+      </el-table-column>
+    </el-table-column>
+  </el-table>
+</template>
+
+<script>
+  export default {
+    data() {
+      return {
+        tableData3: [{
+          date: '2016-05-03',
+          name: 'Tom',
+          state: 'California',
+          city: 'Los Angeles',
+          address: 'No. 189, Grove St, Los Angeles',
+          zip: 'CA 90036'
+        }, {
+          date: '2016-05-02',
+          name: 'Tom',
+          state: 'California',
+          city: 'Los Angeles',
+          address: 'No. 189, Grove St, Los Angeles',
+          zip: 'CA 90036'
+        }, {
+          date: '2016-05-04',
+          name: 'Tom',
+          state: 'California',
+          city: 'Los Angeles',
+          address: 'No. 189, Grove St, Los Angeles',
+          zip: 'CA 90036'
+        }, {
+          date: '2016-05-01',
+          name: 'Tom',
+          state: 'California',
+          city: 'Los Angeles',
+          address: 'No. 189, Grove St, Los Angeles',
+          zip: 'CA 90036'
+        }, {
+          date: '2016-05-08',
+          name: 'Tom',
+          state: 'California',
+          city: 'Los Angeles',
+          address: 'No. 189, Grove St, Los Angeles',
+          zip: 'CA 90036'
+        }, {
+          date: '2016-05-06',
+          name: 'Tom',
+          state: 'California',
+          city: 'Los Angeles',
+          address: 'No. 189, Grove St, Los Angeles',
+          zip: 'CA 90036'
+        }, {
+          date: '2016-05-07',
+          name: 'Tom',
+          state: 'California',
+          city: 'Los Angeles',
+          address: 'No. 189, Grove St, Los Angeles',
+          zip: 'CA 90036'
+        }]
+      }
+    }
+  }
+</script>
+```
+:::
+
 ### Single select
 
 Single row selection is supported.

+ 109 - 0
examples/docs/zh-CN/table.md

@@ -689,6 +689,115 @@
 ```
 :::
 
+### 多级表头
+
+数据结构比较复杂的时候,可使用多级表头来展现数据的层次关系。
+
+:::demo 只需要在 el-table-column 里面嵌套 el-table-column,就可以实现多级表头。
+```html
+<template>
+  <el-table
+    :data="tableData3"
+    border
+    style="width: 100%">
+    <el-table-column
+      prop="date"
+      label="日期"
+      width="150">
+    </el-table-column>
+    <el-table-column label="配送信息">
+      <el-table-column
+        prop="name"
+        label="姓名"
+        width="120">
+      </el-table-column>
+      <el-table-column label="地址">
+        <el-table-column
+          prop="province"
+          label="省份"
+          width="120">
+        </el-table-column>
+        <el-table-column
+          prop="city"
+          label="市区"
+          width="120">
+        </el-table-column>
+        <el-table-column
+          prop="address"
+          label="地址"
+          width="300">
+        </el-table-column>
+        <el-table-column
+          prop="zip"
+          label="邮编"
+          width="120">
+        </el-table-column>
+      </el-table-column>
+    </el-table-column>
+  </el-table>
+</template>
+
+<script>
+  export default {
+    data() {
+      return {
+        tableData3: [{
+          date: '2016-05-03',
+          name: '王小虎',
+          province: '上海',
+          city: '普陀区',
+          address: '上海市普陀区金沙江路 1518 弄',
+          zip: 200333
+        }, {
+          date: '2016-05-02',
+          name: '王小虎',
+          province: '上海',
+          city: '普陀区',
+          address: '上海市普陀区金沙江路 1518 弄',
+          zip: 200333
+        }, {
+          date: '2016-05-04',
+          name: '王小虎',
+          province: '上海',
+          city: '普陀区',
+          address: '上海市普陀区金沙江路 1518 弄',
+          zip: 200333
+        }, {
+          date: '2016-05-01',
+          name: '王小虎',
+          province: '上海',
+          city: '普陀区',
+          address: '上海市普陀区金沙江路 1518 弄',
+          zip: 200333
+        }, {
+          date: '2016-05-08',
+          name: '王小虎',
+          province: '上海',
+          city: '普陀区',
+          address: '上海市普陀区金沙江路 1518 弄',
+          zip: 200333
+        }, {
+          date: '2016-05-06',
+          name: '王小虎',
+          province: '上海',
+          city: '普陀区',
+          address: '上海市普陀区金沙江路 1518 弄',
+          zip: 200333
+        }, {
+          date: '2016-05-07',
+          name: '王小虎',
+          province: '上海',
+          city: '普陀区',
+          address: '上海市普陀区金沙江路 1518 弄',
+          zip: 200333
+        }]
+      }
+    }
+  }
+</script>
+```
+:::
+
 ### 单选
 
 选择单行数据时使用色块表示。

+ 10 - 6
packages/table/src/table-column.js

@@ -126,11 +126,13 @@ export default {
     }
   },
 
-  render() {},
+  render() {
+    return (<div>{ this._t('default') }</div>);
+  },
 
   data() {
     return {
-      isChildColumn: false,
+      isSubColumn: false,
       columns: []
     };
   },
@@ -158,13 +160,15 @@ export default {
 
   created() {
     this.customRender = this.$options.render;
-    this.$options.render = (h) => h('div');
+    this.$options.render = (h) => {
+      return (<div>{ this._t('default') }</div>);
+    };
 
     let columnId = this.columnId = (this.$parent.tableId || (this.$parent.columnId + '_')) + 'column_' + columnIdSeed++;
 
     let parent = this.$parent;
     let owner = this.owner;
-    this.isChildColumn = owner !== parent;
+    this.isSubColumn = owner !== parent;
 
     let type = this.type;
 
@@ -326,12 +330,12 @@ export default {
     const parent = this.$parent;
     let columnIndex;
 
-    if (!this.isChildColumn) {
+    if (!this.isSubColumn) {
       columnIndex = [].indexOf.call(parent.$refs.hiddenColumns.children, this.$el);
     } else {
       columnIndex = [].indexOf.call(parent.$el.children, this.$el);
     }
 
-    owner.store.commit('insertColumn', this.columnConfig, columnIndex);
+    owner.store.commit('insertColumn', this.columnConfig, columnIndex, this.isSubColumn ? parent.columnConfig : null);
   }
 };

+ 107 - 34
packages/table/src/table-header.js

@@ -3,10 +3,75 @@ import ElTag from 'element-ui/packages/tag';
 import Vue from 'vue';
 import FilterPanel from './filter-panel.vue';
 
+const getAllColumns = (columns) => {
+  const result = [];
+  columns.forEach((column) => {
+    if (column.children) {
+      result.push(column);
+      result.push.apply(result, getAllColumns(column.children));
+    } else {
+      result.push(column);
+    }
+  });
+  return result;
+};
+
+const convertToRows = (originColumns) => {
+  let maxLevel = 1;
+  const traverse = (column, parent) => {
+    if (parent) {
+      column.level = parent.level + 1;
+      if (maxLevel < column.level) {
+        maxLevel = column.level;
+      }
+    }
+    if (column.children) {
+      let childrenMax = 1;
+      let colSpan = 0;
+      column.children.forEach((subColumn) => {
+        const temp = traverse(subColumn, column);
+        if (temp > childrenMax) {
+          childrenMax = temp;
+        }
+        colSpan += subColumn.colSpan;
+      });
+      column.colSpan = colSpan;
+    } else {
+      column.colSpan = 1;
+    }
+  };
+
+  originColumns.forEach((column) => {
+    column.level = 1;
+    traverse(column);
+  });
+
+  const rows = [];
+  for (let i = 0; i < maxLevel; i++) {
+    rows.push([]);
+  }
+
+  const allColumns = getAllColumns(originColumns);
+
+  allColumns.forEach((column) => {
+    if (!column.children) {
+      column.rowSpan = maxLevel - column.level + 1;
+    } else {
+      column.rowSpan = 1;
+    }
+    rows[column.level - 1].push(column);
+  });
+
+  return rows;
+};
+
 export default {
   name: 'el-table-header',
 
   render(h) {
+    const originColumns = this.store.states.originColumns;
+    const columnRows = convertToRows(originColumns, this.columns);
+
     return (
       <table
         class="el-table__header"
@@ -26,44 +91,50 @@ export default {
             : ''
         }
         <thead>
-          <tr>
-            {
-              this._l(this.columns, (column, cellIndex) =>
-                <th
-                  on-mousemove={ ($event) => this.handleMouseMove($event, column) }
-                  on-mouseout={ this.handleMouseOut }
-                  on-mousedown={ ($event) => this.handleMouseDown($event, column) }
-                  on-click={ ($event) => this.handleClick($event, column) }
-                  class={ [column.id, column.order, column.align, column.className || '', this.isCellHidden(cellIndex) ? 'is-hidden' : ''] }>
-                  <div class={ ['cell', column.filteredValue && column.filteredValue.length > 0 ? 'highlight' : ''] }>
-                  {
-                    column.renderHeader
-                      ? column.renderHeader.call(this._renderProxy, h, { column, $index: cellIndex, store: this.store, _self: this.$parent.$vnode.context })
-                      : column.label
+          {
+            this._l(columnRows, (columns) =>
+              <tr>
+                {
+                  this._l(columns, (column, cellIndex) =>
+                    <th
+                      colspan={ column.colSpan }
+                      rowspan={ column.rowSpan }
+                      on-mousemove={ ($event) => this.handleMouseMove($event, column) }
+                      on-mouseout={ this.handleMouseOut }
+                      on-mousedown={ ($event) => this.handleMouseDown($event, column) }
+                      on-click={ ($event) => this.handleClick($event, column) }
+                      class={ [column.id, column.order, column.align, column.className || '', this.isCellHidden(cellIndex) ? 'is-hidden' : '', !column.children ? 'is-leaf' : ''] }>
+                      <div class={ ['cell', column.filteredValue && column.filteredValue.length > 0 ? 'highlight' : ''] }>
+                      {
+                        column.renderHeader
+                          ? column.renderHeader.call(this._renderProxy, h, { column, $index: cellIndex, store: this.store, _self: this.$parent.$vnode.context })
+                          : 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>
+                    )
                   }
                   {
-                    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>
+                    !this.fixed && this.layout.gutterWidth
+                      ? <th class="gutter" style={{ width: this.layout.scrollY ? this.layout.gutterWidth + 'px' : '0' }}></th>
                       : ''
                   }
-                  {
-                    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>
-              )
-            }
-            {
-              !this.fixed && this.layout.gutterWidth
-                ? <th class="gutter" style={{ width: this.layout.scrollY ? this.layout.gutterWidth + 'px' : '0' }}></th>
-                : ''
-            }
-          </tr>
+                </tr>
+            )
+          }
         </thead>
       </table>
     );
@@ -168,6 +239,7 @@ export default {
     },
 
     handleMouseDown(event, column) {
+      if (column.children && column.children.length > 0) return;
       /* istanbul ignore if */
       if (this.draggingColumn && this.border) {
         this.dragging = true;
@@ -234,6 +306,7 @@ export default {
     },
 
     handleMouseMove(event, column) {
+      if (column.children && column.children.length > 0) return;
       let target = event.target;
       while (target && target.tagName !== 'TH') {
         target = target.parentNode;

+ 25 - 5
packages/table/src/table-store.js

@@ -52,6 +52,7 @@ const TableStore = function(table, initialState = {}) {
   this.states = {
     rowKey: null,
     _columns: [],
+    originColumns: [],
     columns: [],
     fixedColumns: [],
     rightFixedColumns: [],
@@ -159,13 +160,19 @@ TableStore.prototype.mutations = {
     Vue.nextTick(() => this.table.updateScrollY());
   },
 
-  insertColumn(states, column, index) {
-    let _columns = states._columns;
+  insertColumn(states, column, index, parent) {
+    let array = states._columns;
+    if (parent) {
+      array = parent.children;
+      if (!array) array = parent.children = [];
+    }
+
     if (typeof index !== 'undefined') {
-      _columns.splice(index, 0, column);
+      array.splice(index, 0, column);
     } else {
-      _columns.push(column);
+      array.push(column);
     }
+
     if (column.type === 'selection') {
       states.selectable = column.selectable;
       states.reserveSelection = column.reserveSelection;
@@ -236,6 +243,18 @@ TableStore.prototype.mutations = {
   })
 };
 
+const doFlattenColumns = (columns) => {
+  const result = [];
+  columns.forEach((column) => {
+    if (column.children) {
+      result.push.apply(result, doFlattenColumns(column.children));
+    } else {
+      result.push(column);
+    }
+  });
+  return result;
+};
+
 TableStore.prototype.updateColumns = function() {
   const states = this.states;
   const _columns = states._columns || [];
@@ -246,7 +265,8 @@ TableStore.prototype.updateColumns = function() {
     _columns[0].fixed = true;
     states.fixedColumns.unshift(_columns[0]);
   }
-  states.columns = [].concat(states.fixedColumns).concat(_columns.filter((column) => !column.fixed)).concat(states.rightFixedColumns);
+  states.originColumns = [].concat(states.fixedColumns).concat(_columns.filter((column) => !column.fixed)).concat(states.rightFixedColumns);
+  states.columns = doFlattenColumns(states.originColumns);
   states.isComplex = states.fixedColumns.length > 0 || states.rightFixedColumns.length > 0;
 };
 

+ 8 - 1
packages/theme-default/src/table.css

@@ -86,7 +86,6 @@
       text-overflow: ellipsis;
       vertical-align: middle;
       position: relative;
-      border-bottom: 1px solid var(--table-border-color);
 
       @when center {
         text-align: center;
@@ -101,10 +100,18 @@
       }
     }
 
+    & th.is-leaf, td {
+      border-bottom: 1px solid var(--table-border-color);
+    }
+
     @modifier border {
       & th, td {
         border-right: 1px solid var(--table-border-color);
       }
+
+      & th {
+        border-bottom: 1px solid var(--table-border-color);
+      }
     }
 
     & th {

+ 106 - 0
test/unit/specs/table.spec.js

@@ -985,6 +985,112 @@ describe('Table', () => {
     });
   });
 
+  describe('multi level column', () => {
+    it('should works', done => {
+      const vm = createVue({
+        template: `
+          <el-table :data="testData">
+            <el-table-column prop="name" />
+            <el-table-column label="group">
+              <el-table-column prop="release"/>
+              <el-table-column prop="director"/>
+            </el-table-column>
+            <el-table-column prop="runtime"/>
+          </el-table>
+        `,
+
+        created() {
+          this.testData = null;
+        }
+      }, true);
+
+      setTimeout(_ => {
+        const trs = vm.$el.querySelectorAll('.el-table__header tr');
+        expect(trs.length).equal(2);
+        const firstRowHeader = trs[0].querySelectorAll('th .cell').length;
+        const secondRowHeader = trs[1].querySelectorAll('th .cell').length;
+        expect(firstRowHeader).to.equal(3);
+        expect(secondRowHeader).to.equal(2);
+
+        expect(trs[0].querySelector('th:first-child').getAttribute('rowspan')).to.equal('2');
+        expect(trs[0].querySelector('th:nth-child(2)').getAttribute('colspan')).to.equal('2');
+        destroyVM(vm);
+        done();
+      }, DELAY);
+    });
+
+    it('should works', done => {
+      const vm = createVue({
+        template: `
+          <el-table :data="testData">
+            <el-table-column prop="name" />
+            <el-table-column label="group">
+              <el-table-column label="group's group">
+                <el-table-column prop="release" />
+                <el-table-column prop="runtime"/>
+              </el-table-column>
+              <el-table-column prop="director" />
+            </el-table-column>
+            <el-table-column prop="runtime"/>
+          </el-table>
+        `,
+
+        created() {
+          this.testData = null;
+        }
+      }, true);
+
+      setTimeout(_ => {
+        const trs = vm.$el.querySelectorAll('.el-table__header tr');
+        expect(trs.length).equal(3);
+        const firstRowHeader = trs[0].querySelectorAll('th .cell').length;
+        const secondRowHeader = trs[1].querySelectorAll('th .cell').length;
+        const thirdRowHeader = trs[2].querySelectorAll('th .cell').length;
+        expect(firstRowHeader).to.equal(3);
+        expect(secondRowHeader).to.equal(2);
+        expect(thirdRowHeader).to.equal(2);
+
+        expect(trs[0].querySelector('th:first-child').getAttribute('rowspan')).to.equal('3');
+        expect(trs[0].querySelector('th:nth-child(2)').getAttribute('colspan')).to.equal('3');
+        expect(trs[1].querySelector('th:first-child').getAttribute('colspan')).to.equal('2');
+        expect(trs[1].querySelector('th:nth-child(2)').getAttribute('rowspan')).to.equal('2');
+
+        destroyVM(vm);
+        done();
+      }, DELAY);
+    });
+
+    it('should work in one column', done => {
+      const vm = createVue({
+        template: `
+          <el-table :data="testData">
+            <el-table-column label="group">
+              <el-table-column prop="release"/>
+            </el-table-column>
+          </el-table>
+        `,
+
+        created() {
+          this.testData = null;
+        }
+      }, true);
+
+      setTimeout(_ => {
+        const trs = vm.$el.querySelectorAll('.el-table__header tr');
+        expect(trs.length).equal(2);
+        const firstRowLength = trs[0].querySelectorAll('th .cell').length;
+        const secondRowLength = trs[1].querySelectorAll('th .cell').length;
+        expect(firstRowLength).to.equal(1);
+        expect(secondRowLength).to.equal(1);
+
+        expect(trs[0].querySelector('th:first-child').getAttribute('rowspan')).to.equal('1');
+        expect(trs[0].querySelector('th:first-child').getAttribute('colspan')).to.equal('1');
+        destroyVM(vm);
+        done();
+      }, DELAY);
+    });
+  });
+
   describe('methods', () => {
     const createTable = function(prop = '', opts) {
       return createVue({