基于Vue2.X对WangEditor 5富文本编辑器进行封装与使用,支持单个或多个图片点击、粘贴、拖拽上传,Vue3.X项目也可直接使用

一直想要一个神奇多功能的富文本编辑器,经同事介绍这个 WangEditor 富文本编辑器,发现挺好用的。

传送门:wangEditor

于是用Vue2.X语法自行封装一下,这样在复用组件时,可以少写一些代码,直接组件引用即可。另外,Vue3.X项目也可直接使用,因为Vue3.X是兼容Vue2.X语法的。

注意:如果反复创建与销毁该组件,记得用上v-if

<WangEditor
  ref="wangEditorRef"
  v-if="isShowWangEditor"
  :disabled="editorParams.isDisabled"
  :editorParams="editorParams">
</WangEditor>

导入依赖包,注意Vue2.X和Vue3.X项目导入的依赖包版本会有所不同

// 查看 @wangeditor/editor 版本列表
npm view @wangeditor/editor versions --json

// 导入 @wangeditor/editor 依赖包
npm i --save @wangeditor/editor@5.1.15

// 查看 @wangeditor/editor-for-vue 版本列表
npm view @wangeditor/editor-for-vue versions --json

// 导入 @wangeditor/editor-for-vue 依赖包
npm i --save @wangeditor/editor-for-vue@5.1.12

父组件:index.vue

<template>
  <div style="padding: 100px">
    <WangEditor
      ref="wangEditorRef1"
      :disabled="editorParams1.isDisabled"
      :editorParams="editorParams1">
    </WangEditor>

    <br/>

    <WangEditor
      ref="wangEditorRef2"
      :disabled="editorParams2.isDisabled"
      :editorParams="editorParams2">
    </WangEditor>

    <br/>

    <p align="center">
      <el-button type="primary" @click="handleSubmitClick">完成</el-button>
    </p>
  </div>
</template>

<script>
import WangEditor from './components/wangEditor'

export default {
  components: {
    WangEditor
  },
  data: () => ({
    // editorParams1 必填非空的富文本参数
    editorParams1: {
      content: '', // 富文本内容
      placeholder: '请填写内容', // 富文本占位内容
      uploadImageUrl: 'http://IP:Port/api/uploadFileAction', // 富文本上传图片的地址
      height: '300px', // 富文本最小高度
      isDisabled: false // 富文本是否禁用
    },

    // editorParams2 必填非空的富文本参数
    editorParams2: {
      content: 'HelloWorld!', // 富文本内容
      placeholder: '请填写内容', // 富文本占位内容
      uploadImageUrl: 'http://IP:Port/api/uploadFileAction', // 富文本上传图片的地址
      height: '300px', // 富文本最小高度
      isDisabled: true // 富文本是否禁用
    }
  }),
  methods: {
    /**
     * 获取富文本内容
     */
    async handleSubmitClick () {
      let refs = await this.$refs

      let wangEditorRef1 = refs.wangEditorRef1
      if (wangEditorRef1 != null) {
        console.log(wangEditorRef1.getEditorHtml())
        console.log(wangEditorRef1.getEditorText())
      }

      let wangEditorRef2 = refs.wangEditorRef2
      if (wangEditorRef2 != null) {
        console.log(wangEditorRef2.getEditorHtml())
        console.log(wangEditorRef2.getEditorText())
      }
    }
  }
}
</script>

子组件:wangEditor.vue

<template>
  <div>
    <div class="w-e-for-vue">
      <!-- 工具栏 -->
      <Toolbar
        class="w-e-for-vue-toolbar"
        :editor="editor"
        :defaultConfig="toolbarConfig">
      </Toolbar>

      <!-- 编辑器 -->
      <Editor
        class="w-e-for-vue-editor"
        :style="'height: ' + height"
        :disabled="isDisabled"
        :defaultConfig="editorConfig"
        v-model="content"
        @onChange="onChange"
        @onCreated="onCreated">
      </Editor>
    </div>
  </div>
</template>

<script>
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'

