likes
comments
collection
share

vue3使用exceljs导出富文本编辑器样式

作者站长头像
站长
· 阅读数 13

痛点

我的工作:vue3使用表格展示部分富文本编辑器的html数据。 客户的要求:我想导出的excel也是这个样式,我的要求不高吧。(对对对,你说的都是对的)。

问题分析

  1. 既然要导出富文本的样式,那么就必然要去解析html文本。
  2. 文件最终是由前端生成还是后端生成?
    • 前端生成,一般也是使用exceljs等相关的库处理
    • 后端生成,前端需要先去解析html文本,然后在将数据传给后端去生成excel文件
    • 最后,后端说我很忙,行吧,我是社畜我来搞吧

技术选型

  • excel导出使用exceljs
  • html文本解析尝试使用正则及使用AST语法抽象树去处理,难度过大,已放弃,最终找到了htmlparser2这样一个库

功能实现

我们先看看htmlparser2帮我们解析成啥样了

import { parseDocument } from 'htmlparser2';
const html =
    '<p>1.完成高级设置-100%</p><p><strong>2.完成项目列表</strong> <i>三个tab页面的切换</i></p><p>3.完成新增项目弹窗页面更新</p><p>4.完成填写周报页面</p><p><span style="color:hsl(0,75%,60%);">5.完成周报卡片视图</span></p><p>6.更新节点计划页面</p><p>7.完善填写周报逻辑bug</p><p>8.修改项目编辑页面bug</p><p>9.完成项目列表底部统计</p><p>10.完成项目概览的项目基本信息更新</p>'; 
  const res = parseDocument(html.trim(), {
    recognizeCDATA: true
  });  
  console.log('res', res)

如下图:

vue3使用exceljs导出富文本编辑器样式

最终帮我们解析成了类似dom结构树的数据,我们可以通过name属性知道他的标签,attribs属性获取属性标签,parent,属性指向父级dom,等等。

但是这些层级可能会过深,这时候我们需要把该结构重新处理下,进行拉平处理:

import { parseDocument } from 'htmlparser2';
const newHtmlRes = ref([]);
const html =
    '<p>1.完成高级设置-100%</p><p><strong>2.完成项目列表</strong> <i>三个tab页面的切换</i></p><p>3.完成新增项目弹窗页面更新</p><p>4.完成填写周报页面</p><p><span style="color:hsl(0,75%,60%);">5.完成周报卡片视图</span></p><p>6.更新节点计划页面</p><p>7.完善填写周报逻辑bug</p><p>8.修改项目编辑页面bug</p><p>9.完成项目列表底部统计</p><p>10.完成项目概览的项目基本信息更新</p>'; 
  const res = parseDocument(html.trim(), {
    recognizeCDATA: true
  });  
  console.log('res', res)
  function parseHtmlString(htmlRes, parent) {
    if (htmlRes && htmlRes.length) {
      if (parent) parent.__children = [];
      htmlRes.forEach(item => {
        // console.log('item', item.data);
        if (parent) {
          let parentValue = parent;
          parentValue.__children.push({
            data: item.data,
            name: item.parent.name,
            attribs: {
              ...(item.parent.parent ? item.parent.parent.attribs : {}), // 部分标签的样式可能受父级影响,故而需要将父级的attribs一并获取
              ...item.parent.attribs
            },
            type: item.parent.type
          });
          while (parentValue) {
            if (parentValue.parent && parentValue.parent.__children) {
              const parentIsArray = Array.isArray(parentValue.__children);
              if (parentIsArray) {
                parentValue.parent.__children.push(...parentValue.__children);
              } else {
                parentValue.parent.__children.push(parentValue.__children);
              }
            }
            parentValue = parentValue.parent;
          }
        }
        if (item.children) {
          parseHtmlString(item.children, item);
        }
      });
    }
  }

  parseHtmlString(res.children, null);

  newHtmlRes.value = res.children.map(v => v.__children);

二次转换后结果如下:

vue3使用exceljs导出富文本编辑器样式

这个时候我们就知道我们就清楚,我们有10行,每一行的展示只需要将每一行的data进行拼接处理。

最终代码:

