侧边栏壁纸
博主头像
贯耳症博主等级

瓜虫冬匕ing……

  • 累计撰写 70 篇文章
  • 累计创建 50 个标签
  • 累计收到 905 条评论

目 录CONTENT

文章目录

Vue局部打印组件(上)

贯耳症
2019-12-02 / 0 评论 / 0 点赞 / 2,797 阅读 / 7,249 字
温馨提示:
本网站有 CDN 缓存,一般刷新 3 次左右即可获取最新页面。

后台管理系统当中,报表的打印和导出是非常常用的功能,这次我就记录一下如何手写一个局部打印功能的组件。

最终实现效果:

上图的数据打了马赛克,不过还是基本能看出来实现了局部打印功能,并且表格的样式都能正常显示。为了实现这个局部打印功能,我在中间还是趟了不少坑的。

一、实现局部打印的方式

浏览器的window对象有一个print()方法可以用来打印,但这种方式只能打印整个网页,不能对具体的某个元素进行打印,并且dom元素对象中也没有相关的打印方法,这就使得局部打印必须采取一些技巧才能实现。

第一种方式是新建窗口(标签页),专门放打印内容,然后调用window.print()方法进行打印。但这种方式就要每一个打印内容都需要新建页面(标签页必须得有网址,不能用js凭空创建),不能进行有效的复用。

第二种方式是用iframe标签,在里面放置打印内容,然后调用iframe.contentWindow.print()方法进行打印。这种方式需要处理很多细节上的东西,但处理之后其他地方就可以进行简单的调用了。

综合考虑,最终我选择使用第二种方式,并且使用组件来做而非常规的js文件(可以方便地使用vue的模板渲染表格)。

另外就是使用jquery插件printArea,但是作为一个高端的vue项目,怎么能jQuery这种东西呢?遂弃之。

还有就是printjs、vuePlugs_printjs等纯js插件,printjs可以打印,但样式不能设置成我满意的样式;vuePlugs_printjs样式和原内容一样,但不能将超出内容自动分页,遂弃之。

二、实现局部打印

这里参考了vuePlugs_printjs的源码,在此表示感谢。

<template>
  <div>
    <!-- 打印占位 -->
    <iframe id="printf" src width="0" height="0" frameborder="0"></iframe>
    <!-- 打印内容,我这里是为了打印表格,你也可以替换为自己需要打印的内容 -->
    <table ref="printTable" id="printTable" class="spl-table">
    </table>
  </div>
</template>

<script>
export default {
  name: 'simple-table',
  methods: {
    /**
     * 打印表格内容
     */
    printTable () {
      // 1.获取要打印的内容的一份复制,否则待会儿添加节点时会将原有内容删除
      const content = document.getElementById('printTable').cloneNode(true)
      // 2.获取放置打印内容的iframe
      const ifm = document.getElementById('printf')
      // 3.添加打印内容样式
      let str = ''
      const styles = document.querySelectorAll('style,link')
      for (let i = 0; i < styles.length; i++) {
        str += styles[i].outerHTML
      }
      ifm.contentDocument.write(str)
      // 4.添加打印内容并打印
      // 使iframe中存在body元素,便于使用dom元素的方法
      ifm.contentDocument.write('<div></div>')
      ifm.contentDocument.close()
      ifm.contentDocument.body.appendChild(content)
      ifm.contentWindow.print()
    }
  }
}
</script>

<style scoped>
@import './simple-table.css';
</style>

上面代码中的注释已经写的很全面了,重点就是要将当前页面中的样式定义导入到<ifrme>元素中。并且在导入dom元素时没有将其转为字符串写入,而是直接采用appendChild()方法插入dom元素,这是因为转为字符串插入过程中一些dom元素会丢失导致表格显示异常。不过用了这种dom节点插入的方式之后,原有的节点就会被移动到这里,那么用户重复打印时这个组件的打印功能就失效了,这显然不是我们想要看到的结果。因此在前面创建了一个打印内容的复制,保证在插入节点后原节点依旧存在。

三、超出内容分页、隐藏打印内容