export default {
  name: 'wangEditor',
  components: { Editor, Toolbar },
  props: [
    'editorParams'
  ],
  data () {
    return {
      editor: null, // 富文本编辑器对象
      content: null, // 富文本内容
      placeholder: null, // 富文本占位内容
      uploadImageUrl: null, // 富文本上传图片的地址
      height: '300px', // 富文本最小高度
      isDisabled: false, // 富文本是否禁用

      // 工具栏配置
      toolbarConfig: {
        // toolbarKeys: [], // 显示指定的菜单项
        // excludeKeys: [], // 隐藏指定的菜单项
      },

      // 编辑器配置
      editorConfig: {
        placeholder: '请输入内容......',
        MENU_CONF: ['uploadImage']
      }
    }
  },
  watch: {
    /**
     * 深度监听富文本参数
     */
    'editorParams': {
      handler: function (newVal, oldVal) {
        if (newVal != null) {
          this.content = newVal.content
          this.editorConfig.placeholder = this.placeholder
          this.uploadImageUrl = newVal.uploadImageUrl
          this.setUploadImageConfig()
          this.height = newVal.height
          this.isDisabled = newVal.isDisabled
        }
      },
      immediate: true,
      deep: true
    }
  },
  methods: {
    /**
     * 实例化富文本编辑器
     * 文档链接:https://www.wangeditor.com/
     */
    onCreated (editor) {
      this.editor = Object.seal(editor)
      this.setIsDisabled()
    },

    /**
     * 监听富文本编辑器
     */
    onChange (editor) {
      // console.log('onChange =>', editor.getHtml())
    },

    /**
     * this.editor.getConfig().spellcheck = false
     * 由于在配置中关闭拼写检查,值虽然设置成功,但是依然显示红色波浪线
     * 因此在富文本编辑器组件挂载完成后,操作 Dom 元素设置拼写检查 spellcheck 为假
     */
    async setSpellCheckFasle () {
      let doms = await document.getElementsByClassName('w-e-scroll')
      for (let vo of doms) {
        if (vo) {
          if (vo.children.length > 0) {
            vo.children[0].setAttribute('spellcheck', 'false')
          }
        }
      }
    },

    /**
     * 设置富文本是否禁用
     */
    async setIsDisabled () {
      if (this.editor) {
        this.isDisabled ? (this.editor.disable()) : (this.editor.enable())
      }
    },

    /**
     * 上传图片配置
     */
    setUploadImageConfig () {
      this.editorConfig.placeholder = this.placeholder
      this.editorConfig.MENU_CONF['uploadImage'] = {
        fieldName: 'files', // 文件字段名, 默认值 'wangeditor-uploaded-image'
        maxFileSize: 1 * 1024 * 1024, // 单个文件的最大体积限制,默认为 2M,此次设置为 1M
        maxNumberOfFiles: 10, // 最多可上传几个文件,默认为 100
        allowedFileTypes: ['image/*'], // 选择文件时的类型限制,默认为 ['image/*'] ,若不想限制,则设置为 []
        meta: {// 自定义上传参数,例如传递验证的 token 等,参数会被添加到 formData 中,一起上传到服务端
          token: 'xxx',
          otherKey: 'yyy'
        },
        metaWithUrl: false, // 将 meta 拼接到 URL 参数中,默认 false
        headers: {// 设置 HTTP 请求头信息
          // ...
        },
        server: this.uploadImageUrl, // 上传图片接口地址
        withCredentials: false, // 跨域是否传递 cookie ,默认为 false
        timeout: 5 * 1000, // 超时时间,默认为 10 秒,此次设置为 5 秒

        // 上传之前触发
        onBeforeUpload (file) {
          return file
        },

        // 上传进度的回调函数
        onProgress (progress) {
          console.log('progress', progress)
        },

        // 单个文件上传成功之后
        onSuccess (file, res) {
          console.log(`${file.name} 上传成功`, res)
        },

        // 单个文件上传失败
        onFailed (file, res) {
          console.log(`${file.name} 上传失败`, res)
        },

        // 上传错误,或者触发 timeout 超时
        onError (file, err, res) {
          console.log(`${file.name} 上传出错`, err, res)
        }
      }
    },

    /**
     * 获取编辑器文本内容
     */
    getEditorText () {
      const editor = this.editor
      if (editor != null) {
        return editor.getText()
      }
    },

    /**
     * 获取编辑器Html内容
     */
    getEditorHtml () {
      const editor = this.editor
      if (editor != null) {
        return editor.getHtml()
      }
    }
  },
  created () {

  },
  mounted () {
    this.setSpellCheckFasle() // 设置拼写检查 spellcheck 为假
    document.activeElement.blur() // 取消富文本自动聚焦且禁止虚拟键盘弹出
  },
  /**
   * 销毁富文本编辑器
   */
  beforeDestroy () {
    const editor = this.editor
    if (editor != null) {
      editor.destroy()
    }
  }
}
</script>

