my-print/src/Print.vue

<!-- 页面打印组件 -->
<template>
  <span @click="handlePrint">
    <slot name="button">
      <el-button :type="type" :class="btnClass">{{text}}</el-button>
    </slot>
  </span>
</template>
<script>
import {
  addClass,
  removeClass,
  hasClass,
  setStyle
} from 'element-ui/lib/utils/dom'
/**
 * MyPrint
 * @module widgets/my-print
 * @author 李国师
 * @example
 *
 * // 使用说明
 *  <my-print print-range=".pring-range1">
 *  </my-print>
 *
 * // ----------或使用slot-------
 *
 *  <my-print print-range=".pring-range1">
 *    <el-button type="primary" icon="el-icon-tickets" slot="button"></el-button>
 *  </my-print>
 */
/**
 * 作用域插槽
 * @member slot
 * @property {string} [button] 自定义按钮
 */
export default {
  name: 'MyPrint',
  /**
   * 属性参数
   * @member
   * @property {String} [text = '打印'] 按钮文字
   * @property {String} [type = 'default'] 按钮类型
   * @property {String} [btnClass] 按钮样式名称
   * @property {String} [printRange] 打印区域,css选择器,该参数为必须
   * @property {String} [containerClass = 'my-print-page-container'] 打印内容的容器className
   * @property {Function | HTMLElement | String} [parentDom] 作为父容器的dom对象,或者返回dom对象的方法,dom中必须包含container-class指定的className
   * @property {Object} [rootStyle] 顶层容器的样式,请不要设置position为absolute,relative或static, 否则分页效果会不起作用
   * @property {Function} [beforePrint] 在复制打印区域前执行,须返回promise对象
   * @property {Function} [afterPrint] 在打印完成之后执行
   * @property {String} [removeSelector] 打印时需要删除的元素选择器
   * @property {Array} [styleArray] 打印时须改变的样式数组,示例:[{selector: '.selector', style: {height: 'auto'}}]
   * @property {Function} [domTransfer] 自定义修改打印内容的方法
   * @property {Boolean} [disableDefaultStyle] 禁用默认样式数组,只使用style-array参数
   * @property {Boolean} [preview] 打印后不删除页面打印内容
   */
  props: {
    text: {
      type: String,
      default: '打印'
    },
    type: {
      type: String,
      default: 'default'
    },
    btnClass: {
      type: String,
      default: ''
    },
    printRange: {
      type: String,
      default: '',
      required: true
    },
    containerClass: {
      type: String,
      default: 'my-print-page-container'
    },
    parentDom: [HTMLElement, Function, String],
    rootStyle: {
      type: Object,
      default: () => {
        return {}
      }
    },
    beforePrint: Function,
    afterPrint: Function,
    removeSelector: {
      type: String,
      default: ''
    },
    styleArray: {
      type: Array,
      default() {
        return []
      }
    },
    domTransfer: Function,
    disableDefaultStyle: Boolean,
    preview: Boolean
  },
  data() {
    return {
      handle: null,
      tDom: null,
      defaultStyles: [
        {
          selector: '.el-table',
          style: {
            height: 'auto'
          }
        },
        {
          selector: '.el-table__body-wrapper',
          style: {
            height: 'auto'
          }
        }
      ]
    }
  },
  methods: {
    handlePrint() {
      if (this.beforePrint && typeof this.beforePrint === 'function') {
        this.beforePrint().then(() => {
          // this.print();
          this.cloneDom()
        })
      } else {
        // this.print();
        this.cloneDom()
      }
    },
    domTransferDefault(tDom) {
      // 顶层容器默认的样式,请不要设置position为absolute,relative或static, 否则分页效果会不起作用
      let style = {
        width: '100%',
        'max-width': '1200px',
        'background-color': '#fff',
        padding: '0 80px 0 0',
        'box-sizing': 'border-box'
      }
      // 使用者自行传入的样式,如果有同名样式,会覆盖掉
      if (this.rootStyle) {
        style = Object.assign(style, this.rootStyle)
      }
      setStyle(tDom, style)
      // 在打印之前可能需要删除某些元素,可通过removeSelector传入选择器将其删除
      if (this.removeSelector) {
        const removeDoms = tDom.querySelectorAll(this.removeSelector)
        for (let i = 0; i < removeDoms.length; i++) {
          const item = removeDoms[i]
          if (item) {
            item.parentNode.removeChild(item)
          }
        }
      }
      // 在打印之前可能需要改变某些元素的样式,可以将一个数组通过styleArray传进来
      // 数组格式为 [{selector: '.selector-name',style: {height: 'auto'}}]
      let selectors
      if (!this.disableDefaultStyle) {
        selectors = this.defaultStyles.concat(this.styleArray).concat([
          {
            selector: this.printRange,
            style: {
              height: 'auto'
            }
          }
        ])
      } else {
        selectors = this.styleArray
      }
      // 迭代改变每个打印区域的每个dom元素
      for (let i = 0; i < selectors.length; i++) {
        const elems = tDom.querySelectorAll(selectors[i].selector)
        for (let j = 0; j < elems.length; j++) {
          setStyle(elems[j], selectors[i].style)
        }
      }
      // 如果使用者需要编写自己的转换逻辑,则使用domTransfer参数传递一个处理方法
      let tDomTransfered
      if (this.domTransfer && typeof this.domTransfer === 'function') {
        tDomTransfered = this.domTransfer(tDom, this)
      } else {
        tDomTransfered = tDom
      }
      if (tDomTransfered) {
        return tDomTransfered
      } else {
        throw new Error('domTransfer方法须返回正确的dom元素')
      }
    },
    removePrintPreview() {
      console.log('remove preview')
      // 将打印区域外的元素重新显示
      const rootElems = document.querySelectorAll('body>*')
      for (let i = 0; i < rootElems.length; i++) {
        removeClass(rootElems[i], 'my-print-displaynone')
        removeClass(rootElems[i], 'my-print-hide')
      }
      // 删除打印区域
      document.body.removeChild(this.tDom)
      this.tDom = null
    },
    createDom(innerHTML) {
      const con = document.createElement('div')
      con.innerHTML = innerHTML.toString()
      return con.childNodes[0]
    },
    prependDom(parent, elem) {
      parent.innerHTML = elem.outerHTML + parent.innerHTML
      return parent
    },
    closeAfterPrint() {
      this.handle = document.execCommand('print')
      if (this.handle) {
        // 如果不保留打印预览,则直接删除打印区域
        this.removePrintPreview()
        if (this.afterPrint && typeof this.afterPrint === 'function') {
          this.afterPrint()
        }
      } else {
        setTimeout(() => {
          this.closeAfterPrint()
        }, 300)
      }
    },
    cloneDom() {
      if (this.printRange) {
        // 将打印区域筛选出来用一个div包裹起来
        const contents = document.querySelectorAll(this.printRange)
        const con = document.createElement('div')
        for (let i = 0; i < contents.length; i++) {
          const cloneElem = contents[i].cloneNode(true)
          // 处理canvas元素,使用原来canvas对象的toDataURL方法生成base64,替换复制dom中的canvas
          const canvas = contents[i].querySelectorAll('canvas')
          const canvasClone = cloneElem.querySelectorAll('canvas')
          for (let j = 0; j < canvas.length; j++) {
            const imgSrc = canvas[j].toDataURL()
            const img = document.createElement('img')
            img.src = imgSrc
            canvasClone[j].parentNode.replaceChild(img, canvasClone[j])
          }
          // 添加分页符样式,最后一页不加分页符,否则如果最后一页占满了,会多出一页空白页
          if (i !== contents.length - 1) {
            addClass(cloneElem, 'my-print-page-break')
          }
          // 将复制的打印区域添加到容器中
          con.appendChild(cloneElem)
        }
        // 使用一个容器再包裹,方便后面移除
        const tDom = document.createElement('div')
        // tDom.id = 'body-print-range';
        // 在顶层容器和打印内容之间可以添加DOM元素,但需指定打印内容所在的class名称
        if (this.parentDom) {
          let parentDom
          let parentCon
          if (this.parentDom instanceof HTMLElement) {
            parentDom = this.parentDom
          } else if (this.parentDom instanceof Function) {
            parentDom = this.parentDom()
          } else if (typeof this.parentDom === 'string') {
            parentDom = this.createDom(this.parentDom)
          } else {
            throw new Error('parent-dom必须为dom元素')
          }
          if (!(parentDom instanceof HTMLElement)) {
            throw new Error('parent-dom参数格式错误,无法生成合法的dom')
          }
          // 如果parentDom包含有container-class指定的类,则本身作为容器,否则查找内部是否有该类
          if (hasClass(parentDom, this.containerClass)) {
            parentCon = parentDom
          } else {
            parentCon = parentDom.querySelector('.' + this.containerClass)
          }
          if (parentCon) {
            parentCon.appendChild(con)
            tDom.appendChild(parentDom)
          } else {
            throw new Error('没找到父容器,检查是否缺少container-class参数')
          }
        } else {
          tDom.className = this.containerClass
          tDom.appendChild(con)
        }
        if (tDom) {
          const tDomTransfered = this.domTransferDefault(tDom)
          // body 下的元素都添加my-print-hide样式,打印时隐藏
          const rootElems = document.querySelectorAll('body>*')
          for (let i = 0; i < rootElems.length; i++) {
            addClass(rootElems[i], 'my-print-hide')
          }
          this.tDom = tDomTransfered
          // 打印内容添加到body下
          document.body.appendChild(tDomTransfered)
        }
        // 添加打印预览头部
        const previewTitle = this
          .createDom(`<div class="my-print-preview-title my-print-hide">
                                                    <span class="title">打印预览</span>
                                                    <div class="my-print-preview-close" onclick="handlePrint()">打印</div>
                                                    <div class="my-print-preview-close" onclick="closePrintPreview()">关闭</div>
                                                </div>`)
        this.prependDom(tDom, previewTitle)

        // 如果尚未注册打印方法,则注册一个
        window.closePrintPreview = () => {
          this.removePrintPreview()
        }
        window.handlePrint = () => {
          this.print()
        }
        // 隐藏除打印区域外的内容
        const rootElems = document.querySelectorAll('body>*')
        for (let i = 0; i < rootElems.length; i++) {
          if(rootElems[i] !== tDom) {
            addClass(rootElems[i], 'my-print-hide')
            if(this.preview) {
               addClass(rootElems[i], 'my-print-displaynone')
            }
          }
        }
        addClass(tDom, 'my-print-visible')
        if (!this.preview) {
          addClass(tDom, 'my-print-displaynone')
        }
        // 如果不预览直接打印
        if (!this.preview) {
          this.print()
        }
      }
    },
    print() {
      if (this.printRange) {
        setTimeout(() => {
          this.closeAfterPrint()
        }, 20)
      } else {
        window.print()
      }
    }
  }
}
</script>