wangEditor5在vue中自定义菜单栏--格式刷,上传图片,视频功能

复制粘贴修改一下直接用,也写了相关的注释。

一、安装相关插件

npm install @wangeditor/editor
npm install @wangeditor/editor-for-vue

二、官方关键文档

  1. ButtonMenu:https://www.wangeditor.com/v5/development.html#buttonmenu
  2. 注册菜单到wangEditor:自定义扩展新功能 | wangEditor
  3. insertKeys自定义功能的keys:https://www.wangeditor.com/v5/toolbar-config.html#insertkeys
  4. 自定义上传图片视频功能:菜单配置 | wangEditor
  5. 源码地址:GitHub - wangeditor-team/wangEditor: wangEditor —— 开源 Web 富文本编辑器

三、初始化编辑器(wangEdit.vue) 

<template>
  <div 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="html"
      :defaultConfig="editorConfig"
      :mode="mode"
      @onCreated="onCreated"
      @onChange="onChange"
    />
  </div>
</template>

<script>
// import Location from "@/utils/location";
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
import { Boot, IModuleConf, DomEditor } from "@wangeditor/editor";
import { getToken } from "@/utils/auth";
import MyPainter from "./geshi";
const menu1Conf = {
  key: "geshi", // 定义 menu key :要保证唯一、不重复(重要)
  factory() {
    return new MyPainter(); // 把 `YourMenuClass` 替换为你菜单的 class
  },
};
const module = {
  // JS 语法
  menus: [menu1Conf],

  // 其他功能,下文讲解...
};
Boot.registerModule(module);
export default {
  components: { Editor, Toolbar },
  props: {
    relationKey: {
      type: String,
      default: "",
    },
  },
  created() {
    console.log(this.editorConfig.MENU_CONF.uploadImage.meta.activityKey);
  },
  data() {
    return {
      // 富文本实例
      editor: null,
      // 富文本正文内容
      html: "",
      // 编辑器模式
      mode: "default", // or 'simple'
      // 工具栏配置
      toolbarConfig: {
        //新增菜单
        insertKeys: {
          index: 32,
          keys: ["geshi"],
        },
        //去掉网络上传图片和视频
        excludeKeys: ["insertImage", "insertVideo"],
      },
      // 编辑栏配置
      editorConfig: {
        placeholder: "请输入相关内容......",
        // 菜单配置
        MENU_CONF: {
          // ===================
          // 上传图片配置
          // ===================
          uploadImage: {
            // 文件名称
            fieldName: "contentAttachImage",
            server: Location.serverPath + "/editor-upload/upload-image",
            headers: {
              Authorization: "Bearer " + getToken(),
            },
            meta: {
              activityKey: this.relationKey,
            },
            // 单个文件的最大体积限制,默认为 20M
            maxFileSize: 20 * 1024 * 1024,
            // 最多可上传几个文件,默认为 100
            maxNumberOfFiles: 10,
            // 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []
            allowedFileTypes: ["image/*"],
            // 跨域是否传递 cookie ,默认为 false
            withCredentials: true,
            // 超时时间,默认为 10 秒
            timeout: 5 * 1000,
            // 自定义插入图片操作
            customInsert: (res, insertFn) => {
              if (res.errno == -1) {
                this.$message.error("上传失败!");
                return;
              }
              insertFn(Location.serverPath + res.data.url, "", "");
              this.$message.success("上传成功!");
            },
          },
          // =====================
          // 上传视频配置
          // =====================
          uploadVideo: {
            // 文件名称
            fieldName: "contentAttachVideo",
            server: Location.serverPath + "/editor-upload/upload-video",
            headers: {
              Authorization: "Bearer " + getToken(),
            },
            meta: {
              activityKey: this.relationKey,
            },
            // 单个文件的最大体积限制,默认为 60M
            maxFileSize: 60 * 1024 * 1024,
            // 最多可上传几个文件,默认为 100
            maxNumberOfFiles: 3,
            // 选择文件时的类型限制,默认为 ['video/*'] 。如不想限制,则设置为 []
            allowedFileTypes: ["video/*"],
            // 跨域是否传递 cookie ,默认为 false
            withCredentials: true,
            // 超时时间,默认为 10 秒
            timeout: 15 * 1000,
            // 自定义插入图片操作
            customInsert: (res, insertFn) => {
              if (res.errno == -1) {
                this.$message.error("上传失败!");
                return;
              }
              insertFn(Location.serverPath + res.data.url, "", "");
              this.$message.success("上传成功!");
            },
          },
        },
      },

      // ===== data field end =====
    };
  },
  methods: {
    // =============== Editor 事件相关 ================
    // 1. 创建 Editor 实例对象
    onCreated(editor) {
      this.editor = Object.seal(editor); // 一定要用 Object.seal() ,否则会报错
      this.$nextTick(() => {
        const toolbar = DomEditor.getToolbar(this.editor);
        const curToolbarConfig = toolbar.getConfig();
        console.log("【 curToolbarConfig 】-39", curToolbarConfig);
      });
    },
    // 2. 失去焦点事件
    onChange(editor) {
      this.$emit("change", this.html);
    },

    // =============== Editor操作API相关 ==============
    insertText(insertContent) {
      const editor = this.editor; // 获取 editor 实例
      if (editor == null) {
        return;
      }
      // 执行Editor的API插入
      editor.insertText(insertContent);
    },

    // =============== 组件交互相关 ==================
    // closeEditorBeforeComponent() {
    //   this.$emit("returnEditorContent", this.html);
    // },
    closeContent(){
        this.html=''
    },
    // ========== methods end ===============
  },
  mounted() {
    // ========== mounted end ===============
  },
  beforeDestroy() {
    const editor = this.editor;
    if (editor == null) {
      return;
    }
    editor.destroy();
    console.log("销毁编辑器!");
  },
};
</script>
<style lang="scss" scoped>
// 对默认的p标签进行穿透
::v-deep .editorStyle .w-e-text-container [data-slate-editor] p  {
  margin: 0 !important;
}
</style>
<style src="@wangeditor/editor/dist/css/style.css"></style>
自定义上传图片接口
 uploadImage: {
                        // 文件名称
                        fieldName: "contentAttachImage",
                        // server: '/api/v1/public/uploadFile',
                        headers: {
                            Authorization: "Bearer " + getToken(),
                        },
                        meta: {
                            activityKey: this.relationKey,
                        },
                        // 单个文件的最大体积限制,默认为 20M
                        maxFileSize: 20 * 1024 * 1024,
                        // 最多可上传几个文件,默认为 100
                        maxNumberOfFiles: 10,
                        // 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []
                        allowedFileTypes: ["image/*"],
                        // 跨域是否传递 cookie ,默认为 false
                        withCredentials: true,
                        // 超时时间,默认为 10 秒
                        timeout: 5 * 1000,
                         这里设置
                        customUpload: async (file, insertFn) => {
                            console.log(file, "file");
                            let formData = new FormData()
                            const sub = "order";
                            formData.append('file', file)
                            formData.append("sub", sub);
                            formData.append("type", "1");
                            let res = await getUploadImg(formData)
                            insertFn(res.data.full_path, '', '');
                        },
                        customInsert: (res, insertFn) => {
                            if (res.errno == -1) {
                                this.$message.error("上传失败!");
                                return;
                            }
                            // insertFn(res.data.url, "", "");
                            this.$message.success("上传成功!");
                        },
                    },