<style src="@wangeditor/editor/dist/css/style.css"></style>

<style lang="less" scoped>
  .w-e-full-screen-container {
    z-index: 99;
  }

  .w-e-for-vue {
    margin: 0;
    border: 1px solid #ccc;

    .w-e-for-vue-toolbar {
      border-bottom: 1px solid #ccc;
    }

    .w-e-for-vue-editor {
      height: auto;

      /deep/ .w-e-text-container {

        .w-e-text-placeholder {
          top: 6px;
          color: #666;
        }

        pre {

          code {
            text-shadow: unset;
          }
        }

        p {
          margin: 5px 0;
          font-size: 14px; // 设置编辑器的默认字体大小为14px
        }
      }
    }
  }
</style>

服务端 IndexController.java 的上传图片方法

@Value("${system.upload-file-path}")
private String uploadFilePath;

@ResponseBody
@RequestMapping("uploadImageAction")
@CrossOrigin
public HashMap<String, Object> uploadImageAction(@RequestParam List<MultipartFile> files) {
    HashMap<String, Object> responseMap = new HashMap<>();
    String newsFilePath = uploadFilePath;
    try {
        List<HashMap<String, String>> newFileList = new ArrayList<>();
        for (MultipartFile file : files) {
            String uuidStr = UUID.randomUUID().toString();
            String uuid = uuidStr.substring(0, 8) + uuidStr.substring(9, 13) + uuidStr.substring(14, 18) + uuidStr.substring(19, 23) + uuidStr.substring(24);

            String OldFileName = file.getOriginalFilename();// 原文件名,如:hello.pdf
            int beginIndex = OldFileName.lastIndexOf(".");// 从后匹配"."
            String newFileName = uuid + OldFileName.substring(beginIndex);// 新文件名,如uuid.pdf
            String destFileName = newsFilePath + File.separator + newFileName;// 存储路径 + 新文件名

            // 复制文件到指定目录
            File destFile = new File(destFileName);
            destFile.getParentFile().mkdirs();
            file.transferTo(destFile);

            String url = "http://IP:Port/uploadFilePath/" + newFileName;
            HashMap<String, String> map = new HashMap<>();
            map.put("url", url);
            map.put("alt", "图片描述,非必须");
            map.put("href", "图片的链接,非必须");
            newFileList.add(map);
        }
        responseMap.put("data", newFileList);
        responseMap.put("errno", 0);// 上传成功:值是数字0,不能是字符串
    } catch (FileNotFoundException e) {
        e.printStackTrace();
        responseMap.put("errno", 1);// 上传失败:值不是数字0
        responseMap.put("message", "文件无法找到");
    } catch (IOException e) {
        e.printStackTrace();
        responseMap.put("errno", 1);// 上传失败:值不是数字0
        responseMap.put("message", "文件上传失败");// 上传失败:值不是数字0
    }

    return responseMap;
}

服务端 StaticResourceConfig.java 静态资源配置类

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@Configuration
public class StaticResourceConfig extends WebMvcConfigurationSupport {

    @Value("${system.upload-file-path}")
    private String uploadFilePath;

    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
        registry.addResourceHandler("/uploadFilePath/**").addResourceLocations("file:" + uploadFilePath);
    }
}

服务端 application.yml 配置文件

server:
  port: 8091

system:
  upload-file-path: "D:/uploadFilePath/"

最终效果: