前端导出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);
}
};