js证书批量生成与打包下载

时间:2023-03-09 05:02:20
js证书批量生成与打包下载

前边有提到最近的一个证书生成保存下载打印的需求。

之前实现的是一个单个操作的页面,现在把实现的批量效果和进度效果的代码展示出来。

html

 <button class="btn btn-primary" ng-click="derive()" style="margin-top: 20px;">生成证书(方案1)</button>
<button class="btn btn-primary" ng-click="derive2()" style="margin-top: 20px;">生成证书(方案2)</button>
<button class="btn btn-primary" ng-click="derive3()" style="margin-top: 20px;">生成证书(方案3)</button>
<grid-table data-control="tableControl"></grid-table>
<!-- 进度 -->
<div class="progress_cls" ng-if="progressShow">
<div class="layui-progress layui-progress-big" lay-filter="notifierProgress" lay-showpercent="yes">
<div class="layui-progress-bar" lay-percent="0%">
<!-- 解决数字不出来问题 -->
<span class="layui-progress-text">0%</span>
</div>
</div>
<div class="progress_stitistics_cls">
<div>总数:<span>{{progressTotal}}</span></div>
<div>成功:<span>{{doneNum}}</span></div>
<div>失败:<span>{{failedNum}}</span></div>
</div>
<button class="btn btn-primary" ng-click="closeProgress()" ng-if="progressDone">确定</button>
</div>
<!-- 进度2 -->
<div class="progress_cls" ng-if="progressShow2">
<div class="layui-progress layui-progress-big" lay-filter="notifierProgress2" lay-showpercent="yes">
<div class="layui-progress-bar" lay-percent="0%">
<!-- 解决数字不出来问题 -->
<span class="layui-progress-text">0%</span>
</div>
</div>
<div class="progress_stitistics_cls">
<div>总数:<span>{{progressTotal}}</span></div>
<div>当前完成:<span>{{doneNum}}</span></div>
</div>
<button class="btn btn-primary" ng-click="closeProgress()" ng-if="progressDone2">确定</button>
</div>
<!-- 进度3 -->
<div class="progress_cls" ng-if="progressShow3">
<div class="layui-progress layui-progress-big" lay-filter="notifierProgress3" lay-showpercent="yes">
<div class="layui-progress-bar" lay-percent="0%">
<!-- 解决数字不出来问题 -->
<span class="layui-progress-text">0%</span>
</div>
</div>
<div class="progress_stitistics_cls">
<div>总数:<span>{{progressTotal}}</span></div>
<div>当前完成:<span>{{doneNum}}</span></div>
</div>
<button class="btn btn-primary" ng-click="closeProgress()" ng-if="progressDone3">确定</button>
</div>

css

 #toPrint {
position:absolute;
left: 10000px;
top: 50%;
} #toPrint div {
position:absolute;
font-weight: bold;
} #toPrint img {
position:absolute;
} #textArea {
width: 100%;
height: 100%;
} .printCanvas {
display:inline-block;
} #toPrint3 {
position:absolute;
left: 10000px;
top: 50%;
display: inline-flex;
}

