583 lines
17 KiB
JavaScript
583 lines
17 KiB
JavaScript
/*
|
||
* @Author: Jeffrey Wang
|
||
* @Desc: 整理强大的 SheetJS 功能,依赖 XLSX.js 和 FileSaver
|
||
* @Version: v1.4
|
||
* @Date: 2018-03-24 09:54:17
|
||
* @Last Modified by: Jeffrey Wang
|
||
* @Last Modified time: 2019-01-15 11:49:09
|
||
*/
|
||
layui.define(['jquery'], function(exports){
|
||
var $ = layui.jquery;
|
||
exports('excel', {
|
||
/**
|
||
* 兼容老版本的导出函数
|
||
* @param {[type]} data [description]
|
||
* @param {[type]} filename [description]
|
||
* @param {[type]} type [description]
|
||
* @return {[type]} [description]
|
||
*/
|
||
downloadExl: function(data, filename, type) {
|
||
type = type ? type : 'xlsx';
|
||
this.exportExcel({sheet1: data}, filename+'.'+type, type, null);
|
||
},
|
||
/**
|
||
* 导出Excel并弹出下载框,具体使用方法和范围请参考文档
|
||
* @param data object
|
||
* @param {[type]} filename [description]
|
||
* @param {[type]} type [description]
|
||
* @param {[type]} opt [description]
|
||
* @return {[type]} [description]
|
||
*/
|
||
exportExcel : function(data, filename, type, opt) {
|
||
type = type ? type : 'xlsx';
|
||
filename = filename ? filename : '导出数据.'+type;
|
||
|
||
// 创建一个 XLSX 对象
|
||
var wb = XLSX.utils.book_new();
|
||
// 1. 定义excel对的基本属性
|
||
var Props = {
|
||
Title: filename,
|
||
Subject: 'Export From web browser',
|
||
Author: "excel.wj2015.com",
|
||
Manager: '',
|
||
Company: '',
|
||
Category: '',
|
||
Keywords: '',
|
||
Comments: '',
|
||
LastAuthor: '',
|
||
CreatedData: new Date(),
|
||
};
|
||
opt && opt.Props && (Props = $.extend(Props, opt.Props));
|
||
// 默认进行压缩
|
||
wb.compression = opt ? opt.compression : true
|
||
if(wb.compression !== false) {
|
||
wb.compression = true
|
||
}
|
||
wb.Props = Props;
|
||
// 特殊属性实现,比如合并单元格
|
||
var wbExtend = {
|
||
'!merges': null
|
||
,'!margins': null
|
||
,'!cols': null
|
||
,'!rows': null
|
||
,'!protect': null
|
||
,'!autofilter': null
|
||
};
|
||
opt && opt.extend && (wbExtend = $.extend(wbExtend, opt.extend));
|
||
// 清理空配置
|
||
for (var key in wbExtend) {
|
||
if (!wbExtend.hasOwnProperty(key)) {
|
||
continue;
|
||
}
|
||
if (!wbExtend[key]) {
|
||
delete wbExtend[key];
|
||
}
|
||
}
|
||
|
||
// 判断 data 如果是 sheet 级别数据,自动加 sheet1
|
||
if ($.isArray(data)) {
|
||
data = {sheet1: data};
|
||
}
|
||
|
||
for(var sheet_name in data) {
|
||
if (!data.hasOwnProperty(sheet_name)) {
|
||
continue;
|
||
}
|
||
var content = data[sheet_name];
|
||
// 2. 设置sheet名称
|
||
wb.SheetNames.push(sheet_name);
|
||
// 3. 分配工作表对象到 sheet
|
||
var is_aoa = false;
|
||
if (content.length && content[0] && $.isArray(content[0])) {
|
||
is_aoa = true;
|
||
}
|
||
if (is_aoa) {
|
||
ws = XLSX.utils.aoa_to_sheet(content);
|
||
} else {
|
||
var option = {};
|
||
if (content.length) {
|
||
option.headers = content.unshift();
|
||
option.skipHeader = true;
|
||
// 分离并重组样式
|
||
var splitRes = this.splitContent(content);
|
||
}
|
||
var ws = XLSX.utils.json_to_sheet(content, option);
|
||
// 特殊属性,支持单独设置某个sheet的属性
|
||
if (wbExtend[sheet_name]) {
|
||
$.extend(ws, wbExtend[sheet_name]);
|
||
} else {
|
||
$.extend(ws, wbExtend);
|
||
}
|
||
// 合并样式
|
||
if (typeof splitRes !== 'undefined') {
|
||
this.mergeCellOpt(ws, splitRes.style);
|
||
}
|
||
}
|
||
wb.Sheets[sheet_name] = ws;
|
||
};
|
||
|
||
// 4. 输出工作表
|
||
var wbout = XLSX.write(wb, {bookType: type, type: 'binary', cellStyles: true, compression: wb.compression});
|
||
|
||
// 5. 跨浏览器支持,采用 FileSaver 三方库
|
||
saveAs(new Blob([this.s2ab(wbout)], {type: "application/octet-stream"}), filename);
|
||
},
|
||
/**
|
||
* 分离内容和样式
|
||
* @param {[type]} content [description]
|
||
* @return {[type]} [description]
|
||
*/
|
||
splitContent: function(content) {
|
||
var styleContent = {};
|
||
// 扫描每个单元格,如果是对象则等表格转换完毕后分离出来重新赋值
|
||
for (var line = 0; line < content.length; line++) {
|
||
var lineData = content[line];
|
||
var rowIndex = 0;
|
||
for (var row in lineData) {
|
||
if (!lineData.hasOwnProperty(row)) {
|
||
continue;
|
||
}
|
||
var rowData = lineData[row];
|
||
if (typeof rowData === 'object') {
|
||
// typeof null == object
|
||
if (rowData !== null) {
|
||
styleContent[this.numToTitle(rowIndex+1)+(parseInt(line)+1)] = rowData;
|
||
} else {
|
||
lineData[row] = '';
|
||
}
|
||
} else {
|
||
// JeffreyWang 2019-03-10针对 0 的hack处理
|
||
if (rowData === 0) {
|
||
rowData = {
|
||
v: '0',
|
||
s: {
|
||
alignment: {
|
||
horizontal: 'right'
|
||
}
|
||
}
|
||
}
|
||
}
|
||
styleContent[this.numToTitle(rowIndex+1)+(parseInt(line)+1)] = rowData;
|
||
}
|
||
rowIndex++;
|
||
}
|
||
}
|
||
return {
|
||
content: content,
|
||
style: styleContent
|
||
};
|
||
},
|
||
/**
|
||
* 合并内容和样式
|
||
* @param {[type]} ws [description]
|
||
* @param {[type]} style [description]
|
||
* @return {[type]} [description]
|
||
*/
|
||
mergeCellOpt: function(ws, style) {
|
||
for (var row in style) {
|
||
if (!style.hasOwnProperty(row)) {
|
||
continue;
|
||
}
|
||
var rowOpt = style[row];
|
||
if (ws[row]) {
|
||
// 其他属性做一个初始化
|
||
var otherOpt = ['t', 'w', 'f', 'r', 'h', 'c', 'z', 'l', 's'];
|
||
for (var i = 0; i < otherOpt.length; i++) {
|
||
ws[row][otherOpt[i]] = ws[row][otherOpt[i]];
|
||
}
|
||
$.extend(ws[row], rowOpt);
|
||
}
|
||
}
|
||
},
|
||
// 测试代码:
|
||
// for(i=1;i<100;i++){var change = layui.excel.numToTitle(i);console.log(i, change, layui.excel.titleToNum(change));}
|
||
// numsToTitle备忘录提效
|
||
numsTitleCache: {},
|
||
// titleToTitle 备忘录提效
|
||
titleNumsCache: {},
|
||
/**
|
||
* 将数字(从一开始)转换为 A、B、C...AA、AB
|
||
* @param {[int]} num [description]
|
||
* @return {[type]} [description]
|
||
*/
|
||
numToTitle: function(num) {
|
||
if (this.numsTitleCache[num]) {
|
||
return this.numsTitleCache[num];
|
||
}
|
||
var ans = '';
|
||
if (num > 26) {
|
||
// 要注意小心 26 的倍数导致的无线递归问题
|
||
var dec = num % 26;
|
||
ans = this.numToTitle((num - dec)/26) + this.numToTitle(dec?dec:26);
|
||
this.numsTitleCache[num] = ans;
|
||
this.titleNumsCache[ans] = num;
|
||
return ans;
|
||
} else {
|
||
// A 的 ascii 为 0,顺位相加
|
||
ans = String.fromCharCode(64 + num);
|
||
this.numsTitleCache[num] = ans;
|
||
this.titleNumsCache[ans] = num;
|
||
return ans;
|
||
}
|
||
},
|
||
/**
|
||
* 将A、B、AA、ABC转换为 1、2、3形式的数字
|
||
* @param {[type]} title [description]
|
||
* @return {number} [description]
|
||
*/
|
||
titleToNum: function(title) {
|
||
if (this.titleNumsCache[title]) {
|
||
return this.titleNumsCache[title];
|
||
}
|
||
var len = title.length;
|
||
var total = 0;
|
||
for (var index in title) {
|
||
if (!title.hasOwnProperty(index)) {
|
||
continue;
|
||
}
|
||
var char = title[index];
|
||
var code = char.charCodeAt() - 64;
|
||
total += code * Math.pow(26, len - index - 1);
|
||
}
|
||
this.numsTitleCache[total] = title;
|
||
this.titleNumsCache[title] = total;
|
||
return total;
|
||
},
|
||
/**
|
||
* 批量设置单元格属性
|
||
* @param {array} data [sheet级别的数据]
|
||
* @param {string} range [范围字符串,比如 A1:C12,开始位置默认 A1,结束位置默认整个表格右下角]
|
||
* @param {object} config [批量设置的单元格属性]
|
||
* @param {function} filter [回调函数,传递函数生效,返回值作为新的值(可用于过滤、规则替换样式等骚操作)]
|
||
* @return {array} [重新渲染后的 sheet 数据]
|
||
*/
|
||
setExportCellStyle: function(data, range, config, filter) {
|
||
if (typeof data !== 'object' || !data.length || !data[0] || !Object.keys(data[0]).length) {
|
||
return [];
|
||
}
|
||
|
||
// 以 rowIndex 为键,field 为值
|
||
var fieldKeys = Object.keys(data[0]);
|
||
var maxCol = data.length -1;
|
||
var maxRow = fieldKeys.length - 1;
|
||
// 默认 A1 ~ 右下角
|
||
var startPos = {c: 0, r: 0};
|
||
var endPos = {c: maxCol, r: maxRow};
|
||
|
||
if (range && typeof range === 'string') {
|
||
var rangeArr = range.split(':');
|
||
if (rangeArr[0].length) {
|
||
startPos = this.splitPosition(rangeArr[0]);
|
||
}
|
||
if (typeof rangeArr[1] !== 'undefined' && rangeArr[1] !== '') {
|
||
endPos = this.splitPosition(rangeArr[1]);
|
||
}
|
||
} else {
|
||
// pass
|
||
}
|
||
// position范围限制 - 考虑到特殊情况取消此限制
|
||
// startPos.c = startPos.c < maxCol ? startPos.c : maxCol;
|
||
// endPos.c = endPos.c < maxCol ? endPos.c : maxCol;
|
||
// startPos.r = startPos.r < maxRow ? startPos.r : maxRow;
|
||
// endPos.r = endPos.r < maxRow ? endPos.r : maxRow;
|
||
|
||
if (startPos.c > endPos.c) {
|
||
console.error('开始列不得大于结束列');
|
||
}
|
||
if (startPos.r > endPos.r) {
|
||
console.error('开始行不得大于结束行');
|
||
}
|
||
|
||
// 遍历范围内的数据,进行样式设置,按从上到下从左到右按行遍历
|
||
for (var currentRow = startPos.r; currentRow <= endPos.r; currentRow++) {
|
||
for (var currentCol = startPos.c; currentCol <= endPos.c; currentCol++) {
|
||
// 如果有回调则执行回调判断,否则全部更新,如果遇到超出数据范围的,自动置空
|
||
var row = data[currentRow];
|
||
if (!row) {
|
||
row = {};
|
||
for (var key = 0; key < fieldKeys.length; key++) {
|
||
row[fieldKeys[key]] = '';
|
||
}
|
||
data[currentRow] = row;
|
||
}
|
||
var cell = row[fieldKeys[currentCol]];
|
||
var newCell = null;
|
||
if (cell === null || cell === undefined) {
|
||
cell = '';
|
||
}
|
||
|
||
// 手工合并(相同的则以当前函数config为准)
|
||
if (typeof cell === 'object') {
|
||
newCell = $.extend(true, {}, cell, config);
|
||
} else {
|
||
newCell = $.extend(true, {}, {v: cell}, config);
|
||
}
|
||
|
||
if (
|
||
typeof filter === 'function'
|
||
) {
|
||
newCell = filter(cell, newCell, row, config, currentRow, currentCol, fieldKeys[currentCol]);
|
||
} else {
|
||
}
|
||
// 回写
|
||
data[currentRow][fieldKeys[currentCol]] = newCell;
|
||
}
|
||
}
|
||
return data;
|
||
},
|
||
/**
|
||
* 合并单元格快速生成配置的函数 传入 [ ['开始坐标 A1', '结束坐标 D2'], ['开始坐标 B2', '结束坐标 E3'] ]
|
||
* @param {[type]} origin [description]
|
||
* @return {[type]} [description]
|
||
*/
|
||
makeMergeConfig: function(origin) {
|
||
var merge = [];
|
||
for (var index = 0; index < origin.length; index++) {
|
||
merge.push({
|
||
s: this.splitPosition(origin[index][0]),
|
||
e: this.splitPosition(origin[index][1]),
|
||
});
|
||
}
|
||
return merge;
|
||
},
|
||
/**
|
||
* 自动生成列宽配置
|
||
* @param {$ObjMap} data [A、B、C的宽度映射]
|
||
* @param {number} defaultNum [description]
|
||
* @return {$ObjMap} [description]
|
||
*/
|
||
makeColConfig: function(data, defaultNum) {
|
||
defaultNum = defaultNum > 0 ? defaultNum : 50;
|
||
// 将列的 ABC 转换为 index
|
||
var change = [];
|
||
var startIndex = 0;
|
||
for (var index in data) {
|
||
if (!data.hasOwnProperty(index)) {
|
||
continue;
|
||
}
|
||
var item = data[index];
|
||
if (index.match && index.match(/[A-Z]*/)) {
|
||
var currentIndex = this.titleToNum(index) - 1;
|
||
// 填充未配置的单元格
|
||
while (startIndex < currentIndex) {
|
||
change.push({wpx: defaultNum});
|
||
startIndex++;
|
||
}
|
||
startIndex = currentIndex+1;
|
||
change.push({wpx: item > 0 ? item : defaultNum});
|
||
}
|
||
};
|
||
return change;
|
||
},
|
||
/**
|
||
* 自动生成列高配置
|
||
* @param {[type]} data [description]
|
||
* @param {[type]} defaultNum [description]
|
||
* @return {[type]} [description]
|
||
*/
|
||
makeRowConfig: function(data, defaultNum) {
|
||
defaultNum = defaultNum > 0 ? defaultNum : 10;
|
||
// 将列的 ABC 转换为 index
|
||
var change = [];
|
||
var startIndex = 0;
|
||
for (var index in data) {
|
||
if (!data.hasOwnProperty(index)) {
|
||
continue;
|
||
}
|
||
var item = data[index];
|
||
if (index.match && index.match(/[0-9]*/)) {
|
||
var currentIndex = parseInt(index) - 1;
|
||
// 填充未配置的行
|
||
while (startIndex < currentIndex) {
|
||
change.push({hpx: defaultNum});
|
||
startIndex++;
|
||
}
|
||
startIndex = currentIndex+1;
|
||
change.push({hpx: item > 0 ? item : defaultNum});
|
||
}
|
||
};
|
||
return change;
|
||
},
|
||
/**
|
||
* 将A1分离成 {c: 0, r: 0} 格式的数据
|
||
* @param {string} pos [description]
|
||
* @return {{r: number, c: number}} [description]
|
||
*/
|
||
splitPosition: function(pos) {
|
||
var res = pos.match('^([A-Z]+)([0-9]+)$');
|
||
if (!res) {
|
||
return {c: 0, r: 0};
|
||
}
|
||
// 转换结果相比需要的结果需要减一转换
|
||
return {
|
||
c: this.titleToNum(res[1]) - 1,
|
||
r: parseInt(res[2]) - 1
|
||
}
|
||
},
|
||
/**
|
||
* 将二进制数据转为8位字节
|
||
* @param {[type]} s [description]
|
||
* @return {[type]} [description]
|
||
*/
|
||
s2ab: function(s) {
|
||
var buf = new ArrayBuffer(s.length);
|
||
var view = new Uint8Array(buf);
|
||
for (var i = 0; i < s.length; i++) {
|
||
view[i] = s.charCodeAt(i) & 0xFF;
|
||
}
|
||
return buf;
|
||
},
|
||
/**
|
||
* 将导出的数据格式,转换为可以aoa导出的格式
|
||
* @return {[type]} [description]
|
||
*/
|
||
filterDataToAoaData: function(filterData){
|
||
var aoaData = [];
|
||
layui.each(filterData, function(index, item) {
|
||
var itemData = [];
|
||
for (var i in item) {
|
||
if (!item.hasOwnProperty(i)) {
|
||
continue;
|
||
}
|
||
itemData.push(item[i]);
|
||
}
|
||
aoaData.push(itemData);
|
||
});
|
||
return aoaData;
|
||
},
|
||
/**
|
||
* 梳理导出的数据,包括字段排序和多余数据过滤,具体功能请参见文档
|
||
* @param {[type]} data [需要梳理的数据]
|
||
* @param {[type]} fields [支持数组和对象,用于映射关系和字段排序]
|
||
* @return {[type]} [description]
|
||
*/
|
||
filterExportData: function(data, fields) {
|
||
// PS:之所以不直接引用 data 节省内存,是因为担心如果 fields 可能存在如下情况: { "id": 'test_id', 'test_id': 'new_id' },会导致处理异常
|
||
var exportData = [];
|
||
var true_fields = [];
|
||
// filed 支持两种模式,数组则单纯排序,对象则转换映射关系,为了统一处理,将数组转换为符合要求的映射关系对象
|
||
if (Array.isArray(fields)) {
|
||
for (var i = 0; i< fields.length; i++) {
|
||
true_fields[fields[i]] = fields[i];
|
||
}
|
||
} else {
|
||
true_fields = fields;
|
||
}
|
||
for (var i = 0; i < data.length; i++) {
|
||
var item = data[i];
|
||
exportData[i] = {};
|
||
for (var key in true_fields) {
|
||
if (!true_fields.hasOwnProperty(key)) {
|
||
continue;
|
||
}
|
||
var new_field_name = key;
|
||
var old_field_name = true_fields[key];
|
||
// 如果传入的是回调,则回调的值则为新值
|
||
if (typeof old_field_name === 'function' && old_field_name.apply) {
|
||
exportData[i][new_field_name] = old_field_name.apply(window, [item[new_field_name], item, data]);
|
||
} else {
|
||
if (typeof item[old_field_name] !== 'undefined') {
|
||
exportData[i][new_field_name] = item[old_field_name];
|
||
} else {
|
||
exportData[i][new_field_name] = '';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return exportData;
|
||
},
|
||
/**
|
||
* 梳理导入的数据,参数意义可参考 filterExportData
|
||
* @param {[type]} data [description]
|
||
* @param {[type]} fields [description]
|
||
* @return {[type]} [description]
|
||
*/
|
||
filterImportData: function(data, fields) {
|
||
var that = this;
|
||
layui.each(data, function(fileindex, xlsx) {
|
||
layui.each(xlsx, function(sheetname, content) {
|
||
xlsx[sheetname] = that.filterExportData(content, fields);
|
||
});
|
||
});
|
||
return data;
|
||
},
|
||
/**
|
||
* 读取Excel,支持多文件多表格读取
|
||
* @param {[type]} files [description]
|
||
* @param {[type]} opt [description]
|
||
* @param {Function} callback [description]
|
||
* @return {[type]} [description]
|
||
*/
|
||
importExcel: function(files, opt, callback) {
|
||
var option = {
|
||
header: 'A',
|
||
range: null,
|
||
fields: null,
|
||
};
|
||
$.extend(option, opt);
|
||
var that = this;
|
||
|
||
if (files.length < 1) {
|
||
throw {code: 999, 'message': '传入文件为空'};
|
||
}
|
||
var supportReadMime = [
|
||
'application/vnd.ms-excel',
|
||
'application/msexcel',
|
||
'application/x-msexcel',
|
||
'application/x-ms-excel',
|
||
'application/x-excel',
|
||
'application/x-dos_ms_excel',
|
||
'application/xls',
|
||
'application/x-xls',
|
||
'application/vnd-xls',
|
||
'application/csv',
|
||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||
''
|
||
];
|
||
layui.each(files, function(index, item) {
|
||
if (supportReadMime.indexOf(item.type) === -1) {
|
||
throw {code: 999, message: item.name+'('+item.type+')为不支持的文件类型'};
|
||
}
|
||
});
|
||
|
||
// 按照二进制读取
|
||
var data = {};
|
||
layui.each(files, function(index, item) {
|
||
var reader = new FileReader();
|
||
if (!reader) {
|
||
throw {code: 999, message: '不支持FileReader,请更换更新的浏览器'};
|
||
}
|
||
// 读取excel表格对象
|
||
reader.onload = function(ev) {
|
||
var wb = XLSX.read(ev.target.result, {
|
||
type: 'binary'
|
||
});
|
||
var excelData = {};
|
||
layui.each(wb.Sheets, function(sheet, sheetObj) {
|
||
// 全为空的去掉
|
||
if (wb.Sheets.hasOwnProperty(sheet)) {
|
||
var opt = {
|
||
header: option.header
|
||
};
|
||
if (option.range) {
|
||
opt.range = option.range;
|
||
}
|
||
excelData[sheet] = XLSX.utils.sheet_to_json(sheetObj, opt);
|
||
// 支持梳理数据
|
||
if (option.fields) {
|
||
excelData[sheet] = that.filterExportData(excelData[sheet], option.fields);
|
||
}
|
||
}
|
||
});
|
||
data[index] = excelData;
|
||
// 全部读取完毕才执行
|
||
if (index === files.length - 1) {
|
||
callback && callback.apply && callback.apply(window, [data]);
|
||
}
|
||
};
|
||
reader.readAsBinaryString(item);
|
||
});
|
||
}
|
||
});
|
||
});
|