在线文档编辑onlyoffice集成poi-tl,实现文档编辑并根据模板导出

3,109 阅读5分钟

1 onlyoffice

1.1 简介

ONLYOFFICE Docs是一个开源办公套件,包括文本文档、电子表格、演示文稿和可填写表格的编辑器。它提供以下功能:

  • 创建、编辑和查看文本文档、电子表格、演示文稿和可填写的表格;
  • 与其他队友实时协作处理文件。

1.2 DOCKER安装

docker run -i -t -d -p 8831:80 --restart=always \
-v /home/onlyoffice/DocumentServer/logs:/var/log/onlyoffice \
-v /home/onlyoffice/DocumentServer/data:/var/www/onlyoffice/Data \
-v /home/onlyoffice/DocumentServer/lib:/var/lib/onlyoffice \
-v /home/onlyoffice/DocumentServer/db:/var/lib/postgresql onlyoffice/documentserver:6.4.2

映射端口8831,直接访问ip:8831,出现下图,证明安装成功

image.png

1.3 使用

  • 使用分2部分 1.前端应用文档编辑服务器js操作文档 2.后端编写回调接口处理编辑过的文档
  • 我这边项目采用的minio存储的文档,所以onlyoffice操作的是云文件,流程如下:
graph TD
项目前端代码集成onlyoffice --> 根据云文档url打开文件并编辑 --> 编辑之后调用后端提供的回调函数 -->回调函数将编辑后的文件存回文件服务器

1.3.1 前端js

引入js

<script>
  var href = 'http://47.111.142.000:8831'
  var head = document.getElementsByTagName('head')[0]
  var script = document.createElement('script')
  script.type = 'text/javascript'
  script.src = href + '/web-apps/apps/api/documents/api.js'
  head.appendChild(script);
  (() => {
    var htmlRoot = document.getElementById('htmlRoot')
    var theme = window.localStorage.getItem('__APP__DARK__MODE__')
    if (htmlRoot && theme) {
      htmlRoot.setAttribute('data-theme', theme)
      theme = htmlRoot = null
    }
  })();
</script>

组件部分

<!--onlyoffice 编辑器-->
<template>
  <div id="vabOnlyOffice"></div>
</template>

<script>
export default {
  name: 'VabOnlyOffice',
  props: {
    option: {
      type: Object,
      default: () => {
        return {}
      },
    },
  },
  data () {
    return {
      doctype: '',
      docEditor: null,
    }
  },
  beforeDestroy () {
    if (this.docEditor !== null) {
      this.docEditor.destroyEditor()
      this.docEditor = null
    }
  },
  watch: {
    option: {
      handler: function (n) {
        this.setEditor(n)
        this.doctype = this.getFileType(n.fileType)


      },
      deep: true,
    },
  },
  mounted () {
    if (this.option.url) {
      this.setEditor(this.option)
    }
  },
  methods: {
    async setEditor (option) {
      if (this.docEditor !== null) {
        this.docEditor.destroyEditor()
        this.docEditor = null
      }
      this.doctype = this.getFileType(option.fileType)
      let config = {
        document: {
          //后缀
          fileType: option.fileType,
          key: option.key || '',
          title: option.title,
          permissions: {
            edit: option.isEdit,//是否可以编辑: 只能查看,传false
            print: option.isPrint,
            download: true,
            // "fillForms": true,//是否可以填写表格,如果将mode参数设置为edit,则填写表单仅对文档编辑器可用。 默认值与edit或review参数的值一致。
           review: true //跟踪变化
          },
          url: option.url,
        },
        documentType: this.doctype,
        editorConfig: {
          callbackUrl: option.editUrl,//"编辑word后保存时回调的地址,这个api需要自己写了,将编辑后的文件通过这个api保存到自己想要的位置
          lang: option.lang,//语言设置
          //定制
          customization: {
            autosave: true,//是否自动保存
            chat: true,
            comments: false,
            help: true,
            // "hideRightMenu": false,//定义在第一次加载时是显示还是隐藏右侧菜单。 默认值为false
            //是否显示插件
            plugins: true,
            mentionShare: true,
            // logo: {
            //   image: "https://example.com/logo.png",
            //   imageDark: "https://example.com/dark-logo.png",
            //   url: "http://hy.kingdomsoft.cn/scmy"
            // },
          },
          forcesave: true,
          user: {
            id: option.user.id,
            name: option.user.name
          },
          mode: option.model ? option.model : 'edit',
        },
        width: '100%',
        height: '100%',
        token: option.token || ''
      }

      // eslint-disable-next-line no-undef,no-unused-vars
      this.docEditor = new DocsAPI.DocEditor('vabOnlyOffice', config)

    },
    getFileType (fileType) {
      let docType = ''
      let fileTypesDoc = [
        'doc', 'docm', 'docx', 'dot', 'dotm', 'dotx', 'epub', 'fodt', 'htm', 'html', 'mht', 'odt', 'ott', 'pdf', 'rtf', 'txt', 'djvu', 'xps',
      ]
      let fileTypesCsv = [
        'csv', 'fods', 'ods', 'ots', 'xls', 'xlsm', 'xlsx', 'xlt', 'xltm', 'xltx',
      ]
      let fileTypesPPt = [
        'fodp', 'odp', 'otp', 'pot', 'potm', 'potx', 'pps', 'ppsm', 'ppsx', 'ppt', 'pptm', 'pptx',
      ]
      if (fileTypesDoc.includes(fileType)) {
        docType = 'text'
      }
      if (fileTypesCsv.includes(fileType)) {
        docType = 'spreadsheet'
      }
      if (fileTypesPPt.includes(fileType)) {
        docType = 'presentation'
      }
      return docType
    }
  },
}
</script>

