Skip to content

elementUI多级表头、合并单元格及导出

功能预览

代码实现

多级表头

el-table-column 标签内放入同样的 el-table-column 即可实现多级表头;样式同功能预览所示。

html
<el-table-column label="表头3" prop="data3" align="center">
    <el-table-column label="表头3-1" prop="data3_1"></el-table-column>
    <el-table-column label="表头3-2" prop="data3_2"></el-table-column>
    <el-table-column label="表头3-3" prop="data3_3"></el-table-column>
</el-table-column>

合并单元格

el-table 设置 :span-method="objectSpanMethod" 自定义合并单元格计算函数。计算方式与原生table合并单元格原理相同,设置特殊单元格的rowspancolspan,被占用不显示的单元格需要置为0。

html
<el-table :data="tableData" :span-method="objectSpanMethod" >
    <el-table-column label="表头1" prop="data5"><el-table-column>
    <!-- ......  -->
    <el-table-column label="表头6" prop="data5"><el-table-column>
</el-table>
js
const objectSpanMethod = ({ row, column, rowIndex, columnIndex }) => {
  // 第二行 第四列,占据两行三列
  if (rowIndex == 1 && columnIndex == 3) {
    return {
      rowspan: 2,
      colspan: 3,
    };
  } else if ([1, 2].includes(rowIndex) && [3, 4, 5].includes(columnIndex)) {
    return {
      rowspan: 0,
      colspan: 0,
    };
  }
};

尾行合计

尾行合计可以通过给表格数据追加一行数据方式实现;也可以使用官方提供的给 el-table 设置 show-summary ,自动计算出所有列的合计数据。如需自定义计算方式,可设置 :summary-method="summaryMethod" ,返回值为合计列逐项组成的数组。

js
// 以下计算函数效果为: "合计" 占据1-3列,合并单元格
const summaryMethod = ({ columns, data }) => {
  const sums = []
  columns.forEach((column, index) => {
    if (index == 0) {
      sums[0] = '合计'
    } else {
      sums.push(index)
    }
  })
  // 网上查询的在column设置colSpan方式会同时影响表头,因此使用操作dom的方式处理
  const tds = document.querySelectorAll('.el-table__footer tbody tr td')
  if (tds.length) {
    tds[0].colSpan = 3
    for (let i = 1; i < 3; i++) {
      tds[i].style.display = 'none'
    }
  }
  return sums
}

勾选项&导出为excel

添加一列 el-table-column 并设置type="selection" 显示为多选框;设置 reserve-selection可实现数据刷新勾选项不消失的功能,应用场景如"分页",同时el-table 需设置 row-key="id",id可以替换成其他变量,能保证是每条table数据的唯一值即可。@selection-change="onSelect"在勾选时触发,参数为当前勾选的数据行数组。

html
<el-table ref="tableRef" :data="tableData" @selection-change="onSelect" row-key="id" >
    <el-table-column type="selection" reserve-selection />
</el-table>
js
const tableData = ref([]);
const tableRef = ref(null);
const getTableData = async (flag = "") => {
  if (flag != "changePage") {
    // 清空勾选项
    tableRef.value?.clearSelection();
  }
  tableData.value = (await mergeTableData()).value;
};
getTableData();

const multipleSelection = ref([]);
const onSelect = (val) => {
  multipleSelection.value = val;
};

导出excel方法封装成一个工具函数,具体实现可查阅完整代码。函数第一个参数为文件名,第二个参数为一个由 { 表头名称: 数据列变量名 } 组成的键值对数组;多级表头使用 父级表头.子级表头 的格式拼接。第三个参数是表格数据,对象数组格式。

需要注意的是,数据行的合并单元格无法实现导出,只能处理多级表头的合并单元格。

js
const exportExcel = () => {
  const keyMaps = [
    {
      [" 表头1"]: "data1",
    },
    {
      ["表头2"]: "data2",
    },
    {
      ["表头3.表头3-1"]: "data3_1",
    },
    {
      ["表头3.表头3-2"]: "data3_2",
    },
    {
      ["表头3.表头3-3"]: "data3_3",
    },
  ];
  exportXlsx("导出表", keyMaps, multipleSelection.value);
};

完整代码

