前端報表導出成word文檔(含echarts圖表)
一、問題背景:
前端vue做的各種維度的報表,原來是通過前端整體截屏導出成PDF,但部分報表在遇到跨頁時會被截斷,客戶體驗極差。然後又考慮客戶可能需要修改報表中的一些內容,因此需要導出成word文檔解決跨頁截斷和滿足修改報表內容的問題。前期解決方案預研時試過jacob、poi方案,但jacob只能用於windows平臺(要引用一個dll文件),並且jacob和poi都存在樣式方面的難題。後來通過其他渠道瞭解了freemarker,於是通過freemarker的把前端請求的報表數據填充到模板文件,生成word文檔(導出功能由後端java實現)
二、效果圖
首先上一張效果圖,由於數據保密性,故前端頁面的報表原樣就不展示,導出的word文檔的效果圖和頁面報表幾乎一樣
三、功能點
- 文檔標題
- 文檔標題下方生成日期
- 文檔總體情況概述
- 每個echarts圖表的標題、圖片、圖注
- 水印
四、解決方案
利用freemarker將前端傳入的json格式數據填充入事先設計好的模板文件並生成word文檔
五、實現流程
六、實現步驟
1. 設計模板
按照前端報表展示樣式,設計模板,並將模板中需要動態被參數填充的部分使用佔位符代替,如標題使用${title},圖表標題使用${title_1}、${title_2}、${title_3},圖表總結詞用${summary_1},${summary_2},${summary_3},以此類推.下圖爲使用佔位符替換之後的word模板
2. 另存模板爲xml
上一步設計好模板並替換關鍵內容爲佔位符後,需要保存成xml模板文件,然後將xml模板文件中的圖片base64編碼替換成佔位符,例如下面模板片段
<pkg:part pkg:name="/word/media/image16.png" pkg:contentType="image/png" pkg:compression="store">
<pkg:binaryData>${base64_11}</pkg:binaryData>
</pkg:part>
<pkg:part pkg:name="/word/media/image11.png" pkg:contentType="image/png" pkg:compression="store">
<pkg:binaryData>${base64_9_1}</pkg:binaryData>
</pkg:part>
<pkg:part pkg:name="/word/media/image9.png" pkg:contentType="image/png" pkg:compression="store">
<pkg:binaryData>${base64_8_2}</pkg:binaryData>
</pkg:part>
<pkg:part pkg:name="/word/media/image10.png" pkg:contentType="image/png" pkg:compression="store">
<pkg:binaryData>${base64_8_3}</pkg:binaryData>
</pkg:part>
<pkg:part pkg:name="/word/media/image8.png" pkg:contentType="image/png" pkg:compression="store">
<pkg:binaryData>${base64_8_1}</pkg:binaryData>
</pkg:part>
3. 新建maven工程
本人使用的開發工具是Idea 2018.1版本,創建maven項目並創建包名,結構如下:
export-doc
└─src
│ ├─main
│ │ ├─java
│ │ │ └─com
│ │ │ └─zhuxl
│ │ │ └─exportdoc
│ │ │ │
│ │ │ ├─component
│ │ │ │ └─handler
│ │ │ │
│ │ │ ├─configuration
│ │ │ │
│ │ │ ├─controller
│ │ │ │
│ │ │ ├─entity
│ │ │ │
│ │ │ ├─service
│ │ │ │ │
│ │ │ │ └─impl
│ │ │ │
│ │ │ └─util
│ │ │
│ │ └─resources
│ │
│ └─test
│ └─java
└─pom.xml
4. 添加相關依賴
- 添加spring boot依賴
本demo項目基於spring boot框架,因此需要添加spring-boot-starter-web依賴,並且創建啓動類Application.java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>1.5.13.RELEASE</version>
<optional>true</optional>
</dependency>
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
(exclude = {DataSourceAutoConfiguration.class})
參數表示不自動加載參數連接數據庫,因爲本demo無數據庫連接,僅演示service裏調用工具類方法導出word,不需要操作數據庫,因此需要添加這個參數,否則啓動會報連接數據庫異常。
- 添加swagger依賴
本demo導出word報表請求參數爲json格式,數據量非常大(因爲有echarts報表base64編碼),請求方式爲POST,爲了便於測試,因此集成swagger
<dependency>
<groupId>com.didispace</groupId>
<artifactId>spring-boot-starter-swagger</artifactId>
<version>1.4.1.RELEASE</version>
</dependency>
- 添加lombok依賴
demo中請求參數使用lombok註解@Data或@Getter,@Setter,可以不用寫請求對象的getter和setter方法,在項目編譯階段會自動生成getter和setter方法。
<!-- LOMBOK begin -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
</dependency>
<!-- LOMBOK end -->
- 添加fastjson依賴
demo可能會使用到JSONObject類來設置異常時接口返回的數據
<!-- FASTJSON begin -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.31</version>
</dependency>
<!-- FASTJSON end -->
- 添加freemarker依賴
該依賴爲本次功能實現的核心,主要利用freemarker的api將請求數據構造的map和模板文件作爲參數生成word文件,並返回File文件對象,最後使用response的輸出流將文件返回
<!-- FREEMARKER begin -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.28</version>
</dependency>
<!-- FREEMARKER end -->
5. 創建接口請求參數對象類
使用java類來接收請求的json數據
@Data
@ApiModel(value = "貧困人羣報表導出請求對象")
public class ReportExportWordRequest {
@ApiModelProperty(value = "區域級別", name = "unitLevel")
private Integer unitLevel;
@ApiModelProperty(value = "區域編碼", name = "unitCode")
private String unitCode;
@ApiModelProperty(value = "報表類型", name = "type", notes = "poverty:貧困人羣報告;disable:殘疾人羣報告;poverty_disable:貧困且殘疾人羣報告")
private String type;
@ApiModelProperty(value = "報表標題", name = "title")
private String title;
@ApiModelProperty(value = "報告水印", name = "watermark")
private String watermark;
@ApiModelProperty(value = "報表生成日期", name = "date")
private String date;
@ApiModelProperty(value = "該區域報表描述第一段", name = "description1")
private String description1;
@ApiModelProperty(value = "該區域報表描述第二段", name = "description2")
private String description2;
@ApiModelProperty(value = "報表中每個圖表的內容列表", name = "reports")
private List<ReportContentRequest> reports;
}
@Data
@ApiModel("單個圖表請求對象")
public class ReportContentRequest {
@ApiModelProperty(value = "報表中排列序號", name = "serial")
private Integer serial;
@ApiModelProperty(value = "單個圖表標題", name = "title")
private String title;
@ApiModelProperty(value = "單個圖表base64編碼值", name = "base64")
private String base64;
@ApiModelProperty(value = "單個圖表內容總結", name = "summary")
private String summary;
@ApiModelProperty(value = "該標題下存在多個報表", name = "children")
private List<ReportContentRequest> children;
}
6. 創建導出word工具類
該工具類是實現導出word功能的核心類,讀取模板文件,格式化請求參數,填充模板生成word文檔的功能都在此工具類完成
public class WordGeneratorUtils {
private static Configuration configuration = null;
private static Map<String, Template> allTemplates = null;
private static class FreemarkerTemplate {
public static final String POVERTY = "poverty";
}
static {
configuration = new Configuration(Configuration.VERSION_2_3_28);
configuration.setDefaultEncoding("utf-8");
configuration.setClassForTemplateLoading(WordGeneratorUtils.class, "/freemarker/template");
allTemplates = new HashMap();
try {
allTemplates.put(FreemarkerTemplate.POVERTY, configuration.getTemplate(FreemarkerTemplate.POVERTY + ".ftl"));
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
private WordGeneratorUtils() {
throw new AssertionError();
}
public static File createDoc(Map<String, String> dataMap) {
try {
String name = dataMap.get("title") + dataMap.get("date") + ".doc";
File f = new File(name);
Template t = allTemplates.get(dataMap.get("template"));
// 這個地方不能使用FileWriter因爲需要指定編碼類型否則生成的Word文檔會因爲有無法識別的編碼而無法打開
Writer w = new OutputStreamWriter(new FileOutputStream(f), "utf-8");
t.process(dataMap, w);
w.close();
return f;
} catch (Exception ex) {
ex.printStackTrace();
throw new RuntimeException("生成word文檔失敗");
}
}
public static Map<String, String> parseToMap(ReportExportWordRequest request) {
Map<String, String> datas = new HashMap(32);
//主標題
datas.put("title", request.getTitle());
datas.put("date", request.getDate());
datas.put("watermark", request.getWatermark());
datas.put("description1", request.getDescription1());
datas.put("description2", request.getDescription2());
//遍歷設置報表
List<ReportContentRequest> contents = request.getReports();
datas.put("template", request.getType());
for (ReportContentRequest c : contents) {
if (c.getChildren() == null || c.getChildren().size() == 0) {
//無子報表
datas.put("title_" + c.getSerial(), c.getTitle());
datas.put("base64_" + c.getSerial(), c.getBase64());
datas.put("summary_" + c.getSerial(), c.getSummary());
} else {
//有多個子報表
datas.put("title_" + c.getSerial(), c.getTitle());
for (ReportContentRequest subc : c.getChildren()) {
datas.put("title_" + c.getSerial() + "_" + subc.getSerial(), subc.getTitle());
datas.put("base64_" + c.getSerial() + "_" + subc.getSerial(), subc.getBase64());
datas.put("summary_" + c.getSerial() + "_" + subc.getSerial(), subc.getSummary());
}
}
}
return datas;
}
}
7. 創建業務接口與實現類
ReportService
接口類
public interface ReportService {
File exportWord(ReportExportWordRequest exportWordRequest);
}
ReportServiceImpl
實現類
@Service
public class ReportServiceImpl implements ReportService {
@Override
public File exportWord(ReportExportWordRequest exportWordRequest) {
//解析參數
Map<String, String> datas = WordGeneratorUtils.parseToMap(exportWordRequest);
//導出
File word = WordGeneratorUtils.createDoc(datas);
return word;
}
}
8. 創建Controller類
@Slf4j
@RestController
@RequestMapping("/api/v1/report")
public class ReportController {
@Autowired
private ReportService reportService;
@ApiOperation(value = "貧困人羣綜合分析報告導出word文檔", notes = "貧困人羣綜合分析報告導出word文檔")
@PostMapping("/poverty_export_word.ajax")
public void povertyExportWord(HttpServletRequest request, HttpServletResponse response,
@Valid @RequestBody ReportExportWordRequest exportWordRequest) {
File file = reportService.exportWord(exportWordRequest);
InputStream fin = null;
OutputStream out = null;
try {
// 調用工具類WordGeneratorUtils的createDoc方法生成Word文檔
fin = new FileInputStream(file);
response.setCharacterEncoding("utf-8");
response.setContentType("application/msword");
// 設置瀏覽器以下載的方式處理該文件
// 設置文件名編碼解決文件名亂碼問題
response.addHeader("Content-Disposition", "attachment;filename=" + new String(file.getName().getBytes(), "iso-8859-1"));
out = response.getOutputStream();
byte[] buffer = new byte[512];
int bytesToRead = -1;
// 通過循環將讀入的Word文件的內容輸出到瀏覽器中
while ((bytesToRead = fin.read(buffer)) != -1) {
out.write(buffer, 0, bytesToRead);
}
} catch (Exception e) {
throw new RuntimeException("導出失敗", e);
} finally {
try {
if (fin != null) {
fin.close();
}
if (out != null) {
out.close();
}
if (file != null) {
file.delete();
}
} catch (IOException e) {
throw new RuntimeException("導出失敗", e);
}
}
}
}
9. 創建spring boot 啓動類與yml配置
啓動類在前面已經創建,此處只貼出application.yml基本配置
server:
port: 8080
context-path: /zhuxl
10. 創建swagger配置
@Configuration
@EnableSwagger2
public class SwaggerConfiguration {
@Bean
public Docket api() {
ParameterBuilder parameterBuilder = new ParameterBuilder();
parameterBuilder.name("Access-Token")
.description("令牌")
.modelRef(new ModelRef("string"))
.parameterType("header")
.required(false)
.build();
List<Parameter> parameters = new ArrayList<>();
parameters.add(parameterBuilder.build());
return new Docket(DocumentationType.SWAGGER_2).select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.regex("/api/v1/.*"))
.build()
.globalOperationParameters(parameters)
.apiInfo(apiInfo());
}
}
11. 運行,訪問swagger頁面測試
執行Applicaton類的main方法運行demo,訪問swagger接口頁面,在本demo中訪問地址爲:http://localhost:8080/zhuxl/swagger-ui.html
12. 構造參數測試獲得報表word文件
構造json參數,點擊try it out按鈕,即可進行測試並將文件下載,由於請求參數中base64編碼內容過於複雜,因此貼出的參數中圖片base64編碼省略
{
"unitLevel":"4",
"unitCode":"513429100000",
"type":"poverty",
"title":"XXXX地區貧困人羣總體情況報告",
"watermark":"張三13800138000",
"date":"2018年6月",
"description1":"報告正文開始的第一部分增加對該區域整體描述,描述中加入總關注人羣數等信息",
"description2":"報告正文開始的第一部分增加對該區域整體描述,描述中加入總關注人羣數等信息,報告正文開始的第一部分增加對該區域整體描述,描述中加入總關注人羣數等信息,報告正文開始的第一部分增加對該區域整體描述,描述中加入總關注人羣數等信息,報告正文開始的第一部分增加對該區域整體描述,描述中加入總關注人羣數等信息,報告正文開始的第一部分增加對該區域整體描述,描述中加入總關注人羣數等信息,報告正文開始的第一部分增加對該行政區域整體描述,描述中加入總關注人羣數等信息,報告正文開始的第一部分增加對該行政區域整體描述,描述中加入總關注人羣數等信息,報告正文開始的第一部分增加對該行政區域整體描述,描述中加入總關注人羣數等信息",
"reports":[
{
"serial":1,
"title":"一、貧困人口占比排名",
"base64":"xxxx",
"summary":"截止2018年6月,貧困人口占比最高的是XXX,佔比達到60.8233%;佔比最低的是XXXX,佔比爲11.6273%。",
"children":[]
},
{
"serial":2,
"title":"二、貧困人口關注等級分析",
"base64":"xxxxxxxx",
"summary":"截止2018年6月,總貧困人口中,一般關注等級0人,中度關注等級68156人,重點關注等級1人。三類人羣分別佔總人口的0%,34.2872%,0.0005%;佔貧困總人口的0%,99.9985%,0.0014%。",
"children":[]
},
{
"serial":3,
"title":"三、致貧原因分析",
"base64":"xxxxx",
"summary":"截止2018年6月,貧困人口中,自身發展動力不足原因致貧的人數最多,佔總貧困人口的37.0583%;其它原因致貧的人數最少,佔總貧困人口的0.0161%。",
"children":[]
},
{
"serial":4,
"title":"四、貧困人羣性別分析",
"base64":"xxxx",
"summary":"xxxxxxxxxxxxxxxxxxxxx",
"children":[]
},
{
"serial":5,
"title":"五、貧困人羣年齡分析",
"base64":"xxxxx",
"summary":"",
"children":[]
},
{
"serial":6,
"title":"六、貧困人羣學歷分析",
"base64":"xxxxxx",
"summary":"",
"children":[]
},
{
"serial":7,
"title":"七、貧困人羣民族分析",
"base64":"xxxxx",
"summary":"",
"children":[]
},
{
"serial":8,
"title":"八、貧困人羣脫貧能力分析",
"base64":"",
"summary":"",
"children":[
{
"serial":1,
"title":"1、文化程度分析",
"base64":"xxxxx",
"summary":"截止2018年6月,貧困人口中,中度關注等級中的文盲或半文盲,學齡前學歷人數最多,佔總貧困人口的30.7232%;重度關注等級中的大專及以上學歷人數最少,佔總貧困人口的0%。"
},
{
"serial":2,
"title":"2、勞動能力分析",
"base64":"xxxxxx",
"summary":"截止2018年6月,貧困人口中,中度關注等級中的喪失勞動力勞動力人數最多,佔總貧困人口的51.1466%;重度關注等級中的技能勞動力勞動力人數最少,佔總貧困人口的0%。"
},
{
"serial":3,
"title":"3、健康情況分析",
"base64":"xxxxx",
"summary":"截止2018年6月,貧困人口中,中度關注等級中的健康健康狀況人數最多,佔總貧困人口的96.5653%;重度關注等級中的殘疾健康狀況人數最少,佔總貧困人口的0%。"
}
]
},{
"serial":9,
"title":"九、資產和收入分析",
"base64":"",
"summary":"",
"children":[
{
"serial":1,
"title":"1、家庭收入分析",
"base64":"xxxxx",
"summary":"截止2018年6月,貧困人口中,中度關注等級中的5k-10k收入人數最多,佔總貧困人口的9.0365%;重度關注等級中的15k以上收入人數最少,佔總貧困人口的0%"
},
{
"serial":2,
"title":"2、房產情況分析",
"base64":"xxxxx",
"summary":"截止2018年6月,貧困人口中,中度關注等級中的房屋面積50-100房產人數最多,佔總貧困人口的18.8565%;重度關注等級中的房屋面積100平以上房產人數最少,佔總貧困人口的0%"
},
{
"serial":3,
"title":"3、耕地林地情況分析",
"base64":"xxxxx",
"summary":"截止2018年6月,貧困人口中,中度關注等級中擁有耕地面積5.32畝以上畝的人數最多,佔總貧困人口的7.7556%;重度關注等級中擁有耕地面積5.32畝以上畝的人數最少,佔總貧困人口的0%"
},
{
"serial":4,
"title":"4、新農合、養老保險情況分析",
"base64":"xxxx",
"summary":"截止2018年6月,貧困人口中,中度關注等級中辦理已參加新農合保險的人數最多,佔總貧困人口的99.9956%;重度關注等級中辦理已辦理養老保險保險的人數最少,佔總貧困人口的0%"
}
]
},
{
"serial":10,
"title":"十、預脫貧分析",
"base64":"xxxx",
"summary":"截止2018年6月,預脫貧人口中,2020年預脫貧的人數最多,佔總貧困人口的2.4134%",
"children":[]
},
{
"serial":11,
"title":"十一、生活狀況分析",
"base64":"xxxx",
"summary":"截止2018年6月,貧困人口家庭中,沒有實現衛生廁所的貧困家庭數量佔比最高,佔比爲80.7526%",
"children":[]
}
]
}
13. 打開文件驗證
將swagger接口頁面Response Body請求返回的doc文檔下載並打開,效果圖見文章頂部
七、問題排查
- doc模板設計保存成xml模板文件佔位符分離,如${title_1}可能被分離成$、title_、1、}或者${title_、1}或者其他情況
方案一:手動修改xml中被分離的佔位符,但缺點是如果模板需要做一點改動,保存的xml又需要手動修改,增加無謂的工作量
方案二:將整個佔位符的樣式設置成一樣,但事實上同樣存在被分離的情況
方案三:該方案可完美解決佔位符分離情況,避免修改doc模板保存時重複修改佔位符,點擊查看詳細方案
八、git clone
傳送門:去star
git clone https://github.com/v5zhu/export-doc.git
九、聯繫方式
QQ交流羣:566654343