/**
* Excel 文件读写模块
* @module $ui/utils/excel
*/
import XLSX from 'xlsx';
function setAutoWidth(ws, data) {
/* set worksheet max width per col */
const colWidth = data.map(row => row.map(val => {
/* if null/undefined */
if (val === null) {
return {wch: 10};
/* if chinese */
} else if (val.toString().charCodeAt(0) > 255) {
return {wch: val.toString().length * 2};
} else {
return {wch: val.toString().length};
}
}))
/* start in the first row */
const result = colWidth[0];
for (let i = 1; i < colWidth.length; i++) {
for (let j = 0; j < colWidth[i].length; j++) {
if (result[j].wch < colWidth[i][j].wch) {
result[j].wch = colWidth[i][j].wch;
}
}
}
ws['!cols'] = result;
}
function jsonToArray(key, jsonData) {
return jsonData.map(v => key.map(j => {
return v[j]
}));
}
// fix data,return string
function fixData(data) {
let o = ''
let l = 0
const w = 10240
for (; l < data.byteLength / w; ++l) o += String.fromCharCode.apply(null, new Uint8Array(data.slice(l * w, l * w + w)))
o += String.fromCharCode.apply(null, new Uint8Array(data.slice(l * w)))
return o
}
// get head from excel file,return array
function getHeaderRow(sheet) {
const headers = []
const range = XLSX.utils.decode_range(sheet['!ref'])
let C
const R = range.s.r
/* start in the first row */
for (C = range.s.c; C <= range.e.c; ++C) { /* walk every column in the range */
const cell = sheet[XLSX.utils.encode_cell({c: C, r: R})]
/* find the cell in the first row */
let hdr = 'UNKNOWN ' + C // <-- replace with your desired default
if (cell && cell.t) hdr = XLSX.utils.format_cell(cell)
headers.push(hdr)
}
return headers
}
/**
* 导出文件
* @param {array} key 列标识数组
* @param {array} title 列文本数组
* @param {array} data 行数据
* @param {string} fileName 导出文件名,不含扩展名
* @param {boolean} autoWidth 是否设置自动列宽度
* @return {Promise<any>}
*/
export const write = ({key, data, title, fileName, autoWidth}) => {
return new Promise(resolve => {
const wb = XLSX.utils.book_new();
const arr = jsonToArray(key, data);
arr.unshift(title);
const ws = XLSX.utils.aoa_to_sheet(arr);
if (autoWidth) {
setAutoWidth(ws, arr);
}
XLSX.utils.book_append_sheet(wb, ws, fileName);
XLSX.writeFile(wb, fileName + '.xlsx');
resolve()
})
}
// 获取table的行数据
function addTableRow(obj, arr) {
Array.prototype.forEach.call(obj.rows, r => {
const row = [];
Array.prototype.forEach.call(r.cells, c => {
row.push(c.innerText + '\x0a')
})
arr.push(row)
})
return arr;
}
// 判断是el-table的表格还是 table标签,并获取行数据
function getTableData(table) {
if(!table) {
throw new Error('表格类型错误')
}
let arr = [];
const headerEl = table.querySelector('.el-table__header');
const bodyEl = table.querySelector('.el-table__body');
if(headerEl && bodyEl) {
arr = addTableRow(headerEl, arr)
arr = addTableRow(bodyEl, arr)
} else if(table instanceof HTMLElement && table.tagName === 'TABLE') {
arr = addTableRow(table, arr)
} else {
throw new Error('表格类型错误')
}
return arr;
}
/**
*
* @param {Array|Object} tables 表格数组或者对象, 必须是table标签或者el-table的 $el
* @param {Array|String} sheetNames 表名数组或字符串
* @param {String} fileName 文件名
* @param {autoWidth} autoWidth = true
*/
export const writeFromTable = ({tables, sheetNames, fileName, autoWidth = true}) => {
return new Promise((resolve) => {
const wb = XLSX.utils.book_new();
// 可以传数组(生成多表excel文件)或对象(单表excel文件)
if(tables instanceof Array && sheetNames instanceof Array) {
tables.forEach((table, idx) => {
const arr = getTableData(table);
const ws = XLSX.utils.aoa_to_sheet(arr)
if(autoWidth) {
setAutoWidth(ws, arr)
}
XLSX.utils.book_append_sheet(wb, ws, sheetNames[idx])
})
} else {
const arr = getTableData(tables);
const ws = XLSX.utils.aoa_to_sheet(arr)
if(autoWidth) {
setAutoWidth(ws, arr)
}
XLSX.utils.book_append_sheet(wb, ws, sheetNames)
}
XLSX.writeFile(wb, fileName + '.xlsx');
resolve()
})
}
/**
* 文件转换成JSON数据
* @param {Object|String} data 文件流
* @param {String} type 数据类型 array 或 base64
* @return {{header: Array, results: any[] | * | any[][]}}
*/
export const read = (data, type) => {
/* if type == 'base64' must fix data first */
let workbook
if (type === 'base64') {
const fixedData = fixData(data)
workbook = XLSX.read(btoa(fixedData), {type: 'base64'})
} else {
workbook = XLSX.read(data, {type: type});
}
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const header = getHeaderRow(worksheet);
const results = XLSX.utils.sheet_to_json(worksheet);
return {header, results};
}
/**
* 读取文件
* @param {Object} file 文件框响应的文件对象
* @param {object} options 参数选项,可侦听回调函数,如:onloadstart / onprogress / onerror / onload
* @return {Promise<any>}
*/
export function reader(file, options = {}) {
const reader = new FileReader()
reader.readAsArrayBuffer(file)
return new Promise((resolve, reject) => {
const opt = {
...options,
onerror(e) {
options.onerror && options.onerror(e)
reject(e)
},
onload(e) {
const reuslt = e.target.result
options.onload && options.onload(reuslt)
resolve(reuslt)
}
}
Object.keys(opt).forEach(key => {
reader[key] = opt[key]
})
})
}
// ============== 以下为复杂表头输出函数集合 =============================
const getHeader = function(headers, excelHeader, deep, perOffset) {
let offset = 0;
let cur = excelHeader[deep];
if (!cur) {
cur = excelHeader[deep] = [];
}
pushUndefined(cur, perOffset - cur.length);
for (let i = 0; i < headers.length; i++) {
const head = headers[i];
cur.push(head.name);
// head.hasOwnProperty('child')
if (head.child && Array.isArray(head.child) && head.child.length > 0) {
const childOffset = getHeader(
head.child,
excelHeader,
deep + 1,
cur.length - 1
);
pushNull(cur, childOffset - 1);
offset += childOffset;
} else {
offset++;
}
}
return offset;
};
const pushUndefined = function(arr, count) {
for (let i = 0; i < count; i++) {
arr.push(undefined);
}
};
const pushNull = function(arr, count) {
for (let i = 0; i < count; i++) {
arr.push(null);
}
};
const fillNull = function(arr) {
const max = Math.max(...arr.map(a => a.length));
arr.filter(e => e.length < max).forEach(e => pushNull(e, max - e.length));
};
const extractData = function(selectionData, revealList) {
const headerList = [];
flat(revealList, headerList);
// 结果集
const result = [];
selectionData.forEach(row => {
const rowData = [];
headerList.forEach(prop => {
let value = null;
if (typeof prop === 'function') {
value = prop(row);
} else {
value = row[prop];
}
value = value === null || value === undefined ? '' : value;
rowData.push(value);
});
result.push(rowData);
});
return result;
};
const flat = function(revealList, result) {
revealList.forEach(e => {
if (e.child) {
flat(e.child, result);
} else if (e.exeFun) {
result.push(e.exeFun);
} else if (e.prop) {
result.push(e.prop);
}
});
};
const doMerges = function(arr) {
// 要么横向合并 要么纵向合并
const deep = arr.length;
const merges = [];
for (let y = 0; y < deep; y++) {
// 先处理横向合并
const row = arr[y];
let colSpan = 0;
for (let x = 0; x < row.length; x++) {
if (row[x] === null) {
colSpan++;
if (x + 1 === row.length && (colSpan > 0 && x > colSpan)) {
merges.push({ s: { r: y, c: x - colSpan }, e: { r: y, c: x } });
}
} 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;
}
}
}
// 再处理纵向合并
const 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] != null) {
rowSpan = 0;
} else {
rowSpan++;
}
}
if (rowSpan > 0) {
merges.push({
s: { r: deep - rowSpan - 1, c: x },
e: { r: deep - 1, c: x }
});
}
}
return merges;
};
const aoaToSheet = function(data) {
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],
s: {
font: { name: '宋体', sz: 11, color: { auto: 1 } },
border: {
color: { auto: 1 }
},
alignment: {
/// 自动换行
wrapText: 1,
// 居中
horizontal: 'center',
vertical: 'center',
indent: 0
}
}
};
if (cell.v == null) continue;
const cellRef = 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 if (cell.v instanceof Date) {
// cell.t = 'n'; cell.z = XLSX.SSF._table[14];
// cell.v = this.dateNum(cell.v);
// }
else cell.t = 's';
ws[cellRef] = cell;
}
}
if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range);
return ws;
};
const s2ab = function(s) {
const buf = new ArrayBuffer(s.length);
const view = new Uint8Array(buf);
for (let i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xff;
return buf;
};
const openDownloadXLSXDialog = function(url, saveName) {
if (typeof url === 'object' && url instanceof Blob) {
url = URL.createObjectURL(url); // 创建blob地址
}
const aLink = document.createElement('a');
aLink.href = url;
aLink.download = saveName || ''; // HTML5新增的属性,指定保存文件名,可以不要后缀,注意,file:///模式下不会生效
let 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);
};
/**
* 读取文件
* @param {String} docName 输出文件名称
* @param {Array} rowData 行数据
* @param {Array} colData 表头列数据 {name: '表头名', prop: '字段名', child: []}
* cloData = [ { name: '姓名', prop: 'name' }, { name: '专业技能', child: [ { name: '前端', child: [ { name: 'JavaScript', prop: 'js' } ] } ] } ]
*/
const complexTableExport = function(docName, rowData, colData) {
const sheetName = docName; // '多级表头excel'
const excelHeader = []; // [[], []]; // , []
getHeader(colData, excelHeader, 0, 0);
fillNull(excelHeader);
const merges = doMerges(excelHeader);
const dataList = extractData(rowData, colData);
excelHeader.push(...dataList);
const ws = aoaToSheet(excelHeader);
ws['!merges'] = merges;
const workbook = {
SheetNames: [sheetName],
Sheets: {}
};
workbook.Sheets[sheetName] = ws;
// excel样式
const wopts = {
bookType: 'xlsx',
bookSST: true,
type: 'binary',
cellStyles: true
};
const wbout = XLSX.write(workbook, wopts);
const blob = new Blob([s2ab(wbout)], { type: 'application/octet-stream' });
openDownloadXLSXDialog(blob, sheetName + '.xlsx');
};
// ===========================================
export default { write, writeFromTable, read, reader, complexTableExport }