一、导包
1、导入
yarn add jspdf yarn 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 图表的 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 图表的数据 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 图表的 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 图表的数据 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); } }); }
还没有评论,来说两句吧...