zhouwx
2025-12-03 59a4f02701ef3b232b9f1d54ba0b29a1e8764704
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
// 引入工具(已修正 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 { Parser } from 'htmlparser2';
import CSSOM from 'cssom';
 
// 加载 .docx 模板文件(不变)
function loadFile(url, callback) {
    JSZipUtils.getBinaryContent(url, callback);
}
 
// 下载生成的文档(不变)
export function download(file, name) { }
 
// 辅助函数:base64 转 Uint8Array(不变)
function base64ToUint8Array(base64) {
    const base64WithoutPrefix = base64.replace(/^data:image\/\w+;base64,/, '');
    const binaryString = atob(base64WithoutPrefix);
    const length = binaryString.length;
    const uint8Array = new Uint8Array(length);
    for (let i = 0; i < length; i++) {
        uint8Array[i] = binaryString.charCodeAt(i);
    }
    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;
    }
}
 
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 });
 
    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] || '';
                            }
                            if (tag.startsWith('$')) {
                                const varName = tag.slice(1);
                                return scope[varName] || '';
                            }
                            return '';
                        }
                    };
                }
            });
 
            //  删除这一行!!!v4 构造函数已包含 parser,无需手动 setOptions
            // doc.setOptions({ parser });
 
            // 注入数据并渲染(不变)
            doc.setData(processedData);
            doc.render();
 
            const fileWord = doc.getZip().generate({
                type: 'blob',
                mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            });
 
            saveAs(fileWord, name);
            console.log('导出成功!');
 
        } catch (error) {
            console.error('生成文档失败:', error);
        }
    });
}