前端导出的两种技术方案:模板导出和页面直接导出,vue3+ts

高级C+V工程师 2024-07-25 12:03:01 阅读 65

一,前端HTML转canvas技术方案

这种方案主要是利用现有模板编写html页面样式后将HTML转成canvas图片导出,类似于页面截图,好处是可以自定义页面样式导出且过程较为简单,缺点就是无法1:1还原标准模板样式,有一定偏差需要兼容页面大小且无法满足直接导出word

1.安装依赖

<code>npm install --save html2canvas // 页面转图片

npm install jspdf --save // 图片转pdf

2.示例代码带水印添加

import html2Canvas from 'html2canvas'

import JsPDF from 'jspdf'

// title:下载文件的名称 htmlId:包裹的标签的id

const htmlToPdf = (title: string, htmlId: string) => {

var element = document.querySelector(htmlId) as HTMLElement

window.pageYOffset = 0

document.documentElement.scrollTop = 0

document.body.scrollTop = 0

setTimeout(() => {

// // 以下注释的是增加导出的pdf水印

// const value = '我是水印'

// //创建一个画布

// let can = document.createElement('canvas')

// //设置画布的长宽

// can.width = 400

// can.height = 500

// let cans = can.getContext('2d') as any

// //旋转角度

// cans.rotate((-15 * Math.PI) / 180)

// cans.font = '18px Vedana'

// //设置填充绘画的颜色、渐变或者模式

// cans.fillStyle = 'rgba(200, 200, 200, 0.40)'

// //设置文本内容的当前对齐方式

// cans.textAlign = 'left'

// //设置在绘制文本时使用的当前文本基线

// cans.textBaseline = 'Middle'

// //在画布上绘制填色的文本(输出的文本,开始绘制文本的X坐标位置,开始绘制文本的Y坐标位置)

// cans.fillText(value, can.width / 8, can.height / 2)

// let div = document.createElement('div')

// div.style.pointerEvents = 'none'

// div.style.top = '20px'

// div.style.left = '-20px'

// div.style.position = 'fixed'

// div.style.zIndex = '100000'

// div.style.width = element.scrollHeight + 'px'

// div.style.height = element.scrollHeight + 'px'

// div.style.background =

// 'url(' + can.toDataURL('image/png') + ') left top repeat'

// element.appendChild(div) // 到页面中

html2Canvas(element, {

allowTaint: true,

useCORS: true,

scale: 2, // 提升画面质量,但是会增加文件大小

height: element.scrollHeight, // 需要注意,element的 高度 宽度一定要在这里定义一下,不然会存在只下载了当前你能看到的页面 避雷避雷!!!

windowHeight: element.scrollHeight,

}).then(function (canvas) {

var contentWidth = canvas.width

var contentHeight = canvas.height

// console.log('contentWidth', contentWidth)

// console.log('contentHeight', contentHeight)

// 一页pdf显示html页面生成的canvas高度;

var pageHeight = (contentWidth * 841.89) / 592.28

// 未生成pdf的html页面高度

var leftHeight = contentHeight

// console.log('pageHeight', pageHeight)

// console.log('leftHeight', leftHeight)

// 页面偏移

var position = 0

// a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高 //40是左右页边距

var imgWidth = 595.28 - 40

var imgHeight = (592.28 / contentWidth) * contentHeight

var pageData = canvas.toDataURL('image/jpeg', 1.0)

var pdf = new JsPDF('p', 'pt', 'a4')

// 有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(841.89)

// 当内容未超过pdf一页显示的范围,无需分页

if (leftHeight < pageHeight) {

// console.log('没超过1页')

pdf.addImage(pageData, 'JPEG', 20, 20, imgWidth, imgHeight)

} else {

while (leftHeight > 0) {

// console.log('超过1页')

pdf.addImage(pageData, 'JPEG', 20, position, imgWidth, imgHeight)

leftHeight -= pageHeight

position -= 841.89

// 避免添加空白页

if (leftHeight > 0) {

pdf.addPage()

}

}

}

pdf.save(title + '.pdf')

})

}, 1000)

}

export default htmlToPdf

页面方法使用

