一、导包
1、导入
yarn add jspdf
yarn add jspdf-autotable
2、界面引入
import jsPDF from'jspdf';// 表格插件,这次我这边没用到, 根据需求选择import'jspdf-autotable';
3、创建pdf对象
// 创建一个新的PDF文档实例// 定义页面边距constPAGE_MARGIN=10;const doc =newjsPDF({unit:'mm',// 单位,本示例为mmformat:'a4',// 页面大小orientation:'portrait',// 页面方向,portrait: 纵向,landscape: 横向putOnlyUsedFonts:true,// 只包含使用的字体compress:true,// 压缩文档precision:16// 浮点数的精度});// 设置第一页内容
doc.setPage(1);// 设置字体,第二个参数为fontStyle,引入的字体是什么fontStyle就设置成什么,如果这里要使用blod粗体,则需要再引入转换后的粗体字体文件
doc.setFont('SourceHanSerifCN-Regular','normal');// 添加第一页内容
doc.addImage(getAssetsFile('images/xfaq.png'),'JPEG',13,10,183,50);// 添加标题
doc.setFontSize(20);// 调整为适合你的字体大小
二、引入中文字体
1、下载思源黑体字体
链接: 下载地址
2、转换字体,打开jspdf提供的在线字体转化网站:
链接: 转换地址
不同的字体样式,选择不同的fontStyle
转换完毕后:
3、引入转换后字体文件
// 引入转换后字体文件import'@/assets/fonts/SourceHanSerifCN-Regular-normal.js';import'@/assets/fonts/SourceHanSerifCN-SemiBold-bold.js';
4、界面使用
// 设置字体,第二个参数为fontStyle,引入的字体是什么fontStyle就设置成什么,// 如果这里要使用blod粗体,则需要再引入转换后的粗体字体文件
doc.setFont('SourceHanSerifCN-Regular','normal');
doc.setFont('SourceHanSerifCN-SemiBold','bold');// 请确保已经引入了粗体字体
三、引入echarts图表
对于pdf中图表来说,目前采用echarts图表方式比较合适,
注意事项:
1、多图表渲染比较耗时, 其中要解决同步异步的问题, 采用 await 方式解决
2、需要等到上一个页面的图表渲染完毕后,才能进入到下一页的内容生成, 否则会出现图表错页的问题
1、option 数据
let onePdfEchartsCfg =ref([{option:{title:[{text:'设备总计',x:'200',// 这里将 x 的值调整为 '20'y:'0',// 这里将 y 的值调整为 '20'textStyle:{fontSize:20,fontWeight:'bold'}},{text: deviceTypeSumDataTotal.value,subtext:'设备总数',x:'center',y:'center',textStyle:{fontSize:16,fontWeight:'bold'}}],tooltip:{trigger:'item',formatter:'{a} <br/>{b}: {c} ({d}%)'},legend:{top:'middle',right:'5%',orient:'vertical',icon:'circle',formatter:function(name){const seriesData = deviceTypeSumData.value;const total = seriesData.reduce((acc, cur)=> acc + cur.value,0);const dataIndex = seriesData.findIndex(item=> item.name === name);const value = seriesData[dataIndex].value;// 添加条件判断,避免除法错误const percentage = total !==0?((value / total)*100).toFixed(0):0;return`${name}${value}${percentage}%`;}},series:[{name:'Access From',type:'pie',radius:['40%','70%'],avoidLabelOverlap:false,label:{show:false,position:'center'},emphasis:{label:{show:true,fontSize:40,fontWeight:'bold'}},labelLine:{show:false},data: deviceTypeSumData.value
}]},x:-20,y:135,width:120,height:50,data: deviceTypeSumData.value
},{option:{title:[{text:'设备在线率',x:'200',// 这里将 x 的值调整为 '20'y:'0',// 这里将 y 的值调整为 '20'textStyle:{fontSize:20,fontWeight:'bold'}},{text: deviceTypeSumDataTotal.value,subtext:'设备总数',x:'center',y:'center',textStyle:{fontSize:16,fontWeight:'bold'}}],tooltip:{trigger:'item',formatter:'{a} <br/>{b}: {c} ({d}%)'},legend:{top:'middle',right:'5%',orient:'vertical',icon:'circle',formatter:function(name){const seriesData = deviceOnlineData.value;const total = seriesData.reduce((acc, cur)=> acc + cur.value,0);const dataIndex = seriesData.findIndex(item=> item.name === name);const value = seriesData[dataIndex].value;const percentage =((value / total)*100).toFixed(0);return`${name}${value}${percentage}%`;}},series:[{name:'Access From',type:'pie',radius:['40%','70%'],avoidLabelOverlap:false,label:{show:false,position:'center'},emphasis:{label:{show:true,fontSize:40,fontWeight:'bold'}},labelLine:{show:false},data: deviceOnlineData.value
}]},x:80,y:135,width:120,height:50}]);
2、添加图表
// 在第二页添加echarts图表
onePdfEchartsCfg.value.forEach(async({ option, x, y, width, height, data }, index)=>{// 插入图表图片到 jsPDF 文档awaitgenerateOnePDF(option, doc, x, y, width, height, data, index);});
3、图表方法
functiongenerateOnePDF(option, doc, x, y, width, height, data, index){// 创建一个包含 ECharts 图表的 div 元素const chartContainer = document.createElement('div');
chartContainer.style.width ='700px';
chartContainer.style.height ='300px';
chartContainer.style.marginLeft ='50px';// 设置左边距
document.body.appendChild(chartContainer);// 使用 ECharts 在 chartContainer 中生成图表const chart = echarts.init(chartContainer);
chart.setOption(option);// 监听图表渲染完成事件
chart.on('finished',()=>{// 获取 ECharts 图表的数据 URLconst dataURL = chart.getDataURL({type:'png'});// 移除图表容器
document.body.removeChild(chartContainer);// 将图表数据 URL 转换为图像const img =newImage();
img.src = dataURL;// 指定图表的页码
doc.setPage(2);// 在 PDF 中添加图表图像
doc.addImage(img,'JPEG', x, y, width, height);// 如果是最后一个图表,生成下一页, 解决图表错页的问题if(index === onePdfEchartsCfg.value.length -1){generateTwoPage(doc);}});}
四、完整代码
目前缺少后端接口支撑, 前端先mock一些数据展示, 代码还有很多优化的空间,后续抽时间再调整一波, 有问题或者好的建议,随时评论哈
const mockData =ref({'11月':{oneTitleText:'XXXXX月报',oneDateText:'2023年11月01日-2023年11月30日',oneCompanyNameContent:'XXXXX公司',oneBottomDateText:'2023年11月30日',overviewModule:{allDevice:'3',addDevice:'3',allCompany:'3',addCompany:'3',addsDevice:'3',addsCompany:'3',addAlarm:'0',stopAlarm:'0',stopAlarmTime:'0',waitAlarm:'0',trueFireAlarm:'0',addWarnDevice:'0',stopWarnDevice:'0',waitWarnDevice:'0',deviceOnlineRate:'0',onlineDevice:'0',offlineDevice:'0'},fireDeviceModule:{// 缺图表的数据},alarmModule:{// 缺图表的数据},faultModule:{// 缺图表的数据}},'10月':{oneTitleText:'XXXXX月报',oneDateText:'2023年10月01日-2023年10月31日',oneCompanyNameContent:'XXXXX公司',oneBottomDateText:'2023年10月31日',overviewModule:{allDevice:'3',addDevice:'3',allCompany:'3',addCompany:'3',addsDevice:'3',addsCompany:'3',addAlarm:'0',stopAlarm:'0',stopAlarmTime:'0',waitAlarm:'0',trueFireAlarm:'0',addWarnDevice:'0',stopWarnDevice:'0',waitWarnDevice:'0',deviceOnlineRate:'0',onlineDevice:'0',offlineDevice:'0'},fireDeviceModule:{// 缺图表的数据},alarmModule:{// 缺图表的数据},faultModule:{// 缺图表的数据}},'2022年':{oneTitleText:'XXXXX年报',oneDateText:'2022年01月01日-2022年12月31日',oneCompanyNameContent:'XXXXX公司',oneBottomDateText:'2022年12月31日',overviewModule:{allDevice:'3',addDevice:'3',allCompany:'3',addCompany:'3',addsDevice:'3',addsCompany:'3',addAlarm:'0',stopAlarm:'0',stopAlarmTime:'0',waitAlarm:'0',trueFireAlarm:'0',addWarnDevice:'0',stopWarnDevice:'0',waitWarnDevice:'0',deviceOnlineRate:'0',onlineDevice:'0',offlineDevice:'0'},fireDeviceModule:{// 缺图表的数据},alarmModule:{// 缺图表的数据},faultModule:{// 缺图表的数据}}});// 预览报告constpreviewBt=o=>{
console.info(o);let name ='';if(o.name.includes('月')){
name ='运营商运营消防安全月报-2023年'+ o.name;}else{
name ='运营商运营消防安全年报-'+ o.name;}// 缺少去后台拉取数据接口 模拟
pdfData = mockData.value[o.name];const doc =productReport('preview', name);};// 下载报告constdownloadBt=o=>{
console.info(o);let name ='';if(o.name.includes('月')){
name ='运营商运营消防安全月报-2023年'+ o.name;}else{
name ='运营商运营消防安全年报-'+ o.name;}// 缺少去后台拉取数据接口
pdfData = mockData.value[o.name];const doc =productReport('download', name);};functionproductReport(type, name){// 显示加载动画const loading = ElLoading.service({lock:true,text:'正在生成报告',background:'rgba(0, 0, 0, 0.7)'});// 创建一个新的PDF文档实例// 定义页面边距constPAGE_MARGIN=10;const doc =newjsPDF({unit:'mm',// 单位,本示例为mmformat:'a4',// 页面大小orientation:'portrait',// 页面方向,portrait: 纵向,landscape: 横向putOnlyUsedFonts:true,// 只包含使用的字体compress:true,// 压缩文档precision:16// 浮点数的精度});// 设置第一页内容
doc.setPage(1);// 设置字体,第二个参数为fontStyle,引入的字体是什么fontStyle就设置成什么,如果这里要使用blod粗体,则需要再引入转换后的粗体字体文件
doc.setFont('SourceHanSerifCN-Regular','normal');// 添加第一页内容
doc.addImage(getAssetsFile('images/xfaq.png'),'JPEG',13,10,183,50);// 添加标题
doc.setFontSize(20);// 调整为适合你的字体大小const textWidth =(doc.getStringUnitWidth(pdfData.oneTitleText)* doc.internal.getFontSize())/ doc.internal.scaleFactor;const textX =(doc.internal.pageSize.width - textWidth)/2;const textY =80;// 调整为适合你的位置
doc.text(pdfData.oneTitleText, textX, textY);// 添加日期const dateFontSize =8;
doc.setFontSize(dateFontSize);const dateWidth =(doc.getStringUnitWidth(pdfData.oneDateText)* dateFontSize)/ doc.internal.scaleFactor;const dateX =(doc.internal.pageSize.width - dateWidth)/2;const dateY = textY + doc.getTextDimensions(pdfData.oneTitleText).h +5;
doc.text(pdfData.oneDateText, dateX, dateY);// 计算 "值守商名称:" 文本宽度const companyNameLabel ='值守商名称:';const companyNameLabelFontSize =12;const companyNameLabelWidth =(doc.getStringUnitWidth(companyNameLabel)* companyNameLabelFontSize)/ doc.internal.scaleFactor;// 计算 文本宽度const companyNameContentFontSize =12;const companyNameContentWidth =(doc.getStringUnitWidth(pdfData.oneCompanyNameContent)* companyNameContentFontSize)/ doc.internal.scaleFactor;// 计算文本总宽度const totalWidth = Math.max(companyNameLabelWidth, companyNameContentWidth);// 计算居中位置const center =(doc.internal.pageSize.width - totalWidth)/2;// 添加 "值守商名称:" 文本
doc.setFontSize(companyNameLabelFontSize);
doc.text(companyNameLabel, center -10, dateY + doc.getTextDimensions(pdfData.oneDateText).h +50);// 计算 的位置const companyNameContentY = dateY + doc.getTextDimensions(pdfData.oneDateText).h +50;// 添加 文本
doc.setFontSize(companyNameContentFontSize);
doc.text(pdfData.oneCompanyNameContent, center +20, companyNameContentY);// 添加下方的横线
doc.line(90, companyNameContentY +2, doc.internal.pageSize.width -50, companyNameContentY +2);// 添加编制人员const authorText ='编制人员:';
doc.text(authorText, center -10,152);
doc.line(90,152+2, doc.internal.pageSize.width -50,152+2);// 添加审核人员const reviewerText ='审核人员:';
doc.text(reviewerText, center -10,161);
doc.line(90,161+2, doc.internal.pageSize.width -50,161+2);// 添加底部日期const bottomDateFontSize =12;const bottomDateWidth =(doc.getStringUnitWidth(pdfData.oneBottomDateText)* bottomDateFontSize)/ doc.internal.scaleFactor;// 计算底部日期文本居中位置const bottomDateX =(doc.internal.pageSize.width - bottomDateWidth)/2;
doc.text(pdfData.oneBottomDateText, bottomDateX,270);// 新建第二页
doc.addPage();// 设置第二页内容
doc.setPage(2);
doc.text(pdfData.oneTitleText, dateX,10);
doc.line(10,10+5, doc.internal.pageSize.width -10,10+5);// 在第二页添加标题图标const titleIconX =10;const titleIconY =30;const titleIconWidth =5;const titleIconHeight =5;
doc.addImage(getAssetsFile('images/overview.png'),'PNG', titleIconX, titleIconY, titleIconWidth, titleIconHeight);// 在第二页添加标题文本const titleText1 ='运营总览';
doc.setFontSize(12);
doc.setFont('SourceHanSerifCN-SemiBold','bold');// 请确保已经引入了粗体字体
doc.text(titleText1,16,34);
doc.setFont('SourceHanSerifCN-Regular','normal');// 恢复正常字体
doc.setFontSize(10.5);
doc.text('本月新增设备接入'+
pdfData.overviewModule.addDevice +'个,新增单位接入'+
pdfData.overviewModule.addCompany +'家,截止'+
pdfData.oneBottomDateText +',累计接入设备数'+
pdfData.overviewModule.addsDevice +'个,单位数'+
pdfData.overviewModule.addsCompany +'家;',14,40);
doc.text('本月平台接收到报警'+
pdfData.overviewModule.addAlarm +'个,已处理'+
pdfData.overviewModule.stopAlarm +'个,遗留未处理'+
pdfData.overviewModule.waitAlarm +'个,发生真实火警'+
pdfData.overviewModule.trueFireAlarm +'起;',14,45);
doc.text('本月平台接收到设备故障'+
pdfData.overviewModule.addWarnDevice +'个,已处理'+
pdfData.overviewModule.stopWarnDevice +'个,遗留未处理'+
pdfData.overviewModule.waitWarnDevice +'个;',14,50);
doc.text('平台设备在线率为'+
pdfData.overviewModule.deviceOnlineRate +'%,截止'+
pdfData.oneBottomDateText +',有'+
pdfData.overviewModule.offlineDevice +'个设备处于离线状态;',14,55);// 绘制矩形 x y width heightconst backgroundColor =[247,247,247];// 灰色背景色 RGB
doc.setFillColor.apply(doc, backgroundColor);
doc.rect(10,60,80,20,'F');// 在矩形内添加文本
doc.setTextColor(0,0,0);// 设置文本颜色为黑色
doc.setFontSize(12);
doc.text('资源接入统计',15,65);
doc.setFontSize(10.5);
doc.addImage(getAssetsFile('images/overview.png'),'PNG',15,67.5,4,4);
doc.text('设备数 '+
pdfData.overviewModule.allDevice +' (在线率:'+
pdfData.overviewModule.deviceOnlineRate +'%) 新增 '+
pdfData.overviewModule.addDevice +'',20,71);
doc.addImage(getAssetsFile('images/overview.png'),'PNG',15,72.5,4,4);
doc.text('单位数 '+ pdfData.overviewModule.allCompany +' 新增 '+ pdfData.overviewModule.addCompany +'',20,76);// 绘制矩形 x y width height
doc.setFillColor.apply(doc, backgroundColor);
doc.rect(100,60,80,20,'F');// 在矩形内添加文本
doc.setTextColor(0,0,0);// 设置文本颜色为黑色
doc.setFontSize(12);
doc.text('报警处理',105,65);
doc.setFontSize(10.5);
doc.addImage(getAssetsFile('images/overview.png'),'PNG',105,67.5,4,4);
doc.text('报警总数 '+ pdfData.overviewModule.addAlarm +' 真实火警 '+ pdfData.overviewModule.trueFireAlarm +'',110,71);
doc.addImage(getAssetsFile('images/overview.png'),'PNG',105,72.5,4,4);
doc.text('遗留未处理 '+ pdfData.overviewModule.waitAlarm +'',110,76);// 绘制矩形 x y width height
doc.setFillColor.apply(doc, backgroundColor);
doc.rect(10,85,80,20,'F');// 在矩形内添加文本
doc.setTextColor(0,0,0);// 设置文本颜色为黑色
doc.setFontSize(12);
doc.text('故障处理',15,90);
doc.setFontSize(10.5);
doc.addImage(getAssetsFile('images/overview.png'),'PNG',15,92.5,4,4);
doc.text('故障总数 '+ pdfData.overviewModule.addWarnDevice +'',20,96);
doc.addImage(getAssetsFile('images/overview.png'),'PNG',15,97.5,4,4);
doc.text('遗留未处理 '+ pdfData.overviewModule.waitWarnDevice +'',20,101);// 在第二页添加标题图标
doc.addImage(getAssetsFile('images/overview.png'),'PNG',10,110,5,5);// 在第二页添加标题文本
doc.setFontSize(12);
doc.setFont('SourceHanSerifCN-SemiBold','bold');// 请确保已经引入了粗体字体
doc.text('消防设备统计',16,114);
doc.setFont('SourceHanSerifCN-Regular','normal');// 恢复正常字体
doc.setFontSize(10.5);
doc.text('本月新增设备接入'+
pdfData.overviewModule.addDevice +'个,截止'+
pdfData.oneBottomDateText +',累计接入设备数'+
pdfData.overviewModule.allDevice +'个',14,120);
doc.text('设备品类涵盖独立式烟温感系统,电气火灾系统',14,125);
doc.text('平台设备在线率为'+
pdfData.overviewModule.deviceOnlineRate +'%,其中独立式烟温感系统离线率最高,截止'+
pdfData.oneBottomDateText +',有'+
pdfData.overviewModule.offlineDevice +'个设备处于离线状态',14,130);// 绘制矩形 x y width height
doc.setFillColor.apply(doc, backgroundColor);
doc.rect(10,185,190,40,'F');// 在矩形内添加文本
doc.setTextColor(0,0,0);// 设置文本颜色为黑色
doc.setFontSize(12);
doc.text('设备在线率:'+
pdfData.overviewModule.deviceOnlineRate +'%(在线数/总数:'+
pdfData.overviewModule.onlineDevice +'/'+
pdfData.overviewModule.allDevice +')',15,190);
doc.addImage(getAssetsFile('images/overview.png'),'PNG',15,196.5,4,4);
doc.setFontSize(10.5);
doc.text('防排烟系统 0% (0/0)',20,200);
doc.addImage(getAssetsFile('images/overview.png'),'PNG',15,201.5,4,4);
doc.text('独立式烟温感系统 0% (0/1)',20,205);
doc.addImage(getAssetsFile('images/overview.png'),'PNG',15,206.5,4,4);
doc.text('视频监控系统 0% (0/0)',20,210);// 在矩形内添加文本
doc.addImage(getAssetsFile('images/overview.png'),'PNG',80,196.5,4,4);
doc.setFontSize(10.5);
doc.text('可燃气体系统 0% (0/0)',85,200);
doc.addImage(getAssetsFile('images/overview.png'),'PNG',80,201.5,4,4);
doc.text('电气火灾系统 0% (0/2)',85,205);
doc.addImage(getAssetsFile('images/overview.png'),'PNG',80,206.5,4,4);
doc.text('消防用水系统 0% (0/0)',85,210);// 在矩形内添加文本
doc.addImage(getAssetsFile('images/overview.png'),'PNG',145,196.5,4,4);
doc.setFontSize(10.5);
doc.text('火灾报警系统 0% (0/0)',150,200);
doc.addImage(getAssetsFile('images/overview.png'),'PNG',145,201.5,4,4);
doc.text('充电桩系统 0% (0/0)',150,205);
doc.addImage(getAssetsFile('images/overview.png'),'PNG',145,206.5,4,4);
doc.text('其他系统 0% (0/0)',150,210);// 在第二页添加echarts图表
onePdfEchartsCfg.value.forEach(async({ option, x, y, width, height, data }, index)=>{// 插入图表图片到 jsPDF 文档awaitgenerateOnePDF(option, doc, x, y, width, height, data, index);});// 延迟两秒后获取图表数据setTimeout(()=>{
loading.close();if(type ==='download'){
doc.save(name +'.pdf');return doc;}else{// 将生成的 PDF 转换为数据 URLconst dataURL = doc.output('dataurl',{filename: name +'.pdf'});// 创建一个新窗口进行预览debugger;const previewWindow = window.open();
previewWindow.document.write(`<iframe width='100%' height='100%' src='${dataURL}'></iframe>`);return doc;}},5000);// 2000 毫秒即 2 秒}functiongenerateOnePDF(option, doc, x, y, width, height, data, index){// 创建一个包含 ECharts 图表的 div 元素const chartContainer = document.createElement('div');
chartContainer.style.width ='700px';
chartContainer.style.height ='300px';
chartContainer.style.marginLeft ='50px';// 设置左边距
document.body.appendChild(chartContainer);// 使用 ECharts 在 chartContainer 中生成图表const chart = echarts.init(chartContainer);
chart.setOption(option);// 监听图表渲染完成事件
chart.on('finished',()=>{// 延迟两秒后获取图表数据// 获取 ECharts 图表的数据 URLconst dataURL = chart.getDataURL({type:'png'});// 移除图表容器
document.body.removeChild(chartContainer);// 将图表数据 URL 转换为图像const img =newImage();
img.src = dataURL;
doc.setPage(2);// 在 PDF 中添加图表图像
doc.addImage(img,'JPEG', x, y, width, height);// 如果是最后一个图表,生成下一页, 解决图表错页的问题if(index === onePdfEchartsCfg.value.length -1){generateTwoPage(doc);}});}
版权归原作者 wind croon 所有, 如有侵权,请联系我们删除。