TinyMCE-vue 组件实现上传word解析后返回到富文本编辑器中

26 篇文章 2 订阅
6 篇文章 1 订阅

组件中一个重要的属性介绍

vue-tinymce官网
在这里插入图片描述

@change 该属性此时用到的有两个,一个是input I一个是change

input: 代表用户输入的状态
change: 代表直接植入(如通过代码直接给value赋值)

具体代码附在后台代码之后

后台上传处理

word解析为html字符串

由于2003版doc文件处理比较麻烦,此处直接限制了文件类型为docx

使用了Apache POI xwpf包进行处理

处理过程中遇到了一个坑.

是这样的,poi处理获取图片时,按照文档图片的顺序,命名为image后加正整数,如image1,image2

这就导致一个问题,上传不同的word,会进行图片覆盖

另一个,有个问题,我用的是jeecgboot框架,一开始图片的根目录的上层是当前日期,如(2020-01-01),导致了前端上传成功后,立即进行页面刷新.目前并未找出原因.

但是解决方式就是,每个不同的文档的上层目录不同.我采用的是时间戳

代码

package org.jeecg.modules.sst.util;

import java.io.*;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.HashMap;
import java.util.Map;

import lombok.extern.slf4j.Slf4j;
import org.apache.poi.xwpf.converter.core.BasicURIResolver;
import org.apache.poi.xwpf.converter.core.FileImageExtractor;
import org.apache.poi.xwpf.converter.xhtml.XHTMLConverter;
import org.apache.poi.xwpf.converter.xhtml.XHTMLOptions;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;


/**
 * @author qinlei
 * @description todo
 * @date 2020/11/28 14:58
 */
@Slf4j
@Component
public class WordToHtml {

    private static String uploadpath;//yml文件配置的上传文件的根目录

    private static String staticUrl;//yml文件配置的nginx代理的静态资源根目录
    @Value(value = "${jeecg.path.upload}")
    public void setUploadpath(String uploadPath) {
        uploadpath = uploadPath;
    }
    @Value(value = "${jeecg.path.staticUrl}")
    public void setStaticUrl(String staticUrl) {
        WordToHtml.staticUrl = staticUrl;
    }

    /**
     * 2007版本word转换成html
     *
     * @throws IOException
     */
    public static Map<String, Object> Word2007ToHtml(MultipartFile file) {
        Map<String, Object> result = new HashMap<>();
        if (file.isEmpty()) {
            result.put("success", false);
            result.put("msg", "文件不存在");
            return result;
        } else {
            long milli = LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli();
            String filepath = uploadpath + File.separator + milli + File.separator;
            if (file.getOriginalFilename().endsWith(".docx") || file.getOriginalFilename().endsWith(".DOCX")) {
                ByteArrayOutputStream baos = null;
                try {
                    // 1) 加载word文档生成 XWPFDocument对象
                    InputStream in = file.getInputStream();
                    XWPFDocument document = new XWPFDocument(in);

                    // 2) 解析 XHTML配置 (这里设置IURIResolver来设置图片存放的目录)
                    File imageFolderFile = new File(filepath);
                    XHTMLOptions options = XHTMLOptions.create();
                    // 存放图片的文件夹
                    options.setExtractor(new FileImageExtractor(imageFolderFile));
                    // html中图片的路径
                    options.URIResolver(new BasicURIResolver(staticUrl+"/"+milli));
                    options.setIgnoreStylesIfUnused(false);
                    options.setFragment(true);

                    // 3) 将 XWPFDocument转换成XHTML
                    // OutputStream out = new FileOutputStream(new File(filepath + htmlName));
                    // XHTMLConverter.getInstance().convert(document, out, options);

                    //也可以使用字符数组流获取解析的内容
                    baos = new ByteArrayOutputStream();
                    XHTMLConverter.getInstance().convert(document, baos, options);
                    String content = baos.toString("utf-8");
                    result.put("success", true);
                    result.put("msg", "解析成功");
                    result.put("data", content);
                    return result;
                } catch (IOException e) {
                    result.put("success", false);
                    result.put("msg", "word解析失败!");
                    e.printStackTrace();
                    return result;
                } finally {
                    try {
                        if (baos != null) {
                            baos.close();
                        }
                    } catch (IOException e) {
                        log.error("IO关闭失败", e);
                        e.printStackTrace();
                    }
                }
            }else {
                result.put("success", false);
                result.put("msg", "请使用ms office 2007 版本及其以上! ");
                return result;
            }
        }

    }

}

前端代码

封装的JEditor组件

图片插入的处理:
因为我需要在数据库存储,转为base64显然不合适,所以也在后台处理上传到静态资源服务器,返回nginx代理的地址

<template>
  <div :id="selectorId">
    <editor v-if="!reloading" v-model="myValue" :init="init" :disabled="disabled" @onClick="onClick">
    </editor>
  </div>
</template>

<script>
import tinymce from 'tinymce/tinymce'
import Editor from '@tinymce/tinymce-vue'
import {
  setStore,
  getStore,
  clearStore
} from "@/utils/storage"
import "tinymce/icons/default/icons";
import 'tinymce/themes/silver/theme'
import 'tinymce/plugins/table'
import 'tinymce/plugins/lists'
// import 'tinymce/plugins/contextmenu'
import 'tinymce/plugins/wordcount'
// import 'tinymce/plugins/textcolor'
import 'tinymce/plugins/fullscreen'
// import 'tinymce/icons/default'
import 'tinymce/plugins/advlist'
import 'tinymce/plugins/toc'
import 'tinymce/plugins/preview'
import 'tinymce/plugins/charmap'
import 'tinymce/plugins/autosave'
import 'tinymce/plugins/image'

import {
  uploadAction,
  getFileAccessHttpUrl
} from '@/api/manage'
import {
  getVmParentByName,
  formatDate
} from '@/utils/util'

export default {
  components: {
    Editor
  },
  name: "JEditor",
  props: {
    height: {
      type: Number,
      required: false,
      default: 500
    },
    width: {
      type: Number,
      required: false,
      default: 500
    },
    value: {
      type: String,
      required: false
    },
    triggerChange: {
      type: Boolean,
      default: false,
      required: false
    },
    disabled: {
      type: Boolean,
      default: false
    },
    plugins: {
      type: [String, Array],
      default: 'lists advlist charmap indent2em table image wordcount preview fullscreen autosave'
    },
    toolbar: {
      type: [String, Array],
      default:
        'undo redo | formatselect removeformat | charmap bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | lists advlist | image  table | preview restoredraft fullscreen',
      branding: false
    }
  },
  data() {
    return {
      //初始化配置
      init: {
        language_url: '/tinymce/langs/zh_CN.js',
        language: 'zh_CN',
        skin_url: '/tinymce/skins/lightgray',
        height: this.height,
        width: this.width,
        plugins: this.plugins,
        statusbar: false, //隐藏状态栏
        toolbar: this.toolbar,
        branding: false,
        toolbar_mode: "sliding",
        menubar: false,
        autosave_restore_when_empty: true,
        autosave_ask_before_unload: false,
        autosave_interval: "20s",
        autosave_retention: "20m",
        toolbar_drawer: "floating",
        //图片插入时上传的处理
        images_upload_handler: (blobInfo, success) => {
          let formData = new FormData()
          formData.append('file', blobInfo.blob(), blobInfo.filename());
          formData.append('biz', formatDate("", "yyyy-MM-dd"));
          uploadAction(window._CONFIG['domianURL'] + "/sys/common/upload", formData).then((res) => {
            if (res.success) {
            //这是java处理为base64 未用
              if (res.message == 'local') {
                const img = 'data:image/jpeg;base64,' + blobInfo.base64()
                success(img)
              } else {
              //这里将地址转为http全地址
                let img = getFileAccessHttpUrl(res.message)
                success(img)
              }
            }
          })
        }
      },
      selectorId: new Date().getTime(), //避免重复使用同一个div编辑器
      myValue: this.value,
      reloading: false,
      inlineStyle: this.inline,
    }
  },
  mounted() {
    this.initATabsChangeAutoReload()
  },
  methods: {
    
    reload() {
      this.reloading = true
      this.$nextTick(() => this.reloading = false)
    },
    
    onClick(e) {
      this.$emit('onClick', e, tinymce)
    },
    //可以添加一些自己的自定义事件,如清空内容
    clear() {
      this.myValue = ''
    },
    
    /**
     * 自动判断父级是否是 <a-tabs/> 组件,然后添加事件监听,自动触发reload()
     *
     * 由于 tabs 组件切换会导致 tinymce 无法输入,
     * 只有重新加载才能使用(无论是vue版的还是jQuery版tinymce都有这个通病)
     */
    initATabsChangeAutoReload() {
      // 获取父级
      let tabs = getVmParentByName(this, 'ATabs')
      let tabPane = getVmParentByName(this, 'ATabPane')
      let modal = getVmParentByName(this, 'AModal')
      if (tabs && tabPane) {
        // 用户自定义的 key
        let currentKey = tabPane.$vnode.key
        // 添加事件监听
        tabs.$on('change', (key) => {
          // 切换到自己时执行reload
          if (currentKey === key) {
            this.reload()
          }
        })
      } else if (modal) {
        console.log("tinymce reload!")
        this.reload();
      }
    },
  },
  watch: {
    value(newValue) {
      this.myValue = (newValue == null ? '' : newValue)
    },
    //解决用户输入及代码直接植入的冲突
    myValue(newValue) {
      if (this.triggerChange) {
        this.$emit('change', newValue)
      } else {
        this.$emit('input', newValue)
      }
    }
  }
}
</script>
<style scoped>
</style>

使用

<j-editor :height="450" v-if="visible" :triggerChange="triggerChange" v-model="model.taskContent"/>
<a-upload v-if="upload" name="file" accept=".docx" :action="url.getWordContent" :headers="headers" @change="uploadChange">
    <a-button><a-icon type="upload"/>
         文件上传
    </a-button>
</a-upload>

uploadChange(info) {
        if (info.file.status === 'done') {
          //保证当前仅能看到当前上传的
          if (info.fileList.length > 1) {
            info.fileList.shift()
          }
          if (info.file.response.success == true) {
            this.triggerChange = true
            this.model.taskContent = info.file.response.data
            this.$message.success('上传成功',()=>{
              this.triggerChange = false
            })
          } else {
            this.$message.error(info.file.response.msg)
          }
        }
      },
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qlanto

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值