Bootstrap

vue+elementui+quill富文本框+秀米编辑器和135编辑器

自定义自定义Vue-Quill-Editor富文本框可参照:
https://blog.csdn.net/ccsundhine/article/details/125867053?spm=1001.2014.3001.5502
注意:

  • 秀米官网声明只支持ueditor内核的编辑器内核(本文使用quill富文本框,自定义了一个blot文件,防止quill自动过滤掉秀米和135编辑器里面的section之类的样式)
  • 秀米的第三方对接文档地址:https://ent.xiumi.us/doc2.html
  • 您的网站务必使用https访问,否则会造成用户无法登录秀米账户
  • 秀米官方更新后,在本地开发环境时,无法正常插入数据到编辑器;且在ip环境下无法登录秀米
  • 秀米插入编辑器前需要做图片本地化处理才能正常显示(如果不处理图片就会裂开,上传的图片是存放在秀米的服务器上面的,这样会消耗秀米的服务器资源,所以秀米会禁止外站的图片请求)可以考虑两种方式:quill自定义处理粘贴的文本内容;在index.html通过通过referrer去处理;
  • 秀米以及135编辑器回显的时候,如果有section之类的元素也会被过滤,所以在回显数据的时候也需要处理

在这里插入图片描述

  1. 把秀米编辑器和135编辑器的html文件放入public文件下(秀米这里有两个文件一个新版一个旧版本,自愿引入哪一个)
    在这里插入图片描述
    135EditorDialogPage.html文件:
<!DOCTYPE html>
<html>

<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>135编辑器</title>
  <style>
    html,
    body {
      padding: 0;
      margin: 0;
    }

    #editor135 {
      position: absolute;
      width: 100%;
      height: 100%;
      border: none;
      box-sizing: border-box;
    }

  </style>
</head>

<body>

  <iframe id="editor135" src="//www.135editor.com/simple_editor.html?callback=true&appkey="></iframe>
  <!-- <script type="text/javascript" src="internal.js"></script> -->
  <script>
    var editor135 = document.getElementById('editor135');
    var parent = window.parent;
    window.onload = function () {
      setTimeout(function () {
        editor135.contentWindow.postMessage(parent.getHtml(), '*');
        // parent.getHtml 其实是quill里暴露的 window.getHtml
      }, 3000);
    };
    document.addEventListener("mousewheel", function (event) {
      event.preventDefault();
      event.stopPropagation();
    });
    window.addEventListener('message', function (event) {
      if (typeof event.data !== 'string') return;
      parent.setRichText_135(event.data)
      // editor.setContent(event.data);
      // editor.fireEvent("catchRemoteImage"); 
      // dialog.close();
    }, false);

  </script>
</body>

</html>

xiumi-ue-dialog-v5_new.html文件:

<!DOCTYPE html>
<html>

<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>XIUMI connect</title>
  <style>
    html,
    body {
      padding: 0;
      margin: 0;
    }

    #xiumi {
      position: absolute;
      width: 100%;
      height: 100%;
      border: none;
      box-sizing: border-box;
    }

  </style>
</head>

<body>
  <iframe id="xiumi" src="//xiumi.us/studio/v5#/paper">
  </iframe>
  <!-- <script type="text/javascript" src="dialogs/internal.js"></script> -->
  <script>
    var parent = window.parent;
    console.log('parent: ', parent);
    var xiumi = document.getElementById('xiumi');
    var xiumi_url = window.location.protocol + "//xiumi.us";
    console.log("xiumi_url is %o", xiumi_url);
    xiumi.onload = function () {
      console.log("postMessage to %o", xiumi_url);
      // "XIUMI:3rdEditor:Connect" 是特定标识符,不能修改,大小写敏感
      xiumi.contentWindow.postMessage('XIUMI:3rdEditor:Connect', xiumi_url);
    };
    document.addEventListener("mousewheel", function (event) {
      event.preventDefault();
      event.stopPropagation();
    });
    window.addEventListener('message', function (event) {
      console.log("Received message from xiumi, origin: %o %o", event.origin, xiumi_url);
      console.log('event.data: ', event.data);
      if (event.origin == xiumi_url) {
        console.log("Inserting html");
        parent.setRichText_xm(event.data)
        console.log("Xiumi dialog is closing");
        // dialog.close();
      }
    }, false);

  </script>
</body>

</html>

xiumi-ue-dialog-v5.html文件:

<!DOCTYPE html>
<html>

<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>XIUMI connect</title>
  <style>
    html,
    body {
      padding: 0;
      margin: 0;
    }

    #xiumi {
      position: absolute;
      width: 100%;
      height: 100%;
      border: none;
      box-sizing: border-box;
    }

  </style>
