react+html-docx-js将页面导出为docx

发布于:2025-05-15 ⋅ 阅读:(15) ⋅ 点赞:(0)

1.主要使用:html-docx-js进行前端导出
2.只兼容到word,wps兼容不太好
3.处理分页换行
4.处理页眉

index.tsx

import { saveAs } from 'file-saver';
import htmlToDocxGenerate from './HtmlToDocx';

  const handleExportByHtml = async () => {
    const exportConfig = getSaveParams({
      currentTemplate,
      formConfigParams: formParams
    });
    const filename = getFileName(exportConfig, formParams);

    try {
      const content = document.getElementById('docx-container');
      if (!content) {
        message.error('未找到导出内容');
        return;
      }

      const docxBlob = await htmlToDocxGenerate(content.outerHTML, {
        containerId: 'docx-container',
        pageHeaderId: 'page-header',
        pageBreakClassName: 'page-break'
      });

      saveAs(docxBlob, `${filename ?? '分析报告'}.docx`);
      message.success('材料已下载成功,请查看下载文件夹');
    } catch (err) {
      message.error('导出失败:' + (err as Error).message);
    }
  };

HtmlToDocx/index.ts

import HtmlDocx from 'html-docx-js/dist/html-docx';  // 使用浏览器版本
import _ from 'lodash';
import docxHtml from './pageHtml';

interface DocumentOptions {
  orientation: 'portrait' | 'landscape';
  margins: {
    top: number;
    right: number;
    bottom: number;
    left: number;
    header: number;
    footer: number;
    gutter: number;
  };
}

interface HtmlToDocxOptions {
  containerId?: string;
  chartClassName?: string;
  pageBreakClassName?: string;
  pageHeaderId?: string;
  document?: Partial<DocumentOptions>;
}

const htmlToDocxGenerate = async (originalHtml: string, options: HtmlToDocxOptions = {}): Promise<Blob> => {
  const defaultDocument: DocumentOptions = {
    orientation: 'portrait',
    margins: {
      top: 1440,
      right: 1440,
      bottom: 1440,
      left: 1440,
      header: 720,
      footer: 720,
      gutter: 0,
    },
  };

  const defaultOptions = {
    containerId: 'docx-container',
    chartClassName: 'docx-chart',
    pageBreakClassName: 'page-break',
    pageHeaderId: 'page-header',
  };
  const pageBreakReplaceSymbol = '<div class="page-break-div"></div>';
  const finalOptions = _.merge(defaultOptions, options);
  const { containerId, pageBreakClassName, pageHeaderId } = finalOptions;

  // 创建一个临时的 div 来解析 HTML
  const tempDiv = document.createElement('div');
  tempDiv.innerHTML = originalHtml;

  // 处理页眉
  const pageHeaderElem = tempDiv.querySelector(`#${pageHeaderId}`);
  const pageHeaderHtml = pageHeaderElem?.innerHTML || '';
  pageHeaderElem?.remove();

  // 处理图片尺寸
  const images = tempDiv.querySelectorAll('img');
  images.forEach((img) => {
    const originalWidth = Number(img.getAttribute('width')?.replace('px', '') || 0);
    const originalHeight = Number(img.getAttribute('height')?.replace('px', '') || 0);
    const docxWidth = originalWidth * 0.73;

    img.setAttribute('width', docxWidth.toString());
    img.setAttribute('height', (docxWidth * (originalHeight / originalWidth)).toString());
  });

  // 处理分页符
  const pageBreaks = tempDiv.querySelectorAll(`.${pageBreakClassName}`);
  pageBreaks.forEach((elem) => {
    elem.innerHTML = pageBreakReplaceSymbol;
  });

  // 获取最终的 HTML
  const container = tempDiv.querySelector(`#${containerId}`);
  const html = container?.innerHTML || '';

  const pageBreak = "<span><br clear=all style='page-break-before:always'></span>";
  const finalHtml = _.replace(
    _.replace(docxHtml, '{{pageHeaderHtml}}', pageHeaderHtml),
    '{{docxHtml}}',
    _.replace(html, pageBreakReplaceSymbol, pageBreak)
  );

  const blob = HtmlDocx.asBlob(finalHtml, _.defaultsDeep(options.document || {}, defaultDocument));

  return blob;
};

export default htmlToDocxGenerate;

pageHtml.ts。纯样式,视情况修改,因为我用了富文本,所以还引入了quillCoreCss和quillSnowCss,不需要的可不加。

'use strict';
import quillCoreCss from './quill-core-css';
import quillSnowCss from './quill-snow-css';

const pageHtml = `
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <style>
        ${quillCoreCss}
        ${quillSnowCss}

        <!--
            p.MsoHeader, li.MsoHeader, div.MsoHeader{
                margin:0in;
                margin-top:.0001pt;
                mso-pagination:widow-orphan;
                tab-stops:center 3.0in right 6.0in;
            }
            @page Section1{
                mso-header:h1;
                mso-paper-source:0;
            }
            body div.Section1{
                page:Section1;
                <!-- padding: 0; -->
                <!-- margin-top: 0; -->
                <!-- margin-bottom: 0; -->
            }
            #h1 {
                <!-- margin-left: 100in; -->
            }
         -->

        html, body, div {
            margin: auto;
            padding: 0;
            font-size: 14px;
            color: #000;
            font-family: SimSun, Calibri, "Helvetica Neue", Helvetica, Arial, "PingFang SC", "Hiragino Sans GB", "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;
        }
        .Section1 {
            max-width: 616px;
            margin-top: 24px;
            margin-bottom: 24px;
            padding: 0.2in 0.7in 0.7in 0.7in;
            background-color: #fff;
        }
        .Section1 h2,
        .Section1 h3 {
            color: #31487f;
            /* 由于文字字体不一样大,不能用em,直接用14*14 */
            /* padding-left: 196px;  */
        }
        .Section1 table th,
        .Section1 table td {
            padding: 3px;
        }
        p {
            line-height: 28px;
            text-indent: 24px;
            text-align:justify;
            text-justify:inter-word;
        }
        h1,
        h1 .ql-editor p {
            font-size: 24px !important;
        }

        h2,
        h2 .ql-editor p,
        h2 span {
            font-size: 20px !important;
        }

        h3,
        h3 .ql-editor p,
        h3 span {
            font-size: 18px !important;
        }

        h4 .ql-editor p,
        h4 span {
            font-size: 16px !important;
        }

        h4 {
            font-size: 14px !important;
        }

        .MsoHeader td {
            padding: 8px 0;
        }

        .docx-hidden {
            display: none;
        }
    </style>
</head>
<body>

    <div class="Section1">

        <div style="mso-element:header" id="h1" >
            <span class="MsoHeader">
                {{pageHeaderHtml}}
            </span>
        </div>

        {{docxHtml}}
    </div>

</body>
</html>
`;

export default pageHtml;