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、最终下载模版的结果截图:


1034

被折叠的 条评论
为什么被折叠?