import htmlToPdf from '@/utils/pdf'//引入封装好的ts文件

const exportPdf = (text:string) => {

htmlToPdf(text, '#exportWrapper')

}

二,利用docxtemplater插件配合模板导出

适用于提供模板来导出,且对文档格式和排版有有严格要求的导出

1.需要使用安装的依赖

npm install docxtemplater

npm install pizzip

npm install jszip

npm install jszip-utils

npm install file-saver

npm install docxtemplater-image-module-free

npm install angular-expressions

npm install docx-preview

2.模板创建和书写

需要注意的:

(1)文档模板需使用docx文件格式,原因是docx与zip是可以相互转换的,但doc则不行,因为后续需要借助插件将模板转换成zip

(2)文档需放置于项目public文件夹下

在这里插入图片描述

(3)模板的书写规则和数据源的格式需借助angular-parser 词法解析器使用,具体格式和复杂写法可参考这个博客https://blog.csdn.net/CHANCE_wqp/article/details/133457540

3.模板读取和写入

使用PizZip解压缩读取成二进制,再使用Docxtemplater插件将模板字符替换成数据源抛出blob文件流

需要注意的是模板中图片需要转换成base64图片后再处理,如果数据源中图片为url也需要先将链接的图片转换成base64具体转换代码见下面完整代码实例

<code>

async function transformWord(data: any, callback: Function) {

// 读取并获得模板文件的二进制内容

function loadFile(url: string, callback: (error: any, content: any) => void) {

PizZipUtils.getBinaryContent(url, callback)

}

// orderTemeplate.docx是模板。我们在导出的时候,会根据此模板来导出对应的数据

await loadFile("/orderTemeplate.docx", function (error: Error | null, content) {

// 抛出异常

if (error) {

throw error

}

console.log(content)

const opts = {

centered: true,

fileType: "docx"

}

// @ts-ignore

opts.getImage = (imagePath) => {

if (imagePath.size && imagePath.data) {

return base64DataURLToArrayBuffer(imagePath.data)

}

return base64DataURLToArrayBuffer(imagePath)

}

// @ts-ignore

opts.getSize = () => {

return [160, 80]

}

// 创建一个JSZip实例,内容为模板的内容

const zip: PizZip = new PizZip(content)

const doc = new Docxtemplater()

doc.attachModule(new ImageModule(opts))

doc.loadZip(zip)

// 设置模板变量的值

doc.setData({

...data

})

doc.setOptions({

nullGetter: function () {

//设置空值 undefined 为""

return ""

},

parser: angularParser

})

try {

// 用模板变量的值替换所有模板变量

doc.render()

} catch (error: any) {

throw error

// 当使用json记录时,此处抛出错误信息

}

// 生成一个代表docxtemplater对象的zip文件(不是一个真实的文件,而是在内存中的表示)

const out = doc.getZip().generate({

type: "blob",

mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"

})

callback(out)

})

}

4.异步调用下载

直接下载文件

export const exportWordDocx = async (data: any, fileName: string) => {

transformWord(data, (out: any) => {

saveAs(out, fileName + ".docx")

})

}

如果需要添加预览功能可使用docx-preview的renderAsync进行预览后续同样可使用方案一方式直接导出pdf

export const openFile = async (data: any) => {

transformWord(data, (out: any) => {

const container = document.getElementById("doc-preview") as HTMLElement

renderAsync(out, container, null, {

// renderChanges: true

useBase64URL: true,

ignoreWidth: true

})

})

}

5.完整示例代码

/**

* 前端导出word

* @param {object} data - 字段数据,需与文档模板字段保持一致

* @param {number} fileName - 文件名

* @returns {Blob} 文件流

*/

import Docxtemplater from "docxtemplater"

import PizZip from "pizzip"

import PizZipUtils from "pizzip/utils/index.js"

import { saveAs } from "file-saver"

import ImageModule from "docxtemplater-image-module-free"

import expressions from "angular-expressions"

import { renderAsync } from "docx-preview"

export const exportWordDocx = async (data: any, fileName: string) => {

transformWord(data, (out: any) => {

saveAs(out, fileName + ".docx")

})

}

