React纯前端实现把div(含有Echart)下载导出为word文档

场景描述

前端实现点击按钮后,把某个div内的所有内容导出为word文档(.docx格式)。

  • 将页面导出为文档时,需要设置页边距、行距、首行缩进等段落格式,以及字体大小等等(类似于在word中写文本也需要设置的一些信息)。
  • div内含有Echart生成的图表,我们在导出时,为了防止文档中的图表大小溢出,需要调节图片canvas/img的大小,重新规定它们的widthheight

解决思路

  1. 要下载的div设置id为report(自定义设置),获取这个dom元素document.getElementById("report")
  2. 将这段React代码转为html代码:
    (1)canvas echart图表转为图片;
    (2)做分页,style="page-break-after: always"
    (3)替换一些语法兼容html,比如把 <br>转为<br><br/>
    (4)设置页边距,在html中是设置body的printmarginleftprintmarginrightprintmargintopprintmarginbottom属性。
  3. 获取React代码的style样式代码,可以直接替换为html的格式,也可以重新设置样式以便适应word文档。设置段落格式、字体格式等。
  4. 把上述html代码和style代码合并成一个完整的HTML文档。
  5. 使用准备好的HTML创建Blob对象,调用FileSaver.saveAs把这个Blob导出为.docx文档。

代码实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import React, { useRef, useState, useEffect } from "react";
import { Button, message } from "antd";
import { saveAs } from "file-saver";
import "./style.scss";

const DownloadReportComp = (props) => {
const componentRef = useRef();

const downloadReport = () => {
// 这个dom元素是要导出的pdf的div容器
const element = document.getElementById("report");

// 把当前 vue 所展示的页面对应的 html 转换成一个字符串,这里用到了上面的三个函数,所以,如果是写在外面的话要引用进来
let html = getReportHtml(getHtml(element), getStyle());
const f_name = "打印word报告";

// 使用我们刚刚准备好的html模板并创建Blob对象
let blob = new Blob([html], {
type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document;charset=utf-8",
});
// 调用FileSaver.saveAs导出下载word
saveAs(blob, f_name + ".docx");
};

const getReportHtml = (mhtml, style) => {
return `
Content-Type: text/html; charset="utf-8"
<!DOCTYPE html>
<html>
<head>
<style>
${style}
</style>
</head>
<body>
${mhtml}
</body>
</html>
`;
};

const getHtml = (dom) => {
let _dom = dom || document;

let canvass = _dom.querySelectorAll("canvas");
// let imgRepalce = _dom.querySelectorAll(".imgRepalce");
let imageList = [];

// console.log("canvass", [...canvass], "imgRepalce", imgRepalce);

//canvas echart图表转为图片
for (let k4 = 0; k4 < canvass.length; k4++) {
let imageURL = canvass[k4].toDataURL("image/png");
let img = document.createElement("img");
img.src = imageURL;
img.width = 460;
imageList.push(img.outerHTML);
}
// console.log("imageList", [...imageList]);
//做分页
//style="page-break-after: always"
let pages = _dom.querySelectorAll(".result");
for (let k5 = 0; k5 < pages.length; k5++) {
pages[k5].setAttribute("style", "page-break-after: always");
}
let result = _dom.outerHTML;
//result = result.replace(/<colgroup>(.*?)<\/colgroup>/gi, '')
//result = result.replace(/<canvas (.*?)><\/canvas>/gi, '')

// for (let i = 0; i < canvass.length; i++) {
// result = result.replace(
// canvass[i].outerHTML,
// '<div class="img-replace">' + imageList[i] + "</div>"
// );
// }
for (let i = 0; i < canvass.length; i++) {
result = result.replace(canvass[i].outerHTML, imageList[i]);
}
result = result.replace(/<img (.*?)>/gi, "<img $1></img>");
result = result.replace(/<br>/gi, "<br></br>");
result = result.replace(/<hr>/gi, "<hr></hr>");
// console.log("result", result);
return (
"<body printmarginleft='48' printmarginright='48' printmargintop='32' printmarginbottom='32'>" +
result +
"</body>"
);
};

const getStyle = (notPrint) => {
let str = '<head><meta charset="utf-8"></meta>';
// let str = '<head><meta charset="utf-8"></meta>',
// let styles = document.querySelectorAll("style");
// for (let i = 0; i < styles.length; i++) {
// str += styles[i].outerHTML;
// }
str +=
"<style>.report-passage{line-height: 2em;text-indent: 2em; margin: 0.5em 0;}</style>";

str +=
"<style>" +
(notPrint ? notPrint : ".no-print") +
"{display:none;}</style>";

str +=
"<style>.report-container p{margin: 0.5em 0;text-indent: 0;}</style>";

str += "<style>h5{font-color: #2fb89e;}</style>";
str += "</head>";
// console.log("style", str);
return str;
};

useEffect(() => {}, []);

return (
<div>
<Button
type="primary"
onClick={() => downloadReport()}
style={{ position: "absolute", zIndex: 5, right: 50 }}
>
下载
</Button>
<div
id="report"
ref={componentRef}
style={{
fontSize: "1rem",
}}
className="report-container"
>
<div id="report-passage" className="report-passage">
<div
style={{ textAlign: "center", fontSize: "1.2rem", fontWeight: 700 }}
>
能力评价报告
</div>
<div
style={{ textAlign: "center", fontSize: "1.2rem", fontWeight: 700 }}
>
杨浦区
</div>
<p>一、基本情况</p>
<div>名称:四平路街道。</div>
<div>常住人口:120000 人。</div>
<p>二、风景评估结果</p>
<div>最终得分为12。</div>
<p>三、建筑评估情况</p>
<div>评估结果为15。</div>

{/* 下方是一个自定义组件,里面是Echart生成的一些图表 */}
{/* <PassageChart /> */}
</div>
</div>
</div>
);
};
export default DownloadReportComp;