import XLSX from "xlsx-js-style";
/**
* @author
* @desciption: 用于嵌套表头的表格导出
*/
/**
* @description: 边框样式
*/
const borderAll = {
top: {
style: 'thin',
},
bottom: {
style: 'thin'
},
left: {
style: 'thin'
},
right: {
style: 'thin'
}
}
/**
* @description: 默认单元格样式
*/
const defaultCellStyle = {
/**
* 表头区域样式配置
*/
headerStyle: {
border: borderAll,
font: {name: '宋体', sz: 11, italic: false, underline: false, bold: true},
alignment: {vertical: 'center', horizontal: 'center'},
fill: {fgColor: {rgb: 'FFFFFF'}},
},
/**
* 内容样式配置
*/
dataStyle: {
border: borderAll,
font: {name: '宋体', sz: 11, italic: false, underline: false},
alignment: {vertical: 'center', horizontal: 'left', wrapText: true},
fill: {fgColor: {rgb: 'FFFFFF'}},
}
}
/**
* 默认表头属性映射
* 用于从 tableHeader 对象中提取关键属性(如prop、label等)
*/
const defaultHeaderProps = {
prop: 'prop',
label: 'label',
children: 'children'
}
export default class ExportExcelTool {
constructor(options) {
options = options ? options : {};
/**
* 表格数据
*/
this.tableData = options.tableData
/**
* 表头结构
*/
this.tableHeader = options.tableHeader;
/**
* 表头结构属性映射
* 用于从 tableHeader 对象中提取关键属性(如prop、label等)
*/
this.headerProps = options.headerProps || defaultHeaderProps
/**
* 导出文件名
*/
this.fileName = options.fileName || 'exportExcel'
/**
* 工作表名称
* @type {string}
*/
this.sheetName = options.sheetName || 'Sheet1'
/**
* 表格基础样式
*/
this.cellStyle = options.cellStyle || defaultCellStyle
/**
* 列宽 数组 按顺序为每一列设置宽度,未设置宽度的列默认为 columnWidth: 100
* 例如 {0:200,2:200} 第一列宽度为200,第三列宽度为200 其余列默认
*/
this.columnWidthsArray = options.columnWidthsArray || {}
/**
* 列宽 默认100
*/
this.columnWidth = options.columnWidth || 100
/**
* 数据格式化方法
* 例如:
* formatterValue(value, header){
* return value
* }
*/
this.formatterValue = options.formatterValue
}
/**
* 导出数据Excel
*/
exportData() {
if (!Array.isArray(this.tableHeader) || this.tableHeader.length < 1) {
throw {type: 'error', message: '请选择需要导出的表头!'}
}
if (!Array.isArray(this.tableData) || this.tableData.length < 1) {
throw {type: 'error', message: '请选择需要导出的数据!'}
}
let sheetName = this.sheetName
// excel表头
let excelHeader = this.buildHeader(this.tableHeader, this.headerProps)
// 头部行数,用来固定表头
let headerRows = excelHeader.length
//
let headerList = this.flat(this.tableHeader, this.headerProps)
// 提取数据
let dataList = this.extractData(this.tableData, this.headerProps, headerList)
excelHeader.push(...dataList, [])
// 计算合并
let merges = this.doMerges(excelHeader)
// 生成sheet
let ws = this.aoa_to_sheet(excelHeader, headerRows)
// 单元格合并
ws['!merges'] = merges
// 在生成sheet之后,为每一列设置宽度
ws['!cols'] = [];
const columnWidths = this.columnWidthsArray;
for (let c = 0; c < headerList.length; c++) {
// 如果没有指定宽度,则使用默认宽度
ws['!cols'].push({wpx: columnWidths[c] || this.columnWidth});
}
let workbook = {
SheetNames: [sheetName],
Sheets: {},
}
workbook.Sheets[sheetName] = ws
// excel样式
let wopts = {
bookType: 'xlsx',
bookSST: false,
type: 'binary',
cellStyles: true,
}
let wbout = XLSX.write(workbook, wopts)
let blob = new Blob([this.s2ab(wbout)], {type: 'application/octet-stream'})
this.openDownloadXLSXDialog(blob, this.fileName + '.xlsx')
}
/**
* 构建excel表头
* @param revealList 列表页面展示的表头
* @param headerProps
* @returns {[]} excel表格展示的表头
*/
buildHeader(revealList, headerProps) {
let excelHeader = []
// 构建生成excel表头需要的数据结构
this.getHeader(revealList, headerProps, excelHeader, 0, 0)
let max = Math.max(...excelHeader.map((a) => a.length))
excelHeader.filter((e) => e.length < max).forEach((e) => this.pushRowSpanPlaceHolder(e, max - e.length))
return excelHeader
}
/**
* 生成头部
* @param headers 展示的头部
* @param headerProps 属性映射
* @param excelHeader excel头部
* @param deep 深度
* @param perOffset 前置偏移量
* @returns {number} 后置偏移量
*/
getHeader(headers, headerProps, excelHeader, deep, perOffset) {
let offset = 0
let cur = excelHeader[deep]
if (!cur) {
cur = excelHeader[deep] = []
}
// 填充行合并占位符
this.pushRowSpanPlaceHolder(cur, perOffset - cur.length)
for (let i = 0; i < headers.length; i++) {
let head = headers[i]
cur.push(head[headerProps.label])
if (head.hasOwnProperty(headerProps.children) && Array.isArray(head[headerProps.children]) && head[headerProps.children].length > 0) {
let childOffset = this.getHeader(head[headerProps.children], headerProps , excelHeader, deep + 1, cur.length - 1)
// 填充列合并占位符
this.pushColSpanPlaceHolder(cur, childOffset - 1)
offset += childOffset
} else {
offset++
}
}
return offset
}
/**
* 根据选中的数据和展示的列,生成结果
* @param selectionData
* @param headerProps
* @param headerList
* @returns {*[]}
*/
extractData(selectionData, headerProps, headerList) {
// 导出的结果集
let excelRows = []
// 如果有child集合的话会用到
let dataKeys = new Set(Object.keys(selectionData[0]))
selectionData.some((e) => {
if (e[headerProps.children] && e[headerProps.children].length > 0) {
let childKeys = Object.keys(e[headerProps.children][0])
for (let i = 0; i < childKeys.length; i++) {
dataKeys.delete(childKeys[i])
}
return true
}
})
this.flatData(selectionData, headerProps, (list) => {
excelRows.push(...this.buildExcelRow(dataKeys, headerList, headerProps, list))
})
return excelRows
}
/**
* 根据列和数据生成excel行
* @param mainKeys
* @param headers
* @param headerProps
* @param rawDataList
* @returns {*[]}
*/
buildExcelRow(mainKeys, headers, headerProps, rawDataList) {
// 数据行
let rows = []
for (let i = 0; i < rawDataList.length; i++) {
let cols = []
let rawData = rawDataList[i]
// 提取数据
for (let j = 0; j < headers.length; j++) {
let header = headers[j]
// 父元素键需要行合并
if (rawData['rowSpan'] === 0 && mainKeys.has(header[headerProps.prop])) {
cols.push('!$ROW_SPAN_PLACEHOLDER')
} else {
let value = rawData[header[headerProps.prop]]
// 这里可以格式化你的数据
if (this.formatterValue) {
value = this.formatterValue(value, header)
}
// push进已经格式化了的数据
cols.push(value)
}
}
rows.push(cols)
}
return rows
}
/**
* @description: 需要合并的数据
* @param {*} arr
* @return {*}
*/
doMerges(arr) {
// 要么横向合并 要么纵向合并
let deep = arr.length
let merges = []
for (let y = 0; y < deep; y++) {
// 先处理横向合并
let row = arr[y]
let colSpan = 0
for (let x = 0; x < row.length; x++) {
if (row[x] === '!$COL_SPAN_PLACEHOLDER') {
row[x] = undefined
if (x + 1 === row.length) {
merges.push({s: {r: y, c: x - colSpan - 1}, e: {r: y, c: x}})
}
colSpan++
} else if (colSpan > 0 && x > colSpan) {
merges.push({s: {r: y, c: x - colSpan - 1}, e: {r: y, c: x - 1}})
colSpan = 0
} else {
colSpan = 0
}
}
}
// 再处理纵向合并
let colLength = arr[0].length
for (let x = 0; x < colLength; x++) {
let rowSpan = 0
for (let y = 0; y < deep; y++) {
if (arr[y][x] === '!$ROW_SPAN_PLACEHOLDER') {
arr[y][x] = undefined
if (y + 1 === deep) {
merges.push({s: {r: y - rowSpan, c: x}, e: {r: y, c: x}})
}
rowSpan++
} else if (rowSpan > 0 && y > rowSpan) {
merges.push({s: {r: y - rowSpan - 1, c: x}, e: {r: y - 1, c: x}})
rowSpan = 0
} else {
rowSpan = 0
}
}
}
return merges
}
/**
* 将二维数组转换为一个sheet
* @param data
* @param headerRows
* @returns {{}}
*/
aoa_to_sheet(data, headerRows) {
const ws = {}
const range = {s: {c: 10000000, r: 10000000}, e: {c: 0, r: 0}}
for (let R = 0; R !== data.length; ++R) {
// 处理每个单元格的样式
for (let C = 0; C !== data[R].length; ++C) {
if (range.s.r > R) {
range.s.r = R
}
if (range.s.c > C) {
range.s.c = C
}
if (range.e.r < R) {
range.e.r = R
}
if (range.e.c < C) {
range.e.c = C
}
/// 这里生成cell的时候,使用上面定义的默认样式
const cell = {
v: data[R][C] || ''
}
if (R < headerRows) {
// 头部列表样式
cell.s = this.cellStyle.headerStyle
} else {
// 数据列表样式
cell.s = this.cellStyle.dataStyle
}
const cell_ref = XLSX.utils.encode_cell({c: C, r: R})
if (typeof cell.v === 'number') {
cell.t = 'n'
} else if (typeof cell.v === 'boolean') {
cell.t = 'b'
} else {
cell.t = 's'
}
ws[cell_ref] = cell
}
}
// console.log(ws, range)
if (range.s.c < 10000000) {
ws['!ref'] = XLSX.utils.encode_range(range)
}
return ws
}
/**
* 填充行合并占位符
* @param arr
* @param count
*/
pushRowSpanPlaceHolder(arr, count) {
for (let i = 0; i < count; i++) {
arr.push('!$ROW_SPAN_PLACEHOLDER')
}
}
/**
* 填充列合并占位符
* @param arr
* @param count
*/
pushColSpanPlaceHolder(arr, count) {
for (let i = 0; i < count; i++) {
arr.push('!$COL_SPAN_PLACEHOLDER')
}
}
/**
* @description:铺平数组
* @param {*} list 表格数据
* @param headerProps
* @param {*} eachDataCallBack 构建的行数据
* @return {*}
*/
flatData(list, headerProps, eachDataCallBack) {
let resultList = []
for (let i = 0; i < list.length; i++) {
let data = list[i]
let rawDataList = []
// 每个子元素都和父元素合并成一条数据
if (data[headerProps.children] && data[headerProps.children].length > 0) {
for (let j = 0; j < data[headerProps.children].length; j++) {
delete data[headerProps.children][j].bsm
let copy = Object.assign({}, data, data[headerProps.children][j])
rawDataList.push(copy)
copy['rowSpan'] = j > 0 ? 0 : data[headerProps.children].length
}
} else {
data['rowSpan'] = 1
rawDataList.push(data)
}
resultList.push(...rawDataList)
if (typeof eachDataCallBack === 'function') {
eachDataCallBack(rawDataList)
}
}
return resultList
}
/**
* 将嵌套的列表展平为一维数组。
* @param {Array} revealList - 嵌套的列表,其中每个元素可能包含子元素。
* @param {Object} headerProps - 描述列表项中子元素属性的对象。预期包含一个'children'属性,指明子元素在父元素中的键名。
* @returns {Array} 返回展平后的列表,其中不包含任何嵌套结构。
*/
flat(revealList, headerProps) {
let result = []
revealList.forEach((e) => {
// 检查当前元素是否包含子元素属性
if (e.hasOwnProperty(headerProps.children)) {
// 当前元素确实包含子元素
if (e[headerProps.children]) {
// 子元素非空
if (e[headerProps.children].length > 0) {
// 递归展平子元素,并将结果合并到结果数组中
result.push(...this.flat(e[headerProps.children], headerProps))
} else {
// 子元素为空,将当前元素添加到结果数组
result.push(e)
}
} else {
// 子元素属性存在,但值为空,将当前元素添加到结果数组
result.push(e)
}
} else if (e.hasOwnProperty(headerProps.prop)) {
// 如果元素不包含子元素属性但包含'code'属性,将其添加到结果数组
result.push(e)
}
})
return result
}
s2ab(s) {
let buf = new ArrayBuffer(s.length)
let view = new Uint8Array(buf)
for (let i = 0; i !== s.length; ++i) {
view[i] = s.charCodeAt(i) & 0xff
}
return buf
}
/**
* 打开下载XLSX文件的对话框
* @param {string|Blob} url - 文件的URL或者Blob对象
* @param {string} saveName - 保存文件时的名称,默认为空字符串
*/
openDownloadXLSXDialog(url, saveName) {
if (typeof url == 'object' && url instanceof Blob) {
// 如果url是Blob对象,则创建一个blob URL
url = URL.createObjectURL(url)
}
var aLink = document.createElement('a') // 创建一个链接元素
aLink.href = url // 设置链接地址
aLink.download = saveName || '' // 设置下载时的文件名
var event
if (window.MouseEvent) {
// 用于兼容高版本浏览器的点击事件创建方式
event = new MouseEvent('click')
} else {
// 用于兼容低版本浏览器的点击事件创建方式
event = document.createEvent('MouseEvents')
event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null)
}
aLink.dispatchEvent(event) // 触发链接的点击事件,开始下载
}
}
exportHandle() {
const exportExcelTool = new ExportExcelTool({
tableData: this.tableData, // 必须
tableHeader: _titleTable,// 必须
// 下面参数都有默认值,可选择修改
fileName: this.tableTitle,
sheetName: "学生信息",
columnWidth: 80,
columnWidthsArray: {2: 150, 3: 200, 6: 200},
headerProps: {
prop: "prop",
label: "label",
children: "children",
},
cellStyle: {
/**
* 表头区域样式配置
*/
headerStyle: {
border: {top: {style: 'thin',}, bottom: {style: 'thin'}, left: {style: 'thin'}, right: {style: 'thin'}},
font: {name: '宋体', sz: 13, italic: false, underline: false, bold: true},
alignment: {vertical: 'center', horizontal: 'center'},
fill: {fgColor: {rgb: '719aad'}},
},
/**
* 内容样式配置
*/
dataStyle: {
border: {top: {style: 'thin',}, bottom: {style: 'thin'}, left: {style: 'thin'}, right: {style: 'thin'}},
font: {name: '宋体', sz: 11, italic: true, underline: false},
alignment: {vertical: 'center', horizontal: 'left', wrapText: true},
fill: {fgColor: {rgb: 'ceffca'}},
}
},
/**
* 数据格式化
* @param value 值
* @param header 表头信息
*/
formatterValue(value, header) {
if (header.prop === 'date') {
return value.replace(/-/g, "/")
}
return value
}
})
// 调用方法导出 excel
exportExcelTool.exportData()
}
只是做一个记录 前端导出xlsx 并学习