vue 富文本编辑器 wangeditor 自定义上传图片 以及 解决 复制粘贴 word 没有图片的情况

发布时间 2023-12-02 15:11:50作者: 柯基与佩奇

本人比较喜欢用这一款编辑器,官方文档:(用于 Vue React | wangEditor),很详细。我主要来说说怎么使用customPaste 自定义粘贴的,怎么解决 复制粘贴 word ,没有图片的情况

主要是关于 wangeditor 在 vue2 的使用

效果图:

CHV)HRXJ[I744C32NX0]NL1.png

先把完整代码放这里:

<template>
  <div class="addpost_course">
    <div id="editor" style="border: 1px solid #ccc; width: 534px">
      <Toolbar
        style="border-bottom: 1px solid #ccc"
        :editor="editor"
        :defaultConfig="toolbarConfig"
        :mode="mode"
      />
      <Editor
        style="height: 200px; overflow-y: hidden"
        v-model="content"
        :defaultConfig="editorConfig"
        :mode="mode"
        @onCreated="onCreated"
        @onChange="onChange"
        @onDestroyed="onDestroyed"
        @onMaxLength="onMaxLength"
        @onFocus="onFocus"
        @onBlur="onBlur"
        @customAlert="customAlert"
        @customPaste="customPaste"
      />
    </div>
  </div>
</template>

<script>
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
import { Loading, Message } from "element-ui";
export default {
  components: { Editor, Toolbar },
  data() {
    return {
      // 富文本
      editor: null,
      // 富文本里面的所有内容(带标签的)
      content: "",
      toolbarConfig: {},
      editorConfig: {
        placeholder: "请输入内容...",
        // 所有的菜单配置,都要在 MENU_CONF 属性下
        MENU_CONF: {
          //配置上传图片
          uploadImage: {
            // 自定义上传图片 方法
            customUpload: this.uploadImg,
            // 自定义插入图片 方法
            customInsert: this.insertImg,
            //server必须要配置正确,我这里因为上传图片有点特殊,在下面方法配置了,所以此处不用配置地址
            // server: 'https://xwbdzzz.haiyan.gov.cn:10002/form/temp/update/ajax/img',

            maxFileSize: 4 * 1024 * 1024, // 1M
            // 最多可上传几个文件,默认为 100
            maxNumberOfFiles: 100,
            // 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []
            allowedFileTypes: [],
            // 自定义上传参数,例如传递验证的 token 等。参数会被添加到 formData 中,一起上传到服务端。
            fieldName: "file",
            meta: {
              //官网中把token放到了这里,但是请求的时候会看不到token
            },
            headers: {
              //所以token放这里
              // token: window.sessionStorage.token,
            },
            // 将 meta 拼接到 url 参数中,默认 false
            metaWithUrl: false,
            // 跨域是否传递 cookie ,默认为 false
            withCredentials: false,
            // 超时时间,默认为 10 秒
            timeout: 5 * 1000, // 5 秒

            // 上传之前触发
            // onBeforeUpload(file) {
            //   console.log(file);    // JS 语法
            //     // file 选中的文件,格式如 { key: file }
            //     return file

            //     // 可以 return
            //     // 1. return file 或者 new 一个 file ,接下来将上传
            //     // 2. return false ,不上传这个 file
            // },

            // // 上传进度的回调函数

            // onProgress(progress) {       // JS 语法
            //     // progress 是 0-100 的数字
            //     console.log('progress', progress)
            // },

            // // 单个文件上传成功之后

            // onSuccess(file, res) {          // JS 语法
            //     console.log(`${file.name} 上传成功`, res)
            // },

            // // 单个文件上传失败

            // onFailed(file, res) {           // JS 语法
            //     console.log(`${file.name} 上传失败`, res)
            // },

            // // 上传错误,或者触发 timeout 超时

            // onError(file, err, res) {               // JS 语法
            //     console.log(`${file.name} 上传出错`, err, res)
            // },
          },
          // 配置上传视频(同上传图片)
          uploadVideo: {},
        },
      },
      mode: "default", // or 'simple'


  methods: {

    // 富文本
    onCreated(editor) {
      this.editor = Object.seal(editor); // 一定要用 Object.seal() ,否则会报错
      console.log(this.editor);
    },
    onChange(editor) {
      console.log("onChange", editor.children, this.content);
    },
    onDestroyed(editor) {
      console.log("onDestroyed", editor);
    },
    onMaxLength(editor) {
      console.log("onMaxLength", editor);
    },
    onFocus(editor) {
      console.log("onFocus", editor);
    },
    onBlur(editor) {
      console.log("onBlur", editor);
    },
    customAlert(info, type) {
      window.alert(`customAlert in Vue demo\n${type}:\n${info}`);
    },

    //重点来了: 自定义粘贴。可阻止编辑器的默认粘贴,实现自己的粘贴逻辑。(可以实现复制粘贴 word ,有图片)
    customPaste(editor, event, callback) {
      console.log("ClipboardEvent 粘贴事件对象", event);
      let html = event.clipboardData.getData("text/html"); // 获取粘贴的 html
      // let text = event.clipboardData.getData('text/plain') // 获取粘贴的纯文本
      let rtf = event.clipboardData.getData("text/rtf"); // 获取 rtf 数据(如从 word wsp 复制粘贴)
      var that = this;
      if (html && rtf) {

        // 列表缩进会超出边框,直接过滤掉
        html = html.replace(/text\-indent:\-(.*?)pt/gi, "");

        // 从html内容中查找粘贴内容中是否有图片元素,并返回img标签的属性src值的集合
        const imgSrcs = that.findAllImgSrcsFromHtml(html);

        // 如果有
        if (imgSrcs && Array.isArray(imgSrcs) && imgSrcs.length) {
          // 从rtf内容中查找图片数据
          const rtfImageData = that.extractImageDataFromRtf(rtf);

          // 如果找到
          if (rtfImageData.length) {
            // TODO:此处可以将图片上传到自己的服务器上

            // 执行替换:将html内容中的img标签的src替换成ref中的图片数据,如果上面上传了则为图片路径
            html = that.replaceImagesFileSourceWithInlineRepresentation(
              html,
              imgSrcs,
              rtfImageData
            );
            editor.dangerouslyInsertHtml(html);
          }
        }

        // 阻止默认的粘贴行为
        event.preventDefault();
        return false;
      } else {
        return true;
      }
    },


    //自定义上传图片
    uploadImg(file, insertFn) {
      let imgData = new FormData();
      console.log(file);
      imgData.append("file", file);
      //调用上传图片接口,上传图片
      this.$api.post("/form/temp/update/ajax/img", imgData).then((res) => {
          console.log(res);
          // 插入后端返回的url
          insertFn(res[0].url);
          this.$message({
            type: "success",
            message: "上传成功",
          });
        })
        .catch((error) => {
          this.$message("上传失败,请重新上传");
        });
    },

    // 自定义插入图片
    insertImg(file) {
      console.log(file);
    },


    /**
     * 从html代码中匹配返回图片标签img的属性src的值的集合
     * @param htmlData
     * @return Array
     */
    findAllImgSrcsFromHtml(htmlData) {
      let imgReg = /<img.*?(?:>|\/>)/gi; //匹配图片中的img标签
      let srcReg = /src=[\'\"]?([^\'\"]*)[\'\"]?/i; // 匹配图片中的src

      let arr = htmlData.match(imgReg); //筛选出所有的img
      if (!arr || (Array.isArray(arr) && !arr.length)) {
        return false;
      }

      let srcArr = [];
      for (let i = 0; i < arr.length; i++) {
        let src = arr[i].match(srcReg);
        // 获取图片地址
        srcArr.push(src[1]);
      }

      return srcArr;
    },
    /**
     * 从rtf内容中匹配返回图片数据的集合
     * @param rtfData
     * @return Array
     */
    extractImageDataFromRtf(rtfData) {
      if (!rtfData) {
        return [];
      }

      const regexPictureHeader =
        /{\\pict[\s\S]+?({\\\*\\blipuid\s?[\da-fA-F]+)[\s}]*/;
      const regexPicture = new RegExp(
        "(?:(" + regexPictureHeader.source + "))([\\da-fA-F\\s]+)\\}",
        "g"
      );
      const images = rtfData.match(regexPicture);
      const result = [];

      if (images) {
        for (const image of images) {
          let imageType = false;

          if (image.includes("\\pngblip")) {
            imageType = "image/png";
          } else if (image.includes("\\jpegblip")) {
            imageType = "image/jpeg";
          }

          if (imageType) {
            result.push({
              hex: image
                .replace(regexPictureHeader, "")
                .replace(/[^\da-fA-F]/g, ""),
              type: imageType,
            });
          }
        }
      }

      return result;
    },
    /**
     * 将html内容中img标签的属性值替换
     * @param htmlData html内容
     * @param imageSrcs html中img的属性src的值的集合
     * @param imagesHexSources rtf中图片数据的集合,与html内容中的img标签对应
     * @param isBase64Data 是否是Base64的图片数据
     * @return String
     */
    replaceImagesFileSourceWithInlineRepresentation(
      htmlData,
      imageSrcs,
      imagesHexSources,
      isBase64Data = true
    ) {
      if (imageSrcs.length === imagesHexSources.length) {
        for (let i = 0; i < imageSrcs.length; i++) {
          const newSrc = isBase64Data
            ? `data:${
                imagesHexSources[i].type
              };base64,${this._convertHexToBase64(imagesHexSources[i].hex)}`
            : imagesHexSources[i];

          htmlData = htmlData.replace(imageSrcs[i], newSrc);
        }
      }

      return htmlData;
    },

    /**
     * 十六进制转base64
     */
    _convertHexToBase64(hexString) {
      return btoa(
        hexString
          .match(/\w{2}/g)
          .map((char) => {
            return String.fromCharCode(parseInt(char, 16));
          })
          .join("")
      );
    },



  // 销毁富文本
  beforeDestroy() {
    const editor = this.editor;
    if (editor == null) return;
    editor.destroy(); // 组件销毁时,及时销毁编辑器
  },
};
</script>

//记得引入wangeditor样式
<style src="@wangeditor/editor/dist/css/style.css"></style>
<style lang="scss" scoped>
@import "../mystyle/addpost_course";
</style>

上传图片我这里有点特殊,就不细说了

自定义粘贴:就几点:

  • 实现方法:通过 wangEditor 的编辑器配置 API 中的  customPaste 自定义粘贴
  • 实现步骤: 注意获取粘贴的 html 和获取 rtf 数据 ,用 let,不能用 const,下面会发生修改 -特别注意:允许默认粘贴行为 既可以复制图片,也可以复制文本,还可以图片文本;阻止默认的粘贴行为,只能粘贴复制文字带图片的文本
// 自定义粘贴。可阻止编辑器的默认粘贴,实现自己的粘贴逻辑。(可以实现复制粘贴 word ,有图片)
    customPaste(editor, event, callback) {
      console.log("ClipboardEvent 粘贴事件对象", event);
      let html = event.clipboardData.getData("text/html"); // 获取粘贴的 html,
      // let text = event.clipboardData.getData('text/plain') // 获取粘贴的纯文本
      let rtf = event.clipboardData.getData("text/rtf"); // 获取 rtf 数据(如从 word wsp 复制粘贴)

      var that = this;

      if (html && rtf) {
        // 该条件分支即表示要自定义word粘贴

        // 列表缩进会超出边框,直接过滤掉
        html = html.replace(/text\-indent:\-(.*?)pt/gi, "");

        // 从html内容中查找粘贴内容中是否有图片元素,并返回img标签的属性src值的集合
        const imgSrcs = that.findAllImgSrcsFromHtml(html);

        // 如果有
        if (imgSrcs && Array.isArray(imgSrcs) && imgSrcs.length) {
          // 从rtf内容中查找图片数据
          const rtfImageData = that.extractImageDataFromRtf(rtf);

          // 如果找到
          if (rtfImageData.length) {
            // TODO:此处可以将图片上传到自己的服务器上

            // 执行替换:将html内容中的img标签的src替换成ref中的图片数据,如果上面上传了则为图片路径
            html = that.replaceImagesFileSourceWithInlineRepresentation(
              html,
              imgSrcs,
              rtfImageData
            );
            editor.dangerouslyInsertHtml(html);
          }
        }

        // 阻止默认的粘贴行为 //此处非常关键 允许默认行为 既可以复制图片,也可以复制文本,还可以图片文本
        // event.preventDefault();
        //event.preventDefault();
        return false;
      } else {
        return true;
      }
    },
  • 以上实现步骤涉及到几个方法:
/**
     * 从html代码中匹配返回图片标签img的属性src的值的集合
     * @param htmlData
     * @return Array
     */
    findAllImgSrcsFromHtml(htmlData) {
      let imgReg = /<img.*?(?:>|\/>)/gi; //匹配图片中的img标签
      let srcReg = /src=[\'\"]?([^\'\"]*)[\'\"]?/i; // 匹配图片中的src

      let arr = htmlData.match(imgReg); //筛选出所有的img
      if (!arr || (Array.isArray(arr) && !arr.length)) {
        return false;
      }

      let srcArr = [];
      for (let i = 0; i < arr.length; i++) {
        let src = arr[i].match(srcReg);
        // 获取图片地址
        srcArr.push(src[1]);
      }

      return srcArr;
    },
/**
     * 从rtf内容中匹配返回图片数据的集合
     * @param rtfData
     * @return Array
     */
    extractImageDataFromRtf(rtfData) {
      if (!rtfData) {
        return [];
      }

      const regexPictureHeader =
        /{\\pict[\s\S]+?({\\\*\\blipuid\s?[\da-fA-F]+)[\s}]*/;
      const regexPicture = new RegExp(
        "(?:(" + regexPictureHeader.source + "))([\\da-fA-F\\s]+)\\}",
        "g"
      );
      const images = rtfData.match(regexPicture);
      const result = [];

      if (images) {
        for (const image of images) {
          let imageType = false;

          if (image.includes("\\pngblip")) {
            imageType = "image/png";
          } else if (image.includes("\\jpegblip")) {
            imageType = "image/jpeg";
          }

          if (imageType) {
            result.push({
              hex: image
                .replace(regexPictureHeader, "")
                .replace(/[^\da-fA-F]/g, ""),
              type: imageType,
            });
          }
        }
      }

      return result;
    },
 /**
     * 将html内容中img标签的属性值替换
     * @param htmlData html内容
     * @param imageSrcs html中img的属性src的值的集合
     * @param imagesHexSources rtf中图片数据的集合,与html内容中的img标签对应
     * @param isBase64Data 是否是Base64的图片数据
     * @return String
     */
    replaceImagesFileSourceWithInlineRepresentation(
      htmlData,
      imageSrcs,
      imagesHexSources,
      isBase64Data = true
    ) {
      if (imageSrcs.length === imagesHexSources.length) {
        for (let i = 0; i < imageSrcs.length; i++) {
          const newSrc = isBase64Data
            ? `data:${
                imagesHexSources[i].type
              };base64,${this._convertHexToBase64(imagesHexSources[i].hex)}`
            : imagesHexSources[i];

          htmlData = htmlData.replace(imageSrcs[i], newSrc);
        }
      }

      return htmlData;
    },
/**
     * 十六进制转base64
     */
    _convertHexToBase64(hexString) {
      return btoa(
        hexString
          .match(/\w{2}/g)
          .map((char) => {
            return String.fromCharCode(parseInt(char, 16));
          })
          .join("")
      );
    },

原理:可以去看看他的文章,不错 ( wangEditor 粘贴从 word 复制的带图片内容的最佳实践_ wangeditor 粘贴 word 图片)