四、格式刷功能类js文件


import {
  SlateEditor,
  SlateText,
  SlateElement,
  SlateTransforms,
  DomEditor,
  //   Boot,
} from "@wangeditor/editor";
// Boot.registerMenu(menu1Conf);
import { Editor } from "slate";
export default class MyPainter {
  constructor() {
    this.title = "格式刷"; // 自定义菜单标题
    // 这里是设置格式刷的样式图片跟svg都可以,但是注意要图片大小要小一点,因为要应用到鼠标手势上
    this.iconSvg = ``;
    this.tag = "button"; //注入的菜单类型
    this.savedMarks = null; //保存的样式
    this.domId = null; //这个可要可不要
    this.editor = null; //编辑器示例
    this.parentStyle = null; //储存父节点样式
    this.mark = "";
    this.marksNeedToRemove = []; // 增加 mark 的同时,需要移除哪些 mark (互斥,不能共存的)
  }
  clickHandler(e) {
    console.log(e, "e"); //无效
  }
  //添加或者移除鼠标事件
  addorRemove = (type) => {
    const dom = document.body;
    if (type === "add") {
      dom.addEventListener("mousedown", this.changeMouseDown);
      dom.addEventListener("mouseup", this.changeMouseup);
    } else {
      //赋值完需要做的清理工作
      this.savedMarks = undefined;
      dom.removeEventListener("mousedown", this.changeMouseDown);
      dom.removeEventListener("mouseup", this.changeMouseup);
      document.querySelector("#w-e-textarea-1").style.cursor = "auto";
    }
  };

