祖安之光
2025-07-08 28ac8082ce22a769c7c487616007f36a112b72cc
src/views/build/conpanyFunctionConsult/digitalFileDep/manageType/qualityManual/components/exportDoc.js
@@ -3,7 +3,7 @@
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);
@@ -70,14 +70,12 @@
    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.label}`;
            listHtml += `<li style="margin-bottom: 6pt;">${item.deptName}`;
            if (item.children && item.children.length > 0) {
                listHtml += buildList(item.children);
            }
            listHtml += '</li>';
        });
@@ -89,8 +87,233 @@
    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);
@@ -103,6 +326,10 @@
    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) {
@@ -112,9 +339,11 @@
        try {
            // 加载模板文件内容到 PizZip
            const zip = new PizZip(content);
            const imageModule = new ImageModule(imageOptions);
            const doc = new Docxtemplater(zip, {
                paragraphLoop: true,
                linebreaks: true,
                modules: [imageModule]
            });
            // 设置模板中的占位符数据
@@ -123,6 +352,11 @@
            // 渲染文档
            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',