likes
comments
collection
share

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

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

引言: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默认开启了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)
  }
}

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
评论
请登录