| | |
| | | //引入工具 |
| | | // 引入工具(已修正 htmlparser2 导入) |
| | | import PizZip from 'pizzip'; |
| | | import Docxtemplater from 'docxtemplater'; |
| | | import JSZipUtils from 'jszip-utils'; |
| | | import { saveAs } from 'file-saver'; |
| | | import ImageModule from 'docxtemplater-image-module-free'; // 新增:图片处理模 |
| | | import ImageModule from 'docxtemplater-image-module-free'; |
| | | import { Parser } from 'htmlparser2'; |
| | | import CSSOM from 'cssom'; |
| | | |
| | | // 加载 .docx 模板文件 |
| | | // 加载 .docx 模板文件(不变) |
| | | function loadFile(url, callback) { |
| | | JSZipUtils.getBinaryContent(url, callback); |
| | | } |
| | | |
| | | // 下载生成的文档 |
| | | export function download(file, name) { |
| | | // 下载生成的文档(不变) |
| | | export function download(file, name) { } |
| | | |
| | | } |
| | | // 辅助函数:将 base64 图片转为 Uint8Array(图片模块需要此格式) |
| | | // 辅助函数:base64 转 Uint8Array(不变) |
| | | function base64ToUint8Array(base64) { |
| | | // 去掉 base64 前缀(如 "data:image/png;base64,") |
| | | const base64WithoutPrefix = base64.replace(/^data:image\/\w+;base64,/, ''); |
| | | const binaryString = atob(base64WithoutPrefix); |
| | | const length = binaryString.length; |
| | |
| | | return uint8Array; |
| | | } |
| | | |
| | | // -------------------------- 富文本处理核心函数(不变) -------------------------- |
| | | function getFontSizeFromStyle(styleStr) { |
| | | if (!styleStr) return 12; |
| | | try { |
| | | const style = CSSOM.parse(`.temp { ${styleStr} }`).cssRules[0].style; |
| | | const fontSize = style.getPropertyValue('font-size'); |
| | | if (!fontSize) return 12; |
| | | const sizeNum = parseInt(fontSize.replace(/[^\d]/g, ''), 10); |
| | | return isNaN(sizeNum) ? 12 : sizeNum; |
| | | } catch (e) { |
| | | return 12; |
| | | } |
| | | } |
| | | |
| | | // 生成并下载 Word 文档(templatePath是word文档模版地址,data是对应的数据) |
| | | export function generateWordDocument(templatePath, data, name) { |
| | | loadFile(templatePath, function (error, content) { |
| | | if (error) { |
| | | throw error |
| | | return; |
| | | function htmlToDocxXml(html) { |
| | | let xml = ''; |
| | | let currentFontSize = 12; |
| | | |
| | | const entityMap = { |
| | | ' ': ' ', |
| | | '<': '<', |
| | | '>': '>', |
| | | '&': '&', |
| | | '"': '"', |
| | | ''': "'" |
| | | }; |
| | | |
| | | const parser = new Parser({ |
| | | onopentag: (name, attributes) => { |
| | | |
| | | switch (name) { |
| | | case 'p': |
| | | xml += '<w:p><w:r>'; |
| | | break; |
| | | case 'span': |
| | | const fontSize = getFontSizeFromStyle(attributes.style); |
| | | currentFontSize = fontSize; |
| | | const szVal = currentFontSize * 2; |
| | | xml += `<w:rPr><w:sz w:val="${szVal}"/><w:szCs w:val="${szVal}"/></w:rPr>`; |
| | | break; |
| | | case 'b': |
| | | case 'strong': |
| | | xml += '<w:rPr><w:b/></w:rPr>'; |
| | | break; |
| | | case 'br': |
| | | xml += '<w:br/>'; |
| | | break; |
| | | } |
| | | }, |
| | | ontext: (text) => { |
| | | const processedText = text |
| | | .replace(/ |<|>|&|"|'/g, match => entityMap[match]) |
| | | .replace(/\n/g, '<w:br/>'); |
| | | xml += `<w:t>${processedText}</w:t>`; |
| | | }, |
| | | onclosetag: (name) => { |
| | | switch (name) { |
| | | case 'p': |
| | | xml += '</w:r></w:p>'; |
| | | currentFontSize = 12; |
| | | break; |
| | | case 'span': |
| | | case 'b': |
| | | case 'strong': |
| | | xml += '</w:rPr>'; |
| | | break; |
| | | } |
| | | } |
| | | }, { decodeEntities: true }); |
| | | |
| | | try { |
| | | // 新版创建解析器的正确方式(无需 import AngularParser) |
| | | const parser = (tag, _variable) => { |
| | | parser.write(html); |
| | | parser.end(); |
| | | return xml; |
| | | } |
| | | |
| | | function processRichTextData(data, richTextFields = []) { |
| | | const process = (obj) => { |
| | | if (typeof obj !== 'object' || obj === null) return obj; |
| | | if (Array.isArray(obj)) { |
| | | return obj.map(item => process(item)); |
| | | } |
| | | const processedObj = { ...obj }; |
| | | for (const key in processedObj) { |
| | | if (processedObj.hasOwnProperty(key)) { |
| | | if (richTextFields.includes(key) && typeof processedObj[key] === 'string') { |
| | | const filteredHtml = processedObj[key].replace(/<p>\s*( )?\s*<\/p>/g, ''); |
| | | processedObj[key] = htmlToDocxXml(filteredHtml); |
| | | } else { |
| | | processedObj[key] = process(processedObj[key]); |
| | | } |
| | | } |
| | | } |
| | | return processedObj; |
| | | }; |
| | | return process(data); |
| | | } |
| | | |
| | | // -------------------------- 核心:生成 Word 文档(修正构造函数配置) -------------------------- |
| | | export function generateWordDocument(templatePath, data, name, richTextFields = []) { |
| | | loadFile(templatePath, function (error, content) { |
| | | if (error) { |
| | | console.error('加载模板失败:', error); |
| | | return; |
| | | } |
| | | |
| | | try { |
| | | |
| | | const processedData = processRichTextData(data, richTextFields); |
| | | |
| | | // 图片处理模块(不变) |
| | | const imageModule = new ImageModule({ |
| | | getImage: function (tagValue, tagName) { |
| | | return base64ToUint8Array(tagValue); |
| | | }, |
| | | getSize: function (tagValue, tagName) { |
| | | switch (tagName) { |
| | | case 'avatar': |
| | | return [200, 200]; |
| | | case 'coverImg': |
| | | return [600, 300]; |
| | | default: |
| | | return [80, 130]; |
| | | } |
| | | } |
| | | }); |
| | | |
| | | // 关键修正:将 parser 直接传入构造函数 options,删除 setOptions() |
| | | const zip = new PizZip(content); |
| | | const doc = new Docxtemplater(zip, { |
| | | paragraphLoop: true, |
| | | linebreaks: true, |
| | | raw: true, // 富文本核心:允许插入原始 XML |
| | | modules: [imageModule], // 图片模块 |
| | | nullGetter: () => '', // 空值处理 |
| | | // 直接在这里传入 parser 配置(无需 setOptions()) |
| | | parser: (tag, _variable) => { |
| | | return { |
| | | get(scope) { |
| | | if (scope[tag] !== undefined) { |
| | | return scope[tag] || ''; |
| | | } |
| | | // 处理特殊变量(如 $first) |
| | | if (tag.startsWith('$')) { |
| | | const varName = tag.slice(1); |
| | | return scope[varName] || ''; |
| | |
| | | return ''; |
| | | } |
| | | }; |
| | | }; |
| | | // 1. 配置图片处理规则(核心) |
| | | const imageModule = new ImageModule({ |
| | | // 获取图片:根据传入的图片数据(base64)转为模块需要的格式 |
| | | getImage: function (tagValue, tagName) { |
| | | // tagValue:data 中对应图片键的值(必须是 base64 字符串) |
| | | // tagName:图片占位符的键名(如 "avatar") |
| | | return base64ToUint8Array(tagValue); |
| | | }, |
| | | // 设置图片尺寸:返回 [宽度, 高度](单位:px),可动态调整 |
| | | getSize: function (tagValue, tagName) { |
| | | } |
| | | }); |
| | | |
| | | // 示例:根据不同的图片键,返回不同尺寸 |
| | | switch (tagName) { |
| | | case 'avatar': // 头像:200x200 |
| | | return [200, 200]; |
| | | case 'coverImg': // 封面图:600x300 |
| | | return [600, 300]; |
| | | default: // 默认尺寸:400x300 |
| | | return [80, 100]; |
| | | } |
| | | } |
| | | }); |
| | | // 加载模板文件内容到 PizZip |
| | | const zip = new PizZip(content); |
| | | const doc = new Docxtemplater(zip, { |
| | | paragraphLoop: true, |
| | | linebreaks: true, |
| | | modules: [imageModule] // 关键:注入图片模块 |
| | | }); |
| | | // 删除这一行!!!v4 构造函数已包含 parser,无需手动 setOptions |
| | | // doc.setOptions({ parser }); |
| | | |
| | | // const parser = new AngularParser(); |
| | | // doc.setOptions({ parser }); |
| | | // 注入数据并渲染(不变) |
| | | doc.setData(processedData); |
| | | doc.render(); |
| | | |
| | | const fileWord = doc.getZip().generate({ |
| | | type: 'blob', |
| | | mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', |
| | | }); |
| | | |
| | | // 设置模板中的占位符数据 |
| | | doc.setData(data); |
| | | saveAs(fileWord, name); |
| | | console.log('导出成功!'); |
| | | |
| | | // 渲染文档 |
| | | doc.render(); |
| | | |
| | | // 生成最终的文档 Blob |
| | | const fileWord = doc.getZip().generate({ |
| | | type: 'blob', |
| | | mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', |
| | | }); |
| | | |
| | | saveAs(fileWord, name); |
| | | |
| | | // // 返回生成的文档 Blob |
| | | // resolve(fileWord); |
| | | } catch (error) { |
| | | console.error('Error rendering document:', error); |
| | | throw error |
| | | } |
| | | }); |
| | | |
| | | } catch (error) { |
| | | console.error('生成文档失败:', error); |
| | | } |
| | | }); |
| | | } |