From 59a4f02701ef3b232b9f1d54ba0b29a1e8764704 Mon Sep 17 00:00:00 2001
From: zhouwx <1175765986@qq.com>
Date: 星期三, 03 十二月 2025 14:54:53 +0800
Subject: [PATCH] 修改

---
 src/utils/exportWord.js |  228 +++++++++++++++++++++++++++++++++++++++-----------------
 1 files changed, 158 insertions(+), 70 deletions(-)

diff --git a/src/utils/exportWord.js b/src/utils/exportWord.js
index 53b0013..584f673 100644
--- a/src/utils/exportWord.js
+++ b/src/utils/exportWord.js
@@ -1,22 +1,22 @@
-//引入工具
+// 引入工具(已修正 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;
@@ -27,24 +27,147 @@
     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 = {
+        '&nbsp;': '&#160;',
+        '&lt;': '<',
+        '&gt;': '>',
+        '&amp;': '&',
+        '&quot;': '"',
+        '&#39;': "'"
+    };
+
+    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(/&nbsp;|&lt;|&gt;|&amp;|&quot;|&#39;/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*(&nbsp;)?\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] || '';
@@ -52,61 +175,26 @@
                             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, 130];
-                        }
-                    }
-                });
-                // 加载模板文件内容到 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);
+        }
+    });
 }

--
Gitblit v1.9.2