@change 该属性此时用到的有两个,一个是input I一个是change
input: 代表用户输入的状态
change: 代表直接植入(如通过代码直接给value赋值)
具体代码附在后台代码之后
由于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 = ''
},
/**
* 自动判断父级是否是 组件,然后添加事件监听,自动触发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)
}
}
},