docxtemplater研究word模版---表格,竖向合并单元格

该文章已生成可运行项目,

1、配置word文件

注意:配置一个docx文件保存在public文件下。比如:信息模版.docx

2、安装插件

"docxtemplater": "^3.67.5",
"file-saver": "^2.0.5",
"docx-preview": "^0.3.7",
"pizzip": "^3.2.0",
"xml-js": "^1.6.11"

3、引入插件

import { saveAs } from "file-saver";
import { renderAsync } from "docx-preview";

4、加载Word模板文件

        const fileName = "info.docx";
        const response = await fetch(`/${fileName}`);

        if (!response.ok) {
          throw new Error(`模板文件不存在,状态码:${response.status}`);
        }

        const arrayBuffer = await response.arrayBuffer();

5、初始化docxtemplater

        const PizZip = require("pizzip");
        const Docxtemplater = require("docxtemplater");
        const zip = new PizZip(arrayBuffer);

6、创建 Docxtemplater 实例

        const doc = new Docxtemplater(zip, {
          paragraphLoop: true,
          linebreaks: true,
          nullGetter: () => "",
        });

7、准备模板数据(companies 数据)

        const data = {
          companies: [
            {
              company: "ACME Corp",
              users: [
                {
                  name: "John"
                },
                {
                  name: "Mary"
                },
                {
                  name: "Lynn"
                }
              ]
            },
            {
              company: "Evil Corp",
              users: [
                {
                  name: "Jack"
                },
                {
                  name: "Sean"
                }
              ]
            }
          ]
        };

8、设置数据并渲染

        doc.setData(data);
        doc.render();

9、读取渲染后的 document.xml

const xml = doc.getZip().file("word/document.xml").asText();