  //处理重复键名值不同的情况
  handlerRepeatandNotStyle = (styles) => {
    const addStyles = styles[0];
    const notVal = [];
    for (const style of styles) {
      for (const key in style) {
        const value = style[key];
        if (!addStyles.hasOwnProperty(key)) {
          addStyles[key] = value;
        } else {
          if (addStyles[key] !== value) {
            notVal.push(key);
          }
        }
      }
    }
    for (const key of notVal) {
      delete addStyles[key];
    }
    return addStyles;
  };

  // 获取当前选中范围的父级节点
  getSelectionParentEle = (type, func) => {
    if (this.editor) {
      const parentEntry = SlateEditor.nodes(this.editor, {
        match: (node) => SlateElement.isElement(node),
      });
      let styles = [];
      for (const [node] of parentEntry) {
        styles.push(this.editor.toDOMNode(node).style); //将node对应的DOM对应的style对象加入到数组
      }
      styles = styles.map((item) => {
        //处理不为空的style
        const newItem = {};
        for (const key in item) {
          const val = item[key];
          if (val !== "") {
            newItem[key] = val;
          }
        }
        return newItem;
      });
      type === "get"
        ? func(type, this.handlerRepeatandNotStyle(styles))
        : func(type);
    }
  };

  //获取或者设置父级样式
  getorSetparentStyle = (type, style) => {
    if (type === "get") {
      this.parentStyle = style; //这里是个样式对象 例如{textAlign:'center'}
    } else {
      SlateTransforms.setNodes(
        this.editor,
        { ...this.parentStyle },
        {
          mode: "highest", // 针对最高层级的节点
        }
      );
    }
  };

  //这里是将svg转换为Base64格式
  addmouseStyle = () => {
    const icon = ``; // 这里是给鼠标手势添加图标
    // 将字符串编码为Base64格式
    const base64String = btoa(icon);
    // 生成数据URI
    const dataUri = `data:image/svg+xml;base64,${base64String}`;
    // 将数据URI应用于鼠标图标
    document.querySelector(
      "#w-e-textarea-1"
    ).style.cursor = `url('${dataUri}'), auto`;
  };
  changeMouseDown = () => {}; //鼠标落下

  changeMouseup = () => {
    //鼠标抬起
    if (this.editor) {
      const editor = this.editor;
      const selectTxt = editor.getSelectionText(); //获取文本是否为null
      if (this.savedMarks && selectTxt) {
        //先改变父节点样式
        this.getSelectionParentEle("set", this.getorSetparentStyle);
        // 获取所有 text node
        const nodeEntries = SlateEditor.nodes(editor, {
          //nodeEntries返回的是一个迭代器对象
          match: (n) => SlateText.isText(n), //这里是筛选一个节点是否是 text
          universal: true, //当universal为 true 时,Editor.nodes会遍历整个文档,包括根节点和所有子节点,以匹配满足条件的节点。当universal为 false 时,Editor.nodes只会在当前节点的直接子节点中进行匹配。
        });
        // 先清除选中节点的样式
        for (const node of nodeEntries) {
          const n = node[0]; //{text:xxxx}
          const keys = Object.keys(n);
          keys.forEach((key) => {
            if (key === "text") {
              // 保留 text 属性
              return;
            }
            // 其他属性,全部清除
            SlateEditor.removeMark(editor, key);
          });
        }
        // 再赋值新样式
        for (const key in this.savedMarks) {
          if (Object.hasOwnProperty.call(this.savedMarks, key)) {
            const value = this.savedMarks[key];
            editor.addMark(key, value);
          }
        }
        this.addorRemove("remove");
      }
    }
  };

