前言
项目中需要实现超出A4高度自动新增一页,并且每页都需增加页头页脚的导出pdf功能。 但在网上找的插件大多不能满足该需求,有些写法又很复杂繁琐,也有可能一些好的组件没有找到,因此干脆就自己写了一个。
该组件只用于开发过程中所涉及的功能,因此需求兼容性暂未考虑,仅做个工作小结。当中的错误与不足希望各位大佬指点... -QAQ-
- 项目使用react+antd
先上效果图
01.根据A4规格初始化pdf模块样式
因为我们最终需要打印在A4纸上,因此需要根据其规格来定义dom样式。
据查A4宽高为592.28/841.89,在这我们设pdf宽为960px,那么高度为960 / (592.28 / 841.89)px;
export default props=>{
...
let pdfHeight = 960 / (592.28 / 841.89); // pdf高度;
...
return <div className="container">
<Button type="primary">导出pdf</Button>
<div id="pdfCon"></div>
</div>
}
02.定义渲染函数 - createPdf
封装创建函数,用于后续新增页时调用;其中需记录每页实际内容元素高度,用于自动换页判断条件;
/**
* 创建pdf页
* @param {*} inx 页数索引,初始:0
*/
const createPdf = (inx)=>{
const _i = inx || 0;
pdfHeights[_i] = 0; // 初始化高度
setPdfHeights(pdfHeights)
const parent = document.getElementById('pdfCon'); // 顶级父元素
const pdfDom = document.createElement('div'); // 新增子元素
pdfDom.className = 'pdf modal';
pdfDom.style = `height: ${pdfHeight}px`; // 将pdf高度撑开
parent.appendChild(pdfDom);
createHeader(pdfDom, _i); // 创建页眉
createFooter(pdfDom, _i); // 创建页脚
// 03.pdf表格内部渲染处理
...
}
...
useEffect(()=>{
createPdf(0); // 初始创建一页
},[])
02.1 createHeader函数 (根据需求进行自定义)
/**
* 创建页眉
* @param {*} pdfDom 当前pdf元素
* @param {*} i 页数索引
*/
const createHeader = (pdfDom, i)=>{
// 添加元素 ----------start
const h = document.createElement('div');
h.className = 'header';
...
h.appendChild(...);
...
pdfDom.appendChild(h);
// 添加元素 ----------end
pdfHeights[i] += h.offsetHeight;
setPdfHeights(pdfHeights); // 记录高度
}
02.2 createFooter函数
/**
* 创建页脚
* @param {*} pdfDom 当前pdf元素
* @param {*} i 页数索引
*/
const createFooter = (pdfDom, i)=>{
// 添加元素 ----------start
const f = document.createElement('div');
f.className = 'footer';
f.innerHTML = `<span>第${i+1}页,共${i+1}页</span>`;
pdfDom.appendChild(f);
// 添加元素 ----------end
pdfHeights[i] += f.offsetHeight;
setPdfHeights(pdfHeights); // 记录高度
}
此时执行导出pdf后效果如下图(导出函数在03.3列出)
接下该处理重点的表格了!!!~(●'◡'●)`
03.动态生成表格并自动换页
实现步骤如下:
-
- 定义通用cofing
-
- 根据不同布局定义渲染函数
03.1 根据项目需求设计通用配置变量tableConfig
由上图可以看到,目前项目遇到的需求中大模块布局分为5个;
-
两列左右文案布局:需考虑value超高;
-
两列左右列表布局:由于左右高度不相等,故需考虑左/右列表内标题或内容超高;
-
行内布局: 该布局有几种布局;
3.1. 整行:行超高;
3.2. 左右:左/右超高;
3.3. 内嵌表格:行超高; -
四列左右布局: 行超高;
我们根据以上布局规定以下变量:
布局类型
type:
'lr': 布局1
'flex': 布局2
'con': 布局3
'rLR': 布局4
布局3中dom类型
type:
'1': 单行文字
'2': 左右文字
'3': 嵌套表格
具体如何定义,根据需求或者个人理解...~
综上所述,大致设一个config
let tableConfig = [
{
type: 'lr',
title: 'label',
value: 'value',
},
{
type: 'rLR',
list:[
{ title: 'label', value: 'value' },
{ title: 'label', value: 'value' },
]
},
{
type: 'flex',
list: [
{
title: '我是大标题',
list: [{title: '(一)我是标题我是标题:', value: 'value'}]
},
{
title: '我是大标题',
list: [{title: '(一)我是标题我是标题:', value: 'value'}]
}
]
},
{
type: 'con',
list:[
{
type: '1',
title: 'label',
value: 'value'
},
{
type: '2',
title: 'label',
value: 'value'
},
{
type: '3',
title: 'label',
list: []
},
]
}
]
03.2 布局渲染函数
- table渲染函数 - createTable
/**
* 创建table
* @param {*} pdfDom // 当前操作pdf
* @param {*} i // 当前pdf索引
* @param {*} rInx // 当前渲染tableConfig索引
*/
const createTable = (pdfDom, i, rInx)=>{
const t = document.createElement('div');
t.className = 'table';
pdfDom.appendChild(t);
createDom(pdfDom, t, i, rInx || 0); // table内部dom渲染函数
};
- table内部dom元素渲染函数 - createDom
/**
* 创建table内部dom元素
* @param {*} pdfDom // 当前操作pdf
* @param {*} t // 当前table
* @param {*} i // 当前pdf索引
* @param {*} rInx // 当前渲染tableConfig索引
* @returns
*/
const createDom = async (pdfDom, t, i, rInx)=>{
const config = configs[rInx];
const {type, list} = config;
if(type == 'lr') {
createLR({ pdfDom, t, i, rInx, config });
} else if(type == 'rLR'){
createRlr({ pdfDom, t, i, rInx })
} else if(type == 'flex'){
list.map((v, index)=>{
createFlexDom(pdfDom, t, d1, v, i, index, rInx)
})
} else{
const con = document.createElement('div');
con.className = 'tCon';
for (let index = 0; index < list?.length; index++) {
const v = list[index];
if(!v.hide){
const res = await createConDom(pdfDom, t, con, v, i, index, rInx );
if(res == 1){
createPdf(i+1, rInx);
break;
}
}
if(index == list.length -1){
createDom(pdfDom, t, i, rInx+1)
}
}
}
}
- type == 'lr' 渲染函数 —— createLR
/**
* 渲染'lr'布局dom元素
* @param {*} pdfDom 当前pdf元素
* @param {*} t 当前table
* @param {*} i 当前pdf索引
* @param {*} rInx 当前渲染tableConfig索引
* @param {*} config 当前配置对象
* @returns
*/
const createLR = async ({pdfDom, t, i, rInx, config})=>{
// 添加元素 ----------start
const th = document.createElement('div');
th.className = 'tH';
const l = document.createElement('div');
l.className = 'tD tL';
l.innerHTML = config.title;
const r = document.createElement('div');
r.className = 'tD';
r.innerHTML = config.render ? config.render(data) : config.key && config.title ? data[config.key] : config.value;
th.appendChild(l)
th.appendChild(r)
t.appendChild(th)
// 添加元素 ----------end
const height = pdfHeights[i] + th.offsetHeight;
// 超出pdf最大高度
if(height > pdfHeight){
if(config.row == 1){
t.removeChild(th);
return createPdf(i+1, rInx)
}
const res = await setMaxText({dom:r, i, rInx, layoutType: 'lr'}); // 处理文字跨页
return res ? createPdf(i+1, res) : null;
}
pdfHeights[i] = height;
setPdfHeights(pdfHeights)
createDom(pdfDom, t, i, rInx+1); // 继续渲染下一个dom
}
- type == 'rLR'渲染函数 —— createRlr
/**
* 渲染'rLR'布局dom元素
* @param {*} pdfDom 当前pdf元素
* @param {*} t 当前table
* @param {*} i 当前pdf索引
* @param {*} rInx 当前渲染tableConfig索引
* @returns
*/
const createRlr = ({pdfDom, t, i, rInx})=>{
// 添加元素 ----------start
const th = document.createElement('div');
th.className = 'tH';
const rLR = document.createElement('div');
rLR.className = 'rLR';
list.map(v=>{
const th = document.createElement('div');
th.className = 'tH';
const l = document.createElement('div');
l.className = 'tD tL';
l.innerHTML = v.title;
const r = document.createElement('div');
r.className = 'tD';
r.innerHTML = v.key ? data[v.key] : v.value;
th.appendChild(l);
th.appendChild(r);
rLR.appendChild(th);
})
t.appendChild(rLR);
pdfHeights[i] += rLR.offsetHeight;
setPdfHeights(pdfHeights)
// 添加元素 ----------end
createDom(pdfDom, t, i, rInx+1)
}
- type == 'flex'渲染函数 —— createFlexDom
/**
*
* @param {*} pdfDom 当前pdfDom
* @param {*} t 当前table
* @param {*} th 当前th元素
* @param {*} item 当前config
* @param {*} i 当前pdf索引
* @param {*} fInx flex索引
* @param {*} rInx 当前渲染tableConfig索引
* @returns
*/
const createFlexDom = async(pdfDom, t, th, item, i, fInx, rInx)=>{
let nowTableH = pdfHeights[i]; // 当前pdf高度
// 添加元素 ----------start
const d1 = document.createElement('div');
d1.className = 'con';
const d2 = document.createElement('div');
d2.className = 'tRowFlex';
const d3 = document.createElement('div');
d3.className = 'flexT';
const d4 = document.createElement('div');
d4.className = 'flexH';
// 判断是否显示大标题
if(!item?.hideT){
d3.innerHTML = item.title;
d2.appendChild(d3);
}
// 判断是否显示flex布局的子标题
if(!item?.hideFlexT){
d4.innerHTML = item.msg;
d2.appendChild(d4);
}
d1.appendChild(d2);
th.appendChild(d1);
t.appendChild(th);
// 添加元素 ----------end
const dH1 = nowTableH + d3.offsetHeight;
const dH2 = nowTableH + d4.offsetHeight;
if(dH1 > pdfHeight){
// 大标题超高
d3.remove();
d4.remove();
createPdf(i+1, rInx); // 创建新页
return;
}else if(dH2 > pdfHeight){
// 子标题超高
d4.remove();
item.hideT = true; // 设为true,即已渲染大标题,下次不渲染
createPdf(i+1, rInx);
return;
}
item.hideT = item.hideFlexT = true;
nowTableH += d2.offsetHeight;
// 大小标题都已渲染且不超高,以下绘制文案
createFlexCText({pdfDom, t, d2, item, i, fInx, rInx, nowTableH})
}
- createFlexCText函数
const createFlexCText =async ({pdfDom, t, d2, item, i, fInx, rInx, nowTableH})=>{
for (let cInx = 0; cInx < item.list.length; cInx++) {
const cItem = item.list[cInx];
// 添加元素 ----------start
if(!cItem.hide) {
const d5 = document.createElement('div');
d5.className = 'itemH';
const title = cItem.title;
!cItem.hideT && (d5.innerHTML = title);
const d6 = document.createElement('div');
d6.innerHTML = cItem.value || data.safetyMeasures[cInx].value || '无';
d2.appendChild(d5);
d2.appendChild(d6);
// 添加元素 ----------end
const height1 = nowTableH + d5.offsetHeight;
const height2 = nowTableH + d6.offsetHeight;
if(height1 > pdfHeight){
const res = await setMaxText({type:1, nowTableH, dom: d5, i, rInx, fInx, cInx})
res == 2 && d5.remove();
d6.remove();
// // 创建新table
if(fInx == configs[rInx].list.length -1){
createPdf(i+1, rInx);
}
return;
}else if(height2 > pdfHeight){
cItem.hideT = true;
nowTableH += d5.offsetHeight;
await setMaxText({type:2, nowTableH, dom: d6, i, rInx, fInx, cInx})
if(fInx == configs[rInx].list.length -1){
createPdf(i+1, rInx);
}
return;
}else{
cItem.hide = true;
nowTableH += d5.offsetHeight;
nowTableH += d6.offsetHeight;
if(fInx == configs[rInx].list.length -1 && cInx == item.list.length -1){
pdfHeights[i] = nowTableH;
setPdfHeights([...pdfHeights])
createDom(pdfDom, t, i, rInx+1)
}
}
}
}
}
- type == 'con'渲染函数 —— createConDom
/**
*
* @param {*} t 当前table
* @param {*} con 当前操作con 元素
* @param {*} item 当前config
* @param {*} i 当前pdf索引
* @param {*} fInx flex索引
* @param {*} rInx 当前渲染tableConfig索引
* @returns
*/
const createConDom = async (t, con, item, i, fInx, rInx)=>{
return new Promise(async (resolve, reject)=>{
configs[rInx].list.map((sItem, sInx)=>{
sInx < fInx && (sItem.hide = true)
})
const d1 = document.createElement('div');
d1.className = 'trow';
// con布局中的dom类型判断
if(item.type == '1'){
item.list.map(v=>{
const s = document.createElement('span');
s.innerHTML = `${v.title}:${v.value}`;
d1.appendChild(s);
})
con.appendChild(d1);
t.appendChild(con);
const height = pdfHeights[i] + d1.offsetHeight;
if(height > pdfHeight){
d1.remove()
resolve(1)
return;
}else{
pdfHeights[i] = height;
setPdfHeights([...pdfHeights])
resolve()
}
}else if(item.type == '2'){
const l = document.createElement('div');
l.className = 'trow_l';
const r = document.createElement('div');
r.className = 'trow_r';
l.innerHTML = item.title;
r.innerHTML = item.value;
d1.appendChild(l);
d1.appendChild(r);
con.appendChild(d1);
t.appendChild(con);
const height1 = pdfHeights[i] + l.offsetHeight;
const height2 = pdfHeights[i] + r.offsetHeight;
// 标题是否超高
if(height1 > pdfHeight){
let params = {
nowTableH: pdfHeights[i],
i,
rInx,
fInx,
layoutType: 'con'
}
// 标题及文案处理
height2 > pdfHeight && await setMaxText({ type: 2, dom:r, ...params });
await setMaxText({ type: 1, dom:l, ...params });
return resolve(1);
}else{
pdfHeights[i] += d1.offsetHeight;
setPdfHeights(pdfHeights);
resolve()
}
}else{
const l = document.createElement('div');
l.className = 'trow_l';
if(!item.hideT){
l.innerHTML = item.title;
}
d1.appendChild(l);
con.appendChild(d1);
t.appendChild(con);
const height = pdfHeights[i] + l.offsetHeight;
if(height > pdfHeight){
l.remove()
return resolve(1)
}
item.hideT = true;
pdfHeights[i] = height;
setPdfHeights(pdfHeights)
// 处理内嵌table
const tm = document.createElement('div');
tm.className = 'table miniT';
const h1 = document.createElement('div');
h1.className = 'h';
// 是否渲染表头
if(!item.hideTH){
item.list.map(v=>{
const hD = document.createElement('div');
hD.innerHTML = v.title;
h1.appendChild(hD);
tm.appendChild(h1);
})
}
const h1H = height + h1.offsetHeight;
console.log(h1H, 'h1H');
// table头超高了
if(h1H > pdfHeight){
console.log('table头超高了');
h1.remove();
return resolve(1);
}
// 重复渲染子th
for (let tdI = (item.startTd || 0); tdI < 3; tdI++) {
console.log('渲染td');
const h2 = document.createElement('div');
h2.className = 'h';
item.list.map(v=>{
const tD = document.createElement('div');
tD.innerHTML = v.value || '';
h2.appendChild(tD);
})
tm.appendChild(h2);
con.appendChild(tm);
t.appendChild(con);
const totalH = pdfHeights[i] + tm.offsetHeight;
if(totalH > pdfHeight){
console.log('table- item超高'+tdI);
h2.remove();
item.hideT = true; // 是否隐藏标题
item.hideTH = true; // 是否隐藏表头
item.startTd = tdI; // 记录下一个渲染索引
pdfHeights[i] = pdfHeight;
setPdfHeights(pdfHeights);
return resolve(1);
}
if(tdI == 2){
pdfHeights[i] = totalH;
setPdfHeights(pdfHeights);
resolve();
}
}
}
})
}
- setMaxText函数
/**
/**
*
* @param {*} type 1:标题 2: 值
* @param {*} nowTableH 当前table高度
* @param {*} dom 操作dom
* @param {*} i 当前pdf索引
* @param {*} rInx 当前渲染tableConfig索引
* @param {*} fInx flex索引
* @param {*} cInx 当前config索引
* @param {*} layoutType 布局类型
* @returns
*/
const setMaxText = async ({type, nowTableH, dom, i, rInx, fInx, cInx, layoutType})=>{
return new Promise((resolve, reject)=>{
const str = dom.innerText;
for (let j = 1; j < str.length; j++) {
let _txt = str.substring(0,j);
dom.innerText = _txt;
const amount = nowTableH + dom.offsetHeight;
if(amount > pdfHeight){
let text = str.substring(0,j-1);
dom.innerText = text;
let _sitem = {}
if(layoutType == 'lr'){
_sitem = {
title: '',
type: layoutType,
value: str.substring(j-1).replace(/\n/g, '<br/>')
}
}else if(layoutType == 'con'){
_sitem = configs[rInx].list[fInx];
}else{
nowTableH += dom.offsetHeight;
_sitem = configs[rInx].list[fInx].list[cInx];
}
if(layoutType != 'lr'){
type == 2 && (_sitem.hideT = true);
_sitem[type == 2 ? 'value' : 'title'] = str.substring(j-1);
}
setConfigs({...configs})
resolve(1)
break;
}
if(j == str.length - 1) {
pdfHeights[i] = pdfHeight;
setPdfHeights([...pdfHeights])
resolve()
}
}
})
}
03.3 导出
cdn引入jsPDF库
<script src="https://cdn.bootcss.com/jspdf/1.3.4/jspdf.debug.js"></script>
安装html2canvas插件
yarn add html2canvas
导出函数 —— onSavePdf
const onSavePdf = async ()=>{
var targetList = document.getElementsByClassName("pdf");
console.log(window);
var pdf = new window.jsPDF('', 'pt', 'a4');
for (let i = 0; i < targetList.length; i++) {
let target = targetList[i]
target.style.background = "#FFFFFF";
await setImg(target, pdf)
}
pdf.save("导出的.pdf");
}
// 根据dom生成图片
const setImg = (target, pdf)=>{
return new Promise((resolve, reject)=>{
html2canvas(target, {})
.then(canvas=>{
const contentWidth = canvas.width;
const contentHeight = canvas.height;
//a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高
const imgWidth = 595.28;
const imgHeight = 592.28 / contentWidth * contentHeight;
const pageData = canvas.toDataURL('image/jpeg', 1.0);
pdf.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight);
pdf.addPage();
resolve()
})
})
}
如开头所说,这些仅满足当前项目需求,还有很多情况没有考虑;比如所有布局的标题超高、'rLR'布局值超高等等...