vue 表格树 固定表头
参考网上黄龙的表格树进行完善,并添加固定表头等的功能,目前是在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="{width: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; }
}
width: 100%;
color: #1f2d3d;
color: #495167;
&.autoTable {
overflow: auto;
}
.tree-grid-body {
overflow: auto;
}
table {
width: 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;
width: 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;
width: 14px;
height: 14px;
}
.ms-tree-space::before {
content: "";
}
.selection-wrapper {
position: relative;
text-align: center;
width: 18px;
height: 18px;
margin: 0 auto;
vertical-align: middle;
.selection-checkbox {
position: absolute;
left: 0;
top: 0;
z-index: 2;
width: 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>
以上是 vue 表格树 固定表头 的全部内容, 来源链接: utcz.com/z/380097.html