【java】利用aspose.words的ReportingEngine填充word模板

清清飞扬發表於2024-12-08

詳情見:https://gitee.com/javadog-net/boot-apose

前言

⏲️本文閱讀時長:約10分鐘

🎯主要目標:

1.實現Springboot與aspose-words整合,填充word模板並轉化PDF;
2.前端vue整合vue-pdf實現PDF預覽及下載

word模板重點(詳見圖示)

1.單屬性賦值
2.List迴圈賦值
3.圖片插入
4.對勾特殊符號插入


乾貨程式碼

原始碼

https://gitee.com/javadog-net/boot-apose.git

資料夾描述
boot-apose java後臺
vue-apose 前端vue

對應工具下載

工具描述地址
aspose-words-19.1 word三方庫 https://download.csdn.net/download/baidu_25986059/85390408
javadog-vue-pdf 因原版vue-pdf有相容錯誤,此版本為本人修訂自用版 https://www.npmjs.com/package/javadog-vue-pdf

結果預覽

模板填充前空word模板

我還是沒有放下你

程式碼填充後word模板

web端vue預覽的html的pdf

最終填充後下載的pdf

愛你不只是說說而已


技術涉及

💁‍♂️後端框架

技術名稱參考網站
Spring Boot MVC框架 https://spring.io/projects/spring-boot
Maven 專案構建 http://maven.apache.org
aspose-words 本地依賴word工具包 https://download.csdn.net/download/baidu_25986059/85390408
lombok Java庫 https://projectlombok.org/
hutool 工具類 http://hutool.mydoc.io

💁‍♀️前端框架

技術名稱參考網站
VUE MVVM框架 https://cn.vuejs.org//
Element UI UI庫 https://element.eleme.cn/2.0/#/zh-CN
javadog-vue-pdf PDF檔案線上預覽庫(個人修復相容版) https://www.npmjs.com/package/javadog-vue-pdf
axios 基於promise網路請求庫 http://www.axios-js.com/

正文

雖然浪費的時間有點多,不過磨刀不誤砍柴工

前置條件

  • 後臺springboot基礎專案
  • vue基礎專案

⭐ 如沒有基礎程式碼可以直接下載狗哥Gitee原始碼

步驟解析

後臺
1.下載對應的aspose-words-19.1-jdk16.jar,加入POM本地依賴

因原版收費且會有水印等不確定因素,直接下載jar包本地依賴或者上傳私服

我還在原地等你

 <!-- 本地依賴 aspose-words-->
        <dependency>
            <groupId>com.aspose</groupId>
            <artifactId>aspose-words</artifactId>
            <classifier>jdk16</classifier>
            <scope>system</scope>
            <version>1.0</version>
            <systemPath>${project.basedir}/src/main/resources/lib/aspose-words-19.1-jdk16.jar</systemPath>
        </dependency>
2.放置模板檔案到資源路徑下

3.controller讀取模板檔案並填充資料
  1. 讀取模板並將輸入流轉為doc,並設定檔名及返回型別
  2. 定位【照片】書籤位置,插入圖片
  3. 定位【等級】書籤位置,插入對應字元 書籤插入參考如下
  • 找到需要插入的圖片的地方,滑鼠焦點聚焦

  • 點選【插入】找到書籤並點選,然後錄入書籤名,並點選新增

  • 檢查書籤是否新增成功

  • 更新doc

  • 將基礎資料填充後並轉為PDF

詳見如下程式碼

package apose.javadog.net.controller;
import apose.javadog.net.entity.BaseInfo;
import apose.javadog.net.entity.Education;
import apose.javadog.net.entity.Interview;
import apose.javadog.net.entity.WorkExperience;
import cn.hutool.core.util.CharsetUtil;
import com.aspose.words.Document;
import com.aspose.words.DocumentBuilder;
import com.aspose.words.ReportingEngine;
import com.aspose.words.SaveFormat;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/word")
@Slf4j
public class WordController {

    @GetMapping("/pdf")
    void pdf(HttpServletResponse response){
        // 獲取資源doc路徑下的簡歷interview.doc模板
        final  ClassPathResource classPathResource = new ClassPathResource("doc\\interview.doc");
        // 組裝資料
        final Document doc;
        try (InputStream inputStream = classPathResource.getInputStream();
             ServletOutputStream outputStream = response.getOutputStream()) {
            // 檔名稱
            String fileName = URLEncoder.encode("帥鍋的簡歷.pdf", CharsetUtil.UTF_8);
            response.reset();
            response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setContentType("application/octet-stream;charset=UTF-8");
            // 將輸入流轉為doc
            doc = new Document(inputStream);
            // doc構建
            DocumentBuilder builder = new DocumentBuilder(doc);
            // 定位書籤位置
            builder.moveToBookmark("AVATAR");
            // 插入圖片
            builder.insertImage("https://portrait.gitee.com/uploads/avatars/user/491/1474070_javadog-net_1616995139.png!avatar30");
            // 定位LANGUAGE_LEVEL4書籤位置
            builder.moveToBookmark("LANGUAGE_LEVEL4");
            // 設定字元名稱
            builder.getFont().setName("Wingdings");
            // 設定字元大小
            builder.getFont().setSize(14);
            // 對號字元
            builder.write("\uF0FE");
            // 定位LANGUAGE_LEVEL6書籤位置
            builder.moveToBookmark("LANGUAGE_LEVEL6");
            // 設定字元名稱
            builder.getFont().setName("Wingdings");
            builder.getFont().setSize(20);
            builder.write("□");
            doc.updateFields();
            final ReportingEngine engine = new ReportingEngine();
            // 將資料填充至模板
            engine.buildReport(doc, getInterviewData(), "data");
            // 轉pdf
            doc.save(outputStream, SaveFormat.PDF);
        } catch (Exception e) {
            log.error("生成報告異常,異常資訊:{}", e.getMessage(), e);
            e.printStackTrace();
        }
    }

    private Interview getInterviewData(){
        Interview interview = new Interview();
        this.getBaseInfo(interview);
        this.getEducations(interview);
        this.getWorkExperiences(interview);
        return interview;
    }

    /**
     * @Description: 組裝基本資料
     * @Param: [interview]
     * @return: [apose.javadog.net.entity.Interview]
     * @Author: hdx
     * @Date: 2022/5/10 15:39
     */
    private void getBaseInfo(Interview interview){
        // 基本資料
        BaseInfo baseInfo = new BaseInfo();
        List<String> listStr = new ArrayList<>();
        listStr.add("後端技術棧:有較好的Java基礎,熟悉SpringBoot,SpringCloud,springCloud Alibaba等主流技術,Redis記憶體資料庫、RocketMq、dubbo等,熟悉JUC多執行緒");
        listStr.add("後端模板引擎:thymeleaf、volocity");
        listStr.add("前端技術棧:熟練掌握ES5/ES6/、NodeJs、Vue、React、Webpack、gulp");
        listStr.add("其他技術棧: 熟悉python+selenium、electron");
        baseInfo.setName("狗哥")
                .setBirth("1993年5月14日")
                .setHeight("180")
                .setWeight("70")
                .setNation("漢")
                .setSex("男")
                .setNativePlace("濟南")
                .setMarriage("已婚")
                .setSpecialtys(listStr);
        interview.setBaseInfo(baseInfo);
    }
    /**
     * @Description: 組裝教育經歷
     * @Param: [interview]
     * @return: [apose.javadog.net.entity.Interview]
     * @Author: hdx
     * @Date: 2022/5/10 15:40
     */
    private void getEducations(Interview interview){
        // 高中
        List<Education> educations = new ArrayList<>();
        Education education = new Education();
        education.setStartEndTime("2009-2012")
                .setSchool("山東省實驗中學")
                .setFullTime("是")
                .setProfessional("理科")
                .setEducationalForm("普高");
        educations.add(education);
        // 大學
        Education educationUniversity = new Education();
        educationUniversity.setStartEndTime("2012-2016")
                .setSchool("青島農業大學")
                .setFullTime("是")
                .setProfessional("電腦科學與技術")
                .setEducationalForm("本科");
        educations.add(educationUniversity);
        interview.setEducations(educations);
    }

    /**
     * @Description: 組裝工作經歷
     * @Param: [interview]
     * @return: [apose.javadog.net.entity.Interview]
     * @Author: hdx
     * @Date: 2022/5/10 15:40
     */
    private void getWorkExperiences(Interview interview){
        // 工作記錄
        List<WorkExperience> workExperiences = new ArrayList<>();
        WorkExperience workExperience = new WorkExperience();
        workExperience.setStartEndTime("2009-2012")
                .setWorkUnit("青島XXX")
                .setPosition("開發")
                .setResignation("有更好的學習空間,向醫療領域擴充學習緯度");
        workExperiences.add(workExperience);
        interview.setWorkExperiences(workExperiences);
    }
}

前端
1.下載對應的依賴包

npm install

2.在vue.config.js中配置代理
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  devServer: {
    port: 1026,
    proxy: {
      '/': {
        target: 'http://localhost:8082', //請求本地 需要ipps-boot後臺專案
        ws: false,
        changeOrigin: true
      }
    }
  }
})

npm install

3.在main.js引入所需外掛
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
import axios from 'axios'
Vue.prototype.$http = axios
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
new Vue({
  render: h => h(App),
}).$mount('#app')
4.頁面引入vue-pdf元件
  <pdf v-if="showPdf" ref="pdf" :src="pdfUrl" :page="currentPage" @num-pages="pageCount=$event"
               @page-loaded="currentPage=$event" @loaded="loadPdfHandler">
   </pdf>
5.頁面中使用axios調取介面獲取資料

👽注意responseType型別為blob

this.$http({
      method: 'get',
      url: `/word/pdf`,
      responseType: 'blob'
    }).then(res=>{
      console.log(res)
      this.pdfUrl = this.getObjectURL(res.data)
      console.log(this.pdfUrl)
      const loadingTask = pdf.createLoadingTask(this.pdfUrl)
      // // 注意這裡一定要這樣寫
      loadingTask.promise.then(load => {
        this.numberPage = load.numPages
      }).catch(err => {
        console.log(err)
      })
      this.loading = false;
    })

頁面完整程式碼如下

<template>
  <div class="pdf_wrap">
    <template>
      <el-form ref="form" label-width="80px">
        <div style='text-align: center;margin: 30px;' v-if="loading">
          資料載入中...
        </div>
        <div v-if="loading==false" style="display: flex;align-items: center;">
          <div style="flex: 1;"></div>
          <el-button size="mini" @click="changePdfPage(0)" type="primary">上一頁</el-button>
          <div style="position: relative; margin: 0px 10px; top: -10px;">
            {{currentPage}} / {{pageCount}}  共 {{numberPage}} 頁
          </div>
          <el-button size="mini" @click="changePdfPage(1)" type="primary">下一頁</el-button>
          <el-button size="mini" @click='print' type="primary">列印</el-button>
        </div>
        <div v-show="loading==false">
          <pdf v-if="showPdf" ref="pdf" :src="pdfUrl" :page="currentPage" @num-pages="pageCount=$event"
               @page-loaded="currentPage=$event" @loaded="loadPdfHandler">
          </pdf>
        </div>
      </el-form>
    </template>
  </div>
</template>

<script>
import pdf from 'javadog-vue-pdf'
export default {
  components: {
    pdf
  },
  data () {
    return {
      loading: true,
      showPdf: false,
      currentPage: 1, // pdf檔案頁碼
      pageCount: 1, // pdf檔案總頁數
      fileType: 'pdf', // 檔案型別
      pdfUrl: '',
      numberPage:1
    }
  },
  mounted () {
    this.showPdf = true;
    this.$http({
      method: 'get',
      url: `/word/pdf`,
      responseType: 'blob'
    }).then(res=>{
      console.log(res)
      this.pdfUrl = this.getObjectURL(res.data)
      console.log(this.pdfUrl)
      const loadingTask = pdf.createLoadingTask(this.pdfUrl)
      // // 注意這裡一定要這樣寫
      loadingTask.promise.then(load => {
        this.numberPage = load.numPages
      }).catch(err => {
        console.log(err)
      })
      this.loading = false;
    })
  },
  methods: {
    print(){
      this.$refs.pdf.print(600)
    },
    getObjectURL(file) {
      let url = null
      if (window.createObjectURL !== undefined) { // basic
        url = window.createObjectURL(file)
      } else if (window.webkitURL !== undefined) { // webkit or chrome
        // try {
        let blob = new Blob([file], {
          type: "application/pdf"
        });
        url = window.URL.createObjectURL(blob)
        console.log(url)
      } else if (window.URL !== undefined) { // mozilla(firefox)
        try {
          url = window.URL.createObjectURL(file)
        } catch (error) {
          console.log(error)
        }
      }
      return url
    },
    changePdfPage(val) {
      console.log(val)
      if (val === 0 && this.currentPage > 1) {
        this.currentPage--
        // console.log(this.currentPage)
      }
      if (val === 1 && this.currentPage < this.pageCount) {
        this.currentPage++
        // console.log(this.currentPage)
      }
    },
    // pdf載入時
    loadPdfHandler() {
      console.log('jiazai')
      this.currentPage = 1 // 載入的時候先載入第一頁
      this.loading = false;
    }
  }
}
</script>
<style scoped>
.pdf_wrap {
  background: #fff;
  height: 100vh;
  width: 80vh;
  margin: 0 auto;
}
.pdf_list {
  height: 80vh;
  overflow: scroll;
}
button {
  margin-bottom: 20px;
}
</style>

異常情況

1.vue-pdf原版與webpack版本問題,會啟動不起來,所以本狗才偷樑換柱,改了一下並自用

2.aspose-words-19.1-jdk16.jar 如果採用官網的maven依賴,可能需要自助破解或交費使用

成果展示

相關文章