在上面的代码实现了局部打印功能之后,当打印内容超出一页时,我发现打印预览却依然只有一页,而在正常页面中直接右键打印却可以自动分页,那到底是哪里出了问题呢?

在参考了这篇文章之后,我明白了问题所在:我们的后台管理系统基本上body、html都是固定高度的,此时window的打印功能就只能确定一页的高度,因此我们需要将body和html的高度自适应子元素的高度,即:

html, body {
	height: inherit;
}

但是先别急,这样一设置我们的后台管理页面不就乱了套了嘛,此时就要用到CSS的媒体查询了:

/* 打印时显示 */
@media print {
  .spl-table {
    display: table;
  }

  html, body {
    height: inherit;
  }
}

上面代码中同样将我们要打印的内容显示出来了,因此在普通的样式中我们就要将打印内容隐藏起来。这样就可以实现一个较为完美的局部打印功能了。

附录:简单的表格打印组件

下面是我实现的一个表格打印组件(环境:Vue+iview表格)。iview表格用于在查询界面显示报表,这个表格打印组件用于显示标准的表格打印内容。所以这个组件的各种参数都是以iview中的Table组件的参数来定义的,为的就是不用对同一个数据源做两次改变。

组件内容

<template>
  <div>
    <!-- 打印占位 -->
    <iframe id="printf" src width="0" height="0" frameborder="0"></iframe>
    <!-- 打印内容 -->
    <table ref="printTable" id="printTable" class="spl-table">
      <caption>
        <!-- 自定义header插槽,用于放置自定义标题内容,详见https://cn.vuejs.org/v2/guide/components-slots.html,注意:建议不要与下方title一起使用 -->
        <slot name="header"></slot>
        <div class="spl-fz16">{{title}}</div>
        <div class="spl-fz12">{{subTitle}}</div>
      </caption>
      <thead>
        <!-- 遍历标题 -->
        <tr v-for="n in headLoopData.trNum" :key="n">
          <th
            v-for="(th, thIndex) in headLoopData.deepArray[n - 1]"
            :key="thIndex"
            :rowspan="hasChildren(th) ? 1 : (headLoopData.trNum - n + 1)"
            :colspan="hasChildren(th) ? th.children.length : 1"
          >{{th.title}}</th>
        </tr>
      </thead>
      <tbody>
        <!-- 遍历数据 -->
        <tr v-for="(item, trIndex) in data" :key="trIndex">
          <template v-for="(pptDe, tdIndex) in headLoopData.columnDefines">
            <td
              v-if="rowAndColSpan(trIndex, tdIndex).indexOf(0) < 0"
              :key="tdIndex"
              :rowspan="rowAndColSpan(trIndex, tdIndex)[0]"
              :colspan="rowAndColSpan(trIndex, tdIndex)[1]"
              :class="tdClass(rowAndColSpan(trIndex, tdIndex))"
            >{{tdContent(item, pptDe, trIndex)}}</td>
          </template>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
