Vue el-table封装,支持多级表头,自动高度
引言:el-table的封装想必大家也见过很多了,由于最近要给vue2的老项目后台管理新增一个页面,之前完全不是我写的,所有的代码都没有封装,写起来极其痛苦。说到后台管理系统表格肯定是重头戏,由于手头没有现成的封装好的vue2的el-table组件,于是打开掘金想找找各位大佬封装好的表格,大概逛了一下发现还是少些东西,要么就是普通的简单封装,多级表头没法使用,要么支持多级表头但是又没有自动高度,思来想去决定结合之前大佬封装的代码将功能合并一下,这就有了这篇水文。
实现多级表头
废话不多说,正式开始,首先既然要实现多级表头那么就有两种实现方式,要么是递归子组件,要么使用render渲染,这里我就用递归子组件的方式去实现。通过判断是否存在 children
字段来递归遍历el-table-column
<el-table-column
v-if="item.children && item.children.length"
:label="item.label"
:key="item.prop"
>
<table-column :columns="item.children || []">
<template
v-for="(index, name) in $slots"
:slot="name"
>
<slot :name="name" />
</template>
<template
v-for="(index, name) in $scopedSlots"
#[name]="data"
>
<slot
:name="name"
v-bind="data"
/>
</template>
</table-column>
</el-table-column>
作用域插槽通过下面的方式进行参数传递。
<template
v-for="(index, name) in $scopedSlots"
#[name]="data"
>
<slot
:name="name"
v-bind="data"
/>
</template>
props传参
既然是封装el-table,那么我的想法是尽可能的保留官方的参数,减少使用的心智负担,下面是无孩子时候渲染的el-table-column
,默认加入了文字过长显示tooltip选项,同时要对指定列使用插槽则是根据prop的字段,prop叫什么插槽名称就叫什么:name="item.prop"
,插入该列的头就是在前面加入header即可。header-${item.prop}
,同时加入了render渲染,可以在选项中直接写render函数进行渲染。
<el-table-column
v-else
:key="`${item.prop}-else`"
v-bind="{
'show-overflow-tooltip': true,
...item,
}"
>
<template #header="scope">
<BaseRender
v-if="item.headerRender"
:render="item.headerRender"
:item="item"
:data="scope.row"
:scope="scope"
/>
<span v-else-if="$scopedSlots[`header-${item.prop}`]">
<slot
:name="`header-${item.prop}`"
v-bind="scope"
/>
</span>
<span v-else>{{ scope.column.label }}</span>
</template>
<template slot-scope="scope">
<BaseRender
v-if="item.render"
:render="item.render"
:item="item"
:data="scope.row"
:scope="scope"
/>
<span v-else-if="$scopedSlots[item.prop]">
<slot
:name="item.prop"
v-bind="scope"
/>
</span>
<span v-else>{{ scope.row[item.prop] }}</span>
</template>
</el-table-column>
el-table默认开启了stripe
和border
属性,头样式和行样式有一些默认值,这些可以根据自己的项目变换,除了这三个外所有的属性与官网一致。
下面是封装的el-table的props,其他传参与el-table官方一致
props | 含义 | 类型 | 默认值 |
---|---|---|---|
columns | 表格头列表 | Array | [] |
data | 表格数据 | Array | [] |
autoHeight | 是否开启自动高度 | Boolean | true |
<el-table
class="base-table"
ref="baseTableRef"
:data="data"
:header-cell-style="headerCellStyle"
:cell-style="cellStyle"
v-bind="{
height: autoHeight ? tableHeight : '300px',
stripe: true,
border: true,
...$attrs,
}"
v-on="$listeners"
>
要开启index和selection列只需要在columns中配置这两项
{
type: 'index'
},
{
type: 'selection'
},
实现自动高度
自动高度的实现主要是通过观测高度的变化对高度进行准确赋值。
if (!this.autoHeight) return
const resizeObserver = new ResizeObserver((entries) => {
const { height } = entries.shift().contentRect
this.tableHeight = height
})
resizeObserver.observe(this.$el.parentElement)
this.$once('hook:beforeDestroy', () => {
resizeObserver.disconnect()
})
使用举例
<template>
<div class="base-table">
<BaseTable
:columns="columns"
:data="tableData"
/>
</div>
</template>
<script>
import BaseTable from '@/components/BaseTable/index.vue'
export default {
components: {
BaseTable
},
data () {
return {
columns: [
{
type: 'index'
},
{
type: 'selection'
},
{
prop: 'name',
label: '姓名'
},
{
prop: 'age',
label: '年龄'
},
{
prop: 'sex',
label: '性别'
}
],
tableData: []
}
},
mounted () {
this.initData()
},
methods: {
initData () {
this.tableData = this.generateData()
},
generateData (length = 60) {
return Array.from({ length }).map((it, idx) => {
return {
name: 'name' + idx,
sex: 'test2',
age: idx
}
})
}
}
}
</script>
对名称为name的列使用插槽:内容和头
<BaseTable
:columns="columns"
:data="tableData"
>
<template #header-name>
name的头
</template>
<template #name>
name插槽test
</template>
</BaseTable>
或者使用render的方式
{
prop: 'name',
label: '姓名',
headerRender:(h,{ data }) => {
return (
<span>{data.name + '头'}</span>
)
},
render:(h,{ data }) => {
return (
<span>{data.name}</span>
)
}
}
多级表头写法:通过children字段,数组里面的对象跟columns的格式一样
要对姓名1使用插槽也只需要跟上面一样即可
columns: [
{
type: 'index'
},
{
type: 'selection'
},
{
prop: 'name1',
label: '姓名',
children: [
{
prop: 'name',
label: '姓名1'
},
{
prop: 'name2',
label: '姓名2'
},
{
prop: 'name3',
label: '姓名3'
}
]
},
{
prop: 'age',
label: '年龄'
},
{
prop: 'sex',
label: '性别'
}
]
下面是完整的代码:
Vue2版本
index.vue
<template>
<el-table
class="base-table"
ref="baseTableRef"
:data="data"
:header-cell-style="headerCellStyle"
:cell-style="cellStyle"
v-bind="{
height: autoHeight ? tableHeight : '300px',
stripe: true,
border: true,
...$attrs,
}"
v-on="$listeners"
>
<template slot="append">
<slot name="append" />
</template>
<template v-for="item in columns">
<!-- selection和index的列渲染 -->
<el-table-column
v-if="['selection', 'index'].includes(item.type)"
:key="`${item.type}`"
v-bind="item"
>
<template #header="scope">
<slot
:name="`header-${item.type}`"
v-bind="scope"
/>
</template>
</el-table-column>
<TableColumn
v-else-if="item.children && item.children.length"
:columns="item.children"
:label="item.label"
:key="item.prop"
>
<template
v-for="(index, name) in $slots"
:slot="name"
>
<slot :name="name" />
</template>
<template
v-for="(index, name) in $scopedSlots"
#[name]="data"
>
<slot
:name="name"
v-bind="data"
/>
</template>
</TableColumn>
<el-table-column
v-else
:key="`${item.prop}-else`"
v-bind="{
'show-overflow-tooltip': true,
...item,
}"
>
<template #header="scope">
<BaseRender
v-if="item.headerRender"
:render="item.headerRender"
:item="item"
:data="scope.row"
:scope="scope"
/>
<span v-else-if="$scopedSlots[`header-${item.prop}`]">
<slot
:name="`header-${item.prop}`"
v-bind="scope"
/>
</span>
<span v-else>{{ scope.column.label }}</span>
</template>
<template slot-scope="scope">
<BaseRender
v-if="item.render"
:render="item.render"
:item="item"
:data="scope.row"
:scope="scope"
/>
<span v-else-if="$scopedSlots[item.prop]">
<slot
:name="item.prop"
v-bind="scope"
/>
</span>
<span v-else>{{ scope.row[item.prop] }}</span>
</template>
</el-table-column>
</template>
</el-table>
</template>
<script>
import TableColumn from './table-column.vue'
import BaseRender from './BaseRender.js'
export default {
name: 'BaseTablePro',
components: {
TableColumn,
BaseRender
},
props: {
columns: {
type: Array,
default: () => []
},
data: {
type: Array,
default: () => []
},
headerCellStyle: {
type: Function || Object,
default: () => {
return {
'background-color': '#f5f6f7',
'text-align': 'center'
}
}
},
cellStyle: {
type: Function || Object,
default: () => {
return {
'text-align': 'center'
}
}
},
autoHeight: {
type: Boolean,
default: true
}
},
data () {
return {
tableHeight: null
}
},
mounted () {
this.initTableHeight()
},
methods: {
initTableHeight () {
if (!this.autoHeight) return
const resizeObserver = new ResizeObserver((entries) => {
const { height } = entries.shift().contentRect
this.tableHeight = height
})
resizeObserver.observe(this.$el.parentElement)
this.$once('hook:beforeDestroy', () => {
resizeObserver.disconnect()
})
}
}
}
</script>
<style lang="scss" scoped></style>
table-column.vue
<template>
<el-table-column
v-bind="$attrs"
v-on="$listeners"
>
<template v-for="item in columns">
<el-table-column
v-if="item.children && item.children.length"
:label="item.label"
:key="item.prop"
>
<table-column :columns="item.children || []">
<template
v-for="(index, name) in $slots"
:slot="name"
>
<slot :name="name" />
</template>
<template
v-for="(index, name) in $scopedSlots"
#[name]="data"
>
<slot
:name="name"
v-bind="data"
/>
</template>
</table-column>
</el-table-column>
<el-table-column
v-else
:key="`${item.prop}-else`"
v-bind="{
'show-overflow-tooltip': true,
...item,
}"
>
<template #header="scope">
<BaseRender
v-if="item.headerRender"
:render="item.headerRender"
:item="item"
:data="scope.row"
:scope="scope"
/>
<span v-else-if="$scopedSlots[`header-${item.prop}`]">
<slot
:name="`header-${item.prop}`"
v-bind="scope"
/>
</span>
<span v-else>{{ scope.column.label }}</span>
</template>
<template slot-scope="scope">
<BaseRender
v-if="item.render"
:render="item.render"
:item="item"
:data="scope.row"
:scope="scope"
/>
<span v-else-if="$scopedSlots[item.prop]">
<slot
:name="item.prop"
v-bind="scope"
/>
</span>
<span v-else>{{ scope.row[item.prop] }}</span>
</template>
</el-table-column>
</template>
</el-table-column>
</template>
<script>
import BaseRender from './BaseRender.js'
export default {
name: 'TableColumn',
components: {
BaseRender
},
props: {
columns: {
type: Array,
default: () => []
}
}
}
</script>
<style scoped lang="scss"></style>
BaseRender.js
export default {
functional: true,
render (h, context) {
return context.props.render(h, context.props)
}
}
Vue3版本
index.vue
<template>
<el-table
ref="tableRef"
:header-cell-style="{ background: '#fafafa' }"
stripe
border
height="100%"
highlight-current-row
v-bind="$attrs"
>
<TableColumn
v-for="item in columns"
:key="item.prop || item.label"
:col="item"
>
<template
v-for="slot in Object.keys(customSlots)"
#[slot]="scope"
>
<slot
:name="slot"
v-bind="scope"
/>
</template>
</TableColumn>
</el-table>
</template>
<script setup>
import { getCurrentInstance, reactive, ref, unref } from 'vue'
import TableColumn from './TableColumn'
defineProps({
columns: {
type: Array,
required: true
}
})
const { proxy } = getCurrentInstance()
const customSlots = reactive({
...proxy.$slots
})
const tableRef = ref(null)
// 单选
const setSingleSelect = row => {
unref(tableRef).setCurrentRow(row)
}
defineExpose({
tableRef,
setSingleSelect
})
</script>
<style lang="scss" scoped>
:deep() {
.el-table__header th,
.el-table__body td {
text-align: center;
}
}
</style>
TableColumn.vue
<template>
<el-table-column
v-if="col.type==='selection'"
type="selection"
align="center"
width="60"
v-bind="col"
/>
<el-table-column
v-else-if="col.type==='index'"
type="index"
label="序号"
align="center"
width="80"
v-bind="col"
/>
<el-table-column
v-else-if="!col.children"
:label="col.label"
:prop="col.prop || ''"
v-bind="col"
>
<!-- 自定义 header -->
<template
#header
v-if="col.header"
>
<component
:is="col.header"
:row="col"
/>
</template>
<template #default="scope">
<component
v-if="col.render"
:is="col.render"
:row="scope.row"
/>
<slot
v-else
:name="col.slotName"
:row="scope.row"
>
<span>
{{ scope.row[col.prop] }}
</span>
</slot>
</template>
</el-table-column>
<el-table-column
v-else
:label="col.label"
>
<TableColumn
v-for="t in col.children"
:key="t.prop || t.label"
:col="t"
>
<template
v-for="slot in Object.keys(customSlots)"
#[slot]="scope"
>
<slot
:name="slot"
v-bind="scope"
/>
</template>
</TableColumn>
</el-table-column>
</template>
<script setup>
import { getCurrentInstance, reactive } from 'vue'
defineProps({
col: {
type: Object,
default: () => {}
}
})
const { proxy } = getCurrentInstance()
const customSlots = reactive({
...proxy.$slots
})
</script>
<script>
export default {
name: 'TableColumn'
}
</script>
<style lang="scss" scoped>
</style>
使用举例:传入columns
数组
columns: [
{
type: 'selection'
},
{
type: 'index'
},
{
prop: 'name',
label: '用户名'
},
{
prop: 'realname',
label: '真实姓名',
render: (scope) => {
return (
<ElButton
type="primary"
onClick={() => {
console.log(scope)
ElMessage.success('我是自定义内容')
}}
>
{scope.row.realname}
</ElButton>
)
}
},
{
prop: 'cellphone',
label: '手机号码'
},
{
prop: 'enable',
label: '状态',
slotName: 'status'
},
{
prop: 'createAt',
label: '创建时间',
slotName: 'createAt',
minWidth: '120'
},
{
prop: 'more',
minWidth: '130',
header: (scope) => {
return (
<ElButton>自定义表头</ElButton>
)
}
},
{
prop: 'updateAt',
label: '更新时间',
slotName: 'updateAt',
'show-overflow-tooltip': true
},
{
label: '多级表头',
children: [
{
prop: 'test1',
label: 'State',
width: '120'
}, {
prop: 'test2',
label: 'City',
width: '120'
}, {
prop: 'test3',
label: 'City2',
width: '120'
}
]
},
{
label: '操作',
slotName: 'handler',
fixed: 'right'
}
]
转载自:https://juejin.cn/post/7218916720323985463