这种方案主要是利用现有模板编写html页面样式后将HTML转成canvas图片导出,类似于页面截图,好处是可以自定义页面样式导出且过程较为简单,缺点就是无法1:1还原标准模板样式,有一定偏差需要兼容页面大小且无法满足直接导出word
npm install --save html2canvas // 页面转图片npm install jspdf --save // 图片转pdf
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') } 适用于提供模板来导出,且对文档格式和排版有有严格要求的导出
npm install docxtemplaternpm install pizzipnpm install jszipnpm install jszip-utilsnpm install file-savernpm install docxtemplater-image-module-freenpm install angular-expressionsnpm install docx-preview
需要注意的:
(1)文档模板需使用docx文件格式,原因是docx与zip是可以相互转换的,但doc则不行,因为后续需要借助插件将模板转换成zip
(2)文档需放置于项目public文件夹下
(3)模板的书写规则和数据源的格式需借助angular-parser 词法解析器使用,具体格式和复杂写法可参考这个博客https://blog.csdn.net/CHANCE_wqp/article/details/133457540
使用PizZip解压缩读取成二进制,再使用Docxtemplater插件将模板字符替换成数据源抛出blob文件流
需要注意的是模板中图片需要转换成base64图片后再处理,如果数据源中图片为url也需要先将链接的图片转换成base64具体转换代码见下面完整代码实例
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) }) } 直接下载文件
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 }) }) } /** * 前端导出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) } } } import { openFile, getBase64Sync } from "@/hooks/exportWord.ts"
await getData() //接口获取数据 if (data.customerSignature) { //图片链接转base64 data.customerSignature = await getBase64Sync(FILESERVER_URL + data.customerSignature) } openFile(data) //直接传入数据源字段