环境:vue3、element plus、ts。
应用场景:OA系统打印功能,包含打印支票,表格。
最终实现:通过css 绝对单位mm 进行定位处理结合方法一实现打印支票功能。
请参照MDN文档细致了解,cm,mm等单位在屏幕上显现时与px的转换关系 - CSS:层叠样式表 | MDN (mozilla.org)
全局打印
直接使用window.print(),打印的内容会根据打印纸质大小,结合浏览器布局方式,映射到打印纸张。(不常用)
局部打印
方法一
想要实现一个好的打印功能,我以为需要满足以下5个条件
- 打印页面上一个或者多个指定的元素及子元素
- 多个元素的相对位置没有任何要求
- 保留被打印元素的样式
- 无论是展示元素还是表单输入元素均可被完整打印
- 打印预览时不能影响网页的正常展示
网络上一些较常见的方法基本是获取指定元素的 innerHTML 并放入一个 iframe 之中,为保留样式需要将原网页的所有样式表同步带入 iframe 中,然后调用 iframe 的 print 方法,工作量大不说,并且如果需要的打印的元素的样式有父级元素有依赖,这个操作会丢失父级元素的信息,部分样式可能无法生效,且表单元素的输入也会丢失。还有些实现是修改当前页面的 dom 结构,获取被打印元素,直接挂在 body 下,且 body 下只保留被打印元素,如此一来,当前页面已经发生变化,在打印完成后,还需要进行页面恢复,也是比较复杂的。
实现
@media print {
:has(.print-element) > :not(.print-element):not(:has(.print-element)) {
display: none;
}
}
如此只要在需要被打印的元素上添加 print-element 这个类名,当调用 window.print() 的时候,只有那些有这个类的元素及其父元素和子元素可以正常被预览,并打印出来
也可以使用elements-printer 支持自定义类名,多个不同打印区域。vue 项目通过指令的方式简化调用等
方法二
方法一种已经提及啦较为繁琐的方法二---使用iframe元素
该方法是定义一个vue自定义指令,复用性较高,但不实用于打印支票功能场景,该指令会有一定的左右偏移量。
import type { ObjectDirective, DirectiveBinding } from "vue";
class Print {
dom: HTMLElement | null;
iframeId: string = "print_area_" + new Date().getTime();
canvasClass = "print_area_canvas_2_img";
constructor(id: string) {
this.dom = document.getElementById(id);
this.init();
}
init() {
if (this.dom) {
const win = document.createElement("iframe");
document.body.appendChild(win);
try {
win.setAttribute("id", this.iframeId);
win.style.display = "none";
win.onload = () => this.print(win.contentWindow);
this.write(win.contentDocument);
} catch (e) {
alert(e);
}
} else {
window.print();
}
}
print(win: Window | null) {
if (win) {
win.focus();
win.print();
}
this.clear();
}
clear() {
let iframe = document.getElementById(this.iframeId);
if (iframe) iframe?.parentNode?.removeChild(iframe);
this.dom.querySelectorAll("." + this.canvasClass).forEach((node) => {
if (node.tagName.toLowerCase() === "img") {
node?.parentNode?.removeChild(node);
} else {
node.style.display = "block";
}
});
}
write(doc: Document | null) {
if (doc) {
doc.open();
doc.write(`<!DOCTYPE html><html>${this.head()}${this.body()}</html>`);
doc.close();
}
}
head() {
return `<head><title></title><link rel="stylesheet" href="/static/print.css"></head>`;
}
body() {
this.dom.querySelectorAll("canvas").forEach((node) => {
if (node.style.display !== "none") {
let url = node.toDataURL();
let img = new Image();
img.src = url;
img.style = node.style;
img.className = this.canvasClass;
node.className += this.canvasClass;
node?.parentNode?.appendChild(img);
node.style.display = "none";
}
});
let dom = this.dom.cloneNode(true);
let selectCount = -1;
dom.querySelectorAll("input,select,textarea").forEach((node) => {
if (node.tagName === "INPUT") {
if (["radio", "checkbox"].indexOf(node.getAttribute("type")) >= 0) {
node.checked && node.setAttribute("checked", node.checked);
} else {
node.setAttribute("value", node.value);
}
}
if (node.tagName === "SELECT") {
selectCount++;
for (let i = 0; i < this.dom.querySelectorAll("select").length; i++) {
let select = this.dom.querySelectorAll("select")[i];
!select.getAttribute("newbs") && select.setAttribute("newbs", i + "");
if (select.getAttribute("newbs") === selectCount + "") {
let opSelectedIndex =
this.dom.querySelectorAll("select")[selectCount].selectedIndex;
node.options[opSelectedIndex].setAttribute("selected", true);
}
}
}
if (node.tagName === "TEXTAREA") {
node.innerHTML = node.value;
node.setAttribute("html", node.value);
}
});
return "<body>" + dom.outerHTML + "</body>";
}
}
/**
* @file print dom
* @method v-print:[id]
* @param id: Get dom by id to print, default is window
*/
const print: ObjectDirective = {
beforeMount(el: HTMLElement, binding: DirectiveBinding) {
const id = binding.arg;
// const opts = binding.value;
el.addEventListener("click", () => new Print(id));
},
};
export default print;