当前位置: 首页 > news >正文

【前端倒霉蛋--word导出】

前端导出word,如果时间能重来,我一定交给后端。
这部分的时间我养生养生多好!!!

心得:边查边测试的,想实现的效果与该用什么方法去实现对不上;思路:封装一个方法去获取标签的样式,这里需要注意要和word的渲染格式能对应得上;然后通过对应的标签分别处理;比较恶心的是比如children中写样式,但是居中等却和children平级位置写;富文本内容存在多个标签包裹,有的样式又需要通过标签来标识;以及表格表头有的是在th中,有的却是在td中。

以下代码将就看吧,不会再前端来实现了。
插件使用的是docx

/* eslint-disable */
import {Document,Packer,Paragraph,AlignmentType,VerticalAlign,ShadingType,Table,TextRun,TableCell,TableRow,HeadingLevel,WidthType,BorderStyle,ImageRun
} from 'docx';
import { message } from 'antd';
import { saveAs } from 'file-saver';const getCellStyle = (cell) => {const style = cell.style; // 获取单元格的样式// 获取对齐方式let alignment;if (style.textAlign) {switch (style.textAlign) {case 'left':alignment = AlignmentType.LEFT;break;case 'center':alignment = AlignmentType.CENTER;break;case 'right':alignment = AlignmentType.RIGHT;break;default:alignment = AlignmentType.LEFT; // 默认对齐}} else {alignment = AlignmentType.LEFT; // 默认对齐}// 获取字体大小和颜色const fontSize = style.fontSize ? parseInt(style.fontSize) : 12; // 默认字体大小const color = style.color ? style.color.replace('#', '') : '000000'; // 默认字体颜色// 获取是否粗体和下划线const isBold = style.fontWeight === 'bold' || style.fontWeight === '700';const isUnderline = style.textDecoration === 'underline';return {alignment,fontSize,color,isBold,isUnderline};
};const extractTableRows = (html) => {const parser = new DOMParser();const doc = parser.parseFromString(html, 'text/html');const rows = Array.from(doc.querySelectorAll('tbody tr'));const headers = Array.from(doc.querySelectorAll('thead th')).map(th => {const { alignment, fontSize, color, isBold, isUnderline } = getCellStyle(th); // 获取样式return new TableCell({children: [new Paragraph({text: th.innerText,alignment: alignment,style: {font: {size: fontSize,color: color,bold: isBold,underline: isUnderline},},}),],verticalAlign: VerticalAlign.CENTER,});});// 创建表头行const headerRow = new TableRow({children: headers});let tableRows = []; // 初始化时只加入表头行if (headers && headers.length) {tableRows = [headerRow];}// 提取每一行数据rows.forEach(row => {const cells = Array.from(row.querySelectorAll('td'));// 处理可能存在的空行if (cells.length === 0) return;const tableRow = new TableRow({children: cells.map(cell => {const text = cell.innerText.trim();const { alignment, fontSize, color, isBold, isUnderline } = getCellStyle(cell); // 获取样式return new TableCell({children: [new Paragraph({text: text,alignment: alignment,style: {font: {size: fontSize,color: color,bold: isBold,underline: isUnderline},},}),],verticalAlign: VerticalAlign.CENTER,});}),});// 仅添加数据行tableRows.push(tableRow);});return tableRows;
};const extractContent = async (html) => {const parser = new DOMParser();const doc = parser.parseFromString(html, 'text/html');const imgElement = doc.querySelector('img');const content = [];if (imgElement) {const currentOrigin = window.location.origin;const imageUrl = imgElement.src;const updatedImageUrl = imageUrl.replace(图片url, `${currentOrigin}`);try {const response = await fetch(updatedImageUrl);if (!response.ok) {throw new Error(`HTTP error! Status: ${response.status}`);}const imageBlob = await response.blob();const reader = new FileReader();await new Promise((resolve, reject) => {reader.onloadend = () => {const base64Data = reader.result.split(',')[1]; // 取出 Base64 部分if (base64Data) {// 创建 ImageRun 实例并添加到内容中content.push(new ImageRun({data: base64Data,transformation: { width: imgElement?.width || 600, height: imgElement?.height || 200 },}));} else {console.error('Base64 data is empty.');}resolve();};reader.onerror = (error) => {console.error('Error reading image file:', error);reject(error);};reader.readAsDataURL(imageBlob);});} catch (error) {console.error('Error fetching image:', error);}}return content;
};function rgbToHex(rgb) {const rgbValues = rgb.match(/\d+/g);if (!rgbValues) return null;return `#${((1 << 24) + (parseInt(rgbValues[0]) << 16) + (parseInt(rgbValues[1]) << 8) + parseInt(rgbValues[2])).toString(16).slice(1)}`;
}function extractStylesFromHtml(html) {const tempDiv = document.createElement('div');tempDiv.appendChild(html);document.body.appendChild(tempDiv);tempDiv.style.display = 'none';let results = {color: null,backgroundColor: null,fontSize: null,bold: false,underline: false,italic: false,listType: null, // 用于识别列表类型shading: {type: "ShadingType.CLEAR",fill: "#FFFFFF",color: "#FFFFFF"},textAlign: null,indentation: 0,// 默认缩进hasIndent: false// 是否有缩进};function recursiveExtract(element) {if (element.nodeType === Node.TEXT_NODE) {const textContent = element.textContent.trim();if (textContent) {}} else if (element.nodeType === Node.ELEMENT_NODE) {const computedStyles = window.getComputedStyle(element);const backgroundColor = rgbToHex(computedStyles.backgroundColor);// 如果计算得出的背景颜色是 #000000,并且父元素的背景颜色也不是黑色,则认为是无效的背景色const isDefaultBlack = (backgroundColor === '#000000' && rgbToHex(computedStyles.color) !== '#ffffff') ? '#ffffff' : backgroundColor;const currentStyle = {color: rgbToHex(computedStyles.color) || undefined,backgroundColor: isDefaultBlack,size: parseInt(parseInt(computedStyles.fontSize) / 1.33) + "pt",shading: {type: ShadingType.CLEAR,fill: isDefaultBlack,color: 'FFFFFF',},textAlign: computedStyles.textAlign,};results = { ...results, ...currentStyle };if (computedStyles.fontWeight === 'bold' || parseInt(computedStyles.fontWeight) > 400 || element.nodeName === 'STRONG') {results.bold = true;}if (computedStyles.fontStyle === 'italic' || element.nodeName === 'EM') {results.italic = true;}if (computedStyles.textDecoration.includes('underline')) {results.underline = true;}if (computedStyles.textIndent) {const emValue = parseFloat(computedStyles.textIndent);results.indentation = emValue * 16; // 将 em 转换为像素results.hasIndent = true; // 有缩进}// 处理有序和无序列表if (element.nodeName === 'UL') {results.listType = 'unordered'; // 标记为无序列表for (const child of element.children) {if (child.nodeName === 'LI') {// 对每个列表项进行递归提取for (const liChild of child.childNodes) {recursiveExtract(liChild);}}}} else if (element.nodeName === 'OL') {results.listType = 'ordered'; // 标记为有序列表for (const child of element.children) {if (child.nodeName === 'LI') {// 对每个列表项进行递归提取for (const liChild of child.childNodes) {recursiveExtract(liChild);}}}} else {for (const child of element.childNodes) {recursiveExtract(child);}}}}recursiveExtract(tempDiv);document.body.removeChild(tempDiv);return results;
}const parseHtmlToParagraphs = (html, type) => {const doc = new DOMParser().parseFromString(html, 'text/html');const paragraphs = Array.from(doc.querySelectorAll(type));return paragraphs.map((p) => {const htmlStyles = extractStylesFromHtml(p);// 设置缩进属性const indent = htmlStyles?.hasIndent ? { left: htmlStyles?.indentation, right: 0 } : { left: 0, right: 0 };const textRuns = [];// 处理每个子节点以构建文本运行Array.from(p.childNodes).forEach((child, inx) => {if (child.nodeType === Node.TEXT_NODE) {textRuns.push(new TextRun(child.nodeValue));} else if (child.nodeType === Node.ELEMENT_NODE) {// 获取文本内容const textContent = child.textContent;// 创建 TextRun 对象并设置样式const textRun = new TextRun({text: htmlStyles?.listType ? htmlStyles?.listType == "unordered" ? `\u2022${textContent}` : `${inx + 1}.${textContent}` : textContent,...htmlStyles});textRuns.push(textRun);if (htmlStyles?.listType) {textRuns.push(new TextRun({ text: '', break: true }));}}});// 根据 HTML 样式设置段落对齐方式let alignment;if (htmlStyles?.textAlign) {switch (htmlStyles.textAlign) {case 'center':alignment = AlignmentType.CENTER;break;case 'right':alignment = AlignmentType.RIGHT;break;case 'justify':alignment = AlignmentType.JUSTIFIED;break;default:alignment = AlignmentType.LEFT; // 默认左对齐}} else {alignment = AlignmentType.LEFT; // 如果没有指定,默认为左对齐}// 创建包含所有文本运行的段落return new Paragraph({children: textRuns,alignment: alignment,indent: indent});});
};// 解析 HTML 中的 blockquote
const parseHtmlBlockQuote = (html) => {const doc = new DOMParser().parseFromString(html, 'text/html');return Array.from(doc.querySelectorAll('blockquote')).map((blockquote) => {const textRuns = Array.from(blockquote.childNodes).map((node) => {return new TextRun({text: node.nodeType === Node.TEXT_NODE ? node.textContent : node.innerText,color: "666666", // 字体颜色italics: true, // 设置斜体// break: true, // 换行});});return new Paragraph({children: textRuns,alignment: AlignmentType.LEFT, // 左对齐border: {left: {style: BorderStyle.SINGLE,size: 30,space: 0,color: "CCCCCC",},},shading: {fill: "F1F2F3", // 背景颜色color: "FFFFFF", // 可选type: ShadingType.CLEAR}});});
};const parseHtmlToTable = (html) => {const doc = new DOMParser().parseFromString(html, 'text/html');const headers = Array.from(doc.querySelectorAll('thead th')).map(th =>new TableCell({ children: [new Paragraph(th.innerText)] }));const rows = Array.from(doc.querySelectorAll('tbody tr')).map(row =>new TableRow({children: Array.from(row.querySelectorAll('td')).map(cell =>new TableCell({ children: [new Paragraph(cell.innerText)] })),}));return new Table({rows: [new TableRow({ children: headers })].concat(rows),});
};const parseHtmlToCodeBlock = (html) => {const doc = new DOMParser().parseFromString(html, 'text/html');return Array.from(doc.querySelectorAll('pre')).map((pre) => {const codeContent = pre.innerText || pre.textContent;// 创建段落并包含超链接return new Paragraph({children: [new TextRun({text: codeContent,hyperlink: codeContent,color: "0000FF",underline: true})],spacing: { before: 240, after: 240 },});});
};const parseHtmlToDocumentElements = (html) => {const doc = new DOMParser().parseFromString(html, 'text/html');const elements = [];doc.body.childNodes.forEach(node => {if (node.nodeType === Node.ELEMENT_NODE) {switch (node.tagName) {case 'P':elements.push(...parseHtmlToParagraphs(node.outerHTML, 'p'));break;case 'DIV':elements.push(...parseHtmlToParagraphs(node.outerHTML, 'div'));break;case 'TABLE':elements.push(parseHtmlToTable(node.outerHTML));break;case 'H2':elements.push(new Paragraph({text: node.textContent,heading: 'HEADING_2',}));break;case 'OL':elements.push(...parseHtmlToParagraphs(node.outerHTML, 'ol'));break;case 'UL':elements.push(...parseHtmlToParagraphs(node.outerHTML, 'ul'));break;case 'PRE':elements.push(...parseHtmlToCodeBlock(node.outerHTML));break;case 'BLOCKQUOTE':elements.push(...parseHtmlBlockQuote(node.outerHTML));break;case 'STRONG':case 'B':elements.push(new Paragraph({text: node.textContent,bold: true,}));break;case 'EM':case 'I':elements.push(new Paragraph({text: node.textContent,italics: true,}));break;case 'SPAN':elements.push(new Paragraph({text: node.textContent}));break;default:// 处理未指定的标签,可以选择忽略或者将内容添加为文本段落elements.push(new Paragraph({text: node.textContent,}));break;}}});return elements;
};export const createWordDocument = async (reportLayoutInfoParamList, reportName = '分析报告') => {message.loading('报告文件下载中...', 1);const wordData = [];const sortedData = reportLayoutInfoParamList.sort((a, b) => a.y - b.y);for (const item of sortedData) {try {const html = item.content?.originalEditorHtml || '';if (!html) {console.warn('无效内容: 文件为空');continue; // 跳过无效内容}// 检查是否包含 chart-img 来判断是添加图片还是表格const imgElement = new DOMParser().parseFromString(html, 'text/html').querySelector('img[alt="chart-img"]');if (imgElement || item?.contentType === 'media') {// 提取图片const imageRuns = await extractContent(html);imageRuns.forEach(run => {wordData.push(new Paragraph({children: [run]}),new Paragraph({ text: "" }));});} else if (item?.contentType === 'text') {// 解析 HTML 并获取文档元素const documentElements = parseHtmlToDocumentElements(html);if (documentElements.length === 0) {console.warn('未提取到文本内容');continue; // 跳过未提取到内容的情况}documentElements.forEach(element => {wordData.push(element);});} else {// 提取表格行const tableRows = extractTableRows(html);if (tableRows.length > 0) {wordData.push(new Paragraph({text: "",heading: HeadingLevel.HEADING_1,}),new Table({width: {size: 100,type: WidthType.PERCENTAGE,},rows: tableRows,}),new Paragraph({ text: "" }));} else {console.warn('未提取到表格内容');}}} catch (error) {console.error('处理条目时发生错误:', error);}}if (wordData.length === 0) {message.warning('未生成任何文档内容,无法导出 Word 文件');return;}try {const doc = new Document({sections: [{properties: {},children: wordData,}],});// return;const blob = await Packer.toBlob(doc);saveAs(blob, `${reportName}.docx`);} catch (error) {message.error('导出 Word 文件时发生错误:' + error);}
};

http://www.mrgr.cn/news/58692.html

相关文章:

  • localStorage是什么 做什么用的
  • 为微信小程序换皮肤之配置vant
  • 内容安全与系统构建加速,助力解决生成式AI时代的双重挑战
  • 擎创科技声明
  • ffmpeg视频滤镜: 色温- colortemperature
  • 系统思考—关键指标
  • 社交改运很简单:谋定而后动,三种人群的智慧策略,生成无敌贵人圈
  • 出差日记,记录人生百态
  • 01_Linux基础操作CentOS7学习笔记
  • k8s 1.28.2 集群部署 NFS server 和 NFS Subdir External Provisioner
  • 如何在 .NET中使用Flurl高效处理Http请求
  • 2-133 基于matlab的粒子群算法PSO优化BP神经网络
  • hackme靶机渗透流程
  • 基础巩固:
  • ML2021Spring-hw1(COVID-19 Cases Prediction)
  • MacOS 使用ssh2-python报错ImportError: dlopen ... Library not loaded
  • 视频AI系统工具:强大的图像识别和分析工具Google Cloud Vision API介绍
  • java高性能处理10G大文件
  • 7、哈希表
  • C#从零开始学习(用户界面)(unity Lab4)
  • 软考:缓存击穿和缓存穿透
  • Vue 自定义指令 Directive 的高级使用与最佳实践
  • 线程池——Java
  • Redis和MySQL如何保证数据一致性
  • 洛谷 P1130 红牌
  • 鸿蒙UI系统组件17——富文本展示(RichText)