《前端导出Word文档不再难:从会诊单导出实战解析多种方案》

242 阅读4分钟

引言

“前端导出Word”听起来像是需要后端协助的工作,但其实纯前端完全可以独立完成!无论是简单的表格数据,还是带有复杂格式、图片、页眉页脚的报告,前端都有成熟的方案。

本文将以医疗系统中“导出会诊单为Word文档”的需求为例,带你彻底搞懂前端导出Word的三种主流方案纯文本替换、利用HTML转换、以及操作OOXML。你会惊讶于前端的强大能力。

需求与技术选型

  • 需求: 将多条会诊记录,按患者分组,导出为格式规范的.docx文件。
  • 方案对比:
方案优点缺点适用场景
1. 纯文本替换实现最简单格式控制弱,易出错格式固定、简单的文档
2. HTML转换利用已有CSS,开发快兼容性存疑,格式可能失真格式简单的文档或内部使用
3. 操作OOXML功能最强大,格式精准复杂度最高,需学习成本复杂、高要求的企业级文档

一、实现思路与流程 (方案一:纯文本替换)

这种方案的核心是:准备一个Word模板文件,将其另存为XML或直接修改为.docx,然后在代码中替换预定义的占位符

其核心流程如下图所示:

graph TD
A[用户点击导出Word按钮] --> B[前端Fetch请求模板文件]
B --> C{获取模板成功?} 
C -- 失败 --> D[提示错误并终止] 
C -- 成功 --> E[将模板内容读为文本字符串]
E --> F[遍历待导出的数据] 
F --> G[使用数据值替换模板中的占位符]
G --> H[合并所有替换后的内容]
H --> I[将最终字符串转换为Blob对象]
I --> J[使用FileSaver触发浏览器下载]
J --> K[完成导出]


二、代码实战与详解

1. 准备模板文件

  1. 在Microsoft Word中设计好会诊单的样式。
  2. 在需要动态填充内容的地方,填入唯一的占位符,例如:{name}{consultationNo}{diagnosisOpinion}等。
  3. 将文件另存为XML格式,然后将文件后缀.xml改为.docx。或者,更简单的方法:直接将.docx文件的后缀改为.zip,解压后找到word/document.xml文件,用文本编辑器打开,在其中找到你要替换的文字,改为{placeholder},然后再打包回去并改回.docx后缀。这里我们采用第一种简单方法,保存为template.docx,并放入项目的public/templates/目录下。

2. 核心代码实现

<script setup lang="ts">
import { saveAs } from 'file-saver'; // 强大的前端文件下载库

// 导出Word文档函数
async function exportToWord() {
  try {
    // 1. 定义模板路径
    const templateUrl = '/templates/template.docx';
    console.log('正在加载模板文件...');

    // 2. 使用Fetch API获取模板文件
    const response = await fetch(templateUrl);
    if (!response.ok) {
      throw new Error(`模板加载失败: ${response.status}`);
    }

    // 3. 将响应流读取为文本
    // 注意:这里假设模板是纯文本或XML,但对于真正的.docx文件(二进制),应使用 `await response.blob()` 或 `arrayBuffer()`
    const templateText = await response.text();
    console.log('模板加载成功!');

    // 4. 按患者分组数据
    const patientGroups = groupConsultationsByPatient(printParams.value);

    // 5. 遍历每个患者的所有会诊单
    for (const [patientKey, consultations] of Object.entries(patientGroups)) {
      if (consultations.length === 0) continue;

      // 获取患者基本信息
      const firstConsultation = consultations[0];
      const patientInfo = {
        consultationNo: firstConsultation.consultationNo || '',
        name: firstConsultation.name || '',
        // ... 其他基本信息字段
      };

      let combinedContent = '';

      // 6. 处理每份会诊单
      for (let i = 0; i < consultations.length; i++) {
        const item = consultations[i];
        // 复制一份模板文本
        let consultationContent = templateText;

        // 7. 创建数据对象,映射所有占位符
        const data: any = {
          ...patientInfo,
          diagnosisOpinion: item.diagnosisOpinion || '无',
          abScanDiagnosis: item.abScanDiagnosis || '无',
          // ... 其他需要替换的字段
        };

        // 8. 🎯 核心步骤:循环替换所有占位符
        for (const [key, value] of Object.entries(data)) {
          // 创建正则表达式,全局搜索占位符,如 {name}
          const regex = new RegExp(`{${key}}`, 'g');
          // 执行替换操作
          consultationContent = consultationContent.replace(regex, value as string);
        }

        combinedContent += consultationContent;
        // 添加分页符 (ASCII码为12)
        if (i < consultations.length - 1) {
          combinedContent += '\f';
        }
      }

      // 9. 生成Blob并触发下载
      // 注意: 这里类型是Word的MIME类型
      const blob = new Blob([combinedContent], {
        type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
      });
      // 10. 使用file-saver保存文件
      saveAs(blob, `${patientInfo.consultationNo}-${patientInfo.name}-会诊单.docx`);
      ElMessage.success('导出成功!');
    }
  } catch (error: any) {
    console.error('导出失败:', error);
    ElMessage.error(`导出失败: ${error.message}`);
  }
}

