wangEditor 图文混合粘贴 word/wps 兼容word2010

工作需要,需要从word粘贴到网页端,原本想法是直接富文本编辑框,然后粘贴进去,再做个文件上传也就完了,全网翻了几天,写出来的代码不兼容word2010,政府项目,没办法改吧,

各种正则,最后写出来个半成品,在这里记录一下

用到的三方包 wangEditor  vue3版本

<template>
  <div class="addpost_course">
    <div id="editor" style="border: 1px solid #ccc">
      <Toolbar style="border-bottom: 1px solid #ccc" :editor="editor" :defaultConfig="toolbarConfig" :mode="mode" />
      <Editor
        style="height: 500px; 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'
  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
        // console.log(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)

              html = html.replace(/(<body\b[^>]*?)\s*style=["'][^"']*["']/i, '$1')
              html = html.replace(/<!--.*?-->/gs, '')

              html = html.replace(/<link[^>]*>/gi, '')
              html = html.replace(/<[a-zA-Z]+:[^>]+>[\s\S]*?<\/[a-zA-Z]+:[^>]+>|<[a-zA-Z]+:[^>]+\/>/gi, '')
              html = html.replace(/<span[^>]*>(\s*<!--[^\[>]*\[[^\]]*\]-->\s*)*<img[^>]*>.*?(<!--[^\[>]*\[[^\]]*\]-->\s*)*<\/span>/gi, match => {
                // 从匹配的内容中提取 img 标签
                const imgTag = match.match(/<img[^>]*>/gi)
                return imgTag ? imgTag[0] : match // 仅保留 img 标签
              })
              editor.dangerouslyInsertHtml(html)
            }
          } else {
            console.log('未检测到图片,不做处理,直接插入')
            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)
            console.log('成功')
          })
          .catch(error => {
            console.log('失败')
          })
      },

      // 自定义插入图片
      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 imgReg = /<img.*?(?:>|\/>)/gi // 匹配 img 标签
        let srcReg = /src=['"]?([^'"]*)['"]?/i // 匹配 src

        let arr = htmlData.match(imgReg) //筛选出所有的img

        if (!arr || (Array.isArray(arr) && !arr.length)) {
          const parser = new DOMParser()
          const doc = parser.parseFromString(htmlData, 'text/html')
          const images = doc.querySelectorAll('img')
          return Array.from(images).map(img => img.src)
        }

        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) {
        // 1. 使用 DOMParser 解析 HTML 字符串
        const parser = new DOMParser()
        const doc = parser.parseFromString(htmlData, 'text/html')

        // 2. 获取所有 img 标签
        const images = doc.querySelectorAll('img')
        console.log(images, '获取所有 img 标签')
        // 3. 如果 imageSrcs 和 imagesHexSources 数量匹配
        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]
            console.log(imageSrcs[i], '当前图换的标签')
            // 4. 遍历找到匹配的 img 标签,并替换 src
            images.forEach(img => {
              // 标准化 img.src 和 imageSrcs[i],将反斜杠统一为正斜杠
              const imgSrc = img.getAttribute('src').replace(/\\/g, '/')
              const normalizedImageSrc = imageSrcs[i].replace(/\\/g, '/')
              console.log(imgSrc, normalizedImageSrc, imgSrc == normalizedImageSrc, '替换对比')
              // 比较标准化后的路径
              if (imgSrc === normalizedImageSrc) {
                console.log('Old src:', imgSrc)
                console.log('New src:', newSrc)
                img.setAttribute('src', newSrc) // 替换 src 属性
              }
            })
          }
        }
        console.log(new XMLSerializer().serializeToString(doc), '替换返回的html')
        // 5. 将 DOM 对象重新序列化为字符串并返回
        return new XMLSerializer().serializeToString(doc)
      },

      /**
       * 十六进制转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>
<style src="@wangeditor/editor/dist/css/style.css"></style>
<style>
  .quill-editor {
    -webkit-touch-callout: text !important;
    -webkit-user-select: text !important;
    -khtml-user-select: text !important;
    -moz-user-select: text !important;
    -ms-user-select: text !important;
  }
</style>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值