React+html2canvas+jspdf+antd快速实现前端pdf预览及打印

简介: 文章的总结目标实际上就是一个前端pdf打印组件,由於能在往后的其他项目中得以快速上手,并能根据所在项目需要快速自定义扩展,因此組件非常简陋直白,文章是实践过程的记录产物,并不保证完全正确,仅作参考。

(1)依赖如下:

html2canvas配置参考:http://html2canvas.hertzen.com/configuration

jspdf配置参考: http://mozilla.github.io/pdf.js

"dependencies": {
"html2canvas": "1.4.1",
"jspdf": "2.5.1",
"ahooks": "^2.4.0",
"antd": "^4.5.4"  },

(2)主要流程如下:传进打印模板与模板填充参数  -> ReactDOM渲染模板到真实节点dom -> html2Canvas把节点dom生成图片 -> JsPDF把图片转成pdf -> pdf打印或下载,直接上代码,useHtml2Pdf.js如下:

importReact, { createRef, useState } from'react';
importReactDOMfrom'react-dom';
import { usePersistFn } from'ahooks';
import { Modal } from'antd';
import { FilePdfOutlined } from'@ant-design/icons';
importJsPDFfrom'jspdf';
importhtml2Canvasfrom'html2canvas';
// a4纸的尺寸(偏差 < 1pt)constA4_HEIGHT=840;
constA4_WIDTH=595;
constPdfPrint= () => {
const [loading, setloading] =useState(false);
constpdfRef=createRef();
consthandlePrint=usePersistFn(async ({ Template, isPrint, filename, options, callBack }) => {
setloading(true);
constprintDoc=document.createElement('div');
// 隐藏创建的domprintDoc.setAttribute('style', 'position:fixed;left:0;top:0;z-index: -999');
document.body.appendChild(printDoc);
// 通过React的render能力实现所需dom的渲染(包括css构建、变量填充)ReactDOM.render(<TemplatepdfRef={pdfRef} options={options} />, printDoc, async () => {consttargetDom=pdfRef.current;
// 克隆节点,默认为false,不复制方法属性,为true是全部复制。constcloneDom=targetDom.cloneNode(true);
// 设置克隆节点的css属性,因为之前的层级为0,我们只需要比被克隆的节点层级低即可。cloneDom.style.position='absolute';
cloneDom.style.top='0';
cloneDom.style.index='-999';
// cloneDom.style.height = height;cloneDom.style.display='block';
// 将克隆节点动态追加到body后面。printDoc.appendChild(cloneDom);
// 插件生成base64img图片。constcanvas=awaithtml2Canvas(cloneDom, {
useCORS: true,
y: 0, // 画布开始渲染的y坐标位置scale: window.devicePixelRatio*2,
      });
constcontentWidth=canvas.width;
constcontentHeight=canvas.height;
// 一页pdf显示html页面生成的canvas高度;constpageHeight= (contentWidth/A4_WIDTH) *A4_HEIGHT;
// html页面生成的canvas在pdf中图片的宽高constimgWidth=A4_WIDTH;
constimgHeight= (A4_WIDTH/contentWidth) *contentHeight;
constpageData=canvas.toDataURL('image/jpeg', 1.0);
constpdf=newJsPDF({
unit: 'pt',
format: 'a4',
orientation: 'p',
      });
// 未生成pdf的html页面高度letleftHeight=contentHeight;
// 页面偏移letposition=0;
// 有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(A4_HEIGHT)// 当内容未超过pdf一页显示的范围,无需分页if (leftHeight<pageHeight) {
pdf.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight);
      } else {
while (leftHeight>0) {
pdf.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight);
leftHeight-=pageHeight;
// 避免添加空白页position-=A4_HEIGHT;
if (leftHeight>0) {
pdf.addPage();
          }
        }
      }
// 打印if (isPrint) {
pdf.output('dataurlnewwindow', {
filename: filename??'foo.pdf',
        });
      }
// 下载if (!isPrint) pdf.save(filename??'bar.pdf');
// 删除利用完的dom部分document.body.removeChild(printDoc);
// 执行回调callBack&&callBack();
setloading(false);
    });
  });
// pdf预览constshowPdf= ({ isPrint, options, Template, callBack, filename }) => {
Modal.confirm({
width: 900,
title: '預覽',
icon: <FilePdfOutlined/>,
content: (
<divstyle={{ border: '1px solid #000' }}><Templateoptions={options} /></div>      ),
okText: `${isPrint?'打印' : '下載'}`,
cancelText: '取消',
onOk: () =>handlePrint({ isPrint, options, Template, callBack, filename }),
    });
  };
/*   *@Description:参数注解   *filename(pdf名稱)   *Template(模板)   *isPreviewPdf(是否预览)   *options(模板填充变量)   *callBack(回调函数)   */return {
print: ({ Template, isPreviewPdf, options, callBack, filename }) => {
isPreviewPdf?showPdf({ Template, options, callBack, filename, isPrint: true })
        : handlePrint({ Template, options, callBack, filename, isPrint: true });
    },
download: ({ Template, isPreviewPdf, options, callBack, filename }) => {
isPreviewPdf?showPdf({ Template, options, callBack, filename, isPrint: false })
        : handlePrint({ Template, options, callBack, filename, isPrint: false });
    },
loading,
pdfRef, // 模板引用  };
};
exportdefaultPdfPrint;

(3)为了使模板看起来更整洁,同时也为了区分复杂的业务处理逻辑,本次实践把字段映射与模板分离,字段映射文件map_conf.js如下:

// 对字段进行映射及逻辑处理exportconsttemplateMap= (options) => {
return {
zk1: options.nameAndCardNoZhHant??'--',
zk2: options.applyCodeZhHant??'--',
zk3: options.payTimeZhHant??'--',
zk4: options.activityNameZhHant??'--',
zk5: [
      {
total: options.total??'--',
discount: options.discount??'--',
amount: options.amount??'--',
payType: options.payTypeZhHant??'--',
      },
    ],
zk6: options.nowDateZhHant??'--',
col1: [
      {
title: '序号',
dataIndex: 'total',
width: '20%',
      },
      {
title: '金额',
dataIndex: 'discount',
width: '20%',
      },
      {
title: '统计',
dataIndex: 'amount',
width: '20%',
      },
      {
title: '方式',
dataIndex: 'payType',
width: '20%',
      },
    ],
  };
};

(4)对于模板的样式,目前发现使用pt作为长宽单位较为友好(注意:此非结论,而是快速实践过程中某个较优解,即真实dom与html2Canvas生成的图片误差极小),以下模板及样式仅作参考。

Template.jsx如下:

importReactfrom'react';
import { Table } from'antd';
import { templateMap } from'./map_conf';
import'./Template.less';
// 模板htmlconstTemplate= (props) => {
const { pdfRef, options } =props;
const { zk1, zk2, zk3, zk4, zk5, zk6, col1 } =templateMap(options);
return (
<divclassName="pdf-root"ref={pdfRef}><divclassName="one"><divclassName="title"><span>XX收據</span></div><divclassName="count"><divclassName="row"><spanclassName="label">已收到:</span><spanclassName="value  mocaofont">{zk1}</span></div></div><divclassName="count"><divclassName="item"><spanclassName="label">申请編號:</span><spanclassName="value">{zk2}</span></div><divclassName="item"><spanclassName="label">辦理日期:</span><spanclassName="value">{zk3}</span></div></div><divclassName="count"><divclassName="row"><spanclassName="label">如下项目:</span><spanclassName="value">{zk4}</span></div></div><divclassName="table"><TablerowKey={(_record, index) =>index.toString()}
columns={col1}
dataSource={zk5}
pagination={false}
size="small"bordered/></div></div><divclassName="two"><divclassName="count"><divclassName="row"><spanclassName="label">備注:</span><spanclassName="value">{zk6}</span></div></div></div></div>  );
};
exportdefaultTemplate;

Template.less如下:

.pdf-root {
width: 595pt;
margin: 0auto;
padding: 010pt;
font-size: 8pt;
color: #000;
font-family: 'Microsoft YaHei';
font-weight: 500;
.one {
height: 200pt;
padding-top: 20pt;
.count {
.item {
line-height: 14pt;
      }
.row {
line-height: 14pt;
      }
    }
  }
.two {
height: 210pt;
padding-top: 10pt;
  }
.title {
text-align: center;
line-height: 12pt;
font-size: 12pt;
font-weight: bold;
padding-bottom: 8pt;
  }
.count {
width: 100%;
display: flex;
flex-wrap: wrap;
.item {
width: 50%;
line-height: 13pt;
.label {
font-weight: bold;
      }
    }
.row {
width: 100%;
line-height: 13pt;
.label {
font-weight: bold;
      }
    }
  }
.table {
margin: 2ptauto;
.ant-table-thead {
.ant-table-cell {
font-weight: bold;
      }
    }
.ant-table-cell {
font-size: 8pt!important;
    }
  }
}

(5)使用起来也非常便捷,例子如下:

importReactfrom'react';
import { Button } from'antd';
importuseHtml2Pdffrom'@/components/useHtml2Pdf';
importTemplatefrom'@/templates/Template.jsx';
// 使用constHowToUse= () => {
const { print, loading } =useHtml2Pdf();
consthandleClick= () => {
// mock数据constdata= {
nameAndCardNoZhHant: '张三',
applyCodeZhHant: 'ABC123456789',
payTimeZhHant: '2022-12-12 12:12:12',
activityNameZhHant: '某某收费项',
total: 1,
discount: '2.00',
amount: '5.00',
payTypeZhHant: '支付宝',
nowDateZhHant: '2022-14-14 14:14:14',
    };