vue
<template>
  <el-button
    link
    type="primary"
    @click="exportExcel"
    :disabled="!multipleSelection.length"
  >
    勾选后点击导出
  </el-button>
  <el-table
    ref="tableRef"
    :data="tableData"
    border
    stripe
    @selection-change="onSelect"
    row-key="id"
    show-summary
    :span-method="objectSpanMethod"
  >
    <el-table-column
      type="selection"
      reserve-selection
      width="40"
      fixed="left"
    />
    <el-table-column label="序号" type="index" width="55" fixed="left">
      <template #default="scope">
        {{ scope.$index >= 9 ? scope.$index + 1 : "0" + (scope.$index + 1) }}
      </template>
    </el-table-column>
    <el-table-column
      label="表头1"
      prop="data1"
      width="85px"
      sortable
    ></el-table-column>
    <el-table-column
      label="表头2"
      prop="data2"
      show-overflow-tooltip
    ></el-table-column>
    <el-table-column label="表头5" prop="data5"></el-table-column>
    <el-table-column label="表头6" prop="data6"></el-table-column>
    <el-table-column label="表头3" prop="data3" align="center">
      <el-table-column label="表头3-1" prop="data3_1"></el-table-column>
      <el-table-column label="表头3-2" prop="data3_2"></el-table-column>
      <el-table-column label="表头3-3" prop="data3_3"></el-table-column>
    </el-table-column>
    <el-table-column label="表头4" prop="data3" align="center">
      <el-table-column label="表头4-1" prop="data4_1"></el-table-column>
      <el-table-column label="表头4-2" prop="data4_2"></el-table-column>
      <el-table-column label="表头4-3" prop="data4_3"></el-table-column>
      <el-table-column label="表头4-4" prop="data4_4"></el-table-column>
    </el-table-column>
  </el-table>
</template>

<script setup>
import { ref } from "vue";
import { mergeTableData } from "@utils/mock.js";
import exportXlsx from "@utils/exportMergeExcel.js";

const tableData = ref([]);
const tableRef = ref(null);
const getTableData = async (flag = "") => {
  if (flag != "changePage") {
    // 清空勾选项
    tableRef.value?.clearSelection();
  }
  tableData.value = (await mergeTableData()).value;
};
getTableData();

// 提供给表单用于合并单元格的函数
const objectSpanMethod = ({ row, column, rowIndex, columnIndex }) => {
  // 第二行
  if (rowIndex == 1 && columnIndex == 3) {
    // 第三列
    return {
      rowspan: 2,
      colspan: 3,
    };
  } else if ([1, 2].includes(rowIndex) && [3, 4, 5].includes(columnIndex)) {
    return {
      rowspan: 0,
      colspan: 0,
    };
  }
};

// 导出
const multipleSelection = ref([]);
const onSelect = (val) => {
  multipleSelection.value = val;
};
const exportExcel = () => {
  const keyMaps = [
    {
      ["序号"]: "$index",
    },
    {
      [" 表头1"]: "data1",
    },
    {
      ["表头2"]: "data2",
    },
    {
      ["表头3.表头3-1"]: "data3_1",
    },
    {
      ["表头3.表头3-2"]: "data3_2",
    },
    {
      ["表头3.表头3-3"]: "data3_3",
    },
    {
      ["表头4.表头4-1"]: "data4_1",
    },
    {
      ["表头4.表头4-2"]: "data4_2",
    },
    {
      ["表头4.表头4-3"]: "data4_3",
    },
    {
      ["表头4.表头4-4"]: "data4_4",
    },
  ];
  multipleSelection.value.forEach((item, index) => {
    item.$index = index + 1;
  });
  exportXlsx("导出表", keyMaps, multipleSelection.value);
};
</script>

导出excel函数完整代码

js
// 来源:https://github.com/Momo707577045/merged-excel-import-export-demo/tree/master/export-demo
// 原文章内对第三方库引用有误,这里进行了修改,修复了导出样式不生效的问题
// 修复了单行表头合并错误、数据为0时不展示的问题。
// 整体来说能用,稳定性未知。
// by zcc
import { utils, writeFile } from 'xlsx-js-style'

/**
 * 将数据源,转化为 Excel 单元格数据,并生成 Excel 表头
 * @param dataList 数据源
 * @param textKeyMaps // Excel 中文表头层级与数据源英文层级间的映射表
 * @param headerFirstRow // 表头首行所在行,为了兼容表格顶部还插入有其他 Excel 行的情况,即表格不在首行
 * @returns {
    headerMerges, // 表头合并单元格配置项
    cells, // 表头及数据项的 Excel 单元格数组
  }
 */
