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

相关文章
|
1月前
|
前端开发 JavaScript 开发者
颠覆传统:React框架如何引领前端开发的革命性变革
【10月更文挑战第32天】本文以问答形式探讨了React框架的特性和应用。React是一款由Facebook推出的JavaScript库,以其虚拟DOM机制和组件化设计,成为构建高性能单页面应用的理想选择。文章介绍了如何开始一个React项目、组件化思想的体现、性能优化方法、表单处理及路由实现等内容,帮助开发者更好地理解和使用React。
78 9
|
11天前
|
JavaScript
jquery图片和pdf文件预览插件
EZView.js是一款jquery图片和pdf文件预览插件。EZView.js可以为图片和pdf格式文件生成在线预览效果。支持的文件格式有pdf、jpg、 png、jpeg、gif。
42 16
|
1月前
|
监控 前端开发 数据可视化
3D架构图软件 iCraft Editor 正式发布 @icraft/player-react 前端组件, 轻松嵌入3D架构图到您的项目,实现数字孪生
@icraft/player-react 是 iCraft Editor 推出的 React 组件库,旨在简化3D数字孪生场景的前端集成。它支持零配置快速接入、自定义插件、丰富的事件和方法、动画控制及实时数据接入,帮助开发者轻松实现3D场景与React项目的无缝融合。
116 8
3D架构图软件 iCraft Editor 正式发布 @icraft/player-react 前端组件, 轻松嵌入3D架构图到您的项目,实现数字孪生
|
29天前
|
前端开发 JavaScript 搜索推荐
HTML与CSS在Web组件化中的核心作用及前端技术趋势
本文探讨了HTML与CSS在Web组件化中的核心作用及前端技术趋势。从结构定义、语义化到样式封装与布局控制,两者不仅提升了代码复用率和可维护性,还通过响应式设计、动态样式等技术增强了用户体验。面对兼容性、代码复杂度等挑战,文章提出了相应的解决策略,强调了持续创新的重要性,旨在构建高效、灵活的Web应用。
36 6
|
1月前
|
前端开发 JavaScript 开发者
使用React和Redux构建高效的前端应用
使用React和Redux构建高效的前端应用
36 1
|
1月前
|
前端开发 JavaScript Android开发
前端框架趋势:React Native在跨平台开发中的优势与挑战
【10月更文挑战第27天】React Native 是跨平台开发领域的佼佼者,凭借其独特的跨平台能力和高效的开发体验,成为许多开发者的首选。本文探讨了 React Native 的优势与挑战,包括跨平台开发能力、原生组件渲染、性能优化及调试复杂性等问题,并通过代码示例展示了其实际应用。
66 2
|
29天前
|
前端开发 JavaScript 算法
探索现代前端框架——React 的性能优化策略
探索现代前端框架——React 的性能优化策略
23 0
|
29天前
|
前端开发 JavaScript API
探索现代前端框架——React 的性能优化策略
探索现代前端框架——React 的性能优化策略
30 0
|
2月前
|
前端开发 JavaScript 开发者
深入理解React Hooks:提升前端开发效率的关键
【10月更文挑战第5天】深入理解React Hooks:提升前端开发效率的关键
|
2月前
|
前端开发
深入解析React Hooks:构建高效且可维护的前端应用
本文将带你走进React Hooks的世界,探索这一革新特性如何改变我们构建React组件的方式。通过分析Hooks的核心概念、使用方法和最佳实践,文章旨在帮助你充分利用Hooks来提高开发效率,编写更简洁、更可维护的前端代码。我们将通过实际代码示例,深入了解useState、useEffect等常用Hooks的内部工作原理,并探讨如何自定义Hooks以复用逻辑。