// 按患者姓名分组数据的工具函数
function groupConsultationsByPatient(consultations: any[]) {
  const groups: Record<string, any[]> = {};
  consultations.forEach((consultation) => {
    const key = consultation.name;
    if (!groups[key]) {
      groups[key] = [];
    }
    groups[key].push(consultation);
  });
  return groups;
}
</script>

关键注释:

  • fetch(templateUrl): 通过网络请求获取模板文件。对于生产环境,最好将模板文件放在public目录或CDN上。
  • RegExpreplace: 这是实现替换的核心逻辑。使用正则表达式进行全局(g)替换,确保所有相同的占位符都被替换。
  • Blob: 二进制大对象,允许我们在前端直接操作并在内存中生成文件内容。
  • saveAs(blob, filename)file-saver库的核心方法,模拟了一个文件下载动作。

三、进阶方案:功能强大的docxtemplater

方案一虽然简单,但缺点也很明显:模板制作麻烦,格式控制弱。对于更严肃的项目,推荐使用方案三的增强版——docxtemplater

docxtemplater的工作原理:
它不需要你将Word存为XML,而是直接操作.docx文件(本质是一个ZIP包)。它解压文件,找到内部的document.xml,然后用你的数据替换其中的占位符,最后再重新打包成.docx。它支持循环、条件判断、图片插入等复杂功能。

代码示例:

import Docxtemplater from 'docxtemplater';
import PizZip from 'pizzip';
import { saveAs } from 'file-saver';

// 1. 获取二进制模板
const response = await fetch('/templates/advanced-template.docx');
const arrayBuffer = await response.arrayBuffer();

// 2. 加载Zip包和模板
const zip = new PizZip(arrayBuffer);
const doc = new Docxtemplater().loadZip(zip);

// 3. 设置数据 (支持嵌套结构和循环)
doc.setData({
  name: "张三",
  consultations: [
    { diagnosis: "诊断意见1" },
    { diagnosis: "诊断意见2" }
  ]
});

// 4. 渲染文档
doc.render();

// 5. 生成并导出Blob
const out = doc.getZip().generate({ type: 'blob' });
saveAs(out, 'advanced-report.docx');

四、总结与选择建议

特性纯文本替换docxtemplater (推荐)HTML转换
复杂度
功能极强 (循环、条件、图片)
格式保真 (完美保留原格式)一般
模板制作麻烦简单 (直接在Word里设计)简单

选择建议:

  • 如果你的需求只是导出一份格式固定、非常简单的文档,纯文本替换可以快速上手。
  • 如果你需要导出格式复杂、要求高、且数据动态的企业级文档(如合同、报告、通知),docxtemplater是不二之选。它虽然需要引入额外的库,但带来的灵活性和可靠性是巨大的。
  • HTML转换方案可用于快速将网页内容导出,但对复杂Word格式的支持有限。