前端docx库实现将html页面导出word的详细过程
作者:进阶的小木桩
将HTML转为Word可以通过多种方法实现,包括使用在线工具、编程库以及Office软件等,下面这篇文章主要介绍了前端docx库实现将html页面导出word的详细过程,文中通过代码介绍的非常详细,需要的朋友可以参考下
前言:
最近遇到一个需求,需要将页面的html导出为word文档,并且包含横向和竖向页面,并且可以进行混合方向导出。经过一段时间的实验,发现只有docx这个库满足这个要求。在这里记录一下实现思路以及代码。
一、效果展示
页面内容:
导出样式:
二、解决思路
1、首先是需要在页面上设置哪些部分是需要横向导出,哪些部分是需要竖向导出的。以方便后面进行解析。
2、根据页面样式以及各类html标签进行解析。然后以docx的形式生成,最后导出来。
三、实现代码
1、index.vue
这里 class 中的 section 代表了docx中的一节,也就是一个页面。同时newpage属性控制了是不是要换一个新页,orient属性是页面横向纵向的标识(Z纵向H横向)。也可以根据自己的需求自行添加属性,在后面自己进行对应的解析。
<template> <div> <el-row> <el-col :span="24"> <div> <el-button type="primary" @click="exportToWord" style="float: right">导出</el-button> </div> <div style="overflow-y: auto; height: calc(85vh)" id="export"> <div class="section" orient="Z"> <h1 style="text-align: center">这里是标题1</h1> </div> <div class="section" orient="Z" newpage="true"> <h2 style="text-align: center">这里是标题2</h2> <h3 style="text-align: center">这里是标题3</h3> </div> <div class="section" orient="Z"> <p>这里是一段文字内容</p> </div> <div class="section" orient="Z"> <el-table :data="tableData" :span-method="arraySpanMethod" border style="width: 100%"> <el-table-column prop="id" label="ID" width="180" header-align="center" align="left"/> <el-table-column prop="name" label="姓名" width="" header-align="center" align="left"/> <el-table-column prop="amount1" label="列 1" width="" header-align="center" align="center"/> <el-table-column prop="amount2" label="列 2" width="" header-align="center" align="right"/> <el-table-column prop="amount3" label="列 3" width="" header-align="center" align="left"/> </el-table> </div> <div class="section" orient="H"> <p>这里是横向页面内容</p> </div> <div class="section" orient="Z"> <p>这里是纵向页面内容</p> </div> </div> </el-col> </el-row> </div> </template> <script lang="ts" setup="" name=""> //导出用 import * as htmlDocx from 'html-docx-js-typescript'; import { saveAs } from 'file-saver'; import { exportDocxFromHTML } from '@/utils/exportWord'; //导出word const exportToWord = async () => { let contentElement = document.getElementById('export') as HTMLElement; // 克隆元素 操作新元素 let newDiv = contentElement.cloneNode(true) as HTMLElement; // 这里可以对newDiv进行一些操作... exportDocxFromHTML(newDiv, `test.docx`); }; import type { TableColumnCtx } from 'element-plus' interface User { id: string name: string amount1: string amount2: string amount3: number } interface SpanMethodProps { row: User column: TableColumnCtx<User> rowIndex: number columnIndex: number } const tableData:User[] = [ { id: '12987122', name: 'Tom', amount1: '234', amount2: '3.2', amount3: 10, }, { id: '12987123', name: 'Tom', amount1: '165', amount2: '4.43', amount3: 12, }, { id: '12987124', name: 'Tom', amount1: '324', amount2: '1.9', amount3: 9, }, { id: '12987125', name: 'Tom', amount1: '621', amount2: '2.2', amount3: 17, }, { id: '12987126', name: 'Tom', amount1: '539', amount2: '4.1', amount3: 15, }, ]; const arraySpanMethod = ({ row, column, rowIndex, columnIndex, }: SpanMethodProps) => { if (rowIndex % 2 === 0) { if (columnIndex === 0) { return [1, 2] } else if (columnIndex === 1) { return [0, 0] } } } onMounted(async () => {}); </script> <style lang="scss" scoped></style>
2、exportWord.ts
这个部分是进行了html转换成docx形式的拼接组合。可以根据理解自行调整样式以及解析过程。
import { Document, Packer, Paragraph, TextRun, ImageRun, ExternalHyperlink, WidthType, VerticalAlign, AlignmentType, PageOrientation, HeadingLevel, Table, TableRow, TableCell, BorderStyle, } from 'docx'; import { saveAs } from 'file-saver'; /** * 字符串是否为空 * @param {*} obj * @returns */ export function isEmpty(obj:any) { if (typeof obj == 'undefined' || obj == null || obj === '') { return true } else { return false } }; import { ElMessageBox, ElMessage } from 'element-plus'; // 定义类型 type DocxElement = Paragraph | Table | TextRun | ImageRun | ExternalHyperlink; //保存图片,表格,列表 type ExportOptions = { includeImages: boolean; includeTables: boolean; includeLists: boolean; }; const includeImages = ref(true); const includeTables = ref(true); const includeLists = ref(true); //保存样式对象 type StyleOptions = { bold: boolean; //是否加粗 font: Object; //字体样式 size: number; //字体大小 id: String | null; //样式id }; //横向A4 export const H_properties_A4 = { page: { size: { width: 15840, // A4 横向宽度 (11英寸) height: 12240, // A4 横向高度 (8.5英寸) }, }, }; //纵向A4 export const Z_properties_A4 = { page: { size: { width: 12240, // A4 纵向宽度 (8.5英寸 * 1440 twip/inch) height: 15840, // A4 纵向高度 (11英寸 * 1440) }, orientation: PageOrientation.LANDSCAPE, }, }; //根据html生成word文档 export const exportDocxFromHTML = async (htmlDom: any, filename: any) => { let sections = [] as any; //页面数据 let doms = htmlDom.querySelectorAll('.section'); try { const options: ExportOptions = { includeImages: includeImages.value, includeTables: includeTables.value, includeLists: includeLists.value, }; let preorient = 'Z'; for (let i = 0; i < doms.length; i++) { let dom = doms[i]; let orient = dom.getAttribute('orient'); let newpage = dom.getAttribute('newpage'); if (orient == preorient && newpage != 'true' && sections.length > 0) { //方向一致且不分页,继续从上一个section节添加 // 获取子节点 let childNodes = dom.childNodes; // 递归处理所有节点 let children = []; for (let i = 0; i < childNodes.length; i++) { const node = childNodes[i]; const result = await parseNode(node, options, null); children.push(...result); } if (sections[sections.length - 1].children && children.length > 0) { for (let c = 0; c < children.length; c++) { let one = children[c]; sections[sections.length - 1].children.push(one); } } } else { //否则则新开一个section节 // 获取子节点 let childNodes = dom.childNodes; // 递归处理所有节点 let children = []; for (let i = 0; i < childNodes.length; i++) { const node = childNodes[i]; const result = await parseNode(node, options, null); children.push(...result); } let section = { properties: orient == 'H' ? H_properties_A4 : Z_properties_A4, children: children, }; sections.push(section); preorient = orient; } } if (sections.length > 0) { // 创建Word文档 const doc = new Document({ styles: { default: { heading1: { //宋体 二号 run: { size: 44, bold: true, italics: true, color: '000000', font: '宋体', }, paragraph: { spacing: { after: 120, }, }, }, heading2: { //宋体 小二 run: { size: 36, bold: true, color: '000000', font: '宋体', }, paragraph: { spacing: { before: 240, after: 120, }, }, }, heading3: { //宋体 四号 run: { size: 28, bold: true, color: '000000', font: '宋体', }, paragraph: { spacing: { before: 240, after: 120, }, }, }, heading4: { //宋体 run: { size: 24, bold: true, color: '000000', font: '宋体', }, paragraph: { spacing: { before: 240, after: 120, }, }, }, heading5: { run: { size: 20, bold: true, color: '000000', font: '宋体', }, paragraph: { spacing: { before: 240, after: 120, }, }, }, }, paragraphStyles: [ { id: 'STx4Style', // 样式ID name: '宋体小四号样式', // 可读名称 run: { font: '宋体', // 字体 size: 24, // 字号 }, paragraph: { spacing: { line: 360 }, // 1.5倍行距(240*1.5=360) indent: { firstLine: 400 }, // 首行缩进400twips(约2字符) }, }, { id: 'THStyle', // 样式ID name: '表头样式', // 可读名称 run: { font: '等线', // 字体 size: 20.5, // 字号 }, paragraph: { spacing: { before: 240, after: 120, }, }, }, { id: 'TDStyle', // 样式ID name: '单元格样式', // 可读名称 run: { font: '等线', // 字体 size: 20.5, // 字号 }, // paragraph: { // spacing: { // before: 240, // after: 120, // }, // }, }, ], }, sections: sections, //.filter(Boolean) as (Paragraph | Table)[], }); // 生成并下载文档 await Packer.toBlob(doc).then((blob) => { saveAs(blob, filename); }); } else { ElMessage.error('导出失败,该页面没有要导出的信息!'); } } catch (error) { console.error('导出失败:', error); ElMessage.error('导出失败,请联系管理人员!'); //查看控制台获取详细信息!'); } finally { } }; // 递归转换 DOM 节点为 docx 元素 export const parseNode = async (node: Node, options: ExportOptions, style: any): Promise<DocxElement[]> => { const elements: DocxElement[] = []; // 1、处理文本节点 if (node.nodeType === Node.TEXT_NODE) { const text = node.textContent?.trim(); if (!isEmpty(text)) { const parent = node.parentElement; if (style == null) { let child = new TextRun({ text: text, }); elements.push(child); } else { const isBold = style.bold ? true : parent?.tagName === 'STRONG' || parent?.tagName === 'B'; // const isItalic = parent?.tagName === 'EM' || parent?.tagName === 'I'; // const isUnderline = parent?.tagName === 'U'; const Font = style.font ? style.font : '宋体'; const Size = style.size ? style.size : 24; if (!isEmpty(style.id)) { let child = new TextRun({ text: text, style: style.id, }); elements.push(child); } else { let child = new TextRun({ text: text, bold: isBold, font: Font, size: Size, }); elements.push(child); } } } return elements; } // 2、处理元素节点 if (node.nodeType === Node.ELEMENT_NODE) { const element = node as HTMLElement; const tagName = element.tagName.toUpperCase(); const childNodes = element.childNodes; // 递归处理子节点 let childElements: DocxElement[] = []; for (let i = 0; i < childNodes.length; i++) { const child = childNodes[i]; if (tagName == 'A') { if (style == null) { style = { id: 'Hyperlink', }; } else { style.id = 'Hyperlink'; } } const childResult = await parseNode(child, options, style); childElements = childElements.concat(childResult); } // 根据标签类型创建不同的docx元素 switch (tagName) { case 'H1': return [ new Paragraph({ heading: HeadingLevel.HEADING_1, alignment: AlignmentType.CENTER, children: childElements.filter((e) => e instanceof TextRun) as TextRun[], }), ]; case 'H2': return [ new Paragraph({ heading: HeadingLevel.HEADING_2, alignment: AlignmentType.CENTER, children: childElements.filter((e) => e instanceof TextRun) as TextRun[], }), ]; case 'H3': return [ new Paragraph({ heading: HeadingLevel.HEADING_3, alignment: AlignmentType.LEFT, children: childElements.filter((e) => e instanceof TextRun) as TextRun[], }), ]; case 'H4': return [ new Paragraph({ heading: HeadingLevel.HEADING_4, alignment: AlignmentType.LEFT, children: childElements.filter((e) => e instanceof TextRun) as TextRun[], }), ]; case 'H5': return [ new Paragraph({ heading: HeadingLevel.HEADING_5, alignment: AlignmentType.LEFT, children: childElements.filter((e) => e instanceof TextRun) as TextRun[], }), ]; case 'P': return [ new Paragraph({ children: childElements.filter((e) => e instanceof TextRun) as TextRun[], style: 'STx4Style', // 应用样式ID }), ]; case 'BR': return [new TextRun({ text: '', break: 1 })]; case 'A': const href = element.getAttribute('href'); if (href) { return [ new Paragraph({ children: [ new ExternalHyperlink({ children: childElements.filter((e) => e instanceof TextRun) as TextRun[], link: href, }), ], }), ]; } else { return childElements.filter((e) => e instanceof TextRun) as TextRun[]; } case 'TABLE': return getTable(element, options); // case 'IMG': // if (!options.includeImages) { // return []; // } else { // const src = element.getAttribute('src'); // if (src) { // try { // const response = await fetch(src); // const arrayBuffer = await response.arrayBuffer(); // // return [ // // new ImageRun({ // // data: arrayBuffer, // // transformation: { // // width: 400, // // height: 300, // // }, // // }), // // ]; // return []; // } catch (e) { // console.error('图片加载失败:', e); // return [ // new TextRun({ // text: '[图片加载失败]', // color: 'FF0000', // }), // ]; // } // } else { // return []; // } // } // case 'I': // return childElements.map((e) => { // if (e instanceof TextRun) { // return new TextRun({ // ...e.options, // italics: true, // }); // } // return e; // }); // case 'U': // return childElements.map((e) => { // if (e instanceof TextRun) { // return new TextRun({ // ...e.options, // underline: {}, // }); // } // return e; // }); default: return childElements; } } return elements; }; //获取一个表格 export const getTable = async (element: any, options: ExportOptions) => { if (!options.includeTables) { return []; } else { const rows = Array.from(element.rows); const tableRows = rows.map((row: any) => { const cells = Array.from(row.cells); const tableCells = cells.map(async (cell: any, index: any) => { let textAlign = cell.style.textAlign; //居中/居左 let width = (cell.style.width + '').replace('%', ''); //宽度 let classlist = Array.from(cell.classList); if (classlist && classlist.length > 0) { if (classlist.indexOf('is-left') > -1) { textAlign = 'left'; } else if (classlist.indexOf('is-center') > -1) { textAlign = 'center'; } else if (classlist.indexOf('is-right') > -1) { textAlign = 'right'; } } const cellChildren = []; for (let i = 0; i < cell.childNodes.length; i++) { let childNode = cell.childNodes[i]; if (cell.tagName == 'TH') { const styleoption: StyleOptions = { bold: true, font: '等线', size: 21, id: null, }; const result = await parseNode(childNode, options, styleoption); cellChildren.push( new Paragraph({ alignment: textAlign == 'center' ? AlignmentType.CENTER : textAlign == 'right' ? AlignmentType.RIGHT : AlignmentType.LEFT, // 水平居中/居右/居左 children: result, style: 'THStyle', }) ); } else { const styleoption: StyleOptions = { bold: false, font: '等线', size: 21, id: null, }; const result = await parseNode(childNode, options, styleoption); cellChildren.push( new Paragraph({ alignment: textAlign == 'center' ? AlignmentType.CENTER : textAlign == 'right' ? AlignmentType.RIGHT : AlignmentType.LEFT, // 水平居中/居右/居左 children: result, style: 'TDStyle', }) ); } } // 动态判断是否合并 //const isMergedStart = cell.rowSpan > 1 || cell.colSpan > 1; return new TableCell({ rowSpan: cell.rowSpan, columnSpan: cell.colSpan, verticalAlign: VerticalAlign.CENTER, verticalMerge: cell.rowSpan > 1 ? 'restart' : undefined, width: { size: parseFloat(width), // 设置第一列宽度为250 type: WidthType.PERCENTAGE, //WidthType.DXA, // 单位为twip (1/20 of a point) }, children: cellChildren.filter((e) => e instanceof Paragraph) as Paragraph[], }); // return new TableCell({ // children: cellChildren.filter((e) => e instanceof Paragraph) as Paragraph[], // }); }); return Promise.all(tableCells).then((cells) => { return new TableRow({ children: cells, }); }); }); return Promise.all(tableRows).then((rows) => { return [ new Table({ rows: rows, width: { size: 100, type: WidthType.PERCENTAGE }, }), ]; }); } };
总结
到此这篇关于前端docx库实现将html页面导出word的文章就介绍到这了,更多相关docx库将html页面导出word内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!