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

作者 : admin 本文共14997个字,预计阅读时间需要38分钟 发布时间: 2024-06-9 共3人阅读

一、导包

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
转换完毕后:
vue3 + jspdf + echarts 前端生成 pdf报告 预览+下载插图
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(`<iframe width='100%' height='100%' src='${dataURL}'>`);
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);
}
});
}
本站无任何商业行为
个人在线分享 » vue3 + jspdf + echarts 前端生成 pdf报告 预览+下载
E-->