vue3使用exceljs导出富文本编辑器样式
痛点
我的工作:vue3使用表格展示部分富文本编辑器的html数据。 客户的要求:我想导出的excel也是这个样式,我的要求不高吧。(对对对,你说的都是对的)。
问题分析
- 既然要导出富文本的样式,那么就必然要去解析html文本。
- 文件最终是由前端生成还是后端生成?
- 前端生成,一般也是使用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)
如下图:
最终帮我们解析成了类似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);
二次转换后结果如下:
这个时候我们就知道我们就清楚,我们有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();
});
};
导出效果
最后
部分exceljs的文档并没有细说,需要自己去查阅文档。当然大家可以使用以上代码进行验证。目前只是在一个单元格做了处理,对于实际问题则需要相应的去遍历处理即可。
转载自:https://juejin.cn/post/7270791767179853843