js

  /*
导出数据
*/
$scope.derive = function () {
var toSendList = [];
var passList = [];
for(var i=0;i<$scope.tableControl.rows.length;i++) {
if($scope.tableControl.rows[i].select) {
toSendList.push($scope.tableControl.allData[i]);
//没有数据,原5,现先1
if(1 == $scope.tableControl.allData[i].applyStatus) {
passList.push($scope.tableControl.allData[i]);
}
}
}
if(1 > toSendList.length) {
layer.alert("请选择需要生成证书的记录");
return;
}
if(toSendList.length != passList.length) {
layer.alert("只能对审核通过的记录进行证书生成");
return;
}
layer.confirm("是否确认生成证书?", {
btn: ['确定', '取消']
}, function () {
$scope.progressTotal = toSendList.length;
$scope.doneNum = 0;
$scope.failedNum = 0; layer.closeAll();
var maskLoad = layer.load(1, {shade: [0.8, '#393D49']});
//打开进度
$scope.curProgress = "0%";
$scope.progressShow = true;
$.each(toSendList, function(index, e){
var url = location.origin+"/pages/print/printBatch.html?"+encodeURIComponent(e.studentName)+"&&"+encodeURIComponent(e.applySchoolName);
//services.save_notifier(url).success(function (res) {
$.ajax({
url: location.origin+'/basic/school',
headers: {'token': $rootScope.token},
type: 'get',
dataType: 'json',
contentType: 'application/json;charset=UTF-8',
async: true,
}).success(function (res) {
if ('OK' == res.result) {
$scope.doneNum++;
} else {
$scope.failedNum++;
}
}).error(function (res) {
$scope.failedNum++;
}).always(function () {
//刷新成功和失败数量
if(($scope.doneNum + $scope.failedNum) < $scope.progressTotal) {
//更新进度
//$scope.curProgress = Math.round(($scope.doneNum + $scope.failedNum) / $scope.progressTotal * 100) + "%";
element.progress('notifierProgress', Math.round(($scope.doneNum + $scope.failedNum) / $scope.progressTotal * 100) + "%")
} else {
//完成
$scope.curProgress = "100%";
////进度条渲染需要时间??数据较少时执行完成还没渲染出来--改用定时器
var finishInterval = setInterval(function() {
if($(".layui-progress")[0]) {
element.progress('notifierProgress', "100%");
clearInterval(finishInterval);
}
}, 200);
$scope.progressDone = true;
//去掉转圈
$(".layui-layer-loading").hide();
//layer.closeAll();
}
});
}); });
} //方案2
$scope.derive2 = function () {
//先单线程
var toSendList = [];
var passList = [];
for(var i=0;i<$scope.tableControl.rows.length;i++) {
if($scope.tableControl.rows[i].select) {
toSendList.push($scope.tableControl.allData[i]);
//没有数据,原5,现先1
if(1 == $scope.tableControl.allData[i].applyStatus) {
passList.push($scope.tableControl.allData[i]);
}
}
}
if(1 > toSendList.length) {
layer.alert("请选择需要生成证书的记录");
return;
}
if(toSendList.length != passList.length) {
layer.alert("只能对审核通过的记录进行证书生成");
return;
}
layer.confirm("是否确认生成证书?", {
btn: ['确定', '取消']
}, function () {
$scope.progressTotal = toSendList.length;
$scope.doneNum = 0;
$scope.failedNum = 0; layer.closeAll();
var maskLoad = layer.load(1, {shade: [0.8, '#393D49']});
//打开进度
$scope.curProgress = "0%";
$scope.progressShow2 = true; //弹出隐藏绘图层
$("#printArea").remove();
$("body").append("<div id='printArea'><div id='toPrint'></div></div>"); //绘制证书
suitScreen($scope);
var imgStr = "<img src='" + $scope.printObj.notifierObj.url+"' style='width:"+$scope.printObj.notifierObj.width+"px;height:"+
$scope.printObj.notifierObj.height+"px'><div id='textArea'></div>";
$("#toPrint").append(imgStr);
$("#toPrint").css("margin-top", (0-$scope.printObj.notifierObj.height-60)/2+"px");
$("#toPrint").css("height", $scope.printObj.notifierObj.height+"px");
$("#toPrint").css("width", $scope.printObj.notifierObj.width+"px"); //填充文字
$.each(toSendList, function(index, e){
$("#textArea").empty();
$scope.printObj.paramList[0].objName = e.studentName;
$scope.printObj.paramList[1].objName = e.applySchoolName;
var htmlStr = "";
for(i=0;i<$scope.printObj.paramList.length;i++) {
var nowObj = $scope.printObj.paramList[i];
if(nowObj.fontSize < 12) {
htmlStr += "<div style='font-family:"+nowObj.fontFamily+";font-size:"+nowObj.fontSize+"px;top:"+nowObj.top+"px;left:"+nowObj.left+
//谷歌浏览器字体小于12px时会不再变小,使用-webkit-transform兼容,并设置已左上角作为变换原点
"px;-webkit-transform:scale("+nowObj.fontSize/12+","+nowObj.fontSize/12+");transform-origin:0 0'>"+nowObj.objName+"</div>";
} else {
htmlStr += "<div style='font-family:"+nowObj.fontFamily+";font-size:"+nowObj.fontSize+"px;top:"+nowObj.top+"px;left:"+nowObj.left+
"px'>"+nowObj.objName+"</div>";
}
}
//$("#toPrint").css("margin-left", (0-$scope.printObj.notifierObj.width)/2+"px");
$("#textArea").append(htmlStr); //保存
html2canvas(document.querySelector("#toPrint")).then(function(canvas) {
var type = 'png';//格式可以自定义
var imgData = canvas.toDataURL(type);
imgData = imgData.replace(_fixType(type),'image/octet-stream');
//文件名可以自定义
var filename = '录取通知书_' + e.studentName + '.' + type;
saveFile(imgData,filename);
$scope.doneNum++;
//刷新成功和失败数量
if(($scope.doneNum + $scope.failedNum) < $scope.progressTotal) {
//更新进度
//$scope.curProgress = Math.round(($scope.doneNum + $scope.failedNum) / $scope.progressTotal * 100) + "%";
element.progress('notifierProgress2', Math.round(($scope.doneNum + $scope.failedNum) / $scope.progressTotal * 100) + "%")
} else {
//完成
$scope.curProgress = "100%";
////进度条渲染需要时间??数据较少时执行完成还没渲染出来--改用定时器
var finishInterval = setInterval(function() {
if($(".layui-progress")[0]) {
element.progress('notifierProgress2', "100%");
clearInterval(finishInterval);
}
}, 200);
$scope.progressDone2 = true;
//去掉转圈
$(".layui-layer-loading").hide();
}
});
}); });
} //方案3
$scope.derive3 = function () {
//先单线程
var toSendList = [];
var passList = [];
for(var i=0;i<$scope.tableControl.rows.length;i++) {
if($scope.tableControl.rows[i].select) {
toSendList.push($scope.tableControl.allData[i]);
//没有数据,原5,现先1
if(1 == $scope.tableControl.allData[i].applyStatus) {
passList.push($scope.tableControl.allData[i]);
}
}
}
if(1 > toSendList.length) {
layer.alert("请选择需要生成证书的记录");
return;
}
if(toSendList.length != passList.length) {
layer.alert("只能对审核通过的记录进行证书生成");
return;
}
layer.confirm("是否确认生成证书?", {
btn: ['确定', '取消']
}, function () {
$scope.progressTotal = toSendList.length;
$scope.doneNum = 0;
$scope.failedNum = 0; layer.closeAll();
var maskLoad = layer.load(1, {shade: [0.8, '#393D49']});
//打开进度
$scope.curProgress = "0%";
$scope.progressShow3 = true; //弹出隐藏绘图层
$("#toPrint3").remove();
$("body").append("<div id='toPrint3'></div>"); suitScreen($scope); var allCanvas = $("canvas");
var zip = new JSZip();
//zip.file("readme.txt", "证书\n");
var img = zip.folder("images"); //图片加载是异步,所有用递归来做,否则前边生成的都会被最后一个覆盖
(function loop(n) {
if (n>=toSendList.length) return; var image = new Image();
image.src = $scope.printObj.notifierObj.url;
image.onload = function () { //为异步函数,所以将创建canvas放在onload中. $("#toPrint3").empty();
$("#toPrint3").append("<canvas id='toPrint_' class='printCanvas'></canvas>");
$scope.printObj.paramList[0].objName = toSendList[n].studentName;
$scope.printObj.paramList[1].objName = toSendList[n].applySchoolName; $("#toPrint_").css("margin-top", (0-$scope.printObj.notifierObj.height)/2+"px");
var canvas = document.getElementById("toPrint_");
canvas.width = $scope.printObj.notifierObj.width;
canvas.height = $scope.printObj.notifierObj.height;
var ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0, $scope.printObj.notifierObj.width, $scope.printObj.notifierObj.height);
$.each($scope.printObj.paramList, function(index, e) {
//canvas的字体不会有12px的兼容性问题
ctx.font = "bold "+e.fontSize+"px "+e.fontFamily;
//canvas写字以字体的左下角为基准,因而要再加一个字体大小的高度
ctx.fillText(e.objName, e.left, e.top+e.fontSize);
});
img.file('录取通知书_' + toSendList[n].studentName + '.png', canvas.toDataURL().substring(22), {base64: true});
$scope.doneNum++;
//刷新成功和失败数量
if(($scope.doneNum + $scope.failedNum) < $scope.progressTotal) {
//更新进度
//$scope.curProgress = Math.round(($scope.doneNum + $scope.failedNum) / $scope.progressTotal * 100) + "%";
element.progress('notifierProgress3', Math.round(($scope.doneNum + $scope.failedNum) / $scope.progressTotal * 100) + "%")
} else {
//完成
$scope.curProgress = "100%";
var finishInterval = setInterval(function() {
if($(".layui-progress")[0]) {
element.progress('notifierProgress3', "100%");
clearInterval(finishInterval);
}
}, 200);
$scope.progressDone3 = true;
//去掉转圈
$(".layui-layer-loading").hide(); zip.generateAsync({type:"blob"}).then(function(content) {
saveAs(content, "证书.zip");
});
} loop(n+1);
}
})(0); });
} $scope.closeProgress = function () {
$scope.progressShow = false;
$scope.progressDone = false;
$scope.progressShow2 = false;
$scope.progressDone2 = false;
$scope.progressShow3 = false;
$scope.progressDone3 = false;
layer.closeAll();
} //模板
$scope.printObj = {
notifierObj:{
"url": "/res/img/notifications.png",
"height": "631",
"width": "942"
},
paramList:[{
"objName":"黄大明",
"left":"133",
"top":"191",
"fontSize": "28",
"fontFamily": "KaiTi"
},{
"objName":"SXXX小学",
"left":"460",
"top":"272",
"fontSize": "28",
"fontFamily": "KaiTi"
},{
"objName":"2018",
"left":"195",
"top":"312",
"fontSize": "28",
"fontFamily": "KaiTi"
},{
"objName":"8",
"left":"325",
"top":"312",
"fontSize": "28",
"fontFamily": "KaiTi"
},{
"objName":"31",
"left":"405",
"top":"312",
"fontSize": "28",
"fontFamily": "KaiTi"
}]
} function suitScreen($scope) {
//A4横向标准
var effectiveHeight = 1240;
var effectiveWidth = 1754;
if($scope.printObj.notifierObj.width/effectiveWidth > $scope.printObj.notifierObj.height/effectiveHeight) {
//取最接近的一个属性进行自适应,并适当调小一些
var suitTimes = $scope.printObj.notifierObj.width/effectiveWidth*1.2;
} else {
var suitTimes = $scope.printObj.notifierObj.height/effectiveHeight*1.2;
}
$scope.printObj.notifierObj.width = $scope.printObj.notifierObj.width/suitTimes;
$scope.printObj.notifierObj.height = $scope.printObj.notifierObj.height/suitTimes;
for(i=0;i<$scope.printObj.paramList.length;i++) {
$scope.printObj.paramList[i].fontSize = $scope.printObj.paramList[i].fontSize/suitTimes;
$scope.printObj.paramList[i].left = $scope.printObj.paramList[i].left/suitTimes;
$scope.printObj.paramList[i].top = $scope.printObj.paramList[i].top/suitTimes;
}
} function _fixType(type) {
//imgData是一串string,base64
type = type.toLowerCase().replace(/jpg/i, 'jpeg');
var r = type.match(/png|jpeg|bmp|gif/)[0];
return 'image/' + r;
} function saveFile(data, filename) {
var save_link = document.createElementNS('http://www.w3.org/1999/xhtml', 'a');
save_link.href = data;
save_link.download = filename; //下载
var event = document.createEvent('MouseEvents');
event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
save_link.dispatchEvent(event);
} //方案3使用canvas先画好,然后批量打包保存
function drawNotifier($scope, id, img) {
//canvas需要先定位好,否则画好再动就清除了
//$("#toPrint").css("margin-left", (0-$scope.printObj.notifierObj.width)/2+"px");不可见元素,同样不考虑其左右缩进
$(id).css("margin-top", (0-$scope.printObj.notifierObj.height)/2+"px");
var canvas = document.getElementById(id.substring(1));
canvas.width = $scope.printObj.notifierObj.width;
canvas.height = $scope.printObj.notifierObj.height;
var ctx = canvas.getContext("2d");
var img=new Image();
img.src = $scope.printObj.notifierObj.url;
var deferred=$.Deferred();
var thisObj = $scope.printObj;
//img.onload=function() {
requestAnimationFrame(function() {
//需要onload方法接收,否则画不出
ctx.drawImage(img, 0, 0, thisObj.notifierObj.width, thisObj.notifierObj.height);
//写文字,且要在画好图片之后写,否则会被图片覆盖
$.each(thisObj.paramList, function(index, e) {
//canvas的字体不会有12px的兼容性问题
ctx.font = "bold "+e.fontSize+"px "+e.fontFamily;
//canvas写字以字体的左下角为基准,因而要再加一个字体大小的高度
ctx.fillText(e.objName, e.left, e.top+e.fontSize);
});
deferred.resolve(canvas.toDataURL().substring(22));
})
//}
return deferred.promise(); }