10、解析 XML 为 JSON(使用 compact 模式,便于操作)

        const xmlOptions = { compact: true, ignoreComment: true, spaces: 4 };
        const { xml2js, js2xml } = require("xml-js");
        const json = xml2js(xml, xmlOptions);

 11、找到目标表格,自动合并相同数据的单元格

        try {
          const document = json["w:document"];
          const body = document["w:body"];
          
          // 找到所有表格(可能是数组或单个对象)
          let tables = body["w:tbl"];
          if (!Array.isArray(tables)) {
            tables = tables ? [tables] : [];
          }

          // 辅助函数:提取单元格文本内容
          const getCellText = (cell) => {
            if (!cell || !cell["w:p"]) return "";
            const paragraphs = Array.isArray(cell["w:p"]) 
              ? cell["w:p"] 
              : cell["w:p"] 
                ? [cell["w:p"]] 
                : [];
            
            let text = "";
            paragraphs.forEach((p) => {
              if (p["w:r"]) {
                const runs = Array.isArray(p["w:r"]) 
                  ? p["w:r"] 
                  : [p["w:r"]];
                runs.forEach((r) => {
                  if (r["w:t"]) {
                    const texts = Array.isArray(r["w:t"]) 
                      ? r["w:t"] 
                      : [r["w:t"]];
                    texts.forEach((t) => {
                      if (typeof t === "string") {
                        text += t;
                      } else if (t._text) {
                        text += t._text;
                      }
                    });
                  }
                });
              }
            });
            return text.trim();
          };

          // 辅助函数:确保单元格属性存在
          const ensureCellPr = (cell) => {
            if (!cell["w:tcPr"]) {
              cell["w:tcPr"] = {};
            }
            return cell["w:tcPr"];
          };

          // 辅助函数:添加纵向合并标记
          const addVMerge = (cell, type) => {
            const tcPr = ensureCellPr(cell);
            if (type === "restart") {
              tcPr["w:vMerge"] = {
                _attributes: { "w:val": "restart" },
              };
            } else {
              tcPr["w:vMerge"] = {};
            }
          };

          if (tables.length > 0) {
            // 处理每个表格
            tables.forEach((table, tableIndex) => {
              const rows = Array.isArray(table["w:tr"]) 
                ? table["w:tr"] 
                : table["w:tr"] 
                  ? [table["w:tr"]] 
                  : [];

              console.log(`处理表格 ${tableIndex + 1},共 ${rows.length} 行`);

              if (rows.length < 2) {
                console.log(`表格 ${tableIndex + 1} 行数不足,跳过合并`);
                return;
              }

              // 获取第一行的列数(作为参考)
              const firstRowCells = Array.isArray(rows[0]["w:tc"]) 
                ? rows[0]["w:tc"] 
                : rows[0]["w:tc"] 
                  ? [rows[0]["w:tc"]] 
                  : [];
              const columnCount = firstRowCells.length;

              if (columnCount === 0) {
                console.log(`表格 ${tableIndex + 1} 没有列,跳过`);
                return;
              }

              console.log(`表格 ${tableIndex + 1} 有 ${columnCount} 列`);

              // 对每一列进行检测和合并
              for (let colIndex = 0; colIndex < columnCount; colIndex++) {
                let mergeStart = -1;
                let mergeValue = null;

                // 辅助函数:完成当前合并
                const finishMerge = (endRow) => {
                  if (mergeStart >= 0 && endRow > mergeStart + 1) {
                    const startRow = rows[mergeStart];
                    const startCells = Array.isArray(startRow["w:tc"]) 
                      ? startRow["w:tc"] 
                      : [startRow["w:tc"]];
                    if (startCells[colIndex]) {
                      addVMerge(startCells[colIndex], "restart");
                      for (let i = mergeStart + 1; i < endRow; i++) {
                        const mergeRow = rows[i];
                        const mergeCells = Array.isArray(mergeRow["w:tc"]) 
                          ? mergeRow["w:tc"] 
                          : [mergeRow["w:tc"]];
                        if (mergeCells[colIndex]) {
                          addVMerge(mergeCells[colIndex], "continue");
                        }
                      }
                      console.log(
                        `✅ 列 ${colIndex + 1}:合并行 ${mergeStart + 1}-${endRow}(值:"${mergeValue}")`
                      );
                    }
                  }
                };

                // 遍历每一行
                for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
                  const row = rows[rowIndex];
                  const cells = Array.isArray(row["w:tc"]) 
                    ? row["w:tc"] 
                    : row["w:tc"] 
                      ? [row["w:tc"]] 
                      : [];

                  // 跳过列数不足的行
                  if (colIndex >= cells.length) {
                    finishMerge(rowIndex);
                    mergeStart = -1;
                    mergeValue = null;
                    continue;
                  }

                  const cell = cells[colIndex];
                  const cellText = getCellText(cell);

                  // 如果当前值与合并值相同且不为空,继续合并
                  if (mergeStart >= 0 && cellText === mergeValue && cellText !== "") {
                    continue;
                  }

                  // 如果之前有未完成的合并,先完成它
                  finishMerge(rowIndex);

                  // 开始新的合并(如果当前值不为空)
                  if (cellText !== "") {
                    mergeStart = rowIndex;
                    mergeValue = cellText;
                  } else {
                    mergeStart = -1;
                    mergeValue = null;
                  }
                }

                // 处理最后一组合并(如果存在)
                finishMerge(rows.length);
              }

              console.log(`✅ 表格 ${tableIndex + 1} 合并完成`);
            });
          } else {
            console.warn("⚠️ 未找到表格");
          }
        } catch (mergeError) {
          console.error("合并单元格时出错:", mergeError);
          console.warn("⚠️ 将使用未合并的表格");
        }

12、重新生成 XML 并替换

        const newXml = js2xml(json, xmlOptions);
        doc.getZip().file("word/document.xml", newXml);

