vue:el-table合并单元格,多列合并。封装mergeRowByProp方法,灵活的合并【附源码】

2,220 阅读5分钟

一、合并第一列

<template>
  <el-table :data="tableData" border>
    <el-table-column prop="type" label="种类" />
    <el-table-column prop="code" label="产品编号">
      <template v-slot="scope">
        {{ scope.row.code }}
      </template>
    </el-table-column>
    <el-table-column prop="name" label="产品名称" />
    <el-table-column prop="price" label="价格(元)" />
  </el-table>
</template>

      tableData: [
        {
          type: '水果',
          code: 'apple',
          name: '苹果',
          price: '54'
        },
        {
          type: '水果',
          code: 'banana',
          name: '香蕉',
          price: '74'
        },
        {
          type: '水果',
          code: 'banana',
          name: '香蕉',
          price: '40'
        },
        {
          type: '水果',
          code: 'banana',
          name: '香蕉',
          price: '70'
        },
        {
          type: '水果',
          code: 'orange',
          name: '橙子',
          price: '44'
        },
        {
          type: '水果',
          code: 'orange',
          name: '橙子',
          price: '95'
        },
        {
          type: '水果',
          code: 'orange',
          name: '橙子',
          price: '23'
        },
        {
          type: '水果',
          code: 'pear',
          name: '梨子',
          price: '64'
        },
        {
          type: '零食',
          code: 'chocolate',
          name: '巧克力',
          price: '25'
        },
        {
          type: '零食',
          code: 'pear',
          name: '梨子',
          price: '25'
        }
      ],

image.png

需要将上面这个表格合并单元格,变成下面这样

image.png

el-table添加属性:span-method="spanMethod"

  created() {
    this.spanArrOne = this.getSpanArr(this.tableData, 'type')
  },
  methods: {
    getSpanArr(data, params) {
      let arr = [] // 接收重构数组
      let spanArr = [] // 控制合并的数组
      let pos = 0 // 设置索引
      // 排序
      const list = this.groupBy(data, params)
      list.map(v => (arr = arr.concat(v)))
      arr.map(res => {
        data.shift()
        data.push(res)
      })
      const redata = arr.map(v => v[params])
      redata.reduce((old, cur, i) => {
        if (cur === old) {
          spanArr[pos] += 1
          spanArr.push(0)
        } else {
          spanArr.push(1)
          pos = i
        }
        return cur
      }, {})

      return spanArr
    },
    groupBy(data, params) {
      // 根据某个字段进行排序 输出二维数组
      const groups = {}
      data.forEach(row => {
        const group = row[params]
        groups[group] = groups[group] || []
        groups[group].push(row)
      })
      return Object.values(groups)
    },
    spanMethod({ row, column, rowIndex, columnIndex }) {
      if (columnIndex === 0) {
        const _row = this.spanArrOne[rowIndex]
        const _col = _row > 0 ? 1 : 0
        return { rowspan: _row, colspan: _col }
      }
    }
  }

二、合并第二列

  created() {
    this.spanArrTwo = this.getSpanArr(this.tableData, 'code')
  },
  
  
    spanMethod({ row, column, rowIndex, columnIndex }) {
      if (columnIndex === 0) {
        const _row = this.spanArrOne[rowIndex]
        const _col = _row > 0 ? 1 : 0
        return { rowspan: _row, colspan: _col }
      } else if (columnIndex === 1) {
        const _row = this.spanArrTwo[rowIndex]
        const _col = _row > 0 ? 1 : 0
        return { rowspan: _row, colspan: _col }
      }
    }

image.png

但是这里有问题,第二列的pear分别属于水果和零食种类下的,它俩不应该合并到一起

这是由于code不唯一导致的,为了达到目的,改造数据

    for (const item of this.tableData) {
      item.code = `${item.type}****${item.code}`
    }

image.png

现在第二列的合并也正确了,稍微改下DOM结构

      <template slot-scope="scope">
        <!-- {{ scope.row.code }} -->
        {{ scope.row.code.split('****')[1] }}
      </template>
    </el-table-column>

最终效果:

image.png

完整代码:

<template>
  <el-table :data="tableData" :span-method="spanMethod" border>
    <el-table-column prop="type" label="种类" />
    <el-table-column prop="code" label="产品编号">
      <template slot-scope="scope">
        <!-- {{ scope.row.code }} -->
        {{ scope.row.code.split('****')[1] }}
      </template>
    </el-table-column>
    <el-table-column prop="name" label="产品名称" />
    <el-table-column prop="price" label="价格(元)" />
  </el-table>
