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合并单元格原理相同,设置特殊单元格的rowspan
和colspan
,被占用不显示的单元格需要置为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')
}