以上是三种实现方案,第一种需要一个接口,后两种不需要接口,直接前端生成。

说一下中间遇到的问题:

1.进度条里边的文字显示不出来:

用的是layui的进度条,很简单,就几行代码。

<div class="layui-progress layui-progress-big" lay-filter="notifierProgress" lay-showpercent="yes">
<div class="layui-progress-bar" lay-percent="0%">
</div>
</div>

但是进度百分比文字就是显示不出来,查看元素,官方api里是有span标签的,但是自己的一直没有,可见layui官网也是有点坑。也许一些版本或者新的版本支持吧,但是有个直接有效的解决办法就是,在内部直接写一个span标签。

      <div class="layui-progress layui-progress-big" lay-filter="notifierProgress" lay-showpercent="yes">
<div class="layui-progress-bar" lay-percent="0%">
<!-- 解决数字不出来问题 -->
<span class="layui-progress-text">0%</span>
</div>
</div>

2.进度动态更新:

当前使用的是angular框架,我使用了一个变量来动态刷新,但是实际并没有效果。最终不得不使用组件的方法:

element.progress('notifierProgress', Math.round(($scope.doneNum + $scope.failedNum) / $scope.progressTotal * 100) + "%")

其中第一个参数就是前边的lay-filter

3.对于需要进行绘制又不想让用户看到:

