// 引入工具 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' // 加载 .docx 模板文件 function loadFile(url, callback) { JSZipUtils.getBinaryContent(url, callback); } // 下载生成的文档 export function download(file, name) { saveAs(file, name); } // 处理富文本,提取段落和缩进信息 function processRichText(html) { if (!html) return ''; // 将HTML字符串转换为DOM对象 const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); let result = []; // 处理普通段落 const paragraphs = doc.querySelectorAll('p'); paragraphs.forEach(p => { const style = p.getAttribute('style') || ''; const indentMatch = style.match(/text-indent:\s*(\d+)pt/); const indent = indentMatch ? parseInt(indentMatch[1]) / 24 : 0; const text = p.textContent.trim(); if (text) { const indentStr = indent > 0 ? ' '.repeat(indent) : ''; result.push(indentStr + text); } }); // 处理列表(ul/li) const lists = doc.querySelectorAll('ul'); lists.forEach(ul => { const lis = ul.querySelectorAll('li'); lis.forEach(li => { // 计算缩进层级 let parent = li.parentElement; let indentLevel = 0; while (parent && parent !== ul) { if (parent.tagName === 'UL') indentLevel++; parent = parent.parentElement; } const text = li.textContent.trim(); if (text) { // 使用不同符号表示不同层级 const bullets = ['▪', '•', '▫', '◦']; const bullet = bullets[Math.min(indentLevel, bullets.length - 1)]; const indentStr = ' '.repeat(indentLevel); result.push(indentStr + bullet + ' ' + text); } }); }); return result.join('\n'); // 用两个换行符分隔段落 } function convertTreeToHtml(data) { let html = ''; function buildList(items) { let listHtml = ''; return listHtml; } html = buildList(data); return html; } function generateTableXML(clauses, deptList) { const allDeptNames = [...new Set(deptList.map(item => item.deptName))]; // 构建数据映射 const dataMap = {}; deptList.forEach(item => { if (!dataMap[item.clauseNum]) dataMap[item.clauseNum] = {}; dataMap[item.clauseNum][item.deptName] = item.chooseLab; }); return ` ${allDeptNames.map(() => '').join('')} 条款号 内容描述 ${allDeptNames.map(dept => ` ${dept} `).join('')} ${clauses.map(clause => ` ${clause.clauseNum} ${clause.content} ${allDeptNames.map(dept => ` ${dataMap[clause.clauseNum]?.[dept] ?? 0} `).join('')} `).join('')} `; } function processTableHtml(html) { if (!html) return ''; const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const table = doc.querySelector('table'); if (!table) return ''; // 提取表格结构 const rows = table.querySelectorAll('tr'); const result = []; // 处理表头 const headers = Array.from(rows[0].querySelectorAll('th')) .map(th => `${th.textContent.trim()}`) .join('\t'); result.push(headers); // 处理数据行 for (let i = 1; i < rows.length; i++) { const cells = rows[i].querySelectorAll('td'); const rowData = Array.from(cells).map(cell => { const indent = cell.style.paddingLeft ? ' '.repeat(parseInt(cell.style.paddingLeft)/4) : ''; const content = cell.textContent.trim(); return indent + content; }).join('\t'); result.push(rowData); } return result.join('\n'); } function generateTableHtml(clauses, deptList) { const allDeptNames = [...new Set(deptList.map(item => item.deptName))]; const dataMap = {}; // 构建数据映射 deptList.forEach(item => { if (!dataMap[item.clauseNum]) dataMap[item.clauseNum] = {}; dataMap[item.clauseNum][item.deptName] = item.chooseLab? (item.chooseLab==1?'●':'○'):'○' }); return ` ${allDeptNames.map(dept => ` `).join('')} ${clauses.map(clause => ` ${allDeptNames.map(dept => ` `).join('')} `).join('')}
条款号 内容描述${dept}
${clause.clauseNum} ${clause.content} ${dataMap[clause.clauseNum]?.[dept] ?? '○'}
`; } const base64Regex = /^(?:data:)?image\/(png|jpg|jpeg|svg|svg\+xml);base64,/; const validBase64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/; function base64Parser(tagValue) { if ( typeof tagValue !== "string" || !base64Regex.test(tagValue) ) { return false; } const stringBase64 = tagValue.replace(base64Regex, ""); if (!validBase64.test(stringBase64)) { throw new Error( "Error parsing base64 data, your data contains invalid characters" ); } // For nodejs, return a Buffer if (typeof Buffer !== "undefined" && Buffer.from) { return Buffer.from(stringBase64, "base64"); } // For browsers, return a string (of binary content) : const binaryString = window.atob(stringBase64); 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; } function getDimensionsFromBase64Sync(base64Str) { try { // 去除 data:image/...;base64, 前缀 const base64Data = base64Str.replace(/^data:image\/\w+;base64,/, ''); // 将base64转换为二进制 const binaryString = atob(base64Data); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return parseImageDimensions(bytes); } catch (error) { console.warn('解析图片尺寸失败:', error); return { width: 600, height: 400 }; } } function parseImageDimensions(bytes) { if (bytes.length < 8) { return { width: 600, height: 400 }; } // 检查 PNG 格式 (89 50 4E 47 0D 0A 1A 0A) if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47) { return parsePNGDimensions(bytes); } // 检查 JPEG 格式 (FF D8) if (bytes[0] === 0xFF && bytes[1] === 0xD8) { return parseJPEGDimensions(bytes); } return { width: 600, height: 400 }; } // 解析 PNG 尺寸 function parsePNGDimensions(bytes) { // PNG 的宽高在 IHDR chunk 中(偏移 16-24 字节) if (bytes.length >= 24) { const view = new DataView(bytes.buffer); const width = view.getUint32(16, false); // 大端序 const height = view.getUint32(20, false); return { width, height }; } return { width: 600, height: 400 }; } // 解析 JPEG 尺寸 function parseJPEGDimensions(bytes) { let i = 2; // 跳过 FFD8 while (i < bytes.length - 1) { // JPEG 标记开始 if (bytes[i] === 0xFF) { const marker = bytes[i + 1]; // SOF0, SOF1, SOF2 (Start of Frame markers) if ((marker >= 0xC0 && marker <= 0xC3) || (marker >= 0xC5 && marker <= 0xC7) || (marker >= 0xC9 && marker <= 0xCB) || (marker >= 0xCD && marker <= 0xCF)) { if (i + 7 < bytes.length) { const height = (bytes[i + 5] << 8) | bytes[i + 6]; const width = (bytes[i + 7] << 8) | bytes[i + 8]; return { width, height }; } break; } // 跳过当前段 const length = (bytes[i + 2] << 8) | bytes[i + 3]; i += length + 2; } else { i++; } } return { width: 600, height: 400 }; } const imageOptions = { getImage(tagValue) { return base64Parser(tagValue); }, getSize(img, tagValue, tagName, context) { const dimensions = getDimensionsFromBase64Sync(tagValue); const { width, height } = dimensions; const targetWidth = 600; const scale = targetWidth / width; let targetHeight = height * scale; targetHeight = Math.max(100, Math.min(700, targetHeight)); return [targetWidth, Math.round(targetHeight)]; }, }; const base64DataURLToArrayBuffer = (dataURL) => { // 返回包含 ArrayBuffer 和原始 base64 字符串的对象 const base64Regex = /^data:image\/(png|jpg|jpeg|svg|svg\+xml);base64,/; if (!base64Regex.test(dataURL)) { return { buffer: null, base64: dataURL }; } const stringBase64 = dataURL.replace(base64Regex, ""); let binaryString = window.atob(stringBase64); const len = binaryString.length; const bytes = new Uint8Array(len); for (let i = 0; i < len; i++) { bytes[i] = binaryString.charCodeAt(i); } return { buffer: bytes.buffer, // 图片模块需要的 ArrayBuffer base64: stringBase64 // 保留原始 base64 字符串(不带前缀) }; }; // 生成并下载 Word 文档 export function generateWordDocument(templatePath, data, name) { // 处理部门表格数据 // if (data.clauses && data.duties) { // const tableHtml = generateTableHtml(data.clauses, data.duties); // data.departmentsTable = processTableHtml(tableHtml); // } if (data.productServiceImages && Array.isArray(data.productServiceImages)) { // 确保是纯 base64 字符串数组 data.productServiceImageArray = data.productServiceImages.map(item => typeof item === 'object' ? item.image : item ).filter(img => img && typeof img === 'string'); // 为前10张图片创建单独的图片变量 data.productServiceImageArray.slice(0, 10).forEach((img, index) => { data[`productServiceImage${index + 1}`] = img; }); // 创建带元数据的对象数组 data.productServiceImageObjects = data.productServiceImageArray.map((img, index) => ({ image: img, // 这个字段会作为图片插入 index: index + 1, description: `产品和服务实现过程图 ${index + 1}` })); data.productServiceCount = data.productServiceImageArray.length; data.hasProductServiceImages = data.productServiceImageArray.length > 0; } // 处理富文本字段(如果有) if (data.summaries && typeof data.summaries === 'string') { data.summaries = processRichText(data.summaries); } if (data.policies && typeof data.policies === 'string') { data.policies = processRichText(data.policies); } // 处理树形结构数据(如果有) if (data.deptList && Array.isArray(data.deptList)) { data.departmentsHtml = processRichText(convertTreeToHtml(data.deptList)); } if (data.orgChart && typeof data.orgChart !== 'string') { console.warn("orgChart 不是字符串,可能被意外转换:", data.orgChart); delete data.orgChart; // 避免传递无效数据 } loadFile(templatePath, function (error, content) { if (error) { throw error; } try { // 加载模板文件内容到 PizZip const zip = new PizZip(content); const imageModule = new ImageModule(imageOptions); const doc = new Docxtemplater(zip, { paragraphLoop: true, linebreaks: true, modules: [imageModule] }); // 设置模板中的占位符数据 doc.setData(data); // 渲染文档 doc.render(); // 替换占位符 // let xml = zip.files['word/document.xml'].asText(); // xml = xml.replace('', data.tableXML); // zip.file('word/document.xml', xml); // 生成最终的文档 Blob const fileWord = doc.getZip().generate({ type: 'blob', mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', }); saveAs(fileWord, name); } catch (error) { console.error('Error rendering document:', error); throw error; } }); }