如何实现最真实的web打印

17,227 阅读5分钟

如何实现最真实的web打印

公司组织了体检,在医院发现可以使用身份证快捷打印体检单,觉得很方便,但仔细一看,打印效果极差。

现状

注:如直接调用Ctrl + P,则是打印当前的视口,可以选择打印机及打印机纸张,这显然不满足业务要求;若调用window.print(),则只能将css写在html上一起传到打印机,且无法预设打印机及纸张和其他设置,也不能保存模板。翻阅了一下大部分打印,都是调用window.print()去实现,都存在一下问题:

  • 打印样式、排版问题。
    打印样式与浏览器书写不同,页面显示书写一套css,传给打印机的html字符串也要书写一套css,而且无法兼容所有纸张。
  • 无法限定预设打印机、打印机纸张及保存为模板。
    使用ActiveX(仅在IE下),显然不满足业务要求。
    使用Websocket与本机建立连接获取本机打印信息。
  • 自定义纸张问题。
    无法做到自定义纸张。
  • 浏览器显示与打印结果不一致。
    这个是最为常见也亟待解决的问题。

恰巧需要做打印功能,于是决定重构,解决上面的问题。我采用的是第二种方案并集成PAZU云打印,使用c++开发的一个exe脚本,在运行时与本机Websocket连接返回打印信息。需要详细了解的同学请移步:PAZU官网

打印效果

打印结果在测试后趋向于真实打印,与web显示几乎一致。

打印思路

总体的思路是将打印机设备参数可视化,可配置,可预设到点击打印时传到打印设备上。不支持自定义就先同步到打印机,然后选择。

打印模板设置

一、获取打印数据

比较典型的ERP详情页面,包含主表Form和子表Table明细

1、获取Form
GetSlideInfo () {
  let CondiData = []
  let SlideInfoVnode = this.$refs.SlideInfo.$children[0]
  let FormItems = findComponentsDownward(SlideInfoVnode, 'FormItem')
  FormItems.forEach((item, itemIndex) => {
    let field = item.$children
    if (field.length) {
      const fieldVnode = item.$children[0]
      let fieldValue = this.GetExportFieldValue(fieldVnode)
      let formItem = {
        key: item.prop,
        label: item.label.split(':')[0],
        value: fieldValue,
        index: itemIndex
      }
      Object.assign(formItem, fieldVnode.print)
      CondiData.push(formItem)
    }
  })
  return CondiData
}

使用findComponentsDownward获取所有的FormItem,然后获取FormItemVueComponment,在组件实例里获取到打印信息。
为了支持列表批量打印详情,于是注册了一个v-print

import { typeOf } from '@/libs/util'
export default {
  inserted: (el, { value, arg }, vnode) => {
    if (!value) return
    if (typeOf(value) != 'object') value = {}
    vnode.componentInstance.print = Object.assign(value, { renderType: arg || 'input' })
  },
  update: (el, { value, arg }, vnode) => {
    if (!value) return
    if (typeOf(value) != 'object') value = {}
    vnode.componentInstance.print = Object.assign(value, { renderType: arg || 'input' })
  }
}

然后在页面这样书写,即可在inserted时挂载到组件实例data上。

v-print:pick="{id:'orderBranchId',code:'orderBranchCode',name:'orderBranchName'}"
2、构造打印路由数据

在构造数据之前,我们要造好动态路由并加入前端路由白名单:

{
  path: '/print',
  name: 'print',
  component: Main,
  children: [{
    path: 'print-setting/:sheetType',
    name: 'print-setting',
    meta: {
      title: '打印设置'
    },
    component: () => import('@/view/main-components/print-setting.vue')
  }]
}

要保持数据持久化,于是我将数据保存到store并同时set到sessionStorage:

import { setSession } from '@/libs/session.js'
export default {
  state: {
    printTabs: {}
  },
  mutations: {
    setPrintTab (state, tab) {
      state.printTabs[tab.route] = tab.data
      setSession(tab.route, tab.data)
    }
  }
}

构造好数据之后进入打印页面:

this.setPrintTab({ route: this.$route.meta.code, data: params })
this.$router.push({ name: 'print-setting', params: { sheetType: this.$route.meta.code } })

二、创建打印模板

1、拖拽集成及自适应

打印拖拽项由四个可拖拽的draggable组成,可相互拖拽,这里采用的是vuedraggable。

<Row style="height:100%">
  <Form style="height:100%" justify :label-position="labelPositon" ref="formList">
    <draggable id="formList" :list="formList" style="height:100%" group="people" :animation="150" :ghostClass="cls+'-left-item-chosen'">
      <Col
        :span="8*item.span"
        :class="[cls+'-form-item',{'item-select':item.selected}]"
        v-for="(item, index) in formList"
        :key="index"
        @click.native="handleFormItemClick(item, index)"
      >

        <form-item v-if="!item.blank" :label="item.label" >{{item.value}}</form-item>
        <div :class="cls+'-blank'" v-else></div>
      </Col>
    </draggable>
  </Form>
</Row>

设置4个可拖拽group相同即可相互拖拽。

2、打印机信息

读取本机打印机:

PAZU.TPrinter.getPrinters()

读取当前打印机纸张:

PAZU.TPrinter.getPaperForms()

打印及打印校验:

validatePrintExe () {
  return new Promise((resolve, reject) => {
    PAZU.TPrinter.getPrinters(res => {
      if (res === 'err') {
        resolve(false)
      } else {
        resolve(true)
      }
    })
  })
},
async handlePrint () {
  if (await this.validatePrintExe()) {
    this.doPageSetup()
    PAZU.print('printContent'nullnulltrue)
    api.updatePrintCount({ idsObject.values(this.printData.ids).join('~&z') }, this.printData.action)
  } else {
    this.showPrintDownLoad = true
  }
}

同步自定义纸张到打印机:

handleSynchronizeComfirm () {
  this.$refs.synchronizeForm.validate(valid => {
    if (valid) {
      PAZU.TPrinter.createPaper(this.baseSetting.width, this.baseSetting.height, (res) => {
        if (res) {
          this.$Message.warning(`规格【${res}】的纸张已存在。`)
        } else {
          this.showPrintor = false
          this.$Message.info('同步成功。')
        }
      }, this.synchronize.printName, this.synchronize.pageName)
    }
  })
}

问题汇总

一套打印下来问题重重,需求也巨大,但在写文档章时发现不知该如何把可移植的思想及技术难点单方面的开放出来。这之后的文章我也不知如何写起。

一、如何做到web显示和打印结果一致?

  • web显示和打印样式使用同一套css并配置自适应。

二、如何打印出图片?

  • 必须使用img标签,图片转base64即可。

三、打印空白页问题及处理?

  • 我是严格按照web显示,精确到1px,这样打印下来确实是不会出现空白页,而且排版正确,但当打印页数超过100页时,囿于浏览器px计算小数处理问题,到100页时竟然有大概10mm的误差。然后在每一页加上page-break-after: always之后竟然会出现莫名的空白页。

四、keep-alive动态路由缓存问题

详细可以看我之前写的《修改vue源码实现对key的keep-alive》)
后面我会针对源码详细讲解为何要修改源码,为何动态路由keep-alive失败。vue3.0正式发布之后我会第一之间测试3.0keep-alive。