export const openFile = async (data: any) => {

transformWord(data, (out: any) => {

const container = document.getElementById("doc-preview") as HTMLElement

renderAsync(out, container, null, {

// renderChanges: true

useBase64URL: true,

ignoreWidth: true

})

})

}

async function transformWord(data: any, callback: Function) {

// 读取并获得模板文件的二进制内容

function loadFile(url: string, callback: (error: any, content: any) => void) {

PizZipUtils.getBinaryContent(url, callback)

}

// orderTemeplate.docx是模板。我们在导出的时候,会根据此模板来导出对应的数据

await loadFile("/orderTemeplate.docx", function (error: Error | null, content) {

// 抛出异常

if (error) {

throw error

}

console.log(content)

const opts = {

centered: true,

fileType: "docx"

}

// @ts-ignore

opts.getImage = (imagePath) => {

if (imagePath.size && imagePath.data) {

return base64DataURLToArrayBuffer(imagePath.data)

}

return base64DataURLToArrayBuffer(imagePath)

}

// @ts-ignore

opts.getSize = () => {

return [160, 80]

}

// 创建一个JSZip实例,内容为模板的内容

const zip: PizZip = new PizZip(content)

const doc = new Docxtemplater()

doc.attachModule(new ImageModule(opts))

doc.loadZip(zip)

// 设置模板变量的值

doc.setData({

...data

})

doc.setOptions({

nullGetter: function () {

//设置空值 undefined 为""

return ""

},

parser: angularParser

})

try {

// 用模板变量的值替换所有模板变量

doc.render()

} catch (error: any) {

throw error

// 当使用json记录时,此处抛出错误信息

}

// 生成一个代表docxtemplater对象的zip文件(不是一个真实的文件,而是在内存中的表示)

const out = doc.getZip().generate({

type: "blob",

mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"

})

callback(out)

})

}

/**

* 将base64格式的数据转为ArrayBuffer

* @param {Object} dataURL base64格式的数据

*/

function base64DataURLToArrayBuffer(dataURL: string) {

const base64Regex = /^data:image\/(png|jpg|svg|svg\+xml);base64,/

if (!base64Regex.test(dataURL)) {

return false

}

const stringBase64 = dataURL.replace(base64Regex, "")

let binaryString

if (typeof window !== "undefined") {

binaryString = window.atob(stringBase64)

} else {

binaryString = new Buffer(stringBase64, "base64").toString("binary")

}

const len = binaryString.length

const bytes = new Uint8Array(len)

for (let i = 0; i < len; i++) {

const ascii = binaryString.charCodeAt(i)

bytes[i] = ascii

}

return bytes.buffer

}

/**

* 将图片的url路径转为base64路径

* 可以用await等待Promise的异步返回

* @param {Object} imgUrl 图片路径

*/

export function getBase64Sync(imgUrl: string) {

return new Promise(function (resolve) {

// 一定要设置为let,不然图片不显示

const image = new Image()

//图片地址

image.src = imgUrl

// 解决跨域问题

image.setAttribute("crossOrigin", "*") // 支持跨域图片

// image.onload为异步加载

image.onload = function () {

const canvas = document.createElement("canvas")

canvas.width = image.width

canvas.height = image.height

const context = canvas.getContext("2d")

context?.drawImage(image, 0, 0, image.width, image.height)

//图片后缀名

const ext = image.src.substring(image.src.lastIndexOf(".") + 1).toLowerCase()

//图片质量

const quality = 0.8

//转成base64

const dataurl = canvas.toDataURL("image/" + ext, quality)

//返回

resolve(dataurl)

}

})

}

//处理文档中的一些特殊标签

function angularParser(tag: string) {

return {

get:

tag === "."

? function (s: any) {

return s

}

: function (s: any) {

return expressions.compile(tag.replace(/(’|“|”)/g, "'"))(s)

}

}

}

6.页面中的使用

import { openFile, getBase64Sync } from "@/hooks/exportWord.ts"

await getData() //接口获取数据

if (data.customerSignature) {

//图片链接转base64

data.customerSignature = await getBase64Sync(FILESERVER_URL + data.customerSignature)

}

openFile(data) //直接传入数据源字段



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。