likes
comments
collection
share

Vue el-table封装,支持多级表头,自动高度

作者站长头像
站长
· 阅读数 11

实现多级表头

废话不多说,正式开始,首先既然要实现多级表头那么就有两种实现方式,要么是递归子组件,要么使用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默认开启了stripeborder属性,头样式和行样式有一些默认值,这些可以根据自己的项目变换,除了这三个外所有的属性与官网一致。

下面是封装的el-table的props,其他传参与el-table官方一致

props含义类型默认值
columns表格头列表Array[]
data表格数据Array[]
autoHeight是否开启自动高度Booleantrue

  <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()
      })

使用举例

Vue el-table封装,支持多级表头,自动高度

<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>
    )
  }
}

Vue el-table封装,支持多级表头,自动高度 多级表头写法:通过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: '性别'
        }

      ]

Vue el-table封装,支持多级表头,自动高度

下面是完整的代码:

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)
  }
}

好了,本文就到这里了,收工