wangeditor 学习笔记,多个编辑界面使用"同一个工具栏"

1,580 阅读3分钟

记录自己的一次开发记录和各种需求wangeditor 使用场景,本次介绍的是wangeditor5在vue2中的使用。学会这个在vue3或者其他项目中,也是手拿把捏。慢慢看,终将有所收获。

1.基本使用

  1. 安装
yarn add @wangeditor/editor
# 或者 npm install @wangeditor/editor --save

yarn add @wangeditor/editor-for-vue
# 或者 npm install @wangeditor/editor-for-vue --save

直达官网

  1. 案例效果

image.png 3. 代码如下

<template>
  <div style="border: 1px solid #ccc;">
    <Toolbar v-show="showIndex===0" 
    style="border-bottom: 1px solid #ccc" 
    :editor="editor" 
    :defaultConfig="toolbarConfig"
    :mode="mode" />
    <div class="content">
      <Editor style="height: 300px; overflow-y: hidden;"
      v-model="html"   :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 "@wangeditor/editor/dist/css/style.css";
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
export default {
  components: { Editor, Toolbar },
  data() {
    return {
      editor: null,
      html: "<h2>2024-8-7 立秋</h2><p>阳气收,万物敛云天收夏色,木叶动秋声。</p>",
      toolbarConfig: {},
      editorConfig: { placeholder: "请输入内容..." },
      mode: "default", // or 'simple'通过设置 **mode** 改变富文本模式, **'default'**  默认模式,基础所有功能。 **'simple'**  简洁模式,仅有常用功能。
      showIndex: 0,
    };
  },
  methods: {
    onCreated(editor) {
      this.editor = Object.seal(editor); // 一定要用 Object.seal() ,否则会报错
    },
    onChange(editor) {
      console.log("onChange", editor.children);
    },
    onDestroyed(editor) {
      console.log("onDestroyed", editor);
    },
    onMaxLength(editor) {
      console.log("onMaxLength", editor);
    },
    onFocus(editor) {
      console.log("onFocus", editor);
      this.showIndex = 0;
    },
    onBlur(editor) {
      console.log("onBlur", editor);
    },
    customAlert(info, type) {
      window.alert(`customAlert in Vue demo\n${type}:\n${info}`);
    },
    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)
    },

    // 分割
  },
  mounted() {
    // 模拟 ajax 请求,异步渲染编辑器
    // setTimeout(() => {
    //   this.html = "<p>模拟 Ajax 异步设置内容 HTML</p>";
    // }, 1500);
  },
  beforeDestroy() {
    const editor = this.editor;
    if (editor == null) return;
    editor.destroy(); // 组件销毁时,及时销毁编辑器
  },
};
</script>
<style scoped>
.content {
  width: 100%;
  height: 300px;
  /* background-color: #fcfcfc; */
}
</style>

其他函数使用说明可以参考这里--wangEditor 富文本详解

2. 多个编辑界面使用同一个工具栏

需求需要实现的效果,文章的小标题要不可修改。

image.png 这个需求也是花费了不少时间,实现的基本效果如下: project9.gif

image.png 实现思路:

    1. 首先获取对应的编辑器文本格式,再统一处理成wangeditor所能识别的数据格式
    1. 多个编辑界面就显示多个工具栏,通过onFocus聚焦时,让用户看到的工具栏,也就是用户所选择的编辑器所对应的工具栏
    1. 注意onCreated这个函数的使用

实现过程,可以看下面的代码,慢慢看,终将会有所收获。具体的实现代码如下:

<template>
  <div style="border: 1px solid #ccc;">
    <div v-for="(item,index) in list" :key="index">
      <!-- 通过v-show隐藏工具栏,使得每个编辑界面只对应自己的工具栏 -->
      <div v-show="showIndex===item.index">
        <Toolbar style="border-bottom: 1px solid #ccc" :editor="item['editor'+item.index]" :defaultConfig="item['toolbarConfig'+item.index]" :mode="item['mode'+item.index]" />
      </div>
    </div>
    <div class="content">
      <div v-for="(item,index) in list" :key="index">
        <h4>{{item['title'+item.index]}}</h4>
        <Editor style="height: 100px; overflow-y: hidden;" v-model="item['html'+item.index]" :defaultConfig="item['editorConfig'+item.index]" :mode="item['mode'+item.index]" @onCreated="onCreated(item,$event)" @onChange="onChange" @onDestroyed="onDestroyed" @onMaxLength="onMaxLength" @onFocus="onFocus(item,editor)" @onBlur="onBlur" @customAlert="customAlert" @customPaste="customPaste" />
      </div>
    </div>
  </div>
