前端報表導出成word文檔(含echarts圖表)

前端報表導出成word文檔(含echarts圖表)

一、問題背景:

前端vue做的各種維度的報表,原來是通過前端整體截屏導出成PDF,但部分報表在遇到跨頁時會被截斷,客戶體驗極差。然後又考慮客戶可能需要修改報表中的一些內容,因此需要導出成word文檔解決跨頁截斷和滿足修改報表內容的問題。前期解決方案預研時試過jacob、poi方案,但jacob只能用於windows平臺(要引用一個dll文件),並且jacob和poi都存在樣式方面的難題。後來通過其他渠道瞭解了freemarker,於是通過freemarker的把前端請求的報表數據填充到模板文件,生成word文檔(導出功能由後端java實現)

二、效果圖

首先上一張效果圖,由於數據保密性,故前端頁面的報表原樣就不展示,導出的word文檔的效果圖和頁面報表幾乎一樣
效果圖

三、功能點

  1. 文檔標題
  2. 文檔標題下方生成日期
  3. 文檔總體情況概述
  4. 每個echarts圖表的標題、圖片、圖注
  5. 水印

四、解決方案

利用freemarker將前端傳入的json格式數據填充入事先設計好的模板文件並生成word文檔

五、實現流程

Created with Raphaël 2.1.2開始新建word文檔按照頁面報表佈局與樣式設計文檔模板替換內容爲佔位符另存爲xml文件xml模板佔位符是否分離佔位符完整將圖表生成的base64編碼手動替換成佔位符將文檔保存到工程resource/freemarker/template目錄編寫代碼調用freemarker api生成word文檔訪問swagger接口頁面測試下載,打開查看效果yesno

六、實現步驟

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文檔下載並打開,效果圖見文章頂部

七、問題排查


  1. 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章