一、前言
问题由来,接手公司旧版本的 angular项目进行维护,该项目还是 极低的angular1原始版本,然后要进行新的功能添加导出excel,网上找各类都是 angular4之类的插件不适用,好吧只能凭着之前纯js方法写Excel导出功能,这样其实也就同样可以适用于 vue/react等前端框架,当然各框架本身都有比较好 功能齐全的 Excel导出插件。
二、思路
大致是 前端向后端条件查询 返回 json数据,前端js通过 一份数据参数对照表 将json数据 filter转化及 构造成excel数据结构(html-table)dom模板,然后创建a标签,将模板以 Blob对象 插入a标签href,然后点击导出excel
三、实现
1. excel导出功能-主体代码
// 导出excel功能
$scope.exportExcelClick = function () {
var str = '?';
var param = {
orderCode: $scope.condition.orderCode,
orderStatus: $scope.condition.orderStatus,
vin: $scope.condition.vin,
beginDate: $scope.condition.beginDate === ''?"": typeof ($scope.condition.beginDate) === 'string'?$scope.condition.beginDate : $scope.condition.beginDate.format("yyyy-MM-dd 00:00:00"),
endDate: $scope.condition.endDate === ''?"": typeof ($scope.condition.endDate) === 'string'?$scope.condition.endDate : $scope.condition.endDate.format("yyyy-MM-dd 23:59:59")
}
if ($scope.checkDateSelect(param)) {
// 发请求
WaybillStatusService.exportExcelClickExport(param).success(function (result) {
if (result.success) {
const jsonData = result.data.data
// 参数对照表,filter 数据状态转化,例: 10-'发运' 20-'未发运' 30-'已运抵'
const now_tableContent = [
{ prop: 'orderCode', label: '指令号', filter: null },
{ prop: 'vin', label: '车架号', filter: null },
{ prop: 'waybillType', label: '运单类型', filter: 'waybillAgainPieWaybillType' },
{ prop: 'name', label: '司机', filter: null },
{ prop: 'tmsDriverNo', label: '送车证号', filter: null },
{ prop: 'departureProvince', label: '起运省', filter: null },
{ prop: 'departureCity', label: '起运市', filter: null },
{ prop: 'destProvince', label: '目的省', filter: null },
{ prop: 'destCity', label: '目的市', filter: null },
{ prop: 'orderStatus', label: '订单状态', filter: null }, // filter: 'waybillStatusListOrderStatus'
{ prop: 'tmsCreateTime', label: '创建时间', filter: null }
];
// 创建 table_content demo-数据拼接
const table_content_str = $scope.jsonToTable(jsonData, now_tableContent)
// Worksheet名
var worksheetName = '订单状态列表'
var now_url = 'data:application/vnd.ms-excel;base64,'; // xls
//下载的表格模板数据
var template = '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel"> '
template += '<head><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet>'
template += '<x:Name>' + worksheetName + '</x:Name>'
template += '<x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet>'
template += '</x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]-->'
template += '</head><body><table>' + table_content_str + '</table></body></html>'
// 下载模板
// 方法一
// 数据量较小可用,数据量 比如几千条数据时候,执行会导致失败,chrome浏览器会报 下载失败,网络错误,故放弃此方法
// var myA = document.createElement('a');
// myA.setAttribute('href', now_url + $scope.base64(template))
// myA.setAttribute('download', worksheetName +".xls")
// myA.click()
// 方法二 blob对象
var blob = new Blob([template], {type: "application/vnd.ms-excel;base64"});
var objectUrl = URL.createObjectURL(blob);
var a = document.createElement('a');
document.body.appendChild(a);
a.setAttribute('style', 'display:none');
a.setAttribute('href', objectUrl);
a.setAttribute('download', worksheetName);
a.click();
URL.revokeObjectURL(objectUrl);
} else {
$rootScope.hycadmin.toast({
title: result.message,
timeOut: 3000
});
}
});
}
};
2.根据table参数对照表 json数据 进入返回 table的内容 table_content_demo
// jsonToTable => excel demo ,根据 json 和table参数对照表返回 table的内容 table_content_demo
$scope.jsonToTable = function(json, tableContent) {
const jsonData = json
const tableContentData = tableContent
var all_tr_str = ''
var tr_str = ''
const all_th_str = $scope.returnTableThDemo(tableContent)
for(var i = 0 ; i < jsonData.length ; i++ ){
tr_str = '<tr>'
for (var b = 0; b < tableContentData.length; b++) {
tr_str += $scope.returnTableTrDemo(b, jsonData[i], tableContentData[b].prop, tableContentData[b].filter)
}
tr_str += '</tr>'
// console.log('tr_str', tr_str)
all_tr_str += tr_str
}
return all_th_str + all_tr_str
};
3.根据table参数对照表 表头集合th_demo
// 根据 参数对照表 返回 表头集合th_demo
$scope.returnTableThDemo = function(tableContent) {
var th_str ='<th>';
var th_content = ''
for (var b = 0; b < tableContent.length; b++) {
th_content = th_content + '<td>' + tableContent[b].label + '</td>';
}
th_str = th_content + '</th>';
return th_str
};
4.根据 参数对照表 返回 每行tr_demo
重点 now_td_str = $filter(filter)(v) // 根据 filter 数据状态 转换 字符串名称显示
$filter 为 controller 挂载进来,需另外配置 filter文件
.controller('WaybillStatusCtrl', ['$rootScope', '$scope', '$modal', '$filter', 'WaybillService', 'WaybillStatusService',
function ($rootScope, $scope, $modal, $filter, WaybillService, WaybillStatusService){
.....
})
// 根据 参数对照表 返回 每行tr_demo
$scope.returnTableTrDemo = function(index, item, props, filter) {
var tr_content_str = ''
for(var prop in item){
if (props === prop) {
// 如果值 非null 或 为 0 则 正常显示值
var now_td_str = ''
var new_str = ''
if (filter) {
if (item[prop]|| (item[prop] === 0)) {
now_td_str = $filter(filter)(item[prop]) // 根据 filter 数据状态 转换 字符串名称显示
tr_content_str += "<td>" + now_td_str + "</td>";
} else {
tr_content_str += "<td>\t</td>";
}
} else {
if (item[prop] || (item[prop] === 0)) {
tr_content_str += "<td>" + item[prop] + "\t</td>";
} else {
// 如果值 为null 则 显示空
tr_content_str += "<td>\t</td>";
}
}
}
}
return tr_content_str
};
5. 自行配置各自的 angular-filter.js
/**
* Created by ziHou on 2019/1/17.
*/
'use strict';
var adminFilter = angular.module('admin.filter', [])
// const that = this
.filter('parseTime',function() {
return function(time, cFormat) {
if (time === '' || time === null) {
return ''
}
// 没有定义过滤格式
if (!cFormat) {
if ((time + '').length === 13 || (/000$/).test(time)) {
time = parseInt(time)
cFormat = '{y}-{m}-{d}'
}
}
time = new Date(time)
if (arguments.length === 0) {
return null
}
if ((time + '').length === 10) {
time = +time * 1000
}
const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
var date
if (typeof time === 'object') {
date = time
} else {
date = new Date(parseInt(time))
}
const formatObj = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
a: date.getDay()
}
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, function (result, key) {
var value = formatObj[key]
if (key === 'a') return ['一', '二', '三', '四', '五', '六', '日'][value - 1]
if (result.length > 0 && value < 10) {
value = '0' + value
}
return value || 0
})
return time_str
}
})
.filter('waybillAgainPieWaybillType',function() {
return function (status) {
const v = parseInt(status)
if (v === 10) {
return "普通运单";
} else if (v === 20) {
return "急发单";
} else if (v === 30) {
return "未配钥匙";
}
}
})
.filter('waybillAgainPieWaybillStatus',function() {
return function (status) {
if (status) {
return "已派单";
} else {
return "未派单";
}
}
})
6. 输出base64 ,方法二 不使用 base64
$scope.base64 = function(s) { return window.btoa(unescape(encodeURIComponent(s))) };
四、结言
中间难点 其一 $filter 各数据状态 转化显示 表义信息,这个问题困扰我很久其实主要还是项目angular版本低 filter使用引入限制,翻阅各类前端filter使用及其详解才最后解决,本来是用的笨办法 直接 每个 filter 单独写个过滤 当然那很蠢.....
其二 Blob对象 使用 避免 json数据量过大时 转化报错 亲测2W条数据excel都可以导出,
其三 就是 demo-table 整个结构拼接 需要些html 逻辑拼接 就是脑海里要 有整个table-demo拆分的结构思维,其实最简单的就是 一点点 细分 一点点从外到内 到最细 慢慢拆分 将table-demo结构拆分成一点点,这样最后再 拼接构成整个execl-demo