diff --git a/examples/docs/en-US/table.md b/examples/docs/en-US/table.md index b8f4d7392..a17dfd055 100644 --- a/examples/docs/en-US/table.md +++ b/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 + + + +``` +::: + ### Single select Single row selection is supported. diff --git a/examples/docs/zh-CN/table.md b/examples/docs/zh-CN/table.md index 4d871ec5a..a98d15da3 100644 --- a/examples/docs/zh-CN/table.md +++ b/examples/docs/zh-CN/table.md @@ -689,6 +689,115 @@ ``` ::: +### 多级表头 + +数据结构比较复杂的时候,可使用多级表头来展现数据的层次关系。 + +:::demo 只需要在 el-table-column 里面嵌套 el-table-column,就可以实现多级表头。 +```html + + + +``` +::: + ### 单选 选择单行数据时使用色块表示。 diff --git a/packages/table/src/table-column.js b/packages/table/src/table-column.js index 6844ba67d..776532432 100644 --- a/packages/table/src/table-column.js +++ b/packages/table/src/table-column.js @@ -126,11 +126,13 @@ export default { } }, - render() {}, + render() { + return (
{ this._t('default') }
); + }, 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 (
{ this._t('default') }
); + }; 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); } }; diff --git a/packages/table/src/table-header.js b/packages/table/src/table-header.js index 447d79c2e..b65c3e7d4 100644 --- a/packages/table/src/table-header.js +++ b/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 ( - - { - this._l(this.columns, (column, cellIndex) => - + { + this._l(columns, (column, cellIndex) => + + ) } { - column.sortable - ? this.handleHeaderClick($event, column) }> - - - + !this.fixed && this.layout.gutterWidth + ? : '' } - { - column.filterable - ? this.handleFilterClick($event, column) }> - : '' - } - - - ) - } - { - !this.fixed && this.layout.gutterWidth - ? - : '' - } - + + ) + }
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' : ''] }> -
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) => +
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' : ''] }> +
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 + ? this.handleHeaderClick($event, column) }> + + + + : '' + } + { + column.filterable + ? this.handleFilterClick($event, column) }> + : '' + } +
+
); @@ -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; diff --git a/packages/table/src/table-store.js b/packages/table/src/table-store.js index 3df908b11..689622579 100644 --- a/packages/table/src/table-store.js +++ b/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; - if (typeof index !== 'undefined') { - _columns.splice(index, 0, column); - } else { - _columns.push(column); + insertColumn(states, column, index, parent) { + let array = states._columns; + if (parent) { + array = parent.children; + if (!array) array = parent.children = []; } + + if (typeof index !== 'undefined') { + array.splice(index, 0, column); + } else { + 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; }; diff --git a/packages/theme-default/src/table.css b/packages/theme-default/src/table.css index 506ce31e1..a7d7d8eae 100644 --- a/packages/theme-default/src/table.css +++ b/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 { diff --git a/test/unit/specs/table.spec.js b/test/unit/specs/table.spec.js index f96904606..c00394af2 100644 --- a/test/unit/specs/table.spec.js +++ b/test/unit/specs/table.spec.js @@ -985,6 +985,112 @@ describe('Table', () => { }); }); + describe('multi level column', () => { + it('should works', done => { + const vm = createVue({ + template: ` + + + + + + + + + `, + + 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: ` + + + + + + + + + + + + `, + + 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: ` + + + + + + `, + + 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({