window.print()实现局部打印和整个窗口打印

1,891 阅读3分钟

环境: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;