Luckysheet 打印终极指南(预览视图+打印功能) : 2025 最新实现
前言
目前打印功能 lucky sheet 官方提供了收费的版本,免费的都是社区提供的测试用例,也有很多小伙伴已经实现了相关功能,网上的参考文章也很多,下面针对Luckysheet-crdt 版本做下打印的适配,仅作为参考示例哈。
代码均为个人实现,未参考任何文章、链接,如有雷同,请联系考证!
预览视图
打印效果
预览视图
源码分析
我们分析下源码内部的实现:
搜索一下这个 type='viewPage':
源码已经预留了实现函数 printLineAndNumberCreate,如果我们直接在这个函数内绘制的话,会导致层级问题,我直接说结论了哈,原因是,最后执行了 reflesh:
在刷新视图的最后,再执行视图绘制,就不会有问题了:
到这,已经识别到需要修改的位置了,下面正式来实现一下打印视图效果吧。
视图原理
我们来看下实现视图预览的原理:
Step 1
一张A4的尺寸是固定的,就是 210x297mm ,需要转换为页面像素值!才可以进行绘制哈。
Step 2
还需要通过整个表格的宽高,确定一共可以分成几张A4,并且计算得到每一张A4的边界位置。
Step 3
有了上诉信息,可以进行绘制处理了
代码实现
如何将 mm 转换成 px 呢?源码给我们提供了一种非常巧妙的方法:
// 获取1mm的像素
function getOneMmsPx() {let div = document.createElement("div");div.style.width = "1mm";document.querySelector("body").appendChild(div);let mm1 = div.getBoundingClientRect();let w = mm1.width;$(div).remove();return w;
}
计算一共能分成多少个页面:
// 这里是循环当前行一共可分为多少页for (let r = 0; r < rowCount; r++) {// 如果行被隐藏了 则跳过if (Store.config["rowhidden"] != null && Store.config["rowhidden"][r] != null) continue;// 当前行的 Y 坐标 - 真实的 y 坐标,与可视区无关let current_end_y = Store.visibledatarow[r];// 前一行的 Y 坐标 - 真实的 y 坐标,与可视区无关let prev_end_y = r > 0 ? Store.visibledatarow[r - 1] : 0;// 计算相对于前一个分页线的坐标let relativeCurrentY = current_end_y;let relativePrevY = prev_end_y;// 如果已经有分页线,则基于上一个分页线计算相对位置if (horizontalCells.length > 0) {let lastPageBreak = horizontalCells[horizontalCells.length - 1].rowIndex;let lastPageBreakY = Store.visibledatarow[lastPageBreak];relativeCurrentY = current_end_y - lastPageBreakY;relativePrevY = prev_end_y - lastPageBreakY;}// 计算当前行属于第几个页面(基于页面高度的倍数)let currentPageIndex = Math.floor(relativeCurrentY / paperHeight);// 计算前一行属于第几个页面let prevPageIndex = Math.floor(relativePrevY / paperHeight);// 如果当前行和前一行属于不同的页面,说明当前行是新页面的开始行if (currentPageIndex > prevPageIndex) {horizontalCells.push({ rowIndex: r, y: current_end_y });}}
表格上下文对象:
// 获取canvas上下文const luckysheetTableContent = $("#luckysheetTableContent").get(0).getContext("2d");luckysheetTableContent.save();// 保持与 drawMain 一致的缩放luckysheetTableContent.scale(Store.devicePixelRatio, Store.devicePixelRatio);
开始绘制:
// 一行可以被分为多少页?const rowPageCount = verticalCells.length;// 则每新增一行,都以 rowPageCount 为倍数递增即可for (let i = 0; i < horizontalCells.length; i++) {// 这个是行的 Y 值哈const row = horizontalCells[i];// 为了实现居中,当前间隔的中心坐标是const currentY = row.y;const prevY = i > 0 ? horizontalCells[i - 1].y : 0;const centerY = currentY - (currentY - prevY) / 2 - scrollHeight;// 绘制水平线drawPrintLine(luckysheetTableContent, offsetLeft, row.y - scrollHeight + offsetTop, sheetWidth, row.y - scrollHeight + offsetTop);for (let j = 0; j < verticalCells.length; j++) {// 实现 X 居中,原理类似const col = verticalCells[j];const currentX = col.x;const prevX = j > 0 ? verticalCells[j - 1].x : 0;const centerX = currentX - (currentX - prevX) / 2 - scrollWidth + offsetLeft;// 绘制垂直线 - 类似drawPrintLine(luckysheetTableContent,col.x - scrollWidth + offsetLeft,offsetTop,col.x - scrollWidth + offsetLeft,sheetHeight);drawPageNumbers(luckysheetTableContent, centerX, centerY, i * rowPageCount + j + 1);}}
当然,这里说的比较笼统哈,但是思路是这样的,就是计算全部的行列能被分成几个页面,然后分别记录被切割的单元格 r c 值(我这边设计的是跟随单元格哈,如果大家直接设计为固定宽高,会更简单),说下两者的区别:
正常来说,一张A4的宽高是不会直接对齐边角的,这时候,如果你设计为固定宽高,那么 单元格可能被分成两页打印,理解吧。我这种设计模式就是以最小能绘制的单元格为单位,图例中,会保存 D 单元格,以确保每一个单元格都能完整绘制。
代码中出现了很多参数,含义大家可能不理解,需要理解源码才透彻,什么fill_col_ed
、fill_row_ed、visibledatarow、offset、scroll 啥的,大家多分析下 draw.js @luckysheetDrawMain 方法,一定要先理解源码,才能修改源码。
打印实现
打印就更加简单了,源码给我们提供了 获取选区截图结果的API:
内部的关键函数,就是创建新的 canvas,重新调用 drawMain 渲染主画布,并指定绘制区域即可:
配合上我们刚才实现的页码,可以组合出很多的可能性,什么指定打印页码,指定打印选区,指定打印sheet ,实现思路都是一致的。
// 截取指定页码的页面
function getScreenshotByPageNum(pageNum = 1) {// 通过页码,计算得出有限范围坐标const ch_width = 222;const rh_height = 300;let newCanvas = $("<canvas>").attr({width: Math.ceil(ch_width * Store.devicePixelRatio),height: Math.ceil(rh_height * Store.devicePixelRatio),}).css({ width: ch_width, height: rh_height });// 进行后续的操纵handleScreenshot(newCanvas, ch_width, rh_height);
}
/*** @description 截图的后续处理工作*/
function handleScreenshot(newCanvas, ch_width, rh_height) {let scrollWidth = $("#luckysheet-cell-main").scrollLeft();let scrollHeight = $("#luckysheet-cell-main").scrollTop();// 绘制页面主要内容 - ch_width rh_height 就是指定的页面截图范围luckysheetDrawMain(scrollWidth, scrollHeight, ch_width, rh_height, 1, 1, null, null, newCanvas);let ctx_newCanvas = newCanvas.get(0).getContext("2d");//补上 左边框和上边框ctx_newCanvas.beginPath();ctx_newCanvas.moveTo(0, 0);ctx_newCanvas.lineTo(0, Store.devicePixelRatio * rh_height);ctx_newCanvas.lineWidth = Store.devicePixelRatio * 2;ctx_newCanvas.strokeStyle = luckysheetdefaultstyle.strokeStyle;ctx_newCanvas.stroke();ctx_newCanvas.closePath();ctx_newCanvas.beginPath();ctx_newCanvas.moveTo(0, 0);ctx_newCanvas.lineTo(Store.devicePixelRatio * ch_width, 0);ctx_newCanvas.lineWidth = Store.devicePixelRatio * 2;ctx_newCanvas.strokeStyle = luckysheetdefaultstyle.strokeStyle;ctx_newCanvas.stroke();ctx_newCanvas.closePath();// 补齐统计图 图片let url = newCanvas.get(0).toDataURL("image/png");console.log(url);
}
看,是不是一摸一样? 请注意哈!这种实现方式是通过 canvas 画布绘制转DataUrl 实现的哈,因此,统计图表、图片等 div 创建的,是不能直接截图到画布上的,应该通过新创建的 canvas,通过别的技术,将 div 内容转存到 canvas 上,再进行toDataUrl,就可以了。
总结
大致思路是这样,因为具体的代码很多,有些细节需要大家自行优化。大家如果对这部分还有啥疑问的话,可以留言一起交流。
绘制预览视图部分有很多变量,大家不要觉得难,都是研究 drawMain 中的 单元格绘制 方法中直接拿过来的,有了实现思路后,就感觉不难了。
代码请移步:print.js
🎉麻烦点个 start: Luckysheet-crdt
有疑问欢迎留言交流哦~~~