(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)效果图如下: