小工具知识网

当前位置: 首页 > 行业知识 > vue3 + jspdf + echarts 前端生成 pdf报告 预览+下载

行业知识

vue3 + jspdf + echarts 前端生成 pdf报告 预览+下载

2026-11-13 12:24:43 来源:互联网转载

一、导包

1、导入

yarn add jspdfyarn add jspdf-autotable

2、界面引入

import jsPDF from 'jspdf';// 表格插件,这次我这边没用到, 根据需求选择import 'jspdf-autotable';

3、创建pdf对象

// 创建一个新的PDF文档实例  // 定义页面边距  const PAGE_MARGIN = 10;  const doc = new jsPDF({    unit: 'mm', // 单位,本示例为mm    format: '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} 
{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}
{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 文档    await generateOnePDF(option, doc, x, y, width, height, data, index);  });

3、图表方法

function generateOnePDF(option, doc, x, y, width, height, data, index) {  // 创建一个包含 ECharts 图表的 p 元素  const chartContainer = document.createElement('p');  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 图表的数据 URL    const dataURL = chart.getDataURL({ type: 'png' });    // 移除图表容器    document.body.removeChild(chartContainer);    // 将图表数据 URL 转换为图像    const img = new Image();    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: {      // 缺图表的数据    }  }});// 预览报告const previewBt = 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);};// 下载报告const downloadBt = 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);};function productReport(type, name) {  // 显示加载动画  const loading = ElLoading.service({    lock: true,    text: '正在生成报告',    background: 'rgba(0, 0, 0, 0.7)'  });  // 创建一个新的PDF文档实例  // 定义页面边距  const PAGE_MARGIN = 10;  const doc = new jsPDF({    unit: 'mm', // 单位,本示例为mm    format: '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 height  const 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 文档    await generateOnePDF(option, doc, x, y, width, height, data, index);  });  // 延迟两秒后获取图表数据  setTimeout(() => {    loading.close();    if (type === 'download') {      doc.save(name + '.pdf');      return doc;    } else {      // 将生成的 PDF 转换为数据 URL      const dataURL = doc.output('dataurl', { filename: name + '.pdf' });      // 创建一个新窗口进行预览      debugger;      const previewWindow = window.open();      previewWindow.document.write(``);      return doc;    }  }, 5000); // 2000 毫秒即 2 秒}function generateOnePDF(option, doc, x, y, width, height, data, index) {  // 创建一个包含 ECharts 图表的 p 元素  const chartContainer = document.createElement('p');  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 图表的数据 URL    const dataURL = chart.getDataURL({ type: 'png' });    // 移除图表容器    document.body.removeChild(chartContainer);    // 将图表数据 URL 转换为图像    const img = new Image();    img.src = dataURL;    doc.setPage(2);    // 在 PDF 中添加图表图像    doc.addImage(img, 'JPEG', x, y, width, height);    // 如果是最后一个图表,生成下一页, 解决图表错页的问题    if (index === onePdfEchartsCfg.value.length - 1) {      generateTwoPage(doc);    }  });}

上一篇:怎么建老版服务器?

下一篇:图像处理ASIC设计方法 笔记8 卷积计算芯片的结构