参考网上黄龙的表格树进行完善,并添加固定表头等的功能,目前是在iview的项目中实现,如果想在element中实现的话修改对应的元素标签及相关写法即可。
<!-- @events @on-row-click 单击行或者单击操作按钮方法 @on-selection-change 多选模式下 选中项变化时触发 @on-sort-change 排序时有效,当点击排序时触发 @props data 显示的结构化数据 columns 表格列的配置描述 sortable:true 开启排序功能 showHeader 是否显示表头 type: 'selection'为多选功能 type: 'template' 为操作功能 slot为插槽名 --> <template> <div ref="table" class='autoTable tree-grid'> <!-- <div ref="table" :style="{treeGridWidth}" class='autoTable tree-grid'> --> <div ref="header" class="tree-grid-header" v-if="showHeader"> <table class="table table-bordered hl-tree-table"> <colgroup> <col v-for="(column, index) in cloneColumns" :width="column.width" :align="column.align" :key="index"> <col v-if="showVerticalScrollBar" :width="scrollBarWidth"/> </colgroup> <thead> <tr> <th v-for="(column,index) in cloneColumns" :key="column.key"> <div v-if="column.type === 'selection'" class="selection-wrapper"> <Checkbox v-model="checks" @click="handleCheckAll"></Checkbox> <input type="checkbox" v-model="checks" @click="handleCheckAll" class="selection-checkbox"> </div> <div v-else class="tree-grid-cell">{{renderHeader(column, index)}} <span class="ivu-table-sort" v-if="column.sortable"> <Icon type="md-arrow-dropup" :class="{on: column._sortType === 'asc'}" @click.native="handleSort(index, 'asc')" /> <Icon type="md-arrow-dropdown" :class="{on: column._sortType === 'desc'}" @click.native="handleSort(index, 'desc')" /> </span> </div> </th> <th v-if="showVerticalScrollBar" rowspan="1"></th> </tr> </thead> </table> </div> <div ref="body" class="tree-grid-body" :style="{height:tBodyHeight}"> <table ref="bodyTable" class="table table-bordered hl-tree-table"> <colgroup ref="bodyColgroup"> <col v-for="(column, index) in cloneColumns" :width="column.width" :align="column.align" :key="index"> </colgroup> <tbody> <tr v-for="(item,index) in initGridData" :key="item.id" v-show="show(item)" :class="{'child-tr':item.parent}"> <td v-for="(column,snum) in columns" :key="column.key"> <div v-if="column.type === 'selection'" class="selection-wrapper"> <CheckboxGroup v-model="checkGroup"> <Checkbox :label="item.id"><span></span></Checkbox> </CheckboxGroup> <input type="checkbox" :value="item.id" v-model="checkGroup" @click="handleCheckClick(item,$event,index)" class="selection-checkbox"> </div> <div v-if="column.type === 'template'" class="tree-grid-cell"> <slot :name="column.slot" :scope="item"></slot> </div> <div @click="toggle(index,item)" v-if="!column.type" class="tree-grid-cell"> <template v-if='snum===iconRow()'> <i v-html='item.spaceHtml'></i> <span v-if="item.children&&item.children.length>0" class="tree-grid-arrow" :class="{'tree-grid-arrow-open': item.expanded}" > <i class="ivu-icon ivu-icon-ios-arrow-forward"></i> </span> <i v-else class="ms-tree-space"></i> </template>{{renderBody(item,column)}} </div> </td> </tr> </tbody> </table> </div> </div> </template> <script> let cached export default { name: 'treeGrid', props: { columns: { type: Array, default: () => [] }, data: { type: Array, default: () => [] }, showHeader: { type: Boolean, default: true }, height: { type: [Number, String] } }, data () { return { initGridData: [], // 处理后数据数组 cloneColumns: [], // 处理后的表头数据 showVerticalScrollBar: false, scrollBarWidth: undefined, checkGroup: [], // 复选框数组 checks: false, // 全选 screenWidth: document.body.clientWidth, // 自适应宽 headerHeight: 0, columnsWidth: {}, timer: false, // 控制监听时长 dataLength: 0 // 树形数据长度 } }, computed: { treeGridWidth () { let treeGridWidth = this.$el ? this.$el.offsetWidth : '1000' return treeGridWidth }, tBodyHeight () { return parseFloat(this.height) > 0 ? (parseFloat(this.height) - parseFloat(this.headerHeight)) + 'px' : 'auto' } }, watch: { screenWidth (val) { if (!this.timer) { this.screenWidth = val this.timer = true setTimeout(() => { this.timer = false }, 400) } }, data () { if (this.data) { this.dataLength = this.Length(this.data) this.initData(this.deepCopy(this.data), 1, null) this.checkGroup = this.renderCheck(this.data) if (this.checkGroup.length === this.dataLength) { this.checks = true } else { this.checks = false } this.showScrollBar() } }, columns: { handler () { this.cloneColumns = this.makeColumns() }, deep: true }, checkGroup (data) { this.checkAllGroupChange(data) } }, mounted () { if (this.data) { this.dataLength = this.Length(this.data) this.initData(this.deepCopy(this.data), 1, null) this.cloneColumns = this.makeColumns() this.checkGroup = this.renderCheck(this.data) if (this.checkGroup.length === this.dataLength) { this.checks = true } else { this.checks = false } } // 绑定onresize事件 监听屏幕变化设置宽 this.$nextTick(() => { this.screenWidth = document.body.clientWidth this.headerHeight = this.showHeader ? this.$refs.header.clientHeight : 0 this.cloneColumns = this.makeColumns() this.showScrollBar() }) window.onresize = () => { return (() => { window.screenWidth = document.body.clientWidth window.screenHeight = document.body.clientHeight this.screenWidth = window.screenWidth this.headerHeight = this.showHeader ? this.$refs.header.clientHeight : 0 this.cloneColumns = this.makeColumns() this.showScrollBar() })() } }, methods: { showScrollBar () { this.$nextTick(() => { // console.error(this.$refs.bodyTable.clientHeight, this.$refs.body.clientHeight) if (this.$refs.bodyTable.clientHeight > this.$refs.body.clientHeight) { if (!this.showVerticalScrollBar) { this.showVerticalScrollBar = true this.scrollBarWidth = this.getScrollBarSize() } } else { if (this.showVerticalScrollBar) { this.showVerticalScrollBar = false } } }) }, // 有无多选框折叠位置优化 iconRow () { for (let i = 0, len = this.columns.length; i < len; i++) { if (this.columns[i].type === 'selection') { return 1 } } return 0 }, // 排序事件 handleSort (index, type) { this.cloneColumns.forEach(col => { col._sortType = 'normal' }) if (this.cloneColumns[index]._sortType === type) { this.cloneColumns[index]._sortType = 'normal' } else { this.cloneColumns[index]._sortType = type } this.$emit('on-sort-change', this.cloneColumns[index]['key'], this.cloneColumns[index]['_sortType']) }, // 点击某一行事件 RowClick (data, event, index, text) { let result = this.makeData(data) this.$emit('on-row-click', result, event, index, text) }, // 点击事件 返回数据处理 makeData (data) { const t = this.type(data) let o if (t === 'array') { o = [] } else if (t === 'object') { o = {} } else { return data } if (t === 'array') { for (let i = 0; i < data.length; i++) { o.push(this.makeData(data[i])) } } else if (t === 'object') { for (let i in data) { if (i !== 'spaceHtml' && i !== 'parent' && i !== 'level' && i !== 'expanded' && i !== 'isShow' && i !== 'load') { o[i] = this.makeData(data[i]) } } } return o }, // 处理表头数据 makeColumns () { let columns = this.deepCopy(this.columns) let tableWidth = this.$el.offsetWidth let noWidthLength = 0 let nanWidthLength = 0 let widthSum = 0 columns.forEach((column, index) => { column._index = index column._sortType = 'normal' if (column.width) { if (!/^(-?d+)(.d+)?$/.test(column.width)) { let width = column.width if (width.slice(-1) === '%') { let percent = (column.width).slice(0, -1) column.width = '' column._width = '' nanWidthLength += 1 column._nanWidth = percent } else { this.$Message.error('请输入正确的宽度:数字(例如:100)或者百分比(例如:10%)') } } else { widthSum += column.width column._width = column.width } } else { noWidthLength += 1 column._width = '' } column._width = column.width ? column.width : '' }) if (nanWidthLength > 0) { columns.forEach((column, index) => { if (column._nanWidth) { column.width = parseInt((tableWidth - widthSum) * column._nanWidth / 100) // column.width = (tableWidth - widthSum) * column._nanWidth / 100 column._width = column.width widthSum += column.width } }) } if (noWidthLength > 0) { columns.forEach((column, index) => { if (column._width === '') { column.width = parseInt((tableWidth - widthSum) / noWidthLength) // column.width = (tableWidth - widthSum) / noWidthLength column._width = column.width } }) } return columns }, // 数据处理 增加自定义属性监听 initData (data, level, parent) { this.initGridData = [] let spaceHtml = '' for (let i = 1; i < level; i++) { spaceHtml += '<i class="ms-tree-space"></i>' } data.forEach((item, index) => { item = Object.assign({}, item, { 'parent': parent, 'level': level, 'spaceHtml': spaceHtml }) if ((typeof item.expanded) === 'undefined') { item = Object.assign({}, item, { 'expanded': false }) } if ((typeof item.show) === 'undefined') { item = Object.assign({}, item, { 'isShow': false }) } if ((typeof item.isChecked) === 'undefined') { item = Object.assign({}, item, { 'isChecked': false }) } item = Object.assign({}, item, { 'load': (item.expanded ? 1 : false) }) this.initGridData.push(item) if (item.children && item.expanded) { this.initData(item.children, level + 1, item) } }) }, // 隐藏显示 show (item) { return ((item.level === 1) || (item.parent && item.parent.expanded && item.isShow)) }, toggle (index, item) { let level = item.level + 1 let spaceHtml = '' for (let i = 1; i < level; i++) { spaceHtml += '<i class="ms-tree-space"></i>' } if (item.children) { if (item.expanded) { item.expanded = !item.expanded this.close(index, item) } else { item.expanded = !item.expanded if (item.load) { this.open(index, item) } else { item.load = true item.children.forEach((child, childIndex) => { this.initGridData.splice((index + childIndex + 1), 0, child) // 设置监听属性 this.$set(this.initGridData[index + childIndex + 1], 'parent', item) this.$set(this.initGridData[index + childIndex + 1], 'level', level) this.$set(this.initGridData[index + childIndex + 1], 'spaceHtml', spaceHtml) this.$set(this.initGridData[index + childIndex + 1], 'isShow', true) this.$set(this.initGridData[index + childIndex + 1], 'expanded', false) }) } } } this.showScrollBar() }, open (index, item) { if (item.children) { item.children.forEach((child, childIndex) => { child.isShow = true if (child.children && child.expanded) { this.open(index + childIndex + 1, child) } }) } }, close (index, item) { if (item.children) { item.children.forEach((child, childIndex) => { child.isShow = false child.expanded = false if (child.children) { this.close(index + childIndex + 1, child) } }) } }, // 点击check勾选框, 父子不相关联 handleCheckClick (data, event, index) { data.isChecked = !data.isChecked if (data.isChecked) { this.checkGroup.push(data.id) } else { for (let i = 0; i < this.checkGroup.length; i++) { if (this.checkGroup[i] === data.id) { this.checkGroup.splice(i, 1) } } } this.checkGroup = this.getArray(this.checkGroup) let itemsIds = this.getArray(this.checkGroup.concat(this.All(this.data))) if (this.checkGroup.length === itemsIds.length) { this.checks = true } else { this.checks = false } }, // checkbox 全选 选择事件 handleCheckAll () { this.checks = !this.checks if (this.checks) { this.checkGroup = this.getArray(this.checkGroup.concat(this.All(this.data))) } else { this.checkGroup = [] } // this.$emit('on-selection-change', this.checkGroup) }, // 数组去重 getArray (a) { let hash = {} let len = a.length let result = [] for (let i = 0; i < len; i++) { if (!hash[a[i]]) { hash[a[i]] = true result.push(a[i]) } } return result }, checkAllGroupChange (data) { if (this.dataLength > 0 && data.length === this.dataLength) { this.checks = true } else { this.checks = false } this.$emit('on-selection-change', this.checkGroup) }, All (data) { let arr = [] data.forEach((item) => { arr.push(item.id) if (item.children && item.children.length > 0) { arr = arr.concat(this.All(item.children)) } }) return arr }, // 返回树形数据长度 Length (data) { let length = data.length data.forEach((child) => { if (child.children) { length += this.Length(child.children) } }) return length }, // 返回表头 renderHeader (column, $index) { if ('renderHeader' in this.columns[$index]) { return this.columns[$index].renderHeader(column, $index) } else { return column.title || '#' } }, // 返回内容 renderBody (row, column, index) { return row[column.key] }, // 默认选中 renderCheck (data) { let arr = [] data.forEach((item) => { if (item._checked) { arr.push(item.id) } if (item.children && item.children.length > 0) { arr = arr.concat(this.renderCheck(item.children)) } }) return arr }, // 深度拷贝函数 deepCopy (data) { let t = this.type(data) let o let i let ni if (t === 'array') { o = [] } else if (t === 'object') { o = {} } else { return data } if (t === 'array') { for (i = 0, ni = data.length; i < ni; i++) { o.push(this.deepCopy(data[i])) } return o } else if (t === 'object') { for (i in data) { o[i] = this.deepCopy(data[i]) } return o } }, type (obj) { let toString = Object.prototype.toString let map = { '[object Boolean]': 'boolean', '[object Number]': 'number', '[object String]': 'string', '[object Function]': 'function', '[object Array]': 'array', '[object Date]': 'date', '[object RegExp]': 'regExp', '[object Undefined]': 'undefined', '[object Null]': 'null', '[object Object]': 'object' } return map[toString.call(obj)] }, getScrollBarSize (fresh) { if (this.$isServer) return 0 if (fresh || cached === undefined) { const inner = document.createElement('div') inner.style.width = '100%' inner.style.height = '200px' const outer = document.createElement('div') const outerStyle = outer.style outerStyle.position = 'absolute' outerStyle.top = 0 outerStyle.left = 0 outerStyle.pointerEvents = 'none' outerStyle.visibility = 'hidden' outerStyle.width = '200px' outerStyle.height = '150px' outerStyle.overflow = 'hidden' outer.appendChild(inner) document.body.appendChild(outer) const widthContained = inner.offsetWidth outer.style.overflow = 'scroll' let widthScroll = inner.offsetWidth if (widthContained === widthScroll) { widthScroll = outer.clientWidth } document.body.removeChild(outer) cached = widthContained - widthScroll } return cached } }, beforeDestroy () { window.onresize = null } } </script> <style lang="less"> .tree-grid { @keyframes opacityChild{ 0% { opacity: 0; } 50% { opacity: .5; } 100% { opacity: 1; } } 100%; color: #1f2d3d; color: #495167; &.autoTable { overflow: auto; } .tree-grid-body { overflow: auto; } table { 100%; border-spacing: 0; border-collapse: collapse; &.hl-tree-table { &>tbody{ &>tr { height: 50px; background-color: #fff; border-bottom: 1px solid #e8eaec; &:hover { background-color: #ebf7ff; } } &>.child-tr { background-color: #fff; } } th>label { display: inline-block; margin: 0 12px; } } .ivu-icon { font-size: 18px; } .tree-grid-arrow { cursor: pointer; 14px; text-align: center; display: inline-block; i { position: relative; top: -1px; transition: all .2s ease-in-out; font-size:14px; vertical-align:middle; } &-open { i { transform:rotate(90deg); } } } } .table>tbody>tr>td, .table>tbody>tr>th, .table>thead>tr>td, .table>thead>tr>th { vertical-align: middle; box-sizing: border-box; &:last-child { border-right: 0; } } // .table>tbody>tr>td, // .table>tbody>tr>th { // border-right: 1px solid #ccc; // } .table>thead>tr>th { text-align: left; } .table-bordered>thead>tr>td, .table-bordered>thead>tr>th { height: 32px; padding: 0; vertical-align: middle; background: #E1E4E5; border-right: 1px solid #fff; .tree-grid-cell { padding: 0 12px; } } .tree-grid-cell { padding: 0 12px; font-size: 12px; } .ms-tree-space { position: relative; top: 1px; display: inline-block; font-style: normal; font-weight: 400; line-height: 1em; 14px; height: 14px; } .ms-tree-space::before { content: ""; } .selection-wrapper { position: relative; text-align: center; 18px; height: 18px; margin: 0 auto; vertical-align: middle; .selection-checkbox { position: absolute; left: 0; top: 0; z-index: 2; 18px; height: 18px; vertical-align: middle; opacity: 0; cursor: pointer; } .ivu-checkbox-wrapper { position: absolute; left: 0; top: 0; z-index: 1; margin: 0; line-height: 15px; } } } </style>