</template>
<script>
import "@wangeditor/editor/dist/css/style.css";
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
export default {
  components: { Editor, Toolbar },
  data() {
    return {
      editor: null,
      html: "",
      toolbarConfig: {},
      editorConfig: { placeholder: "请输入内容..." },
      mode: "default", // or 'simple'
      // 分割
      html1: "",
      editor1: null,
      toolbarConfig1: {},
      editorConfig1: { placeholder: "请输入内容..." },
      mode1: "default", // or 'simple'
      showIndex: 1,
      // 数据
      list: [
        {
          index: 1,
          title1: "标题一",
          editor1: "",
          html1: "",
          editorConfig1: { placeholder: "请输入内容..." },
          mode1: "default", // or 'simple'
          toolbarConfig1: {},
        },
        {
          index: 2,
          title2: "标题二",
          editor2: "",
          html2: "",
          editorConfig2: { placeholder: "请输入内容..." },
          mode2: "default", // or 'simple'
          toolbarConfig2: {},
        },
        {
          index: 3,
          title3: "标题三",
          editor3: "",
          html3: "",
          editorConfig3: { placeholder: "请输入内容..." },
          mode3: "default", // or 'simple'
          toolbarConfig3: {},
        },
      ],
      indextag: 1,
    };
  },
  computed: {},
  methods: {
    onCreated(item, $event) {
      let editor = $event;
      this.list[item.index - 1]["editor" + item.index] = Object.seal(editor); // 一定要用 Object.seal() ,否则会报错
      console.log("onCreated", editor);
    },
    onChange(editor) {
      console.log("onChange", editor.children);
    },
    onDestroyed(editor) {
      console.log("onDestroyed", editor);
    },
    onMaxLength(editor) {
      console.log("onMaxLength", editor);
    },
    onFocus(item, editor) {
      this.indextag = item.index - 1; //list元素的下标
      console.log("item", item);
      console.log("onFocus", editor);
      this.showIndex = item.index;
    },
    onBlur(editor) {
      console.log("onBlur", editor);
    },
    customAlert(info, type) {
      window.alert(`customAlert in Vue demo\n${type}:\n${info}`);
    },
    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)
    },
  },
  mounted() {
    // 模拟 ajax 请求,异步渲染编辑器
    // setTimeout(() => {
    //   this.html = "<p>模拟 Ajax 异步设置内容 HTML</p>";
    // }, 1500);
  },
  beforeDestroy() {
    const editor = this.list[this.indextag]["editor" + (this.indextag + 1)];
    if (editor == null) return;
    editor.destroy(); // 组件销毁时,及时销毁编辑器
  },
};
</script>
<style scoped>
.content {
  width: 100%;
  height: 300px;
  background-color: #fcfcfc;
}
</style>

3. 边编辑边保存时,使用防抖

  1. 安装防抖函数库
npm install lodash
  1. 在Vue组件中引入并使用防抖函数
<template>
  <div>
    <input type="text" v-model="searchText" @input="debouncedSearch" placeholder="Search...">
    <ul>
      <li v-for="result in searchResults" :key="result.id">{{ result.name }}</li>
    </ul>
  </div>
</template>
 
<script>
import { debounce } from 'lodash';
 
export default {
  data() {
    return {
      searchText: '',
      searchResults: [],
    };
  },
  methods: {
    // 防抖函数,延迟300毫秒执行搜索操作
    debouncedSearch: debounce(function() {
      // 执行搜索操作,例如发送网络请求或处理数据
      // 可以在这里调用搜索函数
      this.performSearch();
    }, 300),
    performSearch() {
      // 在这里执行实际的搜索操作,例如发送网络请求或处理数据
      // 使用this.searchText来获取输入的搜索文本
      // 然后更新this.searchResults数组来显示搜索结果
    },
  },
};
</script>
  1. debounce函数延迟了debouncedSearch方法的执行,300毫秒内如果有新的输入,将重新计时,直到没有新的输入后触发performSearch方法进行实际的搜索操作。

  2. 通过使用防抖函数,可以节省资源并提高用户体验,避免在频繁触发的事件中重复执行操作。记得在组件销毁前取消防抖函数的注册,避免潜在的内存泄漏问题。

结合这个案例,可以在onChange的时候,进行防抖。如果事件被频繁触发,防抖能保证只有最后一次触发生效! 前面 N 多次的触发都会被忽略。