引用传参

<vab-only-office :option="option" />

option属性

  • templateUrl 为minio文件服务器对应文件的url
  • editUrl 为后端项目开发的回调函数
const getFile = (record) => {
  const userinfo = useUserStore()
  let type = url(record.url)
  show.value = true
  option.url = record.url ? record.url : templateUrl
  option.isEdit = true
  option.lang = 'zh-CN'
  option.title = record.name
  option.fileType = type
  option.isPrint = false
  option.user = { id: userinfo.getUserInfo.id, name: userinfo.getUserInfo.name }
  option.editUrl = `${editsUrl}?reportTemplateId=` + record.id
  // console.log(111)
}

1.3.2 回调函数

@PostMapping("/callback")
@ApiOperation(value = "编辑文档时回调接口")
public void saveDocumentFile(HttpServletRequest request, HttpServletResponse response,
                             @RequestParam("reportTemplateId") Integer reportTemplateId) throws IOException {
    AssertUtil.notNull(reportTemplateId, "报告模板不可为空");
    PrintWriter writer = response.getWriter();

    Scanner scanner = new Scanner(request.getInputStream()).useDelimiter("\A");
    String body = scanner.hasNext() ? scanner.next() : "";
    log.info("body:{}", body);
    JSONObject jsonObj = JSONUtil.parseObj(body);
    Integer status = jsonObj.getInt("status");
    /**
     * 定义文档的状态。可以有以下值:
     * 1 - 正在编辑文档,
     * 2 - 文档已准备好保存,
     * 3 - 发生文档保存错误,
     * 4 - 文档已关闭,没有任何更改,
     * 6 - 正在编辑文档,但保存了当前文档状态,
     * 7 - 强制保存文档时发生错误。
     */
    if (status == 2) {
        String downloadUri = jsonObj.getStr("url");
        log.info("downloadUri:{}", downloadUri);
        reportTemplateService.saveReportTemplate(downloadUri,reportTemplateId);
    }
    writer.write("{"error":0}");
}

它在文档关闭以进行编辑后10 秒收到,该用户的标识符是最后一个将更改发送到文档编辑服务的用户。使用来自对文件进行最后更改的用户的callbackUrl 。

回调函数必须保证onlyoffice能正常访问到的,否则会提示文档无法保存。权限也会导致此问题

这样就配置好了,如下就可以在线编码文档了 image.png api文档 api.onlyoffice.com/editors/bas…

2 集成poi-tl

poi-tl(poi template language)是Word模板引擎,使用Word模板和数据创建很棒的Word文档

2.1 依赖

<dependency> <groupId>com.deepoove</groupId> <artifactId>poi-tl</artifactId> <version>1.12.0</version> </dependency>

可能会引起依赖冲突 poi-tl依赖的apache-poi版本是5.2.2+,如果你的项目引用了低版本,请升级或删除。