很简单的方法,直接给你需要操作的元素绝对定位,然后给他一个很大很大的left(当然上下左右都可以),这样他就飘到屏幕的十万八千里外了,用户除非自己F12看,否则永远也看不到。这个方法之前在上一家公司有使用过。

4.对于canvas画图片异步加载的问题:

这个问题困扰了我很久,因为我要做批量绘制,使用同一个dom,绘制完一个然后清空再绘制下一个。早早就写完了逻辑,一运行也没有报错,燃鹅,打开文件一看,全都是最后一条数据生成的图片。已经使用了image.onload进行绘制了还出现这种问题。然后打断点调试,发现each遍历完了,才走进onload事件。我就又尝试着用定时器、用回调,都没有成功,而且这样比较容易出现问题。最终找到了一个解决办法,那就是用递归。代码如下

 //图片加载是异步,所有用递归来做,否则前边生成的都会被最后一个覆盖
(function loop(n) {
if (n>=toSendList.length) return; var image = new Image();
image.src = $scope.printObj.notifierObj.url;
image.onload = function () { //为异步函数,所以将创建canvas放在onload中. $("#toPrint3").empty();
$("#toPrint3").append("<canvas id='toPrint_' class='printCanvas'></canvas>");
$scope.printObj.paramList[0].objName = toSendList[n].studentName;
$scope.printObj.paramList[1].objName = toSendList[n].applySchoolName; $("#toPrint_").css("margin-top", (0-$scope.printObj.notifierObj.height)/2+"px");
var canvas = document.getElementById("toPrint_");
canvas.width = $scope.printObj.notifierObj.width;
canvas.height = $scope.printObj.notifierObj.height;
var ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0, $scope.printObj.notifierObj.width, $scope.printObj.notifierObj.height);
$.each($scope.printObj.paramList, function(index, e) {
//canvas的字体不会有12px的兼容性问题
ctx.font = "bold "+e.fontSize+"px "+e.fontFamily;
//canvas写字以字体的左下角为基准,因而要再加一个字体大小的高度
ctx.fillText(e.objName, e.left, e.top+e.fontSize);
});
img.file('录取通知书_' + toSendList[n].studentName + '.png', canvas.toDataURL().substring(22), {base64: true});
$scope.doneNum++;
//刷新成功和失败数量
if(($scope.doneNum + $scope.failedNum) < $scope.progressTotal) {
//更新进度
//$scope.curProgress = Math.round(($scope.doneNum + $scope.failedNum) / $scope.progressTotal * 100) + "%";
element.progress('notifierProgress3', Math.round(($scope.doneNum + $scope.failedNum) / $scope.progressTotal * 100) + "%")
} else {
//完成
$scope.curProgress = "100%";
var finishInterval = setInterval(function() {
if($(".layui-progress")[0]) {
element.progress('notifierProgress3', "100%");
clearInterval(finishInterval);
}
}, 200);
$scope.progressDone3 = true;
//去掉转圈
$(".layui-layer-loading").hide(); zip.generateAsync({type:"blob"}).then(function(content) {
saveAs(content, "证书.zip");
});
} loop(n+1);
}
})(0);