节流:如果事件被频繁触发,节流能够减少事件触发的频率,因此,节流是有选择性地执行一部分事件!

防抖相当于回城,节流相当于技能冷却

4. 图片和视频上传

onCreated是编辑器创建完毕时的回调函数

 // 常事件如下:
    onCreated1(editor) {
      this.editor = Object.seal(editor)
      console.log('onCreated', editor)
      // 获取编辑器的配置
      const editorConfig = this.editor.getConfig()
      // 配置富文本图片的上传路径
      editorConfig.MENU_CONF['uploadImage'] = {
        // server对应的 是上传图片的接口
        server: '/api/MyExperiencePrimary/UploadRichResFile',
        fieldName: 'wangeditor-uploaded-image',
        //这里的header是自己的请求头
        headers: this.headers,
        // 自定义插入图片
        customInsert: (res, insertFn) => {
          if (res.IsSuccess) {
          这个数组是存储所有图片视频
            this.conventionList.push(res.Data)
            //insertFn 是回显插入的图片地址,地址由上传成功后端返回
            insertFn(`/Content/ExperFile/${res.Data}`, '', '')
          } else {
            this.$message.error(res.ErrorMessage)
          }
        },
        // 单个文件上传成功之后
        onSuccess(file, res) {
          console.log(`${file.name} 上传成功`, res.Data)
        },
        // 上传错误,或者触发 timeout 超时
        onError(file, err, res) {
          console.log(`${file.name} 上传出错`, err, res)
        },
      }

      // 视频上传
      editorConfig.MENU_CONF['uploadVideo'] = {
        // 上传视频的配置
        fieldName: 'wangeditor-uploaded-video',
        // server对应的 是上传视频的接口
        server: '/api/MyExperiencePrimary/UploadRichResFile',
        maxFileSize: 1024 * 1024 * 300, // 300M
        headers: this.headers,
        // 自定义插入视频
        customInsert: (res, insertFn) => {
          //console.log(this.uploadFileArr)
          console.log(res)
          if (res.IsSuccess) {
            this.conventionList.push(res.Data)
            // 回显插入的视频
            insertFn(`/Content/ExperFile/${res.Data}`, '')
          } else {
            this.$message.error(res.ErrorMessage)
          }
        },
        onSuccess(file, res) {
          console.log(`${file.name} 上传成功`, res)
        },
        onError(file, err, res) {
          console.log(`${file.name} 上传出错`, err, res)
        },
      }

      // // 预览事件 (自定义的)
      // this.editor.on('preview', () => {
      //   this.editorPreHtml = this.editor.getHtml()
      //   console.log(this.editorPreHtml)
      //   this.isPreViewVisible = true
      // })
      // //#endregion
      // // 自定义全屏
      // this.editor.on('cusFullScreen', () => {
      //   this.cusFullScreen = !this.cusFullScreen
      //   console.log(this.cusFullScreen ? '全屏' : '退出全屏')
      // })
      // this.$nextTick(() => {
      //   //获取富文本中的图片和视频
      //   this.oldUploadFileArr = this.getUploadFile()
      //   console.log('初始时', this.oldUploadFileArr)
      // })
    },

详细可以参考这篇文章

在上传图片后,我们可能还会有删除图片的操作,删除的时候需要将后端的图片也删除掉。这里我给出我的思路

  1. 在上传成功之后,保存每次上传成功返回的图片数据
  2. onchange中获得所有的图片和视频数据,二者进行对比,过滤出需要删除的数据。

注:onchange编辑器内容、选区变化时的回调函数。

  // 获取富文本中的图片和视频
    getUploadFile() {
      // 获取富文本中的图片对象信息
      //这个可以获取到编辑器里面所有的图片
      let currImageMap = this.editor.getElemsByType('image')
      console.log('object', currImageMap)
      // 富文本现有的图片
      let currImages = []
      //这里是获取图片的名称
      currImageMap.forEach((item) => {
        let a = item.src.split('/')
        currImages.push(a[3])
      })
      console.log('currImages', currImages)
      // // 获取富文本中的视频对象信息
      let currVideoMap = this.editor.getElemsByType('video')
      // console.log('视频对象:', currVideoMap)
      let currVideoArr = []
      currVideoMap.forEach((item) => {
        let a = item.src.split('/')
        currVideoArr.push(a[3])
      })
      // console.log('视频数组:', currVideoArr)
      let currFile = [...currImages, ...currVideoArr]
      // console.log('currFile', currFile)
      return currFile
    },