export default {
  name: 'simple-table',
  props: {
    // 列定义
    columns: {
      type: Array,
      default: function () {
        return []
      }
    },
    // 表格数据
    data: {
      type: Array,
      default: function () {
        return []
      }
    },
    // 合并单元格策略(要隐藏的单元格需设置span为0,否则会导致表格显示错位)
    spanMethod: {
      type: Function,
      default: function () {
        return [1, 1]
      }
    },
    // 标题
    title: {
      type: String,
      default: ''
    },
    // 二级标题
    subTitle: {
      type: String,
      default: ''
    }
  },
  computed: {
    // 显示标题行所需的变量
    headLoopData () {
      // 行数
      let trNum = 0
      // 每层标题的数据
      const deepArray = []
      // 每列的定义
      const columnDefines = []

      // 计算列定义深度
      function countDeep (obj, num) {
        if (num > 0) {
          deepArray[num - 1].push(obj)
        }
        if (obj.children && obj.children.length) {
          num++
          if (num > trNum) {
            trNum = num
            deepArray.push([])
          }
          obj.children.forEach(item => {
            countDeep(item, num)
          })
        } else {
          columnDefines.push(obj)
        }
      }

      if (this.columns.length) {
        trNum = 1
        deepArray.push([])
      }
      this.columns.forEach(item => {
        countDeep(item, 1)
      })

      return { trNum, deepArray, columnDefines }
    }
  },
  methods: {
    /**
     * 是否有子节点
     * @param th - 列定义数组中的对象
     */
    hasChildren (th) {
      return th.children && th.children.length
    },
    /**
     * 单元格合并策略
     * @param rowIndex - 行索引
     * @param columnIndex - 列索引
     */
    rowAndColSpan (rowIndex, columnIndex) {
      let de = [1, 1]
      if (this.spanMethod && this.spanMethod instanceof Function) {
        de = this.spanMethod({ rowIndex, columnIndex })
      }
      return de
    },
    /**
     * 单元格显示内容
     * @param row - 行数据
     * @param colDe - 列定义
     * @param trIndex - 行索引
     */
    tdContent (row, colDe, trIndex) {
      if (colDe.type === 'index') {
        return trIndex + 1
      } else {
        return colDe.key ? row[colDe.key] : ''
      }
    },
    /**
     * 单元格样式
     * @param spanDef - 单元格合并结果,例:[1,1]
     */
    tdClass (spanDef) {
      if (spanDef[0] > 1 || spanDef[1] > 1) {
        // 合并单元格后设置单元格内容居中显示
        return 'cell-center'
      } else {
        return ''
      }
    },
    /**
     * 打印表格内容
     */
    printTable () {
      // 1.获取要打印的内容的一份复制,否则待会儿添加节点时会将原有内容删除
      const content = document.getElementById('printTable').cloneNode(true)
      // 2.获取放置打印内容的iframe
      const ifm = document.getElementById('printf')
      // 3.添加打印内容样式
      let str = ''
      const styles = document.querySelectorAll('style,link')
      for (let i = 0; i < styles.length; i++) {
        str += styles[i].outerHTML
      }
      ifm.contentDocument.write(str)
      // 4.添加打印内容并打印
      // 使iframe中有body元素
      ifm.contentDocument.write('<div></div>')
      ifm.contentDocument.close()
      ifm.contentDocument.body.appendChild(content)
      ifm.contentWindow.print()
    },
    exportTable () {
      // excel.export_table_to_excel('tryprint', '测试表格.xlsx')
    }
  }
}
</script>

<style scoped>
@import './simple-table.css';
</style>

样式文件

/* 默认隐藏 */
.spl-table {
  width: 100%;
  border-collapse: collapse;
  display: none;
}

/* 打印时显示 */
@media print {
  .spl-table {
    display: table;
  }

  html, body {
    height: inherit;
  }
}

.spl-table caption {
  font-weight: bold;
  line-height: 26px;
}

.spl-table thead tr {
  border-bottom-width: 1px;
  border-bottom-style: solid;
  border-bottom-color: rgb(211, 202, 221);
}

.spl-table thead th {
  padding: 5px 10px;
  font-size: 13px;
  font-family: Verdana;
  color: rgb(95, 74, 121);
  border-style: solid;
  border-width: 1px;
}

.spl-table tbody td {
      padding: 5px 10px;
    font-size: 12px;
    font-family: Verdana;
    color: rgb(95, 74, 121);
    border-style: solid;
    border-width: 1px;
}

.spl-fz16 {
  font-size: 16px;
}

.spl-fz12 {
  font-size: 14px;
}

.cell-center {
  text-align: center;
  vertical-align: center;
}

使用示例

<!-- 打印组件 -->
<simple-table ref="spTable" :columns="tableColumns" :data="tableList" :span-method="controlTableSpan" title="统计报表" sub-title="2019-12-15 ~ 2019-12-30 统计报表"/>
// 执行打印方法
this.$refs['spTable'].printTable()

另外还有一个功能是将表格内容强制分页,同样是利用CSS来做的,思路就是将要分页的表格分成多个table,然后在table之间加上css分页样式:page-break-after: always;即可。不过怎么在这个组件中实现我暂时还没有思路,先在此记录一下。

0

评论区