vue2使用wangeditor5及word导入解析的实现与问题

avatar
作者
筋斗云
阅读量:0

安装

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}}&nbsp;字</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

        若有其他方法,欢迎留言探讨。

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!