import { parseDocument } from 'htmlparser2';
import ExcelJS from 'exceljs';
import tinytinycolor from 'tinytinycolor';
const newHtmlRes = ref([]);
const workbook = new ExcelJS.Workbook();
const html =
    '<p>1.完成高级设置-100%</p><p><strong>2.完成项目列表</strong> <i>三个tab页面的切换</i></p><p>3.完成新增项目弹窗页面更新</p><p>4.完成填写周报页面</p><p><span style="color:hsl(0,75%,60%);">5.完成周报卡片视图</span></p><p>6.更新节点计划页面</p><p>7.完善填写周报逻辑bug</p><p>8.修改项目编辑页面bug</p><p>9.完成项目列表底部统计</p><p>10.完成项目概览的项目基本信息更新</p>'; 
  const res = parseDocument(html.trim(), {
    recognizeCDATA: true
  });  
  console.log('res', res)
  function parseHtmlString(htmlRes, parent) {
    if (htmlRes && htmlRes.length) {
      if (parent) parent.__children = [];
      htmlRes.forEach(item => {
        // console.log('item', item.data);
        if (parent) {
          let parentValue = parent;
          parentValue.__children.push({
            data: item.data,
            name: item.parent.name,
            attribs: {
              ...(item.parent.parent ? item.parent.parent.attribs : {}), // 部分标签的样式可能受父级影响,故而需要将父级的attribs一并获取
              ...item.parent.attribs
            },
            type: item.parent.type
          });
          while (parentValue) {
            if (parentValue.parent && parentValue.parent.__children) {
              const parentIsArray = Array.isArray(parentValue.__children);
              if (parentIsArray) {
                parentValue.parent.__children.push(...parentValue.__children);
              } else {
                parentValue.parent.__children.push(parentValue.__children);
              }
            }
            parentValue = parentValue.parent;
          }
        }
        if (item.children) {
          parseHtmlString(item.children, item);
        }
      });
    }
  }

  parseHtmlString(res.children, null);

  newHtmlRes.value = res.children.map(v => v.__children);
  
  
  const tranfromStyle = str => {
  if (!str) return null;
  let res = {};
  const singlestyle = str.trim().split(';');
  for (let i = 0; i < singlestyle.length; i++) {
    if (singlestyle[i]) {
      let styleRes = singlestyle[i].split(':');
      res[styleRes[0]] = styleRes[1];
    }
  }

  return res;
};

const handleDown = () => {
  const t = tinytinycolor('rgb(0, 0, 238)');

  const worksheet = workbook.addWorksheet('My Sheet1', {
    properties: {
      tabColor: { argb: '#FFB6C1', theme: 2 },
      showGridLines: true
    }
  });
  worksheet.columns = [
    {
      header: 'Name',
      key: 'name',
      width: 40,
      style: {
       alignment: {
        wrapText: true
       }
      }
    },
    { header: 'Age', key: 'age', width: 10, style: {} },
    { header: 'Gender', key: 'gender', width: 10, style: {} }
  ];
  const headerCell1 = worksheet.getCell('A1');
  headerCell1.style.fill = {
    type: 'pattern',
    pattern: 'solid',
    fgColor: { argb: 'FFFFFF00' }
  };
  headerCell1.style.font = {
    color: { argb: t.toHex8(), theme: 0 },
    bold: true,
    size: 14
  };
  headerCell1.style.alignment = {
    horizontal: 'center',
    vertical: 'middle'
  };
  headerCell1.width = 40;
  const cell = worksheet.getCell('A2');
  cell.alignment = { wrapText: true };
  let richTextArray = [];
  for (let i = 0; i < newHtmlRes.value.length; i++) {
    const item = newHtmlRes.value[i];
    let len = item.length - 1;

    for (let j = 0; j < item.length; j++) {
      // console.log('item', );
      const styles = tranfromStyle(item[j].attribs.style);
      let obj = { font: { size: 12 } };
      if (!item[j].data) {
        continue;
      }
      // 加粗
      if (item[j].name === 'strong') {
        obj.font.bold = true;
      }
      // 斜体
      if (item[j].name === 'i') {
        obj.font.italic = true;
      }

      if (styles) {
        if ('color' in styles) {
          const c = tinytinycolor(styles['color']).toHex8();
          obj.font.color = {
            argb: c,
            theme: 0
          };
        }
        if ('font-size' in styles) {
          obj.font.size = Number(styles['font-size'].replace('px', ''));
        } else {
          obj.font.size = 12;
        }
      }
      if (j === len) {
        obj.text = `${item[j].data} \n`;
      } else {
        obj.text = item[j].data;
      }

      richTextArray.push(obj);
    }
  }
  cell.value = {
    richText: richTextArray
  };

  // 下载
  workbook.xlsx.writeBuffer().then(buffer => {
    const blob = new Blob([buffer], {
      type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
    });
    const url = window.URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = url;
    link.setAttribute('download', 'my-file.xlsx');
    document.body.appendChild(link);
    link.click();
  });
};
  

导出效果

vue3使用exceljs导出富文本编辑器样式

最后

部分exceljs的文档并没有细说,需要自己去查阅文档。当然大家可以使用以上代码进行验证。目前只是在一个单元格做了处理,对于实际问题则需要相应的去遍历处理即可。

转载自:https://juejin.cn/post/7270791767179853843
评论
请登录