前端PDF下载以vue3为例

概要

前端直接下载pdf,不用进入后端

整体架构流程

`全局引用方法

例如:
在语言模型中,编码器和解码器都是由一个个的 Transformer 组件拼接在一起形成的。

技术名词解释

  • vue
  • pdf
  • js

技术细节

首先在项目中编写pdf.js文件为最主要的文件,pdf.js编写如下

import jsPDF from 'jspdf'
import html2canvas from 'html2canvas'

const A4_WIDTH = 592.28
const A4_HEIGHT = 841.89

jsPDF.API.output2 = function (outputType = 'save', filename = 'document.pdf') {
  let result = null
  switch (outputType) {
  case 'file':
    result = new File([this.output('blob')], filename, {
      type: 'application/pdf',
      lastModified: Date.now(),
    })
    break
  case 'save':
    result = this.save(filename)
    break
  default:
    result = this.output(outputType)
  }
  return result
}

jsPDF.API.addBlank = function (x, y, width, height) {
  this.setFillColor(255, 255, 255)
  this.rect(x, y, Math.ceil(width), Math.ceil(height), 'F')
}

jsPDF.API.toCanvas = async function (element, width) {
  const canvas = await html2canvas(element, { scale: 4 })
  const canvasWidth = canvas.width
  const canvasHeight = canvas.height
  const height = (width / canvasWidth) * canvasHeight
  const canvasData = canvas.toDataURL('image/jpeg')
  return { width, height, data: canvasData }
}

jsPDF.API.addHeader = async function (x, width, header) {
  if (!(header instanceof HTMLElement)) return
  let __header
  if (this.__header) {
    __header = this.__header
  } else {
    __header = await this.toCanvas(header, width)
    this.__header = __header
  }
  const { height, data } = __header
  this.addImage(data, 'JPEG', x, 0, width, height)
}

jsPDF.API.addFooter = async function (x, width, footer) {
  if (!(footer instanceof HTMLElement)) return
  let __footer
  if (this.__footer) {
    __footer = this.__footer
  } else {
    __footer = await this.toCanvas(footer, width)
    this.__footer = __footer
  }
  const { height, data } = __footer
  this.addImage(data, 'JPEG', x, A4_HEIGHT - height, width, height)
}

/**
 * 生成pdf(处理多页pdf截断问题)
 * @param {Object} param
 * @param {HTMLElement} param.element - 需要转换的dom根节点
 * @param {number} [param.contentWidth=550] - 一页pdf的内容宽度,0-592.28
 * @param {number} [param.contentHeight=800] - 一页pdf的内容高度,0-841.89
 * @param {string} [param.outputType='save'] - 生成pdf的数据类型,添加了'file'类型,其他支持的类型见http://raw.githack.com/MrRio/jsPDF/master/docs/jsPDF.html#output
 * @param {string} [param.filename='document.pdf'] - pdf文件名
 * @param {number} param.x - pdf页内容距页面左边的高度,默认居中显示,为(A4宽度 - contentWidth) / 2)
 * @param {number} param.y - pdf页内容距页面上边的高度,默认居中显示,为(A4高度 - contentHeight) / 2)
 * @param {HTMLElement} param.header - 页眉dom元素
 * @param {HTMLElement} param.footer - 页脚dom元素
 * @param {boolean} [param.headerOnlyFirst=true] - 是否只在第一页添加页眉
 * @param {boolean} [param.footerOnlyLast=true] - 是否只在最后一页添加页脚
 * @param {string} [param.mode='adaptive'] - 生成pdf的模式,支持'adaptive'、'fixed','adaptive'需给dom添加标识,'fixed'需固定布局。
 * @param {string} [param.itemName='item'] - 给dom添加元素标识的名字,'adaptive'模式需在dom中设置
 * @param {string} [param.groupName='group'] - 给dom添加组标识的名字,'adaptive'模式需在dom中设置
 * @returns {Promise} 根据outputType返回不同的数据类型
 */
async function getPdf ({
  element,
  contentWidth = 550,
  contentHeight = 800,
  outputType = 'save',
  filename = 'document.pdf',
  x,
  y,
  header,
  footer,
  headerOnlyFirst = true,
  footerOnlyLast = true,
  mode = 'adaptive',
  itemName = 'item',
  groupName = 'group',
}) {
  debugger
  if (!(element instanceof HTMLElement)) {
    throw new Error('The root element must be HTMLElement.')
  }

  const pdf = new jsPDF({
    unit: 'pt',
    format: 'a4',
    orientation: 'p',
  })
  const { width, height, data } = await pdf.toCanvas(element, contentWidth)
  const baseX = x == null ? (A4_WIDTH - contentWidth) / 2 : x
  const baseY = y == null ? (A4_HEIGHT - contentHeight) / 2 : y
  async function addHeader (isFirst) {
    if (isFirst || !headerOnlyFirst) {
      await pdf.addHeader(baseX, contentWidth, header)
    }
  }
  async function addFooter (isLast) {
    if (isLast || !footerOnlyLast) {
      await pdf.addFooter(baseX, contentWidth, footer)
    }
  }
  function addImage (_x, _y) {
    pdf.addImage(data, 'JPEG', _x, _y, width, height)
  }

  const params = {
    element,
    contentWidth,
    contentHeight,
    itemName,
    groupName,
    pdf,
    baseX,
    baseY,
    width,
    height,
    addImage,
    addHeader,
    addFooter,
  }
  switch (mode) {
  case 'adaptive':
    await outputWithAdaptive(params)
    break
  case 'fixed':
  default:
    await outputWithFixedSize(params)
  }
  return pdf.output2(outputType, filename)
}

async function outputWithFixedSize ({
  pdf,
  baseX,
  baseY,
  height,
  addImage,
  addHeader,
  addFooter,
  contentHeight,
}) {
  const pageNum = Math.ceil(height / contentHeight) // 总页数
  const arr = Array.from({ length: pageNum }).map((_, i) => i)
  for await (const i of arr) {
    addImage(baseX, baseY - i * contentHeight)
    const isFirst = i === 0
    const isLast = i === arr.length - 1
    if (!isFirst) {
      // 用空白遮挡顶部需要隐藏的部分
      pdf.addBlank(0, 0, A4_WIDTH, baseY)
    }
    if (!isLast) {
      // 用空白遮挡底部需要隐藏的部分
      pdf.addBlank(
        0,
        baseY + contentHeight,
        A4_WIDTH,
        A4_HEIGHT - (baseY + contentHeight)
      )
    }
    await addHeader(isFirst)
    await addFooter(isLast)
    if (!isLast) {
      pdf.addPage()
    }
  }
}

async function outputWithAdaptive ({
  element,
  contentWidth,
  itemName,
  groupName,
  pdf,
  baseX,
  baseY,
  addImage,
  addHeader,
  addFooter,
  contentHeight,
}) {
  // 从根节点遍历dom,计算出每页应放置的内容高度以保证不被截断
  const splitElement = () => {
    const res = []
    let pos = 0
    const elementWidth = element.offsetWidth
    function updatePos (height) {
      if (pos + height <= contentHeight) {
        pos += height
        return
      }
      res.push(pos)
      pos = height
    }
    function traversingNodes (nodes) {
      if (nodes.length === 0) return
      nodes.forEach(one => {
        if (one.nodeType !== 1) return
        const { [itemName]: item, [groupName]: group } = one.dataset
        if (item != null) {
          const { offsetHeight } = one
          // dom高度转换成生成pdf的实际高度
          // 代码不考虑dom定位、边距、边框等因素,需在dom里自行考虑,如将box-sizing设置为border-box
          updatePos((contentWidth / elementWidth) * offsetHeight)
        } else if (group != null) {
          traversingNodes(one.childNodes)
        }
      })
    }
    traversingNodes(element.childNodes)
    res.push(pos)
    return res
  }

  const elements = splitElement()
  let accumulationHeight = 0
  let currentPage = 0
  for await (const elementHeight of elements) {
    addImage(baseX, baseY - accumulationHeight)
    accumulationHeight += elementHeight
    const isFirst = currentPage === 0
    const isLast = currentPage === elements.length - 1
    if (!isFirst) {
      pdf.addBlank(0, 0, A4_WIDTH, baseY)
    }
    if (!isLast) {
      pdf.addBlank(
        0,
        baseY + elementHeight,
        A4_WIDTH,
        A4_HEIGHT - (baseY + elementHeight)
      )
    }
    await addHeader(isFirst)
    await addFooter(isLast)
    if (!isLast) {
      pdf.addPage()
    }
    currentPage++
  }
}

export default getPdf

然后在我们全局定义的文件中加入

import getPdf from '@/utils/pdf.js'
Vue.prototype.$getPdf = getPdf

然后在命令行中下载PDF插件

npm install --save html2canvas
npm install --save jspdf

在你所在的文件中使用时所用的代码为:

this.$getPdf({ element: document.getElementById('printArea') })

在这里插入图片描述
前端页面显示如上
例如:

  • API
  • 支持模型类型

小结

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值