// 引入工具
|
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 = '<ul style="font-family: 宋体; font-size: 12pt; line-height: 1.5;">';
|
items.forEach(item => {
|
listHtml += `<li style="margin-bottom: 6pt;">${item.deptName}`;
|
|
if (item.children && item.children.length > 0) {
|
listHtml += buildList(item.children);
|
}
|
listHtml += '</li>';
|
});
|
|
listHtml += '</ul>';
|
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 `
|
<w:tbl xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
<w:tblPr>
|
<w:tblW w:w="10000" w:type="pct"/>
|
<!-- 边框设置 -->
|
<w:tblBorders>
|
<w:top w:val="single" w:sz="4" w:space="0" w:color="000000"/>
|
<w:left w:val="single" w:sz="4" w:space="0" w:color="000000"/>
|
<w:bottom w:val="single" w:sz="4" w:space="0" w:color="000000"/>
|
<w:right w:val="single" w:sz="4" w:space="0" w:color="000000"/>
|
<w:insideH w:val="single" w:sz="4" w:space="0" w:color="000000"/>
|
<w:insideV w:val="single" w:sz="4" w:space="0" w:color="000000"/>
|
</w:tblBorders>
|
<w:tblLook w:val="04A0"/>
|
</w:tblPr>
|
|
<!-- 列宽定义 -->
|
<w:tblGrid>
|
<w:gridCol w:w="1500"/> <!-- 条款号列 -->
|
<w:gridCol w:w="3500"/> <!-- 内容列 -->
|
${allDeptNames.map(() => '<w:gridCol w:w="2000"/>').join('')}
|
</w:tblGrid>
|
|
<!-- 表头 -->
|
<w:tr>
|
<w:tc>
|
<w:tcPr><w:tcW w:w="1500" w:type="dxa"/></w:tcPr>
|
<w:p><w:r><w:rPr><w:b/></w:rPr><w:t>条款号</w:t></w:r></w:p>
|
</w:tc>
|
<w:tc>
|
<w:tcPr><w:tcW w:w="3500" w:type="dxa"/></w:tcPr>
|
<w:p><w:r><w:rPr><w:b/></w:rPr><w:t>内容描述</w:t></w:r></w:p>
|
</w:tc>
|
${allDeptNames.map(dept => `
|
<w:tc>
|
<w:tcPr><w:tcW w:w="2000" w:type="dxa"/></w:tcPr>
|
<w:p><w:r><w:rPr><w:b/></w:rPr><w:t>${dept}</w:t></w:r></w:p>
|
</w:tc>
|
`).join('')}
|
</w:tr>
|
|
<!-- 数据行 -->
|
${clauses.map(clause => `
|
<w:tr>
|
<w:tc><w:p><w:r><w:t>${clause.clauseNum}</w:t></w:r></w:p></w:tc>
|
<w:tc><w:p><w:r><w:t>${clause.content}</w:t></w:r></w:p></w:tc>
|
${allDeptNames.map(dept => `
|
<w:tc>
|
<w:p><w:r><w:t>${dataMap[clause.clauseNum]?.[dept] ?? 0}</w:t></w:r></w:p>
|
</w:tc>
|
`).join('')}
|
</w:tr>
|
`).join('')}
|
</w:tbl>
|
`;
|
}
|
|
|
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 `
|
<table style="width: 100%;border-collapse: collapse;font-family: 'Microsoft YaHei', sans-serif;font-size: 10.5pt;margin-bottom: 12pt;border: 1px solid #ccc">
|
<thead>
|
<tr style="background-color: #f5f5f5;width: 100%">
|
<th style="padding: 6pt 8pt;border: 1px solid #ccc;text-align: center;font-weight: bold;min-width: 60pt">条款号</th>
|
<th style="padding: 6pt 8pt;border: 1px solid #cccccc;text-align: left;font-weight: bold">内容描述</th>
|
${allDeptNames.map(dept => `
|
<th style="padding: 6pt 8pt;border: 1pt solid #cccccc;text-align: center;font-weight: bold;min-width: 50pt">${dept}</th>
|
`).join('')}
|
</tr>
|
</thead>
|
<tbody>
|
${clauses.map(clause => `
|
<tr>
|
<td style="padding: 5pt 8pt;
|
border: 1pt solid #e0e0e0;
|
text-align: center;
|
vertical-align: top">${clause.clauseNum}</td>
|
<td style="
|
padding: 5pt 8pt;
|
border: 1pt solid #e0e0e0;
|
text-align: left;
|
vertical-align: top">${clause.content}</td>
|
${allDeptNames.map(dept => `
|
<td style="
|
padding: 5pt 8pt;
|
border: 1pt solid #e0e0e0;
|
text-align: center;
|
vertical-align: top">
|
${dataMap[clause.clauseNum]?.[dept] ?? '○'}
|
</td>
|
`).join('')}
|
</tr>
|
`).join('')}
|
</tbody>
|
</table>
|
`;
|
}
|
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;
|
}
|
const imageOptions = {
|
getImage(tagValue) {
|
return base64Parser(tagValue);
|
},
|
getSize(img, tagValue, tagName, context) {
|
return [600, 600];
|
},
|
};
|
|
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);
|
// }
|
|
// 生成表格XML
|
// if (data.clauses && data.duties) {
|
// data.tableXML = generateTableXML(data.clauses, data.duties);
|
// }
|
|
// 处理富文本字段(如果有)
|
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('<!-- TABLE_PLACEHOLDER -->', 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;
|
}
|
});
|
}
|