前言:最近遇到一个需求,需要将页面的html导出为word文档,并且包含横向和竖向页面,并且可以进行混合方向导出。经过一段时间的实验,发现只有docx这个库满足这个要求。在这里记录一下实现思路以及代码。

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: stringname: stringamount1: stringamount2: stringamount3: number
}interface SpanMethodProps {row: Usercolumn: TableColumnCtx<User>rowIndex: numbercolumnIndex: 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', // 样式IDname: '宋体小四号样式', // 可读名称run: {font: '宋体', // 字体size: 24, // 字号},paragraph: {spacing: { line: 360 }, // 1.5倍行距(240*1.5=360)indent: { firstLine: 400 }, // 首行缩进400twips(约2字符)},},{id: 'THStyle', // 样式IDname: '表头样式', // 可读名称run: {font: '等线', // 字体size: 20.5, // 字号},paragraph: {spacing: {before: 240,after: 120,},},},{id: 'TDStyle', // 样式IDname: '单元格样式', // 可读名称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), // 设置第一列宽度为250type: 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 },}),];});}
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/89066.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/89066.shtml
英文地址,请注明出处:http://en.pswp.cn/bicheng/89066.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

虚拟主机CPU占用100导致打不开的一次处理

背景 突然有一天&#xff0c;有个客户网站打不开了&#xff0c;发来这样一张图片问题排查 打开阿里云虚拟主机控制面板&#xff0c;CPU 使用率已经达到了100%&#xff0c;这说明网站已经在高负荷运转。分析访问日志发现&#xff0c;网站出现了大量循环路径&#xff0c;其 UserA…

设计模式之工厂模式:对象创建的智慧之道

工厂模式&#xff1a;对象创建的智慧之道 引言&#xff1a;为什么我们需要工厂模式&#xff1f; 在软件开发中&#xff0c;对象创建是最常见的操作之一。当代码中充满new关键字时&#xff0c;系统会面临三大痛点&#xff1a; 紧耦合&#xff1a;客户端代码直接依赖具体实现类扩…

Docker镜像制作案例

1、使用Docker commit制作镜像为ubuntu镜像提供ssh服务①&#xff1a;拉取镜像[rootopenEuler-1 ~]# docker pull ubuntu:18.04②&#xff1a;启动镜像[rootopenEuler-1 ~]# docker run --name c1 -it --rm ubuntu:18.04 bash③&#xff1a;替换aliyun源mv /etc/apt/sources.li…

KeilMDK5如何生成.bin文件

1&#xff1a;主要是要找到fromelf.exe的路径2&#xff1a;接下来要做的要视情况而定&#xff1a;选完fromelf.exe后在输入框中加个空格然后加一串字 : --bin -o ./Obj/L.bin ./Obj/L.axf&#xff0c;如下我设置的L最终会替换成项目名 3&#xff1a;去构建生成编译一下&#…

Ajax接收java后端传递的json对象包含长整型被截断导致丢失精度的解决方案

问题描述 在使用java编写代码的时候,后端返回前端的JSON对象中包含了Long长整型,前端接受的时候丢失了精度问题。 比如: 后端传递的json {"code": "200","msg": "操作成功","data":

MybatisPlus由浅入深

MyBatis-Plus&#xff08;简称 MP&#xff09;是一个 MyBatis 的增强工具&#xff0c;旨在简化开发过程。基本使用步骤1.依赖引入<!-- mysql依赖 --> <dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId>…

蓝牙信号强度(RSSI)与链路质量(LQI)的测量与应用:面试高频考点与真题解析

在蓝牙通信领域&#xff0c;信号强度&#xff08;RSSI&#xff09;和链路质量&#xff08;LQI&#xff09;是评估无线链路性能的核心指标。无论是智能家居设备的连接优化&#xff0c;还是工业物联网中的抗干扰设计&#xff0c;这两个指标都扮演着关键角色。本文将结合面试高频考…

PyTorch的计算图是什么?为什么绘图前要detach?

在PyTorch中&#xff0c;计算图&#xff08;Computational Graph&#xff09; 是自动求导&#xff08;Autograd&#xff09;的核心机制。理解计算图有助于解释为什么在绘图前需要使用 .detach() 方法分离张量。一、什么是计算图&#xff1f; 计算图是一种有向无环图&#xff08…

深度学习入门代码详细注释-ResNet18分类蚂蚁蜜蜂

本项目将基于PyTorch平台迁移ResNet18模型。该模型原采用ImageNet数据集&#xff08;含1000个图像类别&#xff09;进行训练。我们将尝试运用该模型对蚂蚁和蜜蜂进行分类&#xff08;这两个类别未包含在原训练数据集中&#xff09;。 本文的原始代码参考于博客深度学习入门项目…

北京饮马河科技公司 Java 实习面经

北京饮马河科技公司 Java 实习面经 本文作者&#xff1a;程序员小白条 本站地址&#xff1a;https://xbt.xiaobaitiao.top 1&#xff09; 面试官&#xff1a;我看你这块是有一个开源的项目&#xff0c;这个项目主要是做什么的&#xff1f; 我&#xff1a;主要两点是亮点&…

java基础(day07)

目录 OOP编程 方法 方法的调用&#xff1a; 在main入口函数中调用&#xff1a; 动态参数&#xff1a; 方法重载 OOP编程 方法 概念&#xff1a;指为获得某种东西或达到某种目的而采取的手段与行为方式。有时候被称作“方法”&#xff0c;有时候被称作“函数”。例如UUID.…

使用EasyExcel动态合并单元格(模板方法)

1、导入EasyExcel依赖<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>4.0.3</version> </dependency>2、编写实体类Data publci class Student{ ExcelProperty("姓名")pri…

jenkins 流水线比较简单直观的

//全篇没用自定义变量pipeline {agent any// 使用工具自动配置Node.js环境tools {nodejs nodejs22 // 需在Jenkins全局工具中预配置该名称的Node.js安装}//下面拉取代码通过的是流水线片段生成的stages {stage(Checkout Code) {steps {git branch: release-v1.2.6,credentials…

CV目标检测中的LetterBox操作

LetterBox类比理解&#xff1a;想象你要把一张任意形状的照片放进一个正方形的相框里&#xff0c;照片不能变形拉伸&#xff0c;所以你先等比例缩小照片&#xff0c;然后在空余的地方填上灰色背景。第1章 数学原理当我们有一个原始图像的尺寸为 19201080&#xff08;宽高&#…

Leetcode 3614. Process String with Special Operations II

Leetcode 3614. Process String with Special Operations II 1. 解题思路2. 代码实现 题目链接&#xff1a;3614. Process String with Special Operations II 1. 解题思路 这一题思路上是一个逆推的思路。 首先&#xff0c;我们顺序走一轮不难得到最终我们能够获得的字符串…

.NET ExpandoObject 技术原理解析

&#x1f31f; .NET ExpandoObject 技术原理解析 引用&#xff1a; .NET 剖析4.0上ExpandoObject动态扩展对象原理风潇潇人渺渺快意刀山中草 #mermaid-svg-RtpHctpdchPPN1Xo {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mer…

放苹果(信息学奥赛一本通-T1192)

【题目描述】把M个同样的苹果放在N个同样的盘子里&#xff0c;允许有的盘子空着不放&#xff0c;问共有多少种不同的分法&#xff1f;&#xff08;用K表示&#xff09;5&#xff0c;1&#xff0c;1和1&#xff0c;5&#xff0c;1 是同一种分法。【输入】第一行是测试数据的数目…

(懒人救星版)CNN_Kriging_NSGA2_Topsis(多模型融合典范)深度学习+SCI热点模型+多目标+熵权法 全网首例,完全原创,早用早发SCI

全网首例&#xff0c;完全原创&#xff0c;早用早发SCI&#xff08;多模型融合典范&#xff09;机器学习SCI热点模型多目标熵权法(懒人救星版)BP_Kriging_NSGA2_Topsis 改进克里金工作量大&#xff1a;多模型融合创新性&#xff1a;首次结合BP神经网络和克里金多目标利用 BP神…

LeetCode热题100【第一天】

第一题 两数之和 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案&#xff0c;并且你不能使用两次相同的元素。 你可以按任意顺序返回…

AI Linux 运维笔记

运维基本概念 IT运维是指通过专业技术手段&#xff0c;确保企业的IT系统和网络持续、安全、稳定运行&#xff0c;保障业务的连续性。运维涵盖计算机网络、应用系统、硬件环境和服务流程的综合管理。主要分为: 系统运维、数据库运维、自动化运维、容器运维、云计算运维、信创运维…