安装
wangeditor5
官网:https://www.wangeditor.com/v5/
yarn add @wangeditor/editor
# 或者 npm install @wangeditor/editor --save
yarn add @wangeditor/editor-for-vue
# 或者 npm install @wangeditor/editor-for-vue --save
mammoth.js
官网:https://github.com/mwilliamson/mammoth.js
npm install mammoth
若出现依赖包下载失败的情况,可能是镜像问题,可选择使用国内镜像,参考文档:https://blog.csdn.net/hyk521/article/details/140706064
使用
editor.vue:
<template>
<div style="border: 1px solid #ccc;">
<input type="file" id="weWordBtn" style="display:none;"
accept="application/vnd.openxmlformats-officedocument.wordprocessingml.document"/>
<Toolbar
style="border-bottom: 1px solid #ccc"
:editor="editor"
:defaultConfig="toolbarConfig"
:mode="mode"
/>
<Editor
:style="editorStyle"
v-model="html"
:defaultConfig="editorConfig"
:mode="mode"
@onCreated="onCreated"
@onChange="onChange"
@customPaste="customPaste"
/>
</div>
</template>
<script>
import Vue from 'vue';
import {Boot, DomEditor} from '@wangeditor/editor';
import {Editor, Toolbar} from '@wangeditor/editor-for-vue';
import '@wangeditor/editor/dist/css/style.css';
import {uploadPic} from "@/api/fileUpload/upload";
import mammoth from "mammoth";
import {Loading} from "element-ui";
//自定义新菜单
class wordImportMenu {
constructor() {
this.title = 'word导入';
this.iconSvg = '<svg t="1721893685983" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12124" width="16" height="16"><path d="M563.2 1006.933333s-3.413333 0 0 0l-549.546667-102.4c-6.826667-3.413333-13.653333-10.24-13.653333-17.066666V170.666667c0-6.826667 6.826667-13.653333 13.653333-17.066667l546.133334-136.533333c3.413333 0 10.24 0 13.653333 3.413333s6.826667 6.826667 6.826667 13.653333v955.733334c0 3.413333-3.413333 10.24-6.826667 13.653333-3.413333 3.413333-6.826667 3.413333-10.24 3.413333zM34.133333 873.813333l512 95.573334V54.613333L34.133333 184.32v689.493333z" fill="" p-id="12125"></path><path d="M1006.933333 938.666667h-443.733333c-10.24 0-17.066667-6.826667-17.066667-17.066667s6.826667-17.066667 17.066667-17.066667H989.866667v-785.066666H563.2c-10.24 0-17.066667-6.826667-17.066667-17.066667s6.826667-17.066667 17.066667-17.066667h443.733333c10.24 0 17.066667 6.826667 17.066667 17.066667v819.2c0 10.24-6.826667 17.066667-17.066667 17.066667zM358.4 699.733333c-6.826667 0-13.653333-6.826667-17.066667-13.653333l-68.266666-249.173333-68.266667 249.173333c-3.413333 6.826667-6.826667 13.653333-17.066667 13.653333-6.826667 0-13.653333-3.413333-17.066666-10.24l-102.4-307.2c-3.413333-10.24 3.413333-17.066667 10.24-20.48 10.24-3.413333 17.066667 3.413333 20.48 10.24l85.333333 252.586667 71.68-252.586667c3.413333-13.653333 27.306667-13.653333 34.133333 0l71.68 252.586667 85.333334-252.586667c3.413333-10.24 13.653333-13.653333 20.48-10.24 10.24 3.413333 13.653333 13.653333 10.24 20.48l-102.4 307.2c-3.413333 6.826667-10.24 10.24-17.066667 10.24z" fill="" p-id="12126"></path><path d="M904.533333 256h-341.333333c-10.24 0-17.066667-6.826667-17.066667-17.066667s6.826667-17.066667 17.066667-17.066666h341.333333c10.24 0 17.066667 6.826667 17.066667 17.066666s-6.826667 17.066667-17.066667 17.066667zM904.533333 392.533333h-334.506666c-10.24 0-17.066667-6.826667-17.066667-17.066666s6.826667-17.066667 17.066667-17.066667h334.506666c10.24 0 17.066667 6.826667 17.066667 17.066667s-6.826667 17.066667-17.066667 17.066666zM904.533333 529.066667h-341.333333c-10.24 0-17.066667-6.826667-17.066667-17.066667s6.826667-17.066667 17.066667-17.066667h341.333333c10.24 0 17.066667 6.826667 17.066667 17.066667s-6.826667 17.066667-17.066667 17.066667zM904.533333 665.6h-341.333333c-10.24 0-17.066667-6.826667-17.066667-17.066667s6.826667-17.066667 17.066667-17.066666h341.333333c10.24 0 17.066667 6.826667 17.066667 17.066666s-6.826667 17.066667-17.066667 17.066667zM904.533333 802.133333H580.266667c-10.24 0-17.066667-6.826667-17.066667-17.066666s6.826667-17.066667 17.066667-17.066667h324.266666c10.24 0 17.066667 6.826667 17.066667 17.066667s-6.826667 17.066667-17.066667 17.066666z" fill="" p-id="12127"></path></svg>';
this.tag = 'button';
}
//菜单是否需要激活(如选中加粗文本,“加粗”菜单会激活),用不到则返回 false
isActive(editor) {
return false;
}
//获取菜单执行时的 value,用不到则返回空字符串或 false
getValue(editor) {
return '';
}
//菜单是否需要禁用(如选中 H1 ,“引用”菜单被禁用),用不到则返回 false
isDisabled(editor) {
return false; // or true
}
//点击菜单时触发的函数
exec(editor, value) {
document.getElementById('weWordBtn').click();
}
}
const wordImportConf = {
key: 'wordImport',
factory() {
return new wordImportMenu();
}
};
Boot.registerMenu(wordImportConf);
export default Vue.extend({
components: {Editor, Toolbar},
props: {
/* 编辑器的内容 */
value: {
type: String,
default: "",
},
/* 高度 */
height: {
type: Number,
default: 500,
},
/* 是否只读 */
readOnly: {
type: Boolean,
default: false
},
/* 编辑器内提示语 */
placeholder: {
type: String,
default: '请输入内容...'
}
},
data() {
return {
editor: null,
html: '',
toolbarConfig: {
modalAppendToBody: false,
toolbarKeys: ['headerSelect', 'blockquote', '|', 'bold', 'underline', 'italic', 'through', 'code', 'sup', 'sub',
'clearStyle', '|', 'color', 'bgColor', 'fontSize', 'lineHeight', '|', 'bulletedList', 'numberedList', 'todo',
{
'key': 'group-justify',
'title': '对齐',
'iconSvg': '<svg viewBox=\"0 0 1024 1024\"><path d=\"M768 793.6v102.4H51.2v-102.4h716.8z m204.8-230.4v102.4H51.2v-102.4h921.6z m-204.8-230.4v102.4H51.2v-102.4h716.8zM972.8 102.4v102.4H51.2V102.4h921.6z\"></path></svg>',
'menuKeys': ['justifyLeft', 'justifyRight', 'justifyCenter', 'justifyJustify']
},
{
'key': 'group-indent',
'title': '缩进',
'iconSvg': '<svg viewBox=\"0 0 1024 1024\"><path d=\"M0 64h1024v128H0z m384 192h640v128H384z m0 192h640v128H384z m0 192h640v128H384zM0 832h1024v128H0z m0-128V320l256 192z\"></path></svg>',
'menuKeys': ['indent', 'delIndent']
},
'|', 'insertLink', 'uploadImage', 'insertTable', 'codeBlock', 'divider', '|', 'undo', 'redo', '|', '|', 'fullScreen'
],
// excludeKeys: ['fontFamily', 'emotion', 'group-video']
insertKeys: {
index: 32,
keys: ['wordImport']
}
},
editorConfig: {
placeholder: this.placeholder,
readOnly: this.readOnly,
autoFocus: true,
MENU_CONF: {
'uploadImage': {
timeout: 300000,
fieldName: 'files',
maxNumberOfFiles: 10,
allowedFileTypes: ['image/jpeg', 'image/png'],
// allowedFileTypes: ['image/*'],
maxFileSize: 1024 * 1024 * 5,
server: process.env.VUE_APP_BASE_API + '/system/fileStorage/uploadPic',
onError: (e, t, n) => {
this.$message.error('图片上传失败:' + t);
},
onFailed: (e, t) => {
this.$message.error('图片上传失败:未知错误');
},
onSuccess: (e, t) => {
this.$message.success('图片上传成功');
},
customInsert(resp, insertFn) {
insertFn(process.env.VUE_APP_BASE_API + resp.data.url, '', '');
}
}
}
},
mode: 'default'
}
},
computed: {
editorStyle() {
return 'overflow-y: hidden;height: ' + this.height + 'px;';
}
},
watch: {
value: {
handler(val) {
if (val !== this.html) {
this.html = val === null ? "" : val;
}
},
immediate: true,
},
readOnly: {
handler(flag) {
if (this.editor !== null) {
if (flag) {
this.editor.disable();
} else {
this.editor.enable();
}
}
}
}
},
methods: {
onCreated(editor) {
this.editor = Object.seal(editor);
console.log('editor.getConfig()', editor.getConfig())
console.log('editor.getAllMenuKeys()', editor.getAllMenuKeys())
console.log('editor.getConfig().hoverbarKeys', editor.getConfig().hoverbarKeys)
console.log('editor.getMenuConfig(uploadImage)', editor.getMenuConfig('uploadImage'))
},
onChange(editor) {
console.log('toolbar.getConfig().toolbarKeys', DomEditor.getToolbar(editor).getConfig().toolbarKeys)
console.log('editor.children ', editor.children)
this.$emit('onChange', {editor: editor, html: editor.getHtml(), text: editor.getText()});
},
customPaste(editor, event, callback) {
console.log('ClipboardEvent 粘贴事件对象', event)
// const html = event.clipboardData.getData('text/html') // 获取粘贴的 html
// const text = event.clipboardData.getData('text/plain') // 获取粘贴的纯文本
// const rtf = event.clipboardData.getData('text/rtf') // 获取 rtf 数据(如从 word wsp 复制粘贴)
// 自定义插入内容
// editor.insertText('xxx')
// 返回 false ,阻止默认粘贴行为
// event.preventDefault()
// callback(false) // 返回值(注意,vue 事件的返回值,不能用 return)
// 返回 true ,继续默认的粘贴行为
// callback(true)
},
base64ToBlob(imageType, imageBuffer) {
let byteCharacters = atob(imageBuffer);
let byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
let byteArray = new Uint8Array(byteNumbers);
let blob = new Blob([byteArray], {type: imageType});
let imageName = 'e' + new Date().getTime();
return new File([blob], imageName, {type: imageType});
}
},
mounted() {
document.getElementById("weWordBtn").addEventListener("change", (event) => {
let requestLoading = Loading.service({
fullscreen: true,
text: 'word解析中......',
spinner: 'el-icon-loading',
background: 'rgba(217,217,217,0.2)'
});
let editorObj = this.editor;
let _this = this;
if (event.target.files && event.target.files.length > 0) {
let file = event.target.files[0];
mammoth.convertToHtml({arrayBuffer: file.arrayBuffer()}, {
ignoreEmptyParagraphs: true,
transformDocument: mammoth.transforms.paragraph((element) => {
console.log('element', element)
if (element.styleName === null) {
if (element.children && element.children.length > 0) {
for (let i = 0; i < element.children.length; i++) {
let secondChild = element.children[i];
if (secondChild.type === 'hyperlink') {
secondChild.targetFrame = '_blank';
} else if (secondChild.type === 'run') {
if (secondChild.children && secondChild.children.length > 0) {
if (i === 0 && secondChild.children[0].type === 'text') {
let originVal = secondChild.children[0].value;
secondChild.children[0].value = ' ' + originVal;
}
if (secondChild.highlight !== null) {
secondChild.style = 'background-color: ' + secondChild.highlight + ';';
for (let j = 0; j < secondChild.children.length; j++) {
let thirdChild = secondChild.children[j];
thirdChild.style = 'background-color: ' + secondChild.highlight + ';';
}
}
}
} else {
}
}
}
}
return element;
}),
styleMap: ["u => u"],
convertImage: mammoth.images.imgElement(function (image) {
return image.read('base64').then(async (imageBuffer) => {
//本地图片上传至服务器
let result = '';
let imgFile = _this.base64ToBlob(image.contentType, imageBuffer);
let formData = new FormData();
formData.append('files', imgFile);
await uploadPic(formData).then(resp => {
if (resp.code === '200') {
result = process.env.VUE_APP_BASE_API + resp.data.url;
}
}).catch(e => {
console.error('uploadPic-error : ', e)
});
return {src: result}
});
})
}).then(function (result) {
console.log('result', result)
if (result.messages.length > 0) {
_this.$message.warning('发生错误:' + result.messages[0].message);
} else {
if (editorObj !== null) {
editorObj.clear();
editorObj.dangerouslyInsertHtml(result.value);
}
}
requestLoading.close();
}).catch(function (error) {
console.error(error);
requestLoading.close()
});
}
});
},
beforeDestroy() {
if (this.editor !== null) {
this.editor.destroy();
}
}
});
</script>
<style scoped>
</style>
Test.vue:
<template>
<div>
<h1 style="text-align: center">editor测试</h1>
<div style="width: 80%;margin: 0 auto;">
<editor :value="editorHtml" :height="450" :readOnly="readOnly" @onChange="onChange"/>
<div class="test_count">
<span>{{editorCount}} 字</span>
</div>
</div>
<div style="text-align: center;margin-top: 25px;">
<el-button type="primary" @click="control">{{controlText}}</el-button>
<el-button type="primary" @click="submit">提交</el-button>
</div>
</div>
</template>
<script>
import Editor from './editor';
export default {
name: "Test",
components: {Editor},
data() {
return {
readOnly: false,
controlText: '禁用',
editorHtml: '',
editorText: ''
}
},
computed: {
editorCount() {
return this.editorText.replace(/\s*/g, "").replace(/\n/g, "").length;
}
},
mounted() {
},
methods: {
onChange(data) {
if (data.html !== this.editorHtml) {
this.editorHtml = data.html;
this.editorText = data.text;
}
},
control() {
this.readOnly = !this.readOnly;
if (this.readOnly) {
this.controlText = '启用';
} else {
this.controlText = '禁用';
}
},
submit() {
console.log('editorHtml', this.editorHtml)
console.log('editorText', this.editorText)
}
},
};
</script>
<style scoped>
.test_count {
height: 40px;
line-height: 40px;
text-align: right;
padding-right: 20px;
border: 1px solid #ccc;
border-top: none;
}
</style>
页面效果:
word导入问题与解决方案
问题:
mammoth 仅支持简单的样式,对于背景色、颜色字体等高级样式无法支持。
解决方案:
1、修改 mammoth.js 的源码,参考文档:https://blog.csdn.net/Jioho_chen/article/details/124699665
2、前端加一个按钮或触发器,后端 Java 使用 poi 解析 word 内容,具体参考:https://www.cnblogs.com/ismallboy/p/12584761.html
若有其他方法,欢迎留言探讨。