还有在调试中使用的一些方法,其中:

 var deferred=$.Deferred();
var thisObj = $scope.printObj;
//img.onload=function() {
requestAnimationFrame(function() {
//需要onload方法接收,否则画不出
ctx.drawImage(img, 0, 0, thisObj.notifierObj.width, thisObj.notifierObj.height);
//写文字,且要在画好图片之后写,否则会被图片覆盖
$.each(thisObj.paramList, function(index, e) {
//canvas的字体不会有12px的兼容性问题
ctx.font = "bold "+e.fontSize+"px "+e.fontFamily;
//canvas写字以字体的左下角为基准,因而要再加一个字体大小的高度
ctx.fillText(e.objName, e.left, e.top+e.fontSize);
});
deferred.resolve(canvas.toDataURL().substring(22));
})
//}
return deferred.promise();

给方法添加回调,使用promise,这样在调用这个方法时就可以用then进行接收了。

requestAnimationFrame,一个用的好比较有意思的window方法,类似timeout和interval,但是不需要设置时间,此处可以代替onload使用。

5.canvas转base64与文件操作:

 //canvas转图片

 canvas.toDataURL().substring(22)

 //js新建文件和文件填充

 var zip = new JSZip();
//zip.file("readme.txt", "证书\n");
var img = zip.folder("images"); img.file('录取通知书_' + toSendList[n].studentName + '.png', canvas.toDataURL().substring(22), {base64: true}); //js文件打包下载 zip.generateAsync({type:"blob"}).then(function(content) {
saveAs(content, "证书.zip");
});

用到的插件:FileSaver.js, jszip.min.js。用法也比较简单。

6.数据较少时,进度条无法刷新到100%的问题:

数据较少,用时较少,然后进度条渲染出来了,但是一直停在0%。没有研究源码,不知道是因为异步加载的问题还是因为css动画的样式问题还是因为渲染组件异步的问题,总之就是在执行完之后,打断点审查元素并没有生成组件,所有此时更新进度100%也没用。

解决方法:使用定时器

     var finishInterval = setInterval(function() {
if($(".layui-progress")[0]) {
element.progress('notifierProgress3', "100%");
clearInterval(finishInterval);
}
}, 200);

一般遇到这种异步造成还没渲染完成就做了处理的问题,使用定时器或者timeout都是一种解决办法。