</head>

<body>
  <iframe id="xiumi" src="//xiumi.us/studio/v5#/paper">
  </iframe>
  <!-- <script type="text/javascript" src="internal.js"></script> -->
  <script>
    var parent = window.parent;
    var xiumi = document.getElementById('xiumi');
    var xiumi_url = window.location.protocol + "//xiumi.us";
    xiumi.onload = function () {
      console.log("postMessage");
      xiumi.contentWindow.postMessage('ready', xiumi_url);
    };
    document.addEventListener("mousewheel", function (event) {
      event.preventDefault();
      event.stopPropagation();
    });
    window.addEventListener('message', function (event) {
      if (event.origin == xiumi_url) {
        parent.setRichText_xm(event.data)
        // editor.execCommand('insertHtml', event.data);
        // dialog.close();
      }
    }, false);

  </script>
</body>

</html>

  1. 用iframe引入(这里踩了一个坑!!!!src引入文件的时候一定要看清楚自己项目的publicPath )
<div  :style="{ height: fullheight + 'px' }">
	 <!-- quill -->
	 <quill-editor
	          ref="myQuillEditor"
	          v-model="articleForm.artContent"
	          v-screen
	          class="quilleditor"
	          :options="editorOption"
	          style="height: 265px"
	        />
	       <el-dialog
	      :append-to-body="true"
	      :close-on-click-modal="false"
	      :modal-append-to-body="false"
	      title="秀米"
	      top="50px"
	      :visible.sync="visible"
	      width="90%"
	      z-index="99999999"
	    >
	      <!-- 秀米插件弹框 -->
	      <div v-if="visible">
	        <!-- :src="`${baseUrl}static/xiumi-ue-dialog-v5_new.html?time=1267765432`" -->
	        <iframe
	          id="xiumiIframe"
	          frameborder="0"
	          :height="fullheight - 150 + 'px'"
	          src="./static/xiumi-ue-dialog-v5_new.html"
	          width="100%"
	        ></iframe>
	      </div>
	    </el-dialog> 
	     <!-- 135编辑器弹框 -->
	     <el-dialog
	      :append-to-body="true"
	      :close-on-click-modal="false"
	      :modal-append-to-body="false"
	      title="135编辑器"
	      top="50px"
	      :visible.sync="visible2"
	      width="90%"
	      z-index="99999999"
	    >
	      <div v-if="visible2">
	        <iframe
	          id="xiumiIframe"
	          frameborder="0"
	          :height="fullheight - 150 + 'px'"
	          src="./static/135EditorDialogPage.html"
	          width="100%"
	        ></iframe>
	      </div>
	    </el-dialog>
    </div>
  1. 自定义blot.js文件(防止quill过滤)
export default function (Quill) {
  // 引入源码中的BlockEmbed
  const BlockEmbed = Quill.import('blots/block/embed');
  // 定义新的blot类型
  class AppPanelEmbed extends BlockEmbed {
    static create(value) {
      const node = super.create(value);
      // node.setAttribute('contenteditable', 'false');
      // node.setAttribute('width', '100%');
      //   设置自定义html
      node.innerHTML = this.transformValue(value)
      // 返回firstChild,避免被包一层<div class='rich-innerHtml'></div>的无意义标签
      return node.firstChild;
    }

    static transformValue(value) {
      let handleArr = value.split('\n')
      handleArr = handleArr.map(e => e.replace(/^[\s]+/, '')
        .replace(/[\s]+$/, ''))
      return handleArr.join('')
    }

    // 返回节点自身的value值 用于撤销操作
    static value(node) {
      return node.innerHTML
    }
  }
  // blotName
  AppPanelEmbed.blotName = 'AppPanelEmbed';
  // class名将用于匹配blot名称
  AppPanelEmbed.className = 'rich-innerHtml';
  // 标签类型自定义,这玩意还必须加,去掉会报错
  AppPanelEmbed.tagName = 'div';
  Quill.register(AppPanelEmbed, true);
}

  1. 在js中使用 (引入blot文件,配置quill等)
