当前位置:   article > 正文

wangEditor 5 富文本内容(HTML)导入 Excel 富文本格式_html富文本

html富文本

最近接触到一个有点意思的开发需求, 需要在前端富文本编写后, 能在后端导出原样的 Excel 文本, 当然这个 Excel 包含富文本和图片. 前端减少造轮子的过程用了 wangEditor5, 后端里导出 Excel 用了 exceljs

流程

  1. 设置一下 wangEditor5 的菜单, 只设置需要的格式, 保存 HTML 格式文件到后端. 当然, 如果不需要别的地方显示, 也可以保存 json 格式.
  2. 后端调用保存的 HTML 格式数据, 对它进行转化, 转化成 exceljs 支持的富本本
  3. 转化后的数据写入 excel 表格

一、wangEditor5 的菜单

这里前端 wangEditor5 就不在这里写了, 不是重点, 直接给出我的 toolbarKeys

 

js

复制代码

toolbarKeys: [ // 一些常用的菜单 key "blockquote", "bold", // 加粗 "italic", // 斜体 "through", // 删除线 "underline", // 下划线 "bulletedList", // 无序列表 "numberedList", // 有序列表 "color", // 文字颜色 "fontSize", // 字体大小 "uploadImage", // 上传图片 "deleteImage", //删除图片 "divider", // 分割线 "code", // 行内代码 "codeBlock", // 代码块 "undo", // 撤销 "redo", // 重做 ],

二、后端调用保存的 HTML 格式数据, 对它进行转化, 转化成 exceljs 支持的富本本

这个是重点了, exceljs 是支持输出富文本到 Excel 文档的, 但是格式要按它的来, 所以要做格式的转换才行. 详情可以看看官方文档 github.com/exceljs/exc…

Enum: Excel.ValueType.RichText

样式丰富的文本。

例如:

 

js

复制代码

worksheet.getCell('A1').value = { richText: [ { text: 'This is '}, {font: {italic: true}, text: 'italic'}, ] };

但是官方文档也没有说明要怎么转换, 后面我实验所得, 可以去看格式, 字休那里, 按它的格式来设置参数. 当然还有对齐方式等等, 我没有去搞.

下面是我写的一个转换的类, 各位可以参考一下, 去修改成自己相应的格式, 这里面并不全, 思路在这里了, 另外, 我这里转换的格式, 是把图片单独抽到一个单元格去计算宽高, 把文字分隔开来. 各位也可以按实际需求去调整.

思路是先解释出 DOM, 然后转成 JSON, 在把 JSON 转成 exceljs 的富文本格式.

 

js

复制代码

import { JSDOM } from 'jsdom'; import { DOMWindow } from 'jsdom'; import * as ExcelJS from 'exceljs'; export default class HtmlToExcelJS { // 定义一个 Node 类型 private Node: DOMWindow['Node']; /** * HTML 转 ExcelJS 富文本格式数组 * @param html HTML 字符串 * @returns ExcelJS 富文本格式数组 */ public toExcelJS(html): ExcelJS.RichText[] { const dom = new JSDOM(html); this.Node = dom.window.Node; const json = this.htmlToJson(dom.window.document.body); const richTexts = this.jsonToRichText(json); // richTexts 数组里, 只要 img, 分割出数组, 前面一组, 图片一组, 后面一样 const richTextsArr = []; let richTextsTemp = []; for (let i = 0; i < richTexts.length; i++) { if (richTexts[i].img) { richTextsArr.push(richTextsTemp); richTextsTemp = []; richTextsArr.push([richTexts[i]]); } else { richTextsTemp.push(richTexts[i]); if (i === richTexts.length - 1) { richTextsArr.push(richTextsTemp); } } } // richTextsArr 数组, 删除 [ { font: {}, text: '\n' } ] 元素 for (let i = 0; i < richTextsArr.length; i++) { // 最后一个元素为换行符, 删除 if (richTextsArr[i].length === 1 && richTextsArr[i][0].text === '\n') { richTextsArr.splice(i, 1); } } return richTextsArr; } /** * 将 RGB 颜色值转换为 ARGB 颜色值 * @param rgb rgb * @returns ARGB */ private rgbToArgb(rgb) { // 移除 "rgb(" 和 ")",然后将结果分割为一个数组 const parts = rgb.replace(/rgba?\(/, '').replace(/\)/, '').split(','); // 将 RGB 颜色值转换为十六进制 const r = parseInt(parts[0]).toString(16).padStart(2, '0'); const g = parseInt(parts[1]).toString(16).padStart(2, '0'); const b = parseInt(parts[2]).toString(16).padStart(2, '0'); // 返回 ARGB 颜色值 return 'ff' + r + g + b; } /** * HTML 转 JSON * @param element * @returns JSON */ private htmlToJson(element) { const json = { tagName: element.tagName, font: {}, img: {}, children: [], }; for (let i = 0; i < element.attributes.length; i++) { const attr = element.attributes[i]; if (attr.name === 'style' && attr.value.startsWith('font-size')) { // 字体大小 json.font['size'] = attr.value.match(/\d+/)[0]; } else if (attr.name === 'style' && attr.value.startsWith('color')) { // 字体颜色 const rgp = attr.value.replace('color: ', ''); json.font['color'] = { argb: this.rgbToArgb(rgp) }; } else { json.font[attr.name] = attr.value; } // 代码块处理 if (json.tagName === 'CODE' || json.tagName === 'BLOCKQUOTE') { json.font['color'] = { argb: '6A9B59' }; // 绿色 #6A9B59 json.font['code'] = true; } } for (let i = 0; i < element.childNodes.length; i++) { const childNode = element.childNodes[i]; if (childNode.nodeType === this.Node.ELEMENT_NODE) { json.children.push(this.htmlToJson(childNode)); } else if (childNode.nodeType === this.Node.TEXT_NODE) { json.children.push({ text: childNode.textContent }); } } return json; } /** * 处理 JSON 数据 * @param json */ private handleJson(json) { // HR 水平线, 添加分割线 if (json?.tagName === 'HR') { json.children.push({ text: '----------------------------------------' }); } // markdown BLOCKQUOTE 前面添加 > if (json?.tagName === 'BLOCKQUOTE') { json.children.unshift({ text: '> ' }); } // 如果是块级元素,添加换行符, 注意:如果输出到单个单元格,换行符需要添加 if (['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HR', 'CODE', 'BLOCKQUOTE', 'LI'].includes(json.tagName)) { json.children.push({ text: '\n' }); } // 如果是 CODE 标签,添加代码块标记, 和语言标记 if (json?.tagName === 'CODE') { const lang = json?.font?.class?.split('-')[1]; json.children.unshift({ text: '```' + lang + '\n'}); json.children.push({ text: '```' + '\n' }); } // 处理子元素 json?.children?.forEach((child) => { // 如果是列表元素 UL,子类 LI 添加 • 号 if (json?.tagName === 'UL' && child?.tagName === 'LI') { child?.children?.unshift({ text: '• ' }); } // 如果是列表元素 OL,子类 LI 添加序号 if (json?.tagName === 'OL' && child?.tagName === 'LI') { child?.children?.unshift({ text: `${json?.children?.indexOf(child) + 1}. ` }); } this.handleJson(child); }) return json; } /** * 将 JSON 转换为 ExcelJS 富文本 * @param json * @returns ExcelJS 富文本 */ private jsonToRichText(json) { json = this.handleJson(json) const richText = []; // 标签对应的字体样式 const tagToFont = { 'STRONG': { bold: true }, // 字体 粗细 'EM': { italic: true }, // 字体 斜体 'U': { underline: true }, // 字体 下划线 'S': { strike: true }, // 字体 删除线 'BLOCKQUOTE': { size: 14, color: { argb: '6A9B59' } }, // markdown blockquote 'H1': { size: 28 }, // h1 标签 字体大小 'H2': { size: 24 }, // h2 标签 字体大小 'H3': { size: 20 }, // h3 标签 字体大小 'H4': { size: 16 }, // h4 标签 字体大小 'H5': { size: 14 }, // h5 标签 字体大小 'H6': { size: 12 }, // h6 标签 字体大小 }; // 字体属性, 用于提取 font 中的属性, 可以自定义添加去做特别处理, 如 ‘code’ 代码块 const fontProperties = ['size', 'color', 'bold', 'italic', 'underline', 'strike', 'outline', 'name', 'family', 'scheme', 'vertAlign', 'charset', 'code']; function traverse(node, parentFont, parentTagName) { let currentFont = parentFont ? { ...parentFont } : {}; // 如果当前节点有字体样式,将其添加到 currentFont 中 if (node.font) { // 从 node.font 中提取 fontProperties 中的属性 const newFontProperties = fontProperties.reduce((acc, prop) => { if (prop in node.font) { acc[prop] = node.font[prop]; } return acc; }, {}); // 将新的属性合并到 currentFont 中 Object.assign(currentFont, newFontProperties); // 如果父标签有字体样式,继承父标签的字体样式 if (tagToFont[parentTagName]) { Object.assign(currentFont, tagToFont[parentTagName]); } } // 如果当前节点是文本节点,将其添加到 richText 数组中 if (node.children) { for (let child of node.children) { if (child.text) { const textObj = { font: currentFont, text: child.text }; richText.push(textObj); } if (child.children) { traverse(child, currentFont, child.tagName); } } } // 图片处理 if (node.tagName === 'IMG') { const width = node?.font?.style?.match(/width: (\d+)/)?.[1]; const height = node?.font?.style?.match(/height: (\d+)/)?.[1]; const src = node?.font?.src; const textObj = { img: { width, height, src, alt: node?.font?.alt }, text: node?.font?.alt }; richText.push(textObj); } } traverse(json, null, json.tagName); return richText; } }

三、转化后的数据写入 excel 表格

exceljs 用法, 可以直接去官方啊, 这里就不写了, 不是重点, 这里有点要注意的, exceljs 在计算高度时, 有个 bug, 在 pc windows 和 mac 之间, 设置的像素点是不一样的, 比如设置的每一列的宽度或高度, 在二个系统都不一致. 这里在 issues 有解决办法了. 就是对创建的 worksheet 的 views 做一个初始化

 

js

复制代码

const worksheet = workbook.addWorksheet('worksheet', { views: [{}] });

或者

 

js

复制代码

const worksheet = workbook.addWorksheet('worksheet', { properties: { defaultRowHeight: rowHeight }, pageSetup: { paperSize: 9, orientation: 'portrait', fitToPage: true, fitToWidth: 1, fitToHeight: 0 }, views: [{ zoomScale: 100, zoomScaleNormal: 100 }] });

直接上部分代码吧, 参数着吧, 每个项目的实际需求都不一样.

 

js

复制代码

// Html 转 ExcelJS 富文本格式数组 const richTexts = new HtmlToExcelJS().toExcelJS(content?.content) // 遍历富文本数组 for (const index in richTexts) { const richText: any = richTexts[index]; // contentRow2 const contentRow2 = worksheet.getRow(currentRow); // 合并 B 到 J worksheet.mergeCells(`B${currentRow}:J${currentRow}`); if (index === '0') { contentRow2.getCell(1).value = '內容 ' + (Number(key) + 1) + '\n' + 'Meeting Minutes ' + (Number(key) + 1); contentRow2.getCell(1).style = titleCellStyle; // 合并 A currentRow 到 A (currentRow + richTexts.length - 1) worksheet.mergeCells(`A${currentRow}:A${currentRow + richTexts.length - 1}`); } if (richText?.[0]?.img) { // 图片 // 计算图片宽高 async function getImageDimensions(imgBuffer) { try { const metadata = await sharp(imgBuffer).metadata(); let imgWidth = metadata.width; let imgHeight = metadata.height; // 计算图片宽度 字符宽度 * 8 = 像素宽度(8是推算出来的, 1个字符大约是8像素) const rowWidth = (bWidth + cWidth + dWidth + eWidth + fWidth + gWidth + hWidth + iWidth + jWidth) * 8; if (imgWidth > rowWidth) { imgHeight = Math.floor(imgHeight * (rowWidth / imgWidth)); imgWidth = rowWidth; } return { imgWidth, imgHeight }; } catch (err) { console.error('获取图片元数据失败:', err); } } // 写入图片到单元格 async function addImageToWorksheet(imgBuffer, imgWidth, imgHeight) { const imageId = workbook.addImage({ buffer: imgBuffer, extension: 'png', }); worksheet.addImage(imageId, { tl: { col: 1.1, row: currentRow - 1 + 0.01 }, ext: { width: imgWidth, height: imgHeight }, editAs: 'oneCell' }); } // src 去掉域名 const src = richText?.[0]?.img?.src.replace(/https?:\/\/[^/]+/, ''); // 取得当前目录路径 const currentPath = process.cwd(); // 本地图片文件路径 const imgPath = path.join(currentPath, 'public', src); // 图片宽度 let imgageWidth = richText?.[0]?.img?.width // 图片高度 let imgageHeight = richText?.[0]?.img?.height // 图片缓存 let imgBuffer // 本地图片文件不存在, 尝试下载网络图片 if (!fs.existsSync(imgPath)) { const srcHttp = richText?.[0]?.img?.src; try { const response = await axios.get(srcHttp, { responseType: 'arraybuffer' }); if (response) { imgBuffer = Buffer.from(response.data); } } catch (err) { console.error('获取网络图片文件失败:', err); } } else { imgBuffer = fs.readFileSync(imgPath); } if (imgBuffer) { // 如果没有设置图片宽高, 获取图片原宽高 if (!richText?.[0]?.img?.height) { const { imgWidth, imgHeight } = await getImageDimensions(imgBuffer); imgageWidth = imgWidth; imgageHeight = imgHeight; } contentRow2.height = imgageHeight / 1.3; await addImageToWorksheet(imgBuffer, imgageWidth, imgageHeight); } } else { // 文本 contentRow2.getCell(2).value = { richText }; contentRow2.getCell(2).alignment = { vertical: 'top', horizontal: 'left', wrapText: true }; // 计算文本内容的高度 function calculateNumLines(text, font, lineWidth, charWidth, rowHeight) { // 换行符的数量 const matches = text.match(/\n/g); // 行数 let numLines = matches ? matches.length : 0; // 字体高度补偿 if (font && font.size) { const lines = font.size * 1.3 / rowHeight - 1; numLines += lines; } // 计算超出的行数补偿, 不计算代码块code if (!font.code) { // 计算超出的行数补偿, 计算每行的字符数, 一行约 97 字符 const charsPerLine = Math.floor(lineWidth / charWidth); // 超出行数 const outNumLines = Math.ceil(text.length / charsPerLine); if (outNumLines > 1) { numLines += outNumLines - 1; } } return numLines; } const lineWidth = bWidth + cWidth + dWidth + eWidth + fWidth + gWidth + hWidth + iWidth + jWidth; const charWidth = 1.76; // 每个字符的宽度是1.76 // 计算每个富文本的行数 const numLinesArray = richText.map(rt => calculateNumLines(rt.text, rt.font, lineWidth, charWidth, rowHeight)); // 计算总高度 const cellHeight = numLinesArray.reduce((acc, numLines) => acc + numLines * rowHeight, 0); // 设置特定的行高 worksheet.getRow(currentRow).height = cellHeight < (rowHeight * 2) ? (rowHeight * 2) : cellHeight; } currentRow++; }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/889591
推荐阅读
相关标签
  

闽ICP备14008679号