13、生成文档

        const zipData = doc.getZip();
        const docContent = zipData.generate({
          type: "uint8array",
          mimeType:
            "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        });

14、生成 blob

        const blob = new Blob([docContent], {
          type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        });

        this.wordBlob = blob;
        this.downloadFileName = `信息-${new Date().getTime()}.docx`;

15、转换为 ArrayBuffer 用于预览

        const outputArrayBuffer = await blob.arrayBuffer();
        this.loading = false;

16、等待 DOM 更新后再渲染

await this.$nextTick();

17、使用 docx-preview 在页面上渲染

        if (this.$refs.wordContent) {
          try {
            this.$refs.wordContent.innerHTML = "";
            await renderAsync(
              outputArrayBuffer,
              this.$refs.wordContent,
              null,
              {
                className: "docx-wrapper",
                inWrapper: true,
                ignoreWidth: false,
                ignoreHeight: false,
                ignoreFonts: false,
                breakPages: true,
                ignoreLastRenderedPageBreak: true,
                experimental: false,
                trimXmlDeclaration: true,
                useBase64URL: false,
                useMathMLPolyfill: true,
                showChanges: false,
                showComments: false,
                showInserted: true,
                showDeleted: false,
              }
            );

            console.log("文档渲染成功");
            this.$message.success("Word 文档生成并显示成功!");
          } catch (renderError) {
            console.error("文档渲染失败:", renderError);
            this.$refs.wordContent.innerHTML = `
              <div style="padding: 40px; text-align: center; color: #f56c6c;">
                <p style="font-size: 16px; margin-bottom: 10px;">文档渲染失败</p>
                <p style="font-size: 14px; color: #999;">错误信息: ${
                  renderError.message || "未知错误"
                }</p>
                <p style="font-size: 14px; color: #999; margin-top: 10px;">您可以点击"下载文档"按钮下载 Word 文件查看</p>
              </div>
            `;
            this.$message.warning("文档预览失败,但可以下载查看");
          }
        }

18、下载按钮函数

    downloadWord() {
      if (this.wordBlob && this.downloadFileName) {
        saveAs(this.wordBlob, this.downloadFileName);
        this.$message.success("文档下载成功!");
      } else {
        this.$message.warning("文档尚未生成,请先生成文档");
      }
    },

19、全部代码:

<template>
  <div class="word-container">
    <div class="word-card">
      <div class="header-actions">
        <el-button type="primary" @click="downloadWord" :disabled="!wordBlob">
          <i class="el-icon-download"></i> 下载文档
        </el-button>
        <el-button @click="generateWord" :loading="loading">
          <i class="el-icon-refresh"></i> 重新生成
        </el-button>
      </div>

      <div v-if="loading" class="loading">
        <i class="el-icon-loading"></i>
        <p>正在生成 Word 文档...</p>
      </div>
      <div v-else-if="!wordBlob" class="error-message">
        <p>文档生成失败,请重试</p>
      </div>
      <div v-else ref="wordContent" class="word-content"></div>
    </div>
  </div>
</template>

<script>
import { saveAs } from "file-saver";
import { renderAsync } from "docx-preview";

export default {
  name: "InfoPage",
  data() {
    return {
      loading: false,
      wordBlob: null,
      downloadFileName: "",
    };
  },
  mounted() {
    this.generateWord();
  },
  methods: {
    async generateWord() {
      try {
        this.loading = true;

        // 1. 加载Word模板文件
        const fileName = "info.docx";
        const response = await fetch(`/${fileName}`);

        if (!response.ok) {
          throw new Error(`模板文件不存在,状态码:${response.status}`);
        }

        const arrayBuffer = await response.arrayBuffer();

        // 2. 初始化docxtemplater
        const PizZip = require("pizzip");
        const Docxtemplater = require("docxtemplater");
        const zip = new PizZip(arrayBuffer);

        // 3. 创建 Docxtemplater 实例
        const doc = new Docxtemplater(zip, {
          paragraphLoop: true,
          linebreaks: true,
          nullGetter: () => "",
        });

        console.log("✅ Docxtemplater 实例已创建");

        // 4. 准备模板数据(companies 数据)
        const data = {
          companies: [
            {
              company: "ACME Corp",
              users: [
                {
                  name: "John"
                },
                {
                  name: "Mary"
                },
                {
                  name: "Lynn"
                }
              ]
            },
            {
              company: "Evil Corp",
              users: [
                {
                  name: "Jack"
                },
                {
                  name: "Sean"
                }
              ]
            }
          ]
        };

        // 5. 设置数据并渲染
        doc.setData(data);
        doc.render();

        console.log("✅ 模板渲染完成");

        // 6. 读取渲染后的 document.xml
        const xml = doc.getZip().file("word/document.xml").asText();

        // 7. 解析 XML 为 JSON(使用 compact 模式,便于操作)
        const xmlOptions = { compact: true, ignoreComment: true, spaces: 4 };
        const { xml2js, js2xml } = require("xml-js");
        const json = xml2js(xml, xmlOptions);

        console.log("✅ XML 解析完成");

        // 8. 找到目标表格,自动合并相同数据的单元格
        try {
          const document = json["w:document"];
          const body = document["w:body"];
          
          // 找到所有表格(可能是数组或单个对象)
          let tables = body["w:tbl"];
          if (!Array.isArray(tables)) {
            tables = tables ? [tables] : [];
          }

          // 辅助函数:提取单元格文本内容
          const getCellText = (cell) => {
            if (!cell || !cell["w:p"]) return "";
            const paragraphs = Array.isArray(cell["w:p"]) 
              ? cell["w:p"] 
              : cell["w:p"] 
                ? [cell["w:p"]] 
                : [];
            
            let text = "";
            paragraphs.forEach((p) => {
              if (p["w:r"]) {
                const runs = Array.isArray(p["w:r"]) 
                  ? p["w:r"] 
                  : [p["w:r"]];
                runs.forEach((r) => {
                  if (r["w:t"]) {
                    const texts = Array.isArray(r["w:t"]) 
                      ? r["w:t"] 
                      : [r["w:t"]];
                    texts.forEach((t) => {
                      if (typeof t === "string") {
                        text += t;
                      } else if (t._text) {
                        text += t._text;
                      }
                    });
                  }
                });
              }
            });
            return text.trim();
          };

          // 辅助函数:确保单元格属性存在
          const ensureCellPr = (cell) => {
            if (!cell["w:tcPr"]) {
              cell["w:tcPr"] = {};
            }
            return cell["w:tcPr"];
          };

          // 辅助函数:添加纵向合并标记
          const addVMerge = (cell, type) => {
            const tcPr = ensureCellPr(cell);
            if (type === "restart") {
              tcPr["w:vMerge"] = {
                _attributes: { "w:val": "restart" },
              };
            } else {
              tcPr["w:vMerge"] = {};
            }
          };

          if (tables.length > 0) {
            // 处理每个表格
            tables.forEach((table, tableIndex) => {
              const rows = Array.isArray(table["w:tr"]) 
                ? table["w:tr"] 
                : table["w:tr"] 
                  ? [table["w:tr"]] 
                  : [];

              console.log(`处理表格 ${tableIndex + 1},共 ${rows.length} 行`);

              if (rows.length < 2) {
                console.log(`表格 ${tableIndex + 1} 行数不足,跳过合并`);
                return;
              }

              // 获取第一行的列数(作为参考)
              const firstRowCells = Array.isArray(rows[0]["w:tc"]) 
                ? rows[0]["w:tc"] 
                : rows[0]["w:tc"] 
                  ? [rows[0]["w:tc"]] 
                  : [];
              const columnCount = firstRowCells.length;

              if (columnCount === 0) {
                console.log(`表格 ${tableIndex + 1} 没有列,跳过`);
                return;
              }

              console.log(`表格 ${tableIndex + 1} 有 ${columnCount} 列`);

              // 对每一列进行检测和合并
              for (let colIndex = 0; colIndex < columnCount; colIndex++) {
                let mergeStart = -1;
                let mergeValue = null;

                // 辅助函数:完成当前合并
                const finishMerge = (endRow) => {
                  if (mergeStart >= 0 && endRow > mergeStart + 1) {
                    const startRow = rows[mergeStart];
                    const startCells = Array.isArray(startRow["w:tc"]) 
                      ? startRow["w:tc"] 
                      : [startRow["w:tc"]];
                    if (startCells[colIndex]) {
                      addVMerge(startCells[colIndex], "restart");
                      for (let i = mergeStart + 1; i < endRow; i++) {
                        const mergeRow = rows[i];
                        const mergeCells = Array.isArray(mergeRow["w:tc"]) 
                          ? mergeRow["w:tc"] 
                          : [mergeRow["w:tc"]];
                        if (mergeCells[colIndex]) {
                          addVMerge(mergeCells[colIndex], "continue");
                        }
                      }
                      console.log(
                        `✅ 列 ${colIndex + 1}:合并行 ${mergeStart + 1}-${endRow}(值:"${mergeValue}")`
                      );
                    }
                  }
                };

                // 遍历每一行
                for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
                  const row = rows[rowIndex];
                  const cells = Array.isArray(row["w:tc"]) 
                    ? row["w:tc"] 
                    : row["w:tc"] 
                      ? [row["w:tc"]] 
                      : [];

                  // 跳过列数不足的行
                  if (colIndex >= cells.length) {
                    finishMerge(rowIndex);
                    mergeStart = -1;
                    mergeValue = null;
                    continue;
                  }

                  const cell = cells[colIndex];
                  const cellText = getCellText(cell);

                  // 如果当前值与合并值相同且不为空,继续合并
                  if (mergeStart >= 0 && cellText === mergeValue && cellText !== "") {
                    continue;
                  }

                  // 如果之前有未完成的合并,先完成它
                  finishMerge(rowIndex);

                  // 开始新的合并(如果当前值不为空)
                  if (cellText !== "") {
                    mergeStart = rowIndex;
                    mergeValue = cellText;
                  } else {
                    mergeStart = -1;
                    mergeValue = null;
                  }
                }

                // 处理最后一组合并(如果存在)
                finishMerge(rows.length);
              }

              console.log(`✅ 表格 ${tableIndex + 1} 合并完成`);
            });
          } else {
            console.warn("⚠️ 未找到表格");
          }
        } catch (mergeError) {
          console.error("合并单元格时出错:", mergeError);
          console.warn("⚠️ 将使用未合并的表格");
        }

        // 9. 重新生成 XML 并替换
        const newXml = js2xml(json, xmlOptions);
        doc.getZip().file("word/document.xml", newXml);

        console.log("✅ XML 已更新");

        // 10. 生成文档
        const zipData = doc.getZip();
        const docContent = zipData.generate({
          type: "uint8array",
          mimeType:
            "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        });

        // 11. 生成 blob
        const blob = new Blob([docContent], {
          type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        });

        this.wordBlob = blob;
        this.downloadFileName = `信息-${new Date().getTime()}.docx`;

        // 12. 转换为 ArrayBuffer 用于预览
        const outputArrayBuffer = await blob.arrayBuffer();
        this.loading = false;

        // 13. 等待 DOM 更新后再渲染
        await this.$nextTick();

        // 14. 使用 docx-preview 在页面上渲染
        if (this.$refs.wordContent) {
          try {
            this.$refs.wordContent.innerHTML = "";
            await renderAsync(
              outputArrayBuffer,
              this.$refs.wordContent,
              null,
              {
                className: "docx-wrapper",
                inWrapper: true,
                ignoreWidth: false,
                ignoreHeight: false,
                ignoreFonts: false,
                breakPages: true,
                ignoreLastRenderedPageBreak: true,
                experimental: false,
                trimXmlDeclaration: true,
                useBase64URL: false,
                useMathMLPolyfill: true,
                showChanges: false,
                showComments: false,
                showInserted: true,
                showDeleted: false,
              }
            );

            console.log("文档渲染成功");
            this.$message.success("Word 文档生成并显示成功!");
          } catch (renderError) {
            console.error("文档渲染失败:", renderError);
            this.$refs.wordContent.innerHTML = `
              <div style="padding: 40px; text-align: center; color: #f56c6c;">
                <p style="font-size: 16px; margin-bottom: 10px;">文档渲染失败</p>
                <p style="font-size: 14px; color: #999;">错误信息: ${
                  renderError.message || "未知错误"
                }</p>
                <p style="font-size: 14px; color: #999; margin-top: 10px;">您可以点击"下载文档"按钮下载 Word 文件查看</p>
              </div>
            `;
            this.$message.warning("文档预览失败,但可以下载查看");
          }
        }
      } catch (error) {
        this.$message.error("生成 Word 文档失败: " + (error.message || "未知错误"));
        this.loading = false;

        // 处理模板错误
        if (error.properties && error.properties.errors) {
          const errors = error.properties.errors;
          console.error(`发现 ${errors.length} 个错误:`, errors);
          this.$message.error(`模板有 ${errors.length} 个错误,请检查控制台`);
        }
      }
    },
    downloadWord() {
      if (this.wordBlob && this.downloadFileName) {
        saveAs(this.wordBlob, this.downloadFileName);
        this.$message.success("文档下载成功!");
      } else {
        this.$message.warning("文档尚未生成,请先生成文档");
      }
    },
  },
};
</script>

<style lang="scss" scoped>
.word-container {
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  background: linear-gradient(135deg, #74b9ff 0%, #0984e3 50%, #005f99 100%);
  padding: 20px;
}

.word-card {
  width: 100%;
  max-width: 1200px;
  background: #ffffff;
  border-radius: 12px;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
  padding: 20px;
  animation: fadeInUp 0.5s ease-out;
  min-height: 600px;
  display: flex;
  flex-direction: column;
}

@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(30px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.loading {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 400px;
  color: #0984e3;

  i {
    font-size: 32px;
    margin-bottom: 20px;
    animation: rotating 2s linear infinite;
  }

  p {
    font-size: 16px;
    color: #666;
  }
}

@keyframes rotating {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

.header-actions {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-bottom: 20px;
  padding-bottom: 20px;
  border-bottom: 1px solid #eee;
}

.word-content {
  width: 100%;
  min-height: 400px;
  max-height: 80vh;
  overflow-y: auto;
  position: relative;

  :deep(.docx-wrapper) {
    background: #fff;
    padding: 40px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    border-radius: 4px;
    margin: 0 auto;
    max-width: 800px;
    min-height: 400px;
  }
}

.error-message {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 400px;
  color: #f56c6c;
  font-size: 16px;
}

@media (max-width: 768px) {
  .word-card {
    padding: 30px 20px;
    max-width: 100%;
  }
}
</style>

20、最终下载模版的结果截图:

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值