</template>

<script>
export default {
  data() {
    return {
      tableData: [
        {
          type: '水果',
          code: 'apple',
          name: '苹果',
          price: '54'
        },
        {
          type: '水果',
          code: 'banana',
          name: '香蕉',
          price: '74'
        },
        {
          type: '水果',
          code: 'banana',
          name: '香蕉',
          price: '40'
        },
        {
          type: '水果',
          code: 'banana',
          name: '香蕉',
          price: '70'
        },
        {
          type: '水果',
          code: 'orange',
          name: '橙子',
          price: '44'
        },
        {
          type: '水果',
          code: 'orange',
          name: '橙子',
          price: '95'
        },
        {
          type: '水果',
          code: 'orange',
          name: '橙子',
          price: '23'
        },
        {
          type: '水果',
          code: 'pear',
          name: '梨子',
          price: '64'
        },
        {
          type: '零食',
          code: 'chocolate',
          name: '巧克力',
          price: '25'
        },
        {
          type: '零食',
          code: 'pear',
          name: '梨子',
          price: '25'
        }
      ],
      spanArrOne: [],
      spanArrTwo: []
    }
  },
  created() {
    for (const item of this.tableData) {
      item.code = `${item.type}****${item.code}`
    }
    this.spanArrOne = this.getSpanArr(this.tableData, 'type')
    this.spanArrTwo = this.getSpanArr(this.tableData, 'code')
  },
  methods: {
    getSpanArr(data, params) {
      let arr = [] // 接收重构数组
      let spanArr = [] // 控制合并的数组
      let pos = 0 // 设置索引
      // 排序
      const list = this.groupBy(data, params)
      list.map(v => (arr = arr.concat(v)))
      arr.map(res => {
        data.shift()
        data.push(res)
      })
      const redata = arr.map(v => v[params])
      redata.reduce((old, cur, i) => {
        if (cur === old) {
          spanArr[pos] += 1
          spanArr.push(0)
        } else {
          spanArr.push(1)
          pos = i
        }
        return cur
      }, {})

      return spanArr
    },
    groupBy(data, params) {
      // 根据某个字段进行排序 输出二维数组
      const groups = {}
      data.forEach(row => {
        const group = row[params]
        groups[group] = groups[group] || []
        groups[group].push(row)
      })
      return Object.values(groups)
    },
    spanMethod({ row, column, rowIndex, columnIndex }) {
      if (columnIndex === 0) {
        const _row = this.spanArrOne[rowIndex]
        const _col = _row > 0 ? 1 : 0
        return { rowspan: _row, colspan: _col }
      } else if (columnIndex === 1) {
        const _row = this.spanArrTwo[rowIndex]
        const _col = _row > 0 ? 1 : 0
        return { rowspan: _row, colspan: _col }
      }
    }
  }
}
</script>

三、代码优化

合并单元格的这个代码,要是有第三列要合并,不得再加个spanArrThree数组???

而且其他同事需要合并的时候,这一坨方法又要重新写一遍,麻烦...

于是就封装了一下

1、src/utils/kits.js

// 返回合并的数据,如[2, 0, 2, 0]
const getSpanArr = (tableData, prop) => {
  const spanArr = [] // 控制合并的数组
  let index = 0 // 设置索引
  const values = tableData.map(v => v[prop])
  // 将 ['1001', '1001', '1002', '1002'] 转换为 [2, 0, 2, 0]
  values.reduce((old, cur, i) => {
    if (cur === old) {
      spanArr[index] += 1
      spanArr.push(0)
    } else {
      spanArr.push(1)
      index = i
    }
    return cur
  }, '')
  return spanArr
}
/*
  根据prop属性,合并内容相同的行
  
  入参:
    1. el-table span-method属性自带的{ row, column, rowIndex, columnIndex }
    2. 数据源
    3. 需要合并的列
*/
const mergeRowByProp = (
  { row, column, rowIndex, columnIndex },
  tableData,
  mergeList
) => {
  for (const { column, prop } of mergeList) {
    if (columnIndex === column) {
      const spanArr = getSpanArr(tableData, prop)
      const rowspan = spanArr[rowIndex]
      const colspan = rowspan > 0 ? 1 : 0
      return { rowspan, colspan }
    }
  }
}

export { mergeRowByProp }

2、使用

span-method属性传入mergeRowByProp方法

:span-method="(...rest) => mergeRowByProp(...rest, tableData, mergeList)"

需要在data中定义mergeList

      mergeList: [
        { column: 0, prop: 'type' },
        // { column: 1, prop: 'code' },
        // { column: 2, prop: 'name' }
        // { column: 3, prop: 'price' },
      ]

3、效果

合并第一列

      mergeList: [
        { column: 0, prop: 'type' },
        // { column: 1, prop: 'code' },
        // { column: 2, prop: 'name' }
        // { column: 3, prop: 'price' },
      ]
image.png

合并第一列和第二列

      mergeList: [
        { column: 0, prop: 'type' },
        { column: 1, prop: 'code' },
        // { column: 2, prop: 'name' }
        // { column: 3, prop: 'price' },
      ]
image.png

合并一二三列

      mergeList: [
        { column: 0, prop: 'type' },
        { column: 1, prop: 'code' },
        { column: 2, prop: 'name' }
        // { column: 3, prop: 'price' },
      ]
image.png

如果希望第二列基于第一列进行合并,需要在获取到tableData后,对第二列的数据进行改造

  created() {
    for (const item of this.tableData) {
      item.code = `${item.type}****${item.code}`
    }
  }

image.png

4、完整代码

<template>
  <el-table
    :data="tableData"
    :span-method="(...rest) => mergeRowByProp(...rest, tableData, mergeList)"
    border
  >
    <el-table-column prop="type" label="种类" />
    <el-table-column prop="code" label="产品编号">
      <template slot-scope="scope">
        {{ scope.row.code }}
        <!-- {{ scope.row.code.split('****')[1] }} -->
      </template>
    </el-table-column>
    <el-table-column prop="name" label="产品名称" />
    <el-table-column prop="price" label="价格(元)" />
  </el-table>
</template>

<script>
import { mergeRowByProp } from './kits'
export default {
  name: 'MergeTable',
  data() {
    return {
      mergeRowByProp,
      tableData: [
        { type: '水果', code: 'apple', name: '苹果', price: '10' },
        { type: '水果', code: 'banana', name: '苹果', price: '10' },
        { type: '水果', code: 'banana', name: '香蕉', price: '20' },
        { type: '水果', code: 'banana', name: '香蕉', price: '20' },
        { type: '水果', code: 'orange', name: '橙子', price: '30' },
        { type: '水果', code: 'orange', name: '橙子', price: '30' },
        { type: '水果', code: 'orange', name: '橙子', price: '40' },
        { type: '水果', code: 'pear', name: '梨子', price: '40' },
        { type: '零食', code: 'pear', name: '梨子', price: '50' },
        { type: '零食', code: 'chocolate', name: '巧克力', price: '50' }
      ],
      mergeList: [
        { column: 0, prop: 'type' },
        { column: 1, prop: 'code' },
        { column: 2, prop: 'name' }
        // { column: 3, prop: 'price' },
      ]
    }
  },
  created() {
    for (const item of this.tableData) {
      item.code = `${item.type}****${item.code}`
    }
  },
  methods: {}
}
</script>

5、优化

如果希望第二列基于第一列进行合并,第三列基于第二列进行合并...依此类推,如果有很多列都需要合并,或者是很多个表格都需要这样处理,那么每个表格的数据都需要对tableData进行处理,并且在模板中使用split进行切割,未免有些太麻烦,所以将【item.code = ${item.type}****${item.code}】这种操作放到封装的方法中进行处理

src/utils/kits.js改造代码

const separator = '****' // 分隔符
const mergeRowByProp = ({ row, column, rowIndex, columnIndex }, tableData, mergeList) => {
  // 如果合并的列数大于1,需要对数据进行改造,使后面的列依赖于前面的列
  if (mergeList.length > 1) {
    tableData = JSON.parse(JSON.stringify(tableData))
    tableData.forEach(item => {
      mergeList.forEach((m, i) => {
        const { prop } = m
        if (i) {
          // 当前列的内容拼接上前一列的内容,使用分隔符隔开
          // 分隔符的作用只是为了打印数据时看的方便点,加不加都行
          item[prop] = `${item[mergeList[i - 1].prop]}${separator}${item[mergeList[i].prop]}`
        }
      })
    })
  }
  for (const { column, prop } of mergeList) {
    if (columnIndex === column) {
      const spanArr = getSpanArr(tableData, prop)
      const rowspan = spanArr[rowIndex]
      const colspan = rowspan > 0 ? 1 : 0
      return { rowspan, colspan }
    }
  }
}

在进行多列合并时,方法内部会帮我们处理好依赖关系~

四、mergeRowByProp 源码

www.npmjs.com/package/xtt…

/xtt-tools/elementUI/el-table.js

妈妈再也不用担心我合并单元格了...