// 使用print({ Template: Template, options: data });
  };
return (
<Buttonloading={loading} onClick={handleClick}>点击打印</Button>  )
};
exportdefaultHowToUse;

(6)效果图如下:

A1E47F70-7D06-42cf-87D4-25974CAEC408.png

相关文章
|
6天前
|
移动开发 前端开发 UED
React 音频预览组件:Audio Preview
本文介绍如何使用 React 构建音频预览组件,涵盖基础实现、常见问题及解决方案。通过 HTML5 `&lt;audio&gt;` 标签和 React 状态管理,实现播放控制。解决文件路径、浏览器兼容性等问题,并优化性能,避免状态不同步和内存泄漏,提升用户体验。
54 22
|
1月前
|
前端开发 安全 UED
React 文件预览组件:File Preview
在现代Web应用中,文件上传和预览功能至关重要。本文基于React库,详细介绍如何构建文件预览组件,涵盖文件选择器、图片预览、文件大小限制及多种文件类型支持。通过实际代码示例,解析常见问题如跨域请求、文件路径处理和状态管理,并提供解决方案。帮助开发者提升用户体验,减少误操作。
48 2
|
3月前
|
前端开发 JavaScript
除了 jsPDF,还有哪些前端库可以用于生成 PDF?
【10月更文挑战第21天】这些前端库都有各自的特点和优势,你可以根据具体的项目需求、技术栈以及对功能的要求来选择合适的库。不同的库在使用方法、性能表现以及功能支持上可能会有所差异,需要根据实际情况进行评估和选择。
|
5月前
|
前端开发
react使用antd中的Checkbox实现多选
在React项目中,通过Ant Design的Checkbox组件实现多选。引入Checkbox,使用Checkbox.Group来管理Checkbox,设置`value`属性绑定选中项数组,通过`onChange`更新数组。维护一个全选状态,根据选中项数量与总数决定全选按钮状态。全选按钮的`onChange`事件用于控制所有Checkbox的选中状态。
250 1
react使用antd中的Checkbox实现多选
|
4月前
|
XML 前端开发 JavaScript
前端开发进阶:从HTML到React.js
【10月更文挑战第9天】前端开发进阶:从HTML到React.js
|
5月前
|
前端开发 JavaScript 区块链
react18函数组件+antd使用指南-使用代码集合以及报错记录汇总
本文介绍了多个React开发中常见的问题及其解决方案,包括但不限于:1)`useForm`实例未连接到任何`Form`元素的警告及解决方法;2)监听页面滚动事件的实现方式;3)React 18与antd 5.8.6中定制主题的方法;4)React结合antd 4.x版本自定义主题色的步骤;5)解决`ResizeObserver loop`相关报错的技巧;6)处理React设计表单时遇到的CDN资源加载失败问题;7)解决onClick事件传参问题;8)修复类型错误等。每部分均提供详细分析与实用代码示例,帮助开发者快速定位并解决问题。
95 3
|
5月前
|
前端开发 计算机视觉
React使用antd实现可编辑单元格
React结合Ant Design实现可编辑单元格的表格组件,通过EditableRow和EditableCell封装实现单元格编辑功能,并提供saveFun回调保存编辑内容。
198 1
|
5月前
|
前端开发
React按需加载antd步骤以及出现的问题
在使用`babel-plugin-import`插件时,可以在项目的根目录创建`.babelrc`文件或在`package.json`中添加babel配置。这两个文件中不应该存在重复的配置。如果出现"Multiple configuration files found"错误,需要选择其中一个文件进行配置,并删除另一个文件中的babel配置。使用该插件后,可以直接从`antd`引入组件,无需手动引入样式文件。
120 1
|
5月前
|
前端开发 数据安全/隐私保护
react antd 实现修改密码(原密码,新密码,再次输入新密码,新密码增加正则复杂度校验)
文章介绍了如何在React项目中使用Ant Design实现一个修改密码的组件,包括原密码、新密码和再次输入新密码的表单项,并为新密码增加了正则表达式复杂度校验。
116 0
react antd 实现修改密码(原密码,新密码,再次输入新密码,新密码增加正则复杂度校验)
|
4月前
|
JavaScript 前端开发 容器
Vue生成PDF文件攻略:html2canvas与jspdf联手,中文乱码与自动换行难题攻克
Vue生成PDF文件攻略:html2canvas与jspdf联手,中文乱码与自动换行难题攻克
406 0