function transformDataToSheetCells(dataList, textKeyMaps, headerFirstRow = 0) {
  // 获取从 textKeyMaps 解析,拆分后的,中英文 keys 数组
  function getKeysList(textKeyMaps) {
    const chineseKeysList = []
    const englishKeysList = []
    textKeyMaps.forEach(textKeyMap => {
      const keyStr = Object.values(textKeyMap)[0]
      const textStr = Object.keys(textKeyMap)[0]
      englishKeysList.push(keyStr.split('.'))
      chineseKeysList.push(textStr.split('.'))
    })
    return {
      englishKeysList,
      chineseKeysList
    }
  }

  // 获取表头行数
  function getHeaderRowNum(chineseKeysList) {
    let maxLevel = 1
    chineseKeysList.forEach(chineseKeys => {
      maxLevel = Math.max(chineseKeys.length, maxLevel)
    })
    return maxLevel
  }

  // 获取表头行 cell 数据
  function getHeaderRows(headerRowNum, chineseKeysList) {
    const headerRows = []
    // 初始化,全部设置为 ''
    for (let rowIndex = 0; rowIndex < headerRowNum; rowIndex++) {
      const row = new Array(chineseKeysList.length).fill('')
      headerRows.push(row)
    }
    // 将表头 cell 设置为对应的中文
    chineseKeysList.forEach((chineseKeys, colIndex) => {
      for (let rowIndex = 0; rowIndex < chineseKeys.length; rowIndex++) {
        headerRows[rowIndex][colIndex] = chineseKeys[rowIndex]
      }
    })

    // 去除需要合并单元格的每一列中。重复的 cell 数据,重复的,则设置为 ''
    headerRows.forEach(headerRow => {
      let lastColValue = ''
      headerRow.forEach((cell, colIndex) => {
        if (lastColValue !== cell) {
          lastColValue = cell
        } else {
          headerRow[colIndex] = ''
        }
      })
    })

    return headerRows
  }

  // 获取合并单元格配置
  function getMerges(headerRowNum, chineseKeysList) {
    const merges = []
    // 竖向合并
    chineseKeysList.forEach((chineseKeys, colIndex) => {
      // 当前列,每一行都有数据,这无需要竖向合并
      if (chineseKeys.length === headerRowNum) {
        return
      }
      // 否则。存在数据需要竖向合并,竖向合并的行数,即为比最高行数少的行数
      merges.push({
        s: {
          r: chineseKeys.length - 1 + headerFirstRow,
          c: colIndex
        },
        e: {
          r: headerRowNum - 1 + headerFirstRow,
          c: colIndex
        }
      })
    })
    // 横向合并
    for (let rowIndex = 0; rowIndex < headerRowNum; rowIndex++) {
      const rowCells = chineseKeysList.map(chineseKeys => chineseKeys[rowIndex])
      let preCell = '' // 前一个单元格
      let merge = null // 当前合并配置项
      rowCells.forEach((cell, colIndex) => {
        if (cell != undefined && preCell === cell) {
          // 如果二者相同,则证明需要横向合并单元格
          if (!merge) {
            // merge 不存在,则创建,
            merge = {
              s: {
                r: rowIndex + headerFirstRow,
                c: colIndex - 1
              },
              e: {
                r: rowIndex + headerFirstRow,
                c: colIndex
              }
            }
            merges.push(merge) // 添加一个合并对象
          } else {
            merge.e.c = colIndex // 修改其合并结束列
          }
        } else {
          preCell = cell
          merge = null
        }
      })
    }
    return merges
  }

  // 获取转化数据结构为 Excel 数据行
  function getDataRows(dataList) {
    const dataRows = []
    dataList.forEach(dataItem => {
      const cells = []
      englishKeysList.forEach(keyLevel => {
        const value = keyLevel
          .reduce((dataItem, key) => dataItem[key] ?? '', dataItem)
          .toString()
        cells.push(value)
      })
      dataRows.push(cells)
    })
    return dataRows
  }

  const { englishKeysList, chineseKeysList } = getKeysList(textKeyMaps)
  const headerRowNum = getHeaderRowNum(chineseKeysList)
  const headerMerges = getMerges(headerRowNum, chineseKeysList)
  const headerRows = getHeaderRows(headerRowNum, chineseKeysList)
  const dataRows = getDataRows(dataList)

  return {
    headerMerges,
    cells: [...headerRows, ...dataRows]
  }
}

export default (filename, textKeyMaps, data) => {
  let workbook = utils.book_new() // 工作簿

  const { headerMerges, cells } = transformDataToSheetCells(data, textKeyMaps)
  const worksheet = utils.aoa_to_sheet(cells)
  worksheet['!merges'] = headerMerges
  // 所有单元格居中显示
  Object.values(worksheet).forEach(cell => {
    if (cell.v) {
      cell.s = {
        alignment: {
          horizontal: 'center',
          vertical: 'center',
          wrapText: true
        }
      }
    }
  })

  utils.book_append_sheet(workbook, worksheet, filename)
  writeFile(workbook, filename + '.xlsx')
}