前端实现打印
之前开发项目中使用的打印的插件是easy-print,这个插件是基于以hiprint(http://hiprint.io/)的打印插件,我们的项目中主要打印的是表格数据,在测试阶段发现了调用打印会使页面卡死,排查问题,发现原因是因为这个hiprint打印表格类型的时候,如果一行数据展示不下会分页,不会中间截断,但是出现某个字段值太长的情况,一条数据一页放不下,找了半天也没找到相关配置😂(可能是自己的原因没找到吧。。),后来还加了qq群,但是群里也是,怎么说呢,就是问题不一定会得到答复,不过好像还有VIP群,要money才能进😑,我也不确定会不会解决我的问题,所以就,决定还是自己写个简单的。
使用的实现方法是基于window.print,实现思路大概是在当前页面创建一个iframe(这里的iframe使用绝对定位或者固定定位宽高等方式让它在用户可视区域看不到的地方,当然如果说需要做页面预览的可以先生成一个预览页面),将打印的html内容放到iframe里,使iframe作为打印的容器,调用打印,之后再删除iframe。提前说一声,我这里主要用的是文件写入的方式,所以里边的内容都是使用字符串拼接这样的,写的只要是实现表格,所以别的打印内容做了简单的处理,这里如果说需要做统一处理另一种格式的,可以自定义再添加type~
实现打印具体的代码:
// 根据打印的数据类型组装dom const getPrintItemDom = (data) => { if (!data || data.length === 0) { return []; } const printDom = []; data.forEach((itemObj) => { if (itemObj.printElement && itemObj.printElement.children) { const childrenDomArr = getPrintItemDom(itemObj.printElement.children); const parentTitle = itemObj.title ? `<p style="font-weight:bold;font-size:12px;margin:0;line-height: 24px">${itemObj.title}</p>` : ''; const childrenDomStr = `${parentTitle}<div style=${itemObj.renderStyle || ''}>${childrenDomArr.join('')}</div>`; printDom.push(childrenDomStr); } else { let itemDom = itemObj.title ? `<p style="font-weight:bold;font-size:12px;margin:0;line-height: 24px">${itemObj.title}</p>` : ''; if (itemObj.printElement && itemObj.printElement.data && itemObj.printElement.data.length) { switch (itemObj.type) { case 'domstr': itemDom += `<div>${itemObj.printElement.data}</div>`; break; case 'table': const tableHeadTh = itemObj.printElement.columns.map(item => `<th style='border:1px solid #000;border-top:none;border-left:none;width:${item.width}'>${item.title}</th>`); // 表头 const tableData = itemObj.printElement.data.map((item) => { const itemTd = itemObj.printElement.columns.map((citem) => { return `<td style='border:1px solid #000;border-top:none;border-left:none;width:${citem.width};word-break:break-all'>${citem.formatter ? citem.formatter(citem.field, item[citem.field], item) : item[citem.field]}</td>`; }); const itemTr = `<tr>${itemTd.join('')}</tr>`; return itemTr; }); // 表数据 itemDom += `<table style='font-size:12px;border:1px solid #000;border-bottom:none;border-right:none;text-align:center;' cellspacing='0'> ${tableHeadTh.join('')} ${tableData.join('')} </table>`; break; case 'descriptions': const listColumn = itemObj.printElement.columns || 1; // 数值,一行展示几列 /** * [{label: '账号', value: 'a123',... }] */ const totalNum = itemObj.printElement.data.length; // 总共展示项 console.log(listColumn, '=---', totalNum); const ifNeedColspan = totalNum % listColumn; // 展示项的数量取余一行展示的项的数量,如果没有余数则无须列合并,否则最后一项需要进行列合并 const cellStyle = 'border:1px solid #000;border-top:none;border-left:none;word-break:break-all'; const tableBodyDomArr = itemObj.printElement.data.map((item, index) => { let trStr = ''; if (index % listColumn === 0) { trStr += '<tr>'; } let itemTh = `<th style="${cellStyle}">${item.label}</th><td style="${cellStyle}">${item.value}</td>`; // 如果是最后一列并且有展示不完全的行,则最后一个td需要进行列合并 if ((index === totalNum - 1) && ifNeedColspan !== 0) { const colspanNum = (listColumn - ifNeedColspan) * 2 + 1; itemTh = `<th style="${cellStyle}">${item.label}</th><td colspan=${colspanNum} style="${cellStyle}">${item.value}</td>`; } trStr += itemTh; if (index % listColumn === listColumn - 1) { trStr += '</tr>'; } return trStr; }); console.log(tableBodyDomArr, 'tableBodyDomArr'); itemDom += `<table style='font-size:12px;border:1px solid #000;border-bottom:none;border-right:none;text-align:center;' cellspacing='0'> ${tableBodyDomArr.join('')} </table>`; default: break; } } // 如果有title需要将title和content布局 const flexItemDom = `<div style="display:flex;flex-direction:column;${itemObj.renderStyle || ''}">${itemDom}</div>`; printDom.push(flexItemDom); } }); return printDom; } // 执行打印 const getPrintDom = (data) => { const printDom = getPrintItemDom(data); let printDomStr = printDom.join(''); // 最终打印的结构及样式 const iframe = document.createElement('IFRAME'); // 创建iframe let doc = null; // 打印的文档 iframe.setAttribute('style', 'position:absolute;width:0;height:0;left:0;top:0'); // 设置iframe的样式 // iframe.setAttribute('style', 'position:absolute;width:800px;height:900px;left:0;top:0;z-index:2000;background:gold;'); // 设置iframe的样式--调试的时候换成这个 document.body.appendChild(iframe); // 将iframe追加进body doc = iframe.contentWindow.document; // 获取iframe的文档 doc.open(); // 开始写入iframe doc.write(printDomStr); // 写入打印的dom数据 doc.close(); // 结束写入,关闭文档 iframe.contentWindow.focus(); // 使iframe作为打印的容器 iframe.contentWindow.print(); // 调起打印 setTimeout(() => { document.body.removeChild(iframe); // 删除iframe },200); };
使用打印方法的时候数据传参是有参考之前的hiprint,总体大的参数是数组类型,参数结构如下:
const renderData = [ { displayInBlock: { begin: boolean // end同行的最后一个元素,begin同行的第一个元素 } // 是否和某个打印的数据在同一行展示 非必传 type: 'table', // 打印的元素的类型,是domstr/table/descriptions,目前只定义了这三种展示样式,在最后有case,具体的printElement的参数都和type有关 printElement: { columns: columnsData, // 表格类型必传,表头 data: tableData: // 表格类型必传,表格数据 domStr: htmlStr // html字符串 domstr类型必传 }, renderStyle: string // 额外的一些渲染样式,非必传 } ]
columnsData的格式:
const columnsData = [ { title: '性别', // 表头 width: '25%', // 列宽 field: 'sex', // 对应的数据字段的key formatter: (field, value, row) => {} // 自定义展示函数,若不写,则默认展示字段的值 } ]
具体使用case参考:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <button id='actionBtn'>打印</button> </body> <script> // 打印的数据 // 表头 const testTableColumns = [ { title: '账号', width: '25%', field: 'id', }, { title: '姓名', width: '25%', field: 'name', formatter: (field, value, row) => { const returnVal = row.sex === 0 ? `${value}先生` : `${value}女士`; return returnVal; } }, { title: '性别', width: '25%', field: 'sex', formatter: (field, value, row) => { let domStr = `<p style='color: red'>女</p>`; if (value === 0) { domStr = `<p>男</p>` } return domStr; } }, { title: '年龄', width: '25%', field: 'age' } ]; // 表数据 const testTableData = [ { id: '001', name: 'Andy', sex: 1, age: '22' }, { id: '002', name: 'Bob', sex: 0, age: '23' }, { id: '003', name: 'Tom', sex: 0, age: '20' } ]; // 根据打印的数据类型组装dom const getPrintItemDom = (data) => { if (!data || data.length === 0) { return []; } const printDom = []; data.forEach((itemObj) => { if (itemObj.printElement && itemObj.printElement.children) { const childrenDomArr = getPrintItemDom(itemObj.printElement.children); const parentTitle = itemObj.title ? `<p style="font-weight:bold;font-size:12px;margin:0;line-height: 24px">${itemObj.title}</p>` : ''; const childrenDomStr = `${parentTitle}<div style=${itemObj.renderStyle || ''}>${childrenDomArr.join('')}</div>`; printDom.push(childrenDomStr); } else { let itemDom = itemObj.title ? `<p style="font-weight:bold;font-size:12px;margin:0;line-height: 24px">${itemObj.title}</p>` : ''; if (itemObj.printElement && itemObj.printElement.data && itemObj.printElement.data.length) { switch (itemObj.type) { case 'domstr': itemDom += `<div>${itemObj.printElement.data}</div>`; break; case 'table': const tableHeadTh = itemObj.printElement.columns.map(item => `<th style='border:1px solid #000;border-top:none;border-left:none;width:${item.width}'>${item.title}</th>`); // 表头 const tableData = itemObj.printElement.data.map((item) => { const itemTd = itemObj.printElement.columns.map((citem) => { return `<td style='border:1px solid #000;border-top:none;border-left:none;width:${citem.width};word-break:break-all'>${citem.formatter ? citem.formatter(citem.field, item[citem.field], item) : item[citem.field]}</td>`; }); const itemTr = `<tr>${itemTd.join('')}</tr>`; return itemTr; }); // 表数据 itemDom += `<table style='font-size:12px;border:1px solid #000;border-bottom:none;border-right:none;text-align:center;' cellspacing='0'> ${tableHeadTh.join('')} ${tableData.join('')} </table>`; break; case 'descriptions': const listColumn = itemObj.printElement.columns || 1; // 数值,一行展示几列 /** * [{label: '账号', value: 'a123',... }] */ const totalNum = itemObj.printElement.data.length; // 总共展示项 console.log(listColumn, '=---', totalNum); const ifNeedColspan = totalNum % listColumn; // 展示项的数量取余一行展示的项的数量,如果没有余数则无须列合并,否则最后一项需要进行列合并 const cellStyle = 'border:1px solid #000;border-top:none;border-left:none;word-break:break-all'; const tableBodyDomArr = itemObj.printElement.data.map((item, index) => { let trStr = ''; if (index % listColumn === 0) { trStr += '<tr>'; } let itemTh = `<th style="${cellStyle}">${item.label}</th><td style="${cellStyle}">${item.value}</td>`; // 如果是最后一列并且有展示不完全的行,则最后一个td需要进行列合并 if ((index === totalNum - 1) && ifNeedColspan !== 0) { const colspanNum = (listColumn - ifNeedColspan) * 2 + 1; itemTh = `<th style="${cellStyle}">${item.label}</th><td colspan=${colspanNum} style="${cellStyle}">${item.value}</td>`; } trStr += itemTh; if (index % listColumn === listColumn - 1) { trStr += '</tr>'; } return trStr; }); console.log(tableBodyDomArr, 'tableBodyDomArr'); itemDom += `<table style='font-size:12px;border:1px solid #000;border-bottom:none;border-right:none;text-align:center;' cellspacing='0'> ${tableBodyDomArr.join('')} </table>`; default: break; } } // 如果有title需要将title和content布局 const flexItemDom = `<div style="display:flex;flex-direction:column;${itemObj.renderStyle || ''}">${itemDom}</div>`; printDom.push(flexItemDom); } }); return printDom; } // 执行打印 const getPrintDom = (data) => { const printDom = getPrintItemDom(data); let printDomStr = printDom.join(''); // 最终打印的结构及样式 const iframe = document.createElement('IFRAME'); // 创建iframe let doc = null; // 打印的文档 iframe.setAttribute('style', 'position:absolute;width:0;height:0;left:0;top:0'); // 设置iframe的样式 // iframe.setAttribute('style', 'position:absolute;width:800px;height:900px;left:0;top:0;z-index:2000;background:gold;'); // 设置iframe的样式--调试的时候换成这个 document.body.appendChild(iframe); // 将iframe追加进body doc = iframe.contentWindow.document; // 获取iframe的文档 doc.open(); // 开始写入iframe doc.write(printDomStr); // 写入打印的dom数据 doc.close(); // 结束写入,关闭文档 iframe.contentWindow.focus(); // 使iframe作为打印的容器 iframe.contentWindow.print(); // 调起打印 setTimeout(() => { document.body.removeChild(iframe); // 删除iframe },200); }; // 调用打印 const doPrint = () => { const printDatas = [ { // dom字符串,可以试任意的dom字符串 type: 'domstr', printElement: { data: `<h3>打印数据测试</h3>` }, renderStyle: 'text-align:center' }, { // 表格 type: 'table', title: 'table类型的数据', printElement: { columns: testTableColumns, data: testTableData }, renderStyle: 'width:100%;margin-bottom:20px;' }, { // 描述列表,类似于antd的Descriptions展示样式(当然这里只是最基本的样式) type: 'descriptions', title: 'descriptions类型的数据', printElement: { columns: 3, // 一行展示几项数据 data: [ {label: '姓名', value: '张三'}, {label: '年龄', value: '32'}, {label: '电话', value: '8008208000'}, {label: '邮箱', value: '123@qq.com'}, {label: '户籍所在地', value: '北京'}, {label: '民族', value: '汉'}, {label: '职业', value: '老师'}, ] }, renderStyle: 'margin-bottom:20px;' }, { title: '有children的元素', printElement: { children: [{ type: 'domstr', printElement: { data: `<h6>left</h6>` }, renderStyle: 'width: 30%;' }, { type: 'descriptions', printElement: { columns: 2, data: [ { label: 'color', value: 'red'}, { label: 'color', value: 'yellow'}, { label: 'color', value: 'black'}, { label: 'color', value: 'pink'}, ] }, renderStyle: 'flex:1;' }] }, renderStyle: 'display:flex;width:100%;align-items:center' } ] getPrintDom(printDatas); // 调用打印 } // 给button绑定打印事件 const btnEl = document.getElementById('actionBtn'); btnEl.addEventListener('click', doPrint); </script> </html>
效果:
暂时就是这些,写的比较简陋,主要是用来展示打印表格。如果有更好的方法和插件或者意见,欢迎评论~