前端javascript在线生成excel,word模板-通用场景(免费)
文章目录
- 一、Exceljs+FileSaver生成excel模板(带表格循环内容)
- 二、exceljstemplate模板(源码)-可以填充图片,多sheet页,自适应(通用)
- 三、docx-templates生成word模板(已封装)
在前端开发过程中不免会在线生成excel,word文件等功能,涉及到根据上传的实际模板或在线模板来进行内容的填写生成,故此会对下面的内容进行封装
一、Exceljs+FileSaver生成excel模板(带表格循环内容)
模板格式内容: ${占位符}
// 当页面渲染完毕后马上调用下面的函数,这个函数是在当前页面 - 设置 - 生命周期 - 页面加载完成时中被关联的。
export function didMount() {console.log(`「页面 JS」:当前页面地址 ${location.href}`);this.utils.loadScript('https://cdn.bootcdn.net/ajax/libs/exceljs/4.4.0/exceljs.min.js');this.utils.loadScript('https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js');
}
//示例
export function exportExcel() {const templateUrl = ''; // 在线模板 URLconst sheetName = "Sheet1"const variables = {code: "销售报表",address: "广东省深圳市....",numberField_mete69xp: 506,amount: 666};const tableData = [{ index: 1, brand: "手机", objectID: "12312SDF", name: '华为手机', model: "mate40pro", unit: "部", number: 10 },{ index: 2, brand: "电脑", objectID: "sdfs123123", name: '联想笔记本', model: "Air15", unit: "台", number: 20 },{ index: 3, brand: "游戏机", objectID: "A12345", name: 'PS5', model: "ps5pro", unit: "台", number: 5 }];const startRow = 11const fileName = "模板内容.xlsx"generateExcel(templateUrl, sheetName, variables, tableData, startRow, fileName)
}/*** templateUrl: 模板的url下载地址 模板 URL* sheetName: 需要填充的sheet页名称,默认值Sheet1* variables: 普通占位符的字典数据* {code: "销售报表",address: "广东省深圳市福围街道方图集团大厦",}这里的code对象excel的模板格式 ${code} ,address对于模板的 ${address}tableData: 循环表格迭代数据,用于填充excel表中存在多条表格数据的场景[{ index: 1, brand: "手机", objectID: "12312SDF", name: '华为手机', model: "mate40pro", unit: "部", number: 10 },{ index: 2, brand: "电脑", objectID: "sdfs123123", name: '联想笔记本', model: "Air15", unit: "台", number: 20 },{ index: 3, brand: "游戏机", objectID: "A12345", name: 'PS5', model: "ps5pro", unit: "台", number: 5 }]对于excel模板的 ${index},${brand}... excel模板中只需要设置一行数据即可. 注意事项: 这边传进来的{}字典数据顺序表格的行行数据需要按照填写内容的行位置依次填入,顺序不能够错乱.startRow: 表格写入的行位置,用于js判断在多少行才写入表格数据fileName: 生成的文件名*/
async function generateExcel(templateUrl, sheetName = "Sheet1", variables, tableData, startRow, fileName) {const response = await fetch(templateUrl);const arrayBuffer = await response.arrayBuffer();const workbook = new ExcelJS.Workbook();await workbook.xlsx.load(arrayBuffer);const ws = workbook.getWorksheet(sheetName);// --- 1. 替换普通占位符 ---ws.eachRow(row => {row.eachCell(cell => {if (cell.value && typeof cell.value === 'string') {Object.keys(variables).forEach(key => {cell.value = cell.value.replace(`\${${key}}`, variables[key]);});}});});// --- 2.循环表格数据 ---// --- 保存合并单元格 ---const merges = Array.isArray(ws._merges)? ws._merges: ws._merges instanceof Map? [...ws._merges.values()]: Object.values(ws._merges);const templateRow = ws.getRow(startRow);// --- 插入空行 ---if (!tableData || tableData.length === 0) return;if (tableData.length > 1) {ws.spliceRows(startRow + 1, 0, ...Array(tableData.length - 1).fill([]));}// --- 填充数据行并复制样式 ---// 动态生成列字段(取第一个对象的 key 顺序)const columns = Object.keys(tableData[0]);tableData.forEach((item, i) => {const rowNumber = startRow + i;const row = ws.getRow(rowNumber);// 复制模板行样式templateRow.eachCell({ includeEmpty: true }, (cell, colNumber) => {const targetCell = row.getCell(colNumber);targetCell.style = { ...cell.style };});// 动态填充数据columns.forEach((field, colIndex) => {row.getCell(colIndex + 1).value = item[field] || "";});row.commit();});// --- 重新应用合并单元格 ---// 先取消原有合并Object.values(ws._merges).forEach((merge) => {ws.unMergeCells(merge.top, merge.left, merge.bottom, merge.right);});// 然后重新应用合并单元格merges.forEach((merge) => {const offset = tableData.length - 1; // 插入的数据行数if (merge.top >= startRow) {ws.mergeCells(merge.top + offset,merge.left,merge.bottom + offset,merge.right);} else {ws.mergeCells(merge.top, merge.left, merge.bottom, merge.right);}});// --- 3. 输出并下载 ---const buffer = await workbook.xlsx.writeBuffer();const blob = new Blob([buffer], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });saveAs(blob, fileName);
}
注意事项: 如果是填充表格数据, 这边传进来的表格数据的{}字典数据顺序要跟实际excel的表格的行行数据列位置一致,需要按照填写内容的行位置依次填入,顺序不能够错乱.
二、exceljstemplate模板(源码)-可以填充图片,多sheet页,自适应(通用)
基于 exceljs 库的 .xlsx 模板文件填充引擎。理论上支持 exceljs 库的所有 api。
- 普通标签占位符格式:
{{xxx}}
、{{xxx.xxx}}
- 迭代标签占位符格式:
{{@@xxx.xxx}}
项目地址:Github、Gitee
支持浏览器和 node.js 环境下使用。可参考 test 目录下的 test.html 或 test.js。
// 当页面渲染完毕后马上调用下面的函数,这个函数是在当前页面 - 设置 - 生命周期 - 页面加载完成时中被关联的。
export function didMount() {console.log(`「页面 JS」:当前页面地址 ${location.href}`);this.utils.loadScript('https://cdn.bootcdn.net/ajax/libs/exceljs/4.4.0/exceljs.min.js');
}// 判断是否浏览器环境
const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";/** 获取 URL 文件 */
export async function fetchUrlFile(url = '') {if (isBrowser) {const response = await fetch(url);if (!response.ok) throw new Error(`Failed to fetch ${url}, status: ${response.status}`);return response.arrayBuffer();} else {const { get } = /^https:\/\//.test(url) ? require("https") : require("http");return new Promise((resolve, reject) => {get(url, (res) => {if (res.statusCode !== 200) reject(new Error(`Failed to fetch ${url}, status: ${res.statusCode}`));const chunks = [];res.on("data", (chunk) => chunks.push(chunk));res.on("end", () => resolve(Buffer.concat(chunks)));}).on("error", reject);});}
}/** 加载工作簿 */
export async function loadWorkbook(input) {const workbook = new ExcelJS.Workbook(); // 创建工作簿const httpRegex = /^https?:\/\//;if (isBrowser) {if (typeof input === "string" && httpRegex.test(input)) {const arrayBuffer = await this.fetchUrlFile(input);await workbook.xlsx.load(arrayBuffer);} else if (input instanceof Blob || input instanceof ArrayBuffer) {await workbook.xlsx.load(input);} else {throw new Error("Unsupported input type in browser environment.");}} else {if (typeof input === "string") {if (httpRegex.test(input)) {const buffer = await this.fetchUrlFile(input);await workbook.xlsx.load(buffer);} else {await workbook.xlsx.readFile(input);}} else if (input instanceof Buffer || input instanceof ArrayBuffer) {await workbook.xlsx.load(input);} else if (typeof (input).pipe === "function") {await workbook.xlsx.read(input);} else {throw new Error("Unsupported input type in Node.js environment.");}}return workbook;
}/** 保存工作簿 */
export async function saveWorkbook(workbook, output) {if (isBrowser) {const buffer = await workbook.xlsx.writeBuffer();const blob = new Blob([buffer], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });const link = document.createElement("a");link.href = URL.createObjectURL(blob);link.download = output;link.click();URL.revokeObjectURL(link.href);} else {await workbook.xlsx.writeFile(output);}
}/** 占位符范围 */
function placeholderRange(worksheet,placeholder = "{{#placeholder}}",clearMatch = true
) {console.log("worksheet", worksheet)console.log("placeholder", placeholder)let result = null;worksheet.eachRow((row, rowNumber) => {row.eachCell((cell, colNumber) => {if (typeof cell.value === "string" && cell.value.includes(placeholder)) {result = { start: { row: rowNumber, col: colNumber }, end: { row: rowNumber, col: colNumber } };if (clearMatch) cell.value = cell.value.replace(new RegExp(placeholder, "g"), "");}});});return result;
}/*** 处理单元格标签(普通标签替换和迭代标签信息收集)* @param {string} target 单元格值* @param {Record<string, any>} worksheetData 工作表数据* @param {Array<{iterStartRow: number; iterFieldNames: string[]; iterFieldName: string}>} iterationTags 迭代标签信息* @param {string[]} iterFieldNames 行迭代字段* @param {number} rowNumber 行号* @returns {string} 处理后的单元格值*/
export function processCellTags(target, worksheetData, iterationTags, iterFieldNames, rowNumber) {// 匹配所有 {{xxx.xxx}} 占位符const regex = /{{\s*[\w.]+\s*}}/g;if (regex.test(target)){const placeholders = target.match(regex);if (!placeholders) return target;placeholders.forEach((placeholder) => {// 去掉 {{ }} 两侧空格const keyPath = placeholder.replace(/{{\s*|\s*}}/g, '');const fields = keyPath.split('.');let value = worksheetData;let isMatched = true;for (let i = 0; i < fields.length; i++) {if (value && fields[i] in value) {value = value[fields[i]];} else {isMatched = false;break;}}// 替换占位符if (isMatched) {// 如果是完整单元格且 value 不是对象,直接替换为纯值if (target.trim() === placeholder.trim() && typeof value !== 'object') {target = value;} else {// 多占位符或部分替换target = target.split(placeholder).join(value || '');}}});}// 迭代标签信息搜集else if (/{{@@\w+\.\w+}}/.test(target)) {// 单元格中仅匹配一个迭代标签占位符// TODO 单元格中含多个不同的迭代标签const iterFieldName = target.match(/{{@@(\w+)\.\w+}}/)[1];// 数据存在且为数组类型if (iterFieldName in worksheetData &&Array.isArray(worksheetData[iterFieldName]) &&worksheetData[iterFieldName].length > 0) {if (iterFieldNames.length === 0) {iterFieldNames.push(iterFieldName);iterationTags.push({ iterStartRow: rowNumber, iterFieldNames: iterFieldNames, iterFieldName: iterFieldName });} else {if (!iterFieldNames.includes(iterFieldName)) {iterFieldNames.push(iterFieldName);// 迭代标签字段长度不一致,取最长的(后续按最大长度复制行)const lastIterationTag = iterationTags[iterationTags.length - 1];if (worksheetData[iterFieldName].length > worksheetData[lastIterationTag.iterFieldName].length) {lastIterationTag.iterFieldName = iterFieldName;}}}}}console.log("target", target)return target;
}/*** 获取工作表合并信息* @param {ExcelJS.Worksheet} worksheet* @returns {Array<{start: {row: number; col: number}; end: {row: number; col: number}}>}*/
export function sheetMergeInfo(worksheet) {return worksheet.model.merges.map((merge) => {// C30:D30const [startCell, endCell] = merge.split(":");return {start: { row: worksheet.getCell(startCell).row, col: worksheet.getCell(startCell).col },end: { row: worksheet.getCell(endCell).row, col: worksheet.getCell(endCell).col },};});
}/*** 填充Excel模板* @param {ExcelJS.Workbook} workbook* @param {Array<Record<string, any>>} workbookData - 包含模板数据的数组对象* @param {boolean} parseImage - 是否解析图片,默认为 false* @returns {Promise<ExcelJS.Workbook>}*/
export async function fillTemplate(workbook, workbookData, parseImage = false) {// 第一步:复制行并替换占位符// 工作表待合并单元格信息const sheetDynamicMerges = {};// NOTE 工作簿的sheetId是按工作表创建的顺序从1开始递增let sheetIndex = 0;workbook.eachSheet((worksheet, sheetId) => {const sheetData = workbookData[sheetIndex++];if (!(sheetData && typeof sheetData === "object" && !Array.isArray(sheetData))) {return;}// 普通标签替换和迭代标签信息收集/** @type {Array<{iterStartRow: number; iterFieldNames: string[]; iterFieldName: string}>} */const sheetIterTags = [];worksheet.eachRow((row, rowNumber) => {// 行迭代字段const rowIterFields = [];row.eachCell((cell, colNumber) => {const cellType = cell.type;// 字符串值// || ExcelJS.ValueType.SharedStringif (cellType === ExcelJS.ValueType.String) {cell.value = this.processCellTags(cell.value, sheetData, sheetIterTags, rowIterFields, rowNumber);}// 富文本值 && cell.value.richTextelse if (cellType === ExcelJS.ValueType.RichText) {cell.value.richText.forEach((item) => {item.text = this.processCellTags(item.text, sheetData, sheetIterTags, rowIterFields, rowNumber);});}});});// 迭代标签处理if (sheetIterTags.length === 0) {return;}// 合并单元格信息// NOTE 合并信息是静态的,不会随着行增加而实时更新const sheetMerges = this.sheetMergeInfo(worksheet);// 迭代行并替换迭代字段占位符let iterOffset = 0;sheetIterTags.forEach(({ iterStartRow, iterFieldNames, iterFieldName }, iterTagIndex) => {// 调整后的起始行const adjustedStartRow = iterStartRow + iterOffset;const iterDataLength = sheetData[iterFieldName].length;// 多行的情况下,需要复制多行if (iterDataLength > 1) {// 一次性复制多行// NOTE 复制的行不会复制合并信息worksheet.duplicateRow(adjustedStartRow, iterDataLength - 1, true);// 筛选出与当前模板行相关的合并单元格信息,并应用到其复制的行const merges = sheetMerges.filter((merge) => {return merge.start.row <= iterStartRow && merge.end.row >= iterStartRow;});if (merges.length > 0) {if (!sheetDynamicMerges[sheetId]) {sheetDynamicMerges[sheetId] = [];}// NOTE 在浏览器环境,动态增加的行会使其后面的行取消合并单元格const startFixIndex = isBrowser ? (iterTagIndex === 0 ? 1 : 0) : 1;for (let i = startFixIndex; i < iterDataLength; i++) {for (const merge of merges) {sheetDynamicMerges[sheetId].push([merge.start.row + i + iterOffset,merge.start.col,merge.end.row + i + iterOffset,merge.end.col,]);}}}}// 替换迭代行中的占位符for (let i = 0; i < iterDataLength; i++) {const currentRow = worksheet.getRow(adjustedStartRow + i);// 遍历当前行的单元格currentRow.eachCell((cell, colNumber) => {// 字符串值if (cell.type === ExcelJS.ValueType.String) {if (typeof cell.value === "string") {// 遍历单元格中的多个迭代字段iterFieldNames.forEach((iterField) => {// 单元格中包含当前迭代字段的占位符if (cell.value.includes(`{{@@${iterField}\.`)) {// 当前迭代字段索引数据const currentIterFieldData = sheetData[iterField][i];if (currentIterFieldData !== undefined) {// 迭代字段数据for (const field in currentIterFieldData) {const placeholder = `{{@@${iterField}.${field}}}`;if (cell.value.includes(placeholder)) {// 完全匹配,替换单元格内容为迭代字段数据if (cell.value.length === placeholder.length && typeof currentIterFieldData[field] !== "object") {cell.value = currentIterFieldData[field];break;}// 包含其他内容,部分替换为迭代字段数据else {cell.value = cell.value.replace(new RegExp(placeholder, "g"),currentIterFieldData[field] || "");}}}} else {// 清空单元格内容cell.value = null;}}});}}// TODO 迭代标签单元格为富文本值});}// 更新行号偏移量iterOffset += iterDataLength - 1;});// 修正在浏览器环境,动态增加的行会使其后面的行取消合并单元格if (isBrowser) {const iterRows = sheetIterTags.map(({ iterStartRow }) => iterStartRow);sheetMerges.forEach((merge) => {// 迭代后的偏移行let mergeOffset = 0;sheetIterTags.forEach(({ iterStartRow, iterFieldName }) => {if (Array.isArray(sheetData[iterFieldName])) {if (!iterRows.includes(merge.start.row) && merge.start.row > iterStartRow) {mergeOffset += sheetData[iterFieldName].length - 1;}}});if (mergeOffset) {if (!sheetDynamicMerges[sheetId]) {sheetDynamicMerges[sheetId] = [];}sheetDynamicMerges[sheetId].push([merge.start.row + mergeOffset,merge.start.col,merge.end.row + mergeOffset,merge.end.col,]);}});}});// 第二步:动态行单元格合并处理if (Object.keys(sheetDynamicMerges).length > 0) {// 将工作簿保存到内存中的缓冲区const buffer = await workbook.xlsx.writeBuffer();// 从缓冲区重新加载工作簿await workbook.xlsx.load(buffer);// 处理合并单元格workbook.eachSheet((worksheet, sheetId) => {if (sheetDynamicMerges[sheetId]) {sheetDynamicMerges[sheetId].forEach((merge) => {try {worksheet.mergeCells(merge);} catch (error) {console.warn(`Fail to merge cells ${merge}`);}});}});}// 第三步:填充图片parseImage && (await this.fillImage(workbook));return workbook;
}/*** 填充图片* @param {ExcelJS.Workbook} workbook*/
export async function fillImage(workbook) {const filledImageMap = new Map();const invalidImageSet = new Set();const urlRegex = /https?:\/\/[^\s]+(?:\.(jpe?g|png|gif))?/i;const base64Regex = /data:image\/(jpeg|gif|png);base64,[^\s]+/i;// NOTE eachSheet、eachRow、eachCell都是同步方法,不会等待异步操作完成// 遍历每个工作表for (let i = 0; i < workbook.worksheets.length; i++) {const worksheet = workbook.worksheets[i];const sheetMerges = sheetMergeInfo(worksheet);// 遍历每一行for (let rowNumber = 1; rowNumber <= worksheet.rowCount; rowNumber++) {const row = worksheet.getRow(rowNumber);// 遍历每个单元格for (let colNumber = 1; colNumber <= row.cellCount; colNumber++) {const cell = row.getCell(colNumber);if (typeof cell.value !== "string") {continue;}let targetRegex = null;let imageId = 0;// URL图片if (urlRegex.test(cell.value)) {targetRegex = urlRegex;const matches = cell.value.match(urlRegex);const imageUrl = matches[0];const imageExt = matches[1] || "png";if (invalidImageSet.has(imageUrl)) {continue;}if (filledImageMap.has(imageUrl)) {imageId = filledImageMap.get(imageUrl);} else {let fileContent = null;try {fileContent = await this.fetchUrlFile(imageUrl);} catch (imgErr) {invalidImageSet.add(imageUrl);console.warn(`Fail to load image ${imageUrl}`);continue;}// 将图片添加到工作簿中imageId = workbook.addImage({buffer: fileContent,extension: imageExt === "jpg" ? imageExt : "jpeg",});filledImageMap.set(imageUrl, imageId);}}// Base64图片else if (base64Regex.test(cell.value)) {targetRegex = base64Regex;const matches = cell.value.match(base64Regex);const imageData = matches[0];const imageExt = matches[1];if (filledImageMap.has(imageData)) {imageId = filledImageMap.get(imageData);} else {imageId = workbook.addImage({base64: imageData,extension: imageExt,});filledImageMap.set(imageData, imageId);}}if (!targetRegex) {continue;}// 将图片添加到工作表中const merge = sheetMerges.find((merge) => {return merge.start.row === rowNumber && merge.start.col === colNumber;});// 坐标系基于零,A1 的左上角将为 {col:0,row:0},右下角为 {col:1,row:1}worksheet.addImage(imageId, {// 左上角tl: {col: merge ? merge.start.col - 1 : colNumber - 1,row: merge ? merge.start.row - 1 : rowNumber - 1,},// 右下角br: {col: merge ? merge.end.col : colNumber,row: merge ? merge.end.row : rowNumber,},});// 去除图片地址cell.value = cell.value.replace(targetRegex, "");}}}return workbook;
}/*** 渲染Xlsx模板* @param {string|ArrayBuffer|Blob|Buffer} input - 输入数据,可以是本地路径、URL地址、ArrayBuffer、Blob、Buffer* @param {Array<Record<string, any>>} data - 包含模板数据的数组对象* @param {string} output - 输出文件路径或文件名* @param {{parseImage?: boolean; beforeSave?: (workbook: ExcelJS.Workbook) => void|Promise<void>}} options 配置项* @returns {Promise<void>}*/
export async function renderXlsxTemplate(input = '', data = [], output = '', options = { parseImage: false, beforeSave: undefined }) {const workbook = await this.loadWorkbook(input);// 填充模板await this.fillTemplate(workbook, data, options.parseImage === true);if (options.beforeSave) await options.beforeSave(workbook);await this.saveWorkbook(workbook, output);
}/** 点击处理函数* 示例*/
export async function handleXlsxTemplate() {const xlsxFile ="https://raw.githubusercontent.com/cshaptx4869/exceljs-xlsx-template/refs/heads/main/test/assets/template.xlsx";const officialsealFile ="https://raw.githubusercontent.com/cshaptx4869/exceljs-xlsx-template/refs/heads/main/test/assets/officialseal.png";const imageUrl = "https://s2.loli.net/2025/03/07/ELZY594enrJwF7G.png";const data = [{name: "John",items: [{ no: "No.1", name: "JavaScript" },{ no: "No.2", name: "CSS" },{ no: "No.3", name: "HTML" },{ no: "No.4", name: "Node.js" },{ no: "No.5", name: "Three.js" },{ no: "No.6", name: "Vue" },{ no: "No.7", name: "React" },{ no: "No.8", name: "Angular" },{ no: "No.9", name: "UniApp" },],projects: [{ name: "Project 1", description: "Description 1", image: imageUrl },{ name: "Project 2", description: "Description 2", image: imageUrl },{ name: "Project 3", description: "Description 3", image: imageUrl },],},{name: "SGW",user: {first_name: "石国旺",last_name: "旺",},phone: "00874****",invoice_date: "15/05/2008",invoice_number: "54548",items: [{ name: "description", unit_price: 300 },{ name: "HTML", unit_price: 400 },],subtotal: 700,tax: 140,grand_total: 840,},];try {await this.renderXlsxTemplate(xlsxFile, data, `template-${Date.now()}.xlsx`, {parseImage: true,async beforeSave(workbook) {const worksheet = workbook.getWorksheet("新报关单");if (!worksheet) return;const response = await fetch(officialsealFile);if (!response.ok) return;const buffer = await response.arrayBuffer();const imageId = workbook.addImage({ buffer, extension: "png" });const range = placeholderRange(worksheet, "{{#officialseal}}");if (range) {worksheet.addImage(imageId, { tl: { col: range.start.col, row: range.start.row - 4 }, ext: { width: 200, height: 200 } });}},});} catch (error) {console.error("Error processing Excel file:", error);}
}
三、docx-templates生成word模板(已封装)
/**
* 尊敬的用户,你好:页面 JS 面板是高阶用法,一般不建议普通用户使用,如需使用,请确定你具备研发背景,能够自我排查问题。当然,你也可以咨询身边的技术顾问或者联系宜搭平台的技术支持获得服务(可能收费)。
* 我们可以用 JS 面板来开发一些定制度高功能,比如:调用阿里云接口用来做图像识别、上报用户使用数据(如加载完成打点)等等。
* 你可以点击面板上方的 「使用帮助」了解。
*/// 当页面渲染完毕后马上调用下面的函数,这个函数是在当前页面 - 设置 - 生命周期 - 页面加载完成时中被关联的。
export function didMount() {console.log(`「页面 JS」:当前页面地址 ${location.href}`);// console.log(`「页面 JS」:当前页面 id 参数为 ${this.state.urlParams.id}`);// 更多 this 相关 API 请参考:https://www.yuque.com/yida/support/ocmxyv#OCEXd// document.title = window.loginUser.userName + ' | 宜搭';//创建,编辑,读取zip压缩等文件this.utils.loadScript('https://cdn.jsdelivr.net/npm/pizzip@3.2.0/dist/pizzip.min.js');//处理word模板this.utils.loadScript('https://cdnjs.cloudflare.com/ajax/libs/docxtemplater/3.36.0/docxtemplater.min.js');//用于saveAs生成文件this.utils.loadScript('https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js');//可选,用于读取二进制文件this.utils.loadScript('https://cdnjs.cloudflare.com/ajax/libs/jszip-utils/0.1.0/jszip-utils.min.js');
}
//示例
export function generateDoc(){//demoUrl代表导出的word文档模板路径,let demoUrl = ''; //在线的word的url地址//docxData代表模板文档里定义的dept、applyDate 等字段整合给docxData传入即可。let docxData = {name: "张三",address: "北京市海淀区",items: [{ product: "苹果", qty: 3, price: 5 },{ product: "香蕉", qty: 2, price: 4 },{ product: "橘子", qty: 5, price: 3 },],}//fileName代表导出的文件名,方面重命名等操作let fileName = "生成word模板文件.docx"exportWordDocx(demoUrl, docxData, fileName)
}/*** 封装方法* 接收三个入参,demoUrl代表导出的word文档模板路径,docxData代表模板文档里定义的dept、applyDate 等字段整合给docxData传入即可。fileName代表导出的文件名,方面重命名等操作* 将demoUrl传入给JSZipUtils.getBinaryContent方法读取模板文件的二进制内容,之后创建一个PizZip实例,内容为模板的内容,再创建并加载docxtemplater实例对象。使用doc.setData方法设置模板变量的值,对象的键需要和模板上的变量名一致,值就是你要放在模板上的值。这里有一个地方需要注意的是:如果你的定义放在模板上的值为null或者undefined,最后导出来的word文档里,相对应的地方会直接显示undefined。解决方法:doc.setOptions 方法里的nullGetter值返回设置为空即可。最后,通过saveAs方法导出Word文档。*/
export const exportWordDocx = (demoUrl, docxData, fileName) => {// 读取并获得模板文件的二进制内容JSZipUtils.getBinaryContent(demoUrl,function (error, content) {// 抛出异常if (error) {throw error;}// 创建一个PizZip实例,内容为模板的内容let zip = new PizZip(content);// 创建并加载docxtemplater实例对象let doc = new docxtemplater().loadZip(zip);// 去除未定义值所显示的undefineddoc.setOptions({nullGetter: function () {return "";}}); // 设置角度解析器// 设置模板变量的值,对象的键需要和模板上的变量名一致,值就是你要放在模板上的值doc.setData({...docxData,});try {// 用模板变量的值替换所有模板变量doc.render();} catch (error) {// 抛出异常let e = {message: error.message,name: error.name,stack: error.stack,properties: error.properties,};console.log(JSON.stringify({ error: e }));throw error;}// 生成一个代表docxtemplater对象的zip文件(不是一个真实的文件,而是在内存中的表示)let out = doc.getZip().generate({type: "blob",mimeType:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",});// 将目标文件对象保存为目标类型的文件,并命名saveAs(out, fileName);});
}