  getValue(editor) {
    // return "MyPainter"; // 标识格式刷菜单
    const mark = this.mark;
    console.log(mark, "mark");
    const curMarks = Editor.marks(editor);
    // 当 curMarks 存在时,说明用户手动设置,以 curMarks 为准
    if (curMarks) {
      return curMarks[mark];
    } else {
      const [match] = Editor.nodes(editor, {
        // @ts-ignore
        match: (n) => n[mark] === true,
      });
      return !!match;
    }
  }

  isActive(editor, val) {
    const isMark = this.getValue(editor);
    return !!isMark;
    //  return !!DomEditor.getSelectedNodeByType(editor, "geshi");
    // return false;
  }

  isDisabled(editor) {
    //是否禁用
    return false;
  }
  exec(editor, value) {
    //当菜单点击后触发
    // console.log(!this.isActive());
    console.log(value, "value");
    this.editor = editor;
    this.domId = editor.id.split("-")[1]
      ? `w-e-textarea-${editor.id.split("-")[1]}`
      : undefined;
    if (this.isDisabled(editor)) return;
    const { mark, marksNeedToRemove } = this;
    if (value) {
      // 已,则取消
      editor.removeMark(mark);
    } else {
      // 没有,则执行
      editor.addMark(mark, true);
      this.savedMarks = SlateEditor.marks(editor); // 获取当前选中文本的样式
      this.getSelectionParentEle("get", this.getorSetparentStyle); //获取父节点样式并赋值
    //   this.addmouseStyle(); //点击之后给鼠标添加样式
      this.addorRemove("add"); //处理添加和移除事件函数
      // 移除互斥、不能共存的 marks
      if (marksNeedToRemove) {
        marksNeedToRemove.forEach((m) => editor.removeMark(m));
      }
    }
    if (
      editor.isEmpty() ||
      editor.getHtml() == "<p><br></p>" ||
      editor.getSelectionText() == ""
    )
      return; //这里是对没有选中或者没内容做的处理
  }
}

五、页面应用组件

 <el-form-item label="内容">
 <WangEdit v-model="form.content" ref="wangEdit"  @change="change"></WangEdit>
  </el-form-item>


// js
const WangEdit = () => import("@/views/compoments/WangEdit.vue");
export default {
  name: "Notice",
  components: {
    WangEdit,
  },
    data(){
    return{
          form:{
         }
    }
    },

 methods: {
     change(val) {
            console.log(val,'aa');
            this.form.content=val
        },
     // 取消按钮
    cancel() {
      this.open = false;
      this.form={};
      this.$refs.wangEdit.closeContent();
    },
}

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Vue 3自定义组件的v-model默认绑定的是`modelValue`而不是`value`,接收的方法由`input`改为`@update:modelValue`。例如,在一个父组件,我们可以这样使用自定义组件: ``` <template> <child v-model="message" /> </template> ``` 然后在子组件,我们可以这样定义props和setup函数: ``` <script setup lang="ts"> import { defineProps, defineEmits } from 'vue' const props = defineProps({ modelValue: String }) const emits = defineEmits(['update:modelValue']) const onInput = (e) => { emits['update:modelValue'](e.target.value) } </script> <template> <input type="text" :value="modelValue" @input="onInput"> </template> ``` 通过这种方式,我们可以像使用原生的`v-model`一样在父组件使用自定义组件,并且可以正确地双向绑定`modelValue`的值。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [vue3 自定义组件v-model](https://blog.csdn.net/weixin_46694059/article/details/128935137)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [vue3自定义组件使用v-model](https://blog.csdn.net/qq_42075072/article/details/123800801)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值