<script>
	 // 秀米引入
	  import blotSelect from './components/blot.js'
	  blotSelect(Quill)
	  // 工具栏配置(可根据自己的需求配置quill工具栏)
	  const toolbarOptions = [
	    ['insertMetric'], //秀米
	    ['otEdit'], //135编辑器
	  ]
	export default {
		data() {
		      return {
		       msg: undefined,
               imgFile: undefined,
		      //富文本内容
		     articleForm:{
				artContent:""
				},
		        visible: false,//秀米
		        visible2: false,135编辑器
		        selection: {}, // 光标位置
		        fullheight: document.documentElement.clientHeight, // 给quill容器设置了个高度
		        quill: null, // 待初始化的编辑器
		        //quill配置
		       editorOption: {
			          modules: {
			            toolbar: {
			              container: toolbarOptions, //自定义工具栏
			              handlers: {
			                that: this,
			                // 秀米
			                insertMetric: function () {
			                  let self = this.handlers.that
			                  self.visible = true
			                },
			                // 135编辑器
			                otEdit: function () {
			                  let self = this.handlers.that
			                  self.visible2 = true
			                },
			              },
			            },
			          },
			          //主题
			          theme: 'snow',
			          placeholder: '请输入正文',
			        },
		       }
		   },
		    watch: {
		      value(newVal, oldVal) {
		        console.log(newVal, oldVal)
		        if (newVal) {
		          this.articleForm.artContent = newVal
		        } else if (!newVal) {
		          this.articleForm.artContent = ''
		        }
		      },
		  },
		  created() {
		      this.articleForm.artContent = this.value
	       },
	       mounted() {
		      this._initEditor()//初始化编辑器
		      this.initButton() //自定义图标(秀米,135)
		      // 暴露方法绑定到window上,给public\xiumi-ue-dialog-v5.html使用
		      window.setRichText_xm = this.setRichText_xm
		      window.setRichText_135 = this.setRichText_135
		      // 调用135页面的时候 带入数据 getHtml()
		      window.getHtml = this.getHtml
		    },
		    methods:{
		    	 // 初始化编辑器
			      _initEditor() {
			        // 初始化编辑器
			        this.quill = this.$refs.myQuillEditor.quill
			        // 双向绑定代码 v-model
			        this.quill.on('text-change', () => {
			          this.emitChange()
			          this.selection = this.quill.getSelection()
			        })
			        // 插入内容
			        this.firstSetHtml()
			        // 粘贴板监听
			        this.listenPaste()
			      },
			      //秀米编辑器
			      setRichText_xm(e) {
			        const index = this.selection ? this.selection.index : 0
			        // console.log('光标位置',index)
			        this.quill.insertEmbed(index || 0, 'AppPanelEmbed', e)
			        this.visible = false
			      },
			        //135编辑器
			      setRichText_135(e) {
			        const index = this.selection ? this.selection.index : 0
			        //这个主要是用来处理在135编辑器添加导出到quill再点击135编辑器返回到quill的重复内容
			        this.quill.setContents([
			          { insert: '', attributes: { bold: true } },
			          { insert: '\n' },
			        ])
			        this.quill.insertEmbed(index || 0, 'AppPanelEmbed', e)
			        this.visible2 = false
			      },
			       emitChange() {
				        // 获取到quill 根dom中的html
				        let html = this.articleForm.artContent
				        const quill = this.quill
				        const text = this.quill.getText()
				        if (html === '<p><br></p>') html = ''
				        // v-model相关
				        this.$emit('input', html)
				        this.$emit('change', { html, text, quill })
				        // 返回quill中文本长度
				        // bug注意:这个方法无法计算秀米代码的中的文字长度!
				        this.$emit('getConetntLength', this.quill.getLength())
				      },
				       // 回显内容时检查秀米代码
					      firstSetHtml() {
					        // value 为回显内容
					        if (this.value) {
					          // 判断是否有秀米和或135元素
					          if (
					            this.value.indexOf('xiumi.us') > -1 ||
					            this.value.indexOf('135editor.com') > -1
					          ) {
					            const originNode = new DOMParser().parseFromString(
					              this.value,
					              'text/html'
					            ).body.childNodes
					            this.nodesInQuill(originNode)
					          } else {
					            // 正常插入
					            this.quill.clipboard.dangerouslyPasteHTML(this.value)
					          }
					        }
					      },
				       // 根据node类型分发处理
				      nodesInQuill(originNode) {
				        for (let i = originNode.length - 1; i >= 0; i--) {
				          if (originNode[i].localName === 'section') {
				            // 秀米类型代码,走新blot
				            this.setRichText_xm(originNode[i].outerHTML, 0)
				            this.setRichText_135(originNode[i].outerHTML, 0)
				          } else {
				            // 正常插入
				            this.quill.clipboard.dangerouslyPasteHTML(
				              0,
				              originNode[i].outerHTML
				            )
				          }
				        }
				      },
				     
				     // 监听粘贴板(请求接口把秀米图片本地化处理)  
				     //注意!注意!注意!如果是本地复制粘贴的话,会走此方法,如果是线上直接点击秀米编辑器上的√ 直接导入,可以考虑在chang时贴入此段代码
				      listenPaste() {
				        var that = this
				        var imageArr = []
				        this.quill.root.addEventListener('paste', (e) => {
				          that.msg = (e.clipboardData || window.clipboardData).getData(
				            'text/html'
				          )
				          // //匹配图片
				          var imgReg = /<img.*?(?:>|\/>)/gi // eslint-disable-line
				          // //匹配src属性
				          var srcReg = /src=[\'\"]?([^\'\"]*)[\'\"]?/i // eslint-disable-line
				          if (that.msg) {
				            if (
				              that.msg.indexOf('xiumi.us') > -1 ||
				              that.msg.indexOf('_135editor') > -1
				            ) {
				              that.msg.replace(imgReg, function (txt) {
				                return txt.replace(srcReg, function (src) {
				                  var img_src = src.match(srcReg)[1]
				                  //正则把?x-oss-process后面的都去掉
				                  img_src = img_src.replace(/\?.*/i, '')
				                  imageArr.push(img_src)
				                })
				              })
				
				              const parmas = {
				                urlList: imageArr,
				              }
				              if (imageArr.length != 0) {
				                //    如果有图片则 请求接口上传图片
				                uploadUrlImgs(parmas)
				                  .then((res) => {
				                    if (res.data && res.data.length) {
				                      var index = 0
				                      while (index < res.data.length) {
				                        //接口返回图片根据index替换
				                        that.msg = that.msg.replace(
				                          imageArr[index],
				                          res.data[index]
				                        )
				                        index++
				                        that.$emit('change', that.msg)
				                        // 富文本
				                        const value = new DOMParser().parseFromString(
				                          that.msg,
				                          'text/html'
				                        ).body.childNodes // 获取nodes
				                        e.preventDefault() // 阻止复制动作
				                        e.stopPropagation() // 阻止冒泡
				                        that.nodesInQuill(value) // 根据不同标签,使用不同的插入方法
				                      }
				                    } else {
				                      // 富文本
				                      const value1 = new DOMParser().parseFromString(
				                        that.msg,
				                        'text/html'
				                      ).body.childNodes // 获取nodes
				                      e.preventDefault() // 阻止复制动作
				                      e.stopPropagation() // 阻止冒泡
				                      that.nodesInQuill(value1) // 根据不同标签,使用不同的插入方法
				                    }
				                  })
				                  .catch(() => {})
				              } else {
				                // 富文本
				                const value1 = new DOMParser().parseFromString(
				                  that.msg,
				                  'text/html'
				                ).body.childNodes // 获取nodes
				                e.preventDefault() // 阻止复制动作
				                e.stopPropagation() // 阻止冒泡
				                that.nodesInQuill(value1) // 根据不同标签,使用不同的插入方法
				              }
				            }
				          }
				        })
				        //以下是不需要请求接口,直接使用秀米的图片地址
				        // this.quill.root.addEventListener('paste', (e) => {
				        //   let msg = (e.clipboardData || window.clipboardData).getData(
				        //     'text/html'
				        //   ) // 获取粘贴板文本
				        //   if (msg) {
				        //     if (
				        //       msg.indexOf('xiumi.us') > -1 ||
				        //       msg.indexOf('_135editor') > -1
				        //     ) {
				        //       let value = new DOMParser().parseFromString(msg, 'text/html').body
				        //         .childNodes // 获取nodes
				        //       e.preventDefault() // 阻止复制动作
				        //       e.stopPropagation() // 阻止冒泡
				        //       this.nodesInQuill(value) // 根据不同标签,使用不同的插入方法
				        //     }
				        //   }
				        // })
				      },
				      //  自定义图标(秀米,135)
				      initButton() {
				        const sourceEditorButton = document.querySelector('.ql-insertMetric')
				        sourceEditorButton.innerHTML = `<button id="custom-button-xiumi" title="秀米" ></button>`
				        const sourceEditorButtonotEdit = document.querySelector('.ql-otEdit')
				        sourceEditorButtonotEdit.innerHTML = `<button id="custom-button-135" title="135编辑器" ></button>`
				      },
				
				      // 获取html内容
				      getHtml() {
				        return this.articleForm.artContent
				      },
		}
}
</script>

5.css

<style lang="scss" scoped>
	  ::v-deep(#custom-button-xiumi) {
	    background-size: contain;
	    background-repeat: no-repeat;
	    height: 16px;
	    width: 33px;
	    background-image: url('../../../assets/img/xiumi-connect-icon.png');
	  }
	  ::v-deep(#custom-button-135) {
	    background-size: contain;
	    background-repeat: no-repeat;
	    height: 16px;
	    width: 33px;
	    background-image: url('../../../assets/img/editor-135-icon.png');
	  }
  </style>

本文参考:https://www.freesion.com/article/97151190977/
https://blog.csdn.net/qq_41621896/article/details/121975513

;