2.2 基础配置

  1. 文本 {{var}}
  2. 图片标签以@开始 {{@var}}
  3. 表格标签以#开始 {{#var}}
  4. 列表标签以*开始 {{*var}}
  5. 区块对由前后两个标签组成,开始标签以?标识,结束标签以/标识 {{?sections}}{{/sections}}
  6. 嵌套又称为导入、包含或者合并,以+标识 {{+var}}

2.3 多图片使用

  • 模板配置

{{?videoDatas}}{{@url}}{{/videoDatas}}

  • 后端数据填充
private void putPig(BisExperiment bisExperiment, Map<String, Object> map) {
    String videoData = bisExperiment.getVideoData();
    if (StrUtil.isNotBlank(videoData)) {
        List<Map<String, PictureRenderData>> videoDatas = Lists.newArrayList();
        Arrays.asList(videoData.split(StringPool.COMMA)).stream().forEach(t -> {
            Map<String, PictureRenderData> pictureRenderDataHashMap = new HashMap<>();
            pictureRenderDataHashMap.put("url", Pictures.ofUrl(t).create());
            videoDatas.add(pictureRenderDataHashMap);
        });
        map.put("videoDatas", videoDatas);
    }
}

2.4 表格使用

  • 模板配置

image.png

  • 后端数据填充
private void putEquipTable(BisExperiment bisExperiment, Map<String, Object> map, ConfigureBuilder builder) {
    List<ExperimentEquipment> equipments = experimentEquipmentService.listByIds(Arrays.asList(bisExperiment.getEquipmentIds().split(StringPool.COMMA)));

    LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
    //这里可以指定一个config类,用来指定一些规则,也可以改变模板中{{}}的这种格式
    builder.bind("equipments",policy);
    map.put("equipments", equipments);
}

2.5 图表使用

  • 模板配置
  1. 插入图表

image.png 2. 图表配置

image.png

image.png

  • 后端数据填充
/**
 * 组装曲线数据
 * @param map
 */
private void putChart(Map<String, Object> map) {
    ChartMultiSeriesRenderData chart = Charts
            .ofMultiSeries("ChartTitle", new String[] { "中文", "English" })
            .addSeries("countries", new Double[] { 15.0, 6.0 })
            .addSeries("speakers", new Double[] { 223.0, 119.0 })
            .create();
    map.put("barChart", chart);
}

2.6 区块对使用

区块对由前后两个标签组成,开始标签以?标识,结束标签以/标识:{{?sections}}{{/sections}}

区块对开始和结束标签中间可以包含多个图片、表格、段落、列表、图表等,开始和结束标签可以跨多个段落,也可以在同一个段落,但是如果在表格中使用区块对,开始和结束标签必须在同一个单元格内,因为跨多个单元格的渲染行为是未知的。

  • 模板配置

image.png

  • 后端数据填充
@Data
public class EquipmentPoiDataDTO {

    @ApiModelProperty("试验设备名称")
    private String equipName;

    @ApiModelProperty("设定参数表格")
    private TableRenderData settingParam;

    @ApiModelProperty("曲线数据")
    private ChartMultiSeriesRenderData lineChart;
}
map.put("equipmentDatas",equipmentPoiDataDTOS);

2.7 模板导出

  • 根据url获取模板流
//下载模板的文件流
InputStream inputStream = fileService.download(template.getUrl().replace(endpoint + bucket, StringPool.EMPTY));
  • 根据模板填充数据,并获取输出流
    private ByteArrayInputStream getByteArrayInputStream(Map<String, Object> map, InputStream inputStream, ConfigureBuilder builder) throws Exception {
        //创建文件输出流
        ByteArrayOutputStream fos = new ByteArrayOutputStream();

        XWPFTemplate compile = XWPFTemplate.compile(inputStream,builder.build());
        compile.render(map);
        compile.writeAndClose(fos);

//        //创建文档
//        MyXWPFDocument xwpfDocument = new MyXWPFDocument(inputStream);
//        WordExportUtil.exportWord07(xwpfDocument, map);
//        //上传数据
//        xwpfDocument.write(fos);
        //转换成输入流
        return new ByteArrayInputStream(fos.toByteArray());
    }
  • 最后导出,或者重新存储回MINIO

3 效果展示

image.png

image.png

4 总结

通过onlyoffice在线配置模板,模板通过poi-tl语法配置我们要的效果,通过minio存储模板,和导出根据模板生成文档,最后通过onlyoffice对生成的文档进行修改,达到全程在线操作的效果.