场景:纯前端导出excel数据,涉及到列合并、行合并。
注)当前数据表头固定,行内数据不固定。以第一列WM为判断条件,相同名字的那几行数据合并单元格。合并的那几行数据,后面的列按需求进行合并。
注)本示例是用 vue3+element plus 实现的。
要求导出Excel效果图:
一、按需引入插件
javascript
npm i -S file-saver xlsx
npm i -D script-loader
二、导出实现
1、封装方法
excel导出的数据都是二维数组格式,如果是常规的list数据,需要转成二维数组。
注)生成的excel封装的方法如下(支持表头合并、导出的 excel 支持生成多个sheet工作表、表格可自适应宽度、自适应高度、合并表格)
【步骤】
1、导出操作涉及到使用 OutExcelSheet.exportSheetExcel 函数来导出一个名为 karlaExport导出.xlsx 的 Excel 文件。【三个参数:sheetData、mergesHeader 和文件名】
2、sheetData 是一个数组,用于存储要导出的表格数据。在代码中,使用了一个名为 sheet1 的对象来定义表格的名称、数据、合并单元格和行高等信息。
3、mergesHeader 是一个数组,用于指定要合并的行和列的范围。在给定的代码中,合并了一些特定的行和列,以创建标题行和表头的合并效果。
4、最后,通过调用 OutExcelSheet.exportSheetExcel 函数,并传递以上参数,将生成的 Excel 文件导出到本地。
javascript
import XLSX from 'xlsx-js-style'
import FileSaver from 'file-saver'
export default {
// 三个参数:sheetData、mergesHeader 和文件名。
exportSheetExcel(sheetData, mergerArr, fileName = 'karlaExport.xlsx') {
const wb = XLSX.utils.book_new() // 创建一个新工作簿
for (let i = 0; i < sheetData.length; i++) {
const sheet = sheetData[i]
// 检查数据项是否存在
if (!sheet.data) {
continue // 如果数据项不存在,则跳过当前循环
}
const ws = XLSX.utils.aoa_to_sheet(sheet.data) // 将数据数组转换为工作表
// 设置合并单元格
ws['!merges'] = sheet.merges && sheet.merges.length > 0 ? [...sheet.merges, ...(mergerArr || [])] : mergerArr;
// 设置列宽为自适应
if (sheet.data.length > 0) {
ws['!cols'] = sheet.data[0].map((_, index) => ({ wch: 15 }))
}
// 设置行高
if (sheet.rowHeights && sheet.rowHeights.length > 0) {
ws['!rows'] = sheet.rowHeights.map((height) => ({ hpt: height, hpx: height }))
}
const borderAll = {
top: { style: 'thin' },
bottom: { style: 'thin' },
left: { style: 'thin' },
right: { style: 'thin' }
}
// 设置单元格样式
for (const key in ws) {
if (ws.hasOwnProperty(key)) {
const cell = ws[key]
if (typeof cell === 'object') {
cell.s = {
border: borderAll,
alignment: {
horizontal: 'center',
vertical: 'center',
wrapText: true
},
font: {
sz: 12,
color: {
rgb: '000000'
}
},
numFmt: 'General',
fill: {
fgColor: { rgb: 'FFFFFF' }
}
}
}
}
}
XLSX.utils.book_append_sheet(wb, ws, sheet.name) // 将工作表添加到工作簿并指定名称
}
const wbout = XLSX.write(wb, { bookType: 'xlsx', type: 'array' }) // 将工作簿转换为数组
const file = new Blob([wbout], { type: 'application/octet-stream' }) // 创建Blob对象
FileSaver.saveAs(file, fileName) // 下载文件
},
// 二维数组中空的数据设置为 0
emptyValues(array, defaultValue) {
for (let i = 0; i < array.length; i++) {
for (let j = 0; j < array[i].length; j++) {
if (array[i][j] === null || array[i][j] === undefined || array[i][j] === '') {
array[i][j] = defaultValue
}
}
}
return array
},
// 生成excel列表数据
handleExcelTable(columnHeader, list) {
if (list.length === 0) return []
// 表头
const tableColumn = Object.keys([columnHeader][0])
// 表格生成的数据
const sheet = [tableColumn]
list.forEach((item) => {
const row = tableColumn.map((column) => item[column])
sheet.push(row)
})
// 表头匹配对应的中文
const firstRow = sheet[0].map((column) => columnHeader[column])
sheet[0] = firstRow
return sheet || []
}
}
2、前端代码,导出,用的mock数据,是转换后的二维数组,封装的方法中有写
javascript
<script lang="ts" setup>
// 引入导出excel 封装的方法
import OutExcelSheet from '@/hooks/web/outToExcelManySheet'
defineOptions({ name: 'export' })
/** 导出 */
const exportLoading = ref(false)
const handleExport = async () => {
// 表格合并需要添加一行合并表头
const header = [
'WM',
'Total Leave days',
'Paid Leave Date',
'Actual working days of previous 3 months',
'',
'',
'Payout of commission 3 months',
'',
'',
'Average commission / day',
'Commission during leave',
'Leave Payout',
'Total Leave Payout'
]
// 表头
const columnsHeader = {
name: 'WM',
leaveDays: 'Total Leave days',
paidLeaveDate: 'Paid Leave Date',
actualDaysOneMonth: 'Actual working days of previous 3 months(近三月)',
actualDaysTwoMonth: 'Actual working days of previous 3 months(近两月)',
actualDaysThreeMonth: 'Actual working days of previous 3 months(近一月)',
payoutCommissionOneMonthPrice: 'Payout of commission 3 months(近三月)',
payoutCommissionTwoMonthPrice: 'Payout of commission 3 months(近二月)',
payoutCommissionThreeMonthPrice: 'Payout of commission 3 months(近一月)',
averageCommission: 'Average commission/day',
commissionDuringLeave: 'Commission during leave',
leavePayout: 'Leave Payout',
totalLeavePayout: 'Total Leave Payout'
}
// mock 导出数据(带表头)
const exportList = [
[
'WM',
'Total Leave days',
'Paid Leave Date',
'Actual working days of previous 3 months\t(第一个月)',
'Actual working days of previous 3 months\t(第二个月)',
'Actual working days of previous 3 months\t(第三个月)',
'Payout of commission 3 months\t第一个月)',
'Payout of commission 3 months\t(第二个月)',
'Payout of commission 3 months\t(第三个月)',
'Average commission / day',
'Commission during leave',
'Leave Payout',
'Total Leave Payout'
],
[
'karla',
5,
'2023-01-01',
'10',
'18',
'18',
'10000',
'20000',
'30000',
'1400',
'640',
'760',
'0.00'
],
[
'karla',
5,
'2023-01-04',
'10',
'18',
'18',
'10000',
'20000',
'30000',
'1400',
'1600',
'0.00',
'0.00'
],
[
'karla',
5,
'2023-01-06',
'10',
'18',
'18',
'10000',
'20000',
'30000',
'1400',
'1800',
'0.00',
'0.00'
],
[
'karla',
5,
'2023-01-24',
'10',
'18',
'18',
'10000',
'20000',
'30000',
'1400',
'0.00',
'0.00',
'0.00'
],
[
'karla',
5,
'2023-01-18',
'10',
'18',
'18',
'10000',
'20000',
'30000',
'1400',
'1600',
'0.00',
'0.00'
],
[
'York',
2,
'2023-01-18',
'28',
'24',
'18',
'10000',
'20000',
'30000',
'1500',
'1800',
'0.00',
'666'
],
[
'York',
2,
'2023-01-24',
'28',
'24',
'18',
'10000',
'20000',
'30000',
'1500',
'700',
'800',
'666'
],
[
'Caleb',
1,
'2023-01-29',
'22',
'15',
'17',
'8899.12',
'7833',
'1455.63',
'1366.8',
'734.8',
'632',
'0.00'
]
]
// 生成表格
const sheet1 = {
name: 'LeavePay',
// data: [header, ...OutExcelSheet.handleExcelTable(columnsHeader, list.value)], // 常规list数据用封装的方法处理二维数据
data: [header, ...exportList], // 使用处理好的mock数据
merges: [],
rowHeights: [{ hpx: 20 }, { hpx: 20 }]
}
// 合并:第0列、第1列、第三列、第四列、第五列、第六列、第七列和第八列的相同值进行行合并
const mergedRows = new Map()
for (let i = 1; i < sheet1.data.length; i++) {
const cellValue0 = sheet1.data[i][0]
const cellValue1 = sheet1.data[i][1]
const cellValue3 = sheet1.data[i][3]
const cellValue4 = sheet1.data[i][4]
const cellValue5 = sheet1.data[i][5]
const cellValue6 = sheet1.data[i][6]
const cellValue7 = sheet1.data[i][7]
const cellValue8 = sheet1.data[i][8]
const prevValue0 = sheet1.data[i - 1][0]
const prevValue1 = sheet1.data[i - 1][1]
const prevValue3 = sheet1.data[i - 1][3]
const prevValue4 = sheet1.data[i - 1][4]
const prevValue5 = sheet1.data[i - 1][5]
const prevValue6 = sheet1.data[i - 1][6]
const prevValue7 = sheet1.data[i - 1][7]
const prevValue8 = sheet1.data[i - 1][8]
if (
cellValue0 === prevValue0 &&
cellValue1 === prevValue1 &&
cellValue3 === prevValue3 &&
cellValue4 === prevValue4 &&
cellValue5 === prevValue5 &&
cellValue6 === prevValue6 &&
cellValue7 === prevValue7 &&
cellValue8 === prevValue8
) {
if (mergedRows.has(cellValue0)) {
// 更新合并的结束行索引
mergedRows.get(cellValue0).end = i
} else {
// 添加新的合并信息
mergedRows.set(cellValue0, { start: i - 1, end: i })
}
}
}
// 添加行合并信息到 mergesHeader
for (const [value, { start, end }] of mergedRows.entries()) {
sheet1.merges.push({ s: { r: start, c: 0 }, e: { r: end, c: 0 } })
sheet1.merges.push({ s: { r: start, c: 1 }, e: { r: end, c: 1 } })
sheet1.merges.push({ s: { r: start, c: 3 }, e: { r: end, c: 3 } })
sheet1.merges.push({ s: { r: start, c: 4 }, e: { r: end, c: 4 } })
sheet1.merges.push({ s: { r: start, c: 5 }, e: { r: end, c: 5 } })
sheet1.merges.push({ s: { r: start, c: 6 }, e: { r: end, c: 6 } })
sheet1.merges.push({ s: { r: start, c: 7 }, e: { r: end, c: 7 } })
sheet1.merges.push({ s: { r: start, c: 8 }, e: { r: end, c: 8 } })
sheet1.merges.push({ s: { r: start, c: 12 }, e: { r: end, c: 12 } })
}
// 合并表头
const mergesHeader = [
// 行合并
{ s: { r: 0, c: 3 }, e: { r: 0, c: 5 } },
{ s: { r: 0, c: 6 }, e: { r: 0, c: 8 } },
// 列合并(r 表示行索引,c 表示列索引)
{ s: { r: 0, c: 0 }, e: { r: 1, c: 0 } }, // 第0列的第0行和第1行合并
{ s: { r: 0, c: 1 }, e: { r: 1, c: 1 } }, // 第1列的第0行和第1行合并
{ s: { r: 0, c: 2 }, e: { r: 1, c: 2 } }, // 第2列的第1行和第1行合并
{ s: { r: 0, c: 9 }, e: { r: 1, c: 9 } },
{ s: { r: 0, c: 10 }, e: { r: 1, c: 10 } },
{ s: { r: 0, c: 11 }, e: { r: 1, c: 11 } },
{ s: { r: 0, c: 12 }, e: { r: 1, c: 12 } }
]
const sheetData = [sheet1]
// 导出
OutExcelSheet.exportSheetExcel(sheetData, mergesHeader, `karla导出.xlsx`)
}
</script>