vue 表格树 固定表头

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

回到顶部