安装
wangeditor5
官网:https://www.wangeditor.com/v5/
yarn add @wangeditor/editor
# 或者 npm install @wangeditor/editor --saveyarn 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"/><Toolbarstyle="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';}//菜单是否需要激活(如选中加粗文本,“加粗”菜单会激活),用不到则返回 falseisActive(editor) {return false;}//获取菜单执行时的 value,用不到则返回空字符串或 falsegetValue(editor) {return '';}//菜单是否需要禁用(如选中 H1 ,“引用”菜单被禁用),用不到则返回 falseisDisabled(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
若有其他方法,欢迎留言探讨。