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

安装

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

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

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值