Эх сурвалжийг харах

feat:添加适配vue2的富文本编辑器组件

lh_hub 1 долоо хоног өмнө
parent
commit
bd15bc31ce

+ 3 - 1
package.json

@@ -1,7 +1,7 @@
 {
   "name": "@jfcloudvip/front-toolkit",
   "author": "lh_hub <2844316064@qq.com>",
-  "version": "1.2.1",
+  "version": "1.3.1",
   "description": "jfcloud内部前端公共功能工具包",
   "main": "src/index.js",
   "repository": {
@@ -15,6 +15,8 @@
   },
   "dependencies": {
     "@jfcloudvip/vue-anti-debug-plugin": "latest",
+    "@wangeditor-next/editor": "^5.3.1",
+    "@wangeditor-next/editor-for-vue2": "^1.0.2",
     "crypto-js": "^4.1.1",
     "dayjs": "^1.10.7"
   }

+ 283 - 0
src/components/RichTextEditor/index.vue

@@ -0,0 +1,283 @@
+<template>
+  <div
+    class="rich-text-editor"
+    :style="{ border: border ? '1px solid #ccc' : '0px solid #ccc' }"
+    :key="JSON.stringify(readOnly)"
+  >
+    <Toolbar
+      v-show="showToolbar"
+      ref="toolbarRef"
+      class="rich-text-editor-toolbar"
+      :editor="editor"
+      :defaultConfig="toolbarConfig"
+      :mode="mode"
+      :style="toolbarConfig.style"
+    />
+    <Editor
+      ref="editorRef"
+      class="rich-text-editor-editor"
+      v-model="html"
+      :defaultConfig="editorConfig"
+      :mode="mode"
+      style="overflow-y: auto"
+      :style="editorConfig.style"
+      @onCreated="onCreated"
+      @onMaxLength="onMaxLength"
+      @onChange="onChange"
+      @customPaste="customPaste"
+      @customAlert="customAlert"
+    />
+  </div>
+</template>
+
+<script>
+import { Editor, Toolbar } from "@wangeditor-next/editor-for-vue2";
+
+import {
+  initEditorConfig,
+  initToolbarConfig,
+  initUploadImage,
+} from "./initData";
+
+import "@wangeditor-next/editor/dist/css/style.css";
+
+export default {
+  components: { Editor, Toolbar },
+  props: {
+    value: {
+      type: String,
+      default: "",
+    },
+
+    /**
+     * 主题
+     * default 默认 | simple 简约
+     * 工具栏的配置也会有所不同
+     */
+    mode: {
+      type: String,
+      default: "simple",
+    },
+
+    /** 是否显示边框 */
+    border: {
+      type: Boolean,
+      default: true,
+    },
+
+    /** 是否只读 */
+    readOnly: {
+      type: Boolean,
+      default: initToolbarConfig.readOnly,
+    },
+
+    placeholder: {
+      type: String,
+      default: "请输入内容...",
+    },
+
+    /** 是否显示工具栏 */
+    showToolbar: {
+      type: Boolean,
+      default: true,
+    },
+
+    /** 工具栏配置 */
+    toolbarConfig: {
+      type: Object,
+      default: () => initToolbarConfig,
+    },
+
+    /** 编辑器配置 */
+    defaultEditorConfig: {
+      type: Object,
+      default: () => initEditorConfig,
+    },
+    /** 自定义粘贴事件。可阻止编辑器的默认粘贴,实现自己的粘贴逻辑 */
+    customPaste: {
+      type: Function,
+      default(editor, event, callback) {
+        // console.log(editor.getConfig());
+        // console.log(editor.getConfig().MENU_CONF.uploadImage);
+        // // 阻止默认粘贴行为
+        // event.preventDefault()
+        // // 获取粘贴的HTML数据
+        // let clipboardData = event.clipboardData || window.clipboardData
+        // let pastedHtml = clipboardData.getData('text/html')
+        // if (pastedHtml) {
+        //   // 处理粘贴内容,移除 <p><br></p> 标签
+        //   pastedHtml = pastedHtml.replace(/<p><br><\/p>/g, '')
+        //   // 插入处理后的内容到编辑器中
+        //   editor.dangerouslyInsertHtml(pastedHtml)
+        // }
+        // callback(false)
+      },
+    },
+    /** 自定义编辑器 alert */
+    customAlert: {
+      type: Function,
+      default: (s, t) => {},
+    },
+  },
+  data() {
+    return {
+      editor: null,
+      html: "",
+    };
+  },
+  computed: {
+    editorConfig() {
+      // console.log(this.$props);
+      const config = this.$props.defaultEditorConfig;
+      const MENU_CONF = config.MENU_CONF;
+      const that = this;
+      // console.log({
+      //   ...config,
+      //   readOnly: Boolean(this.$props.readOnly),
+      //   placeholder: this.$props.placeholder,
+      //   MENU_CONF: {
+      //     ...MENU_CONF,
+      //     uploadImage: {
+      //       onBeforeUpload(file) {
+      //         const f = Object.values(file)[0];
+      //         if (f.size > initUploadImage.base64LimitSize) {
+      //           that.$message.error(initUploadImage.overflowErrMsg);
+      //         }
+      //         return false;
+      //       },
+      //       onError(file, err, res) {
+      //         if (file.size > initUploadImage.base64LimitSize) {
+      //           that.$message.error(initUploadImage.overflowErrMsg);
+      //         }
+      //       },
+      //       ...MENU_CONF.uploadImage,
+      //     },
+      //   },
+      // });
+      return {
+        ...config,
+        readOnly: Boolean(this.$props.readOnly),
+        placeholder: this.$props.placeholder,
+        MENU_CONF: {
+          ...MENU_CONF,
+          uploadImage: {
+            /** 上传之前触发 */
+            onBeforeUpload(file) {
+              const f = Object.values(file)[0];
+              if (f.size > initUploadImage.maxFileSize) {
+                that.$message.error(initUploadImage.overflowErrMsg);
+                return false;
+              }
+            },
+            /** 上传错误,或者触发 timeout 超时 */
+            onError(file, err, res) {
+              const f = file;
+              if (f.size > initUploadImage.maxFileSize) {
+                that.$message.error(initUploadImage.overflowErrMsg);
+              }
+            },
+            /** 上传失败 */
+            onFailed(file, res) {
+              that.$message.error(`${file.name} 上传失败`);
+            },
+            ...MENU_CONF.uploadImage,
+          },
+        },
+      };
+    },
+  },
+  watch: {
+    html: {
+      handler(v) {
+        // console.log(this.$props);
+        // console.log("v", v);
+        // doSomething
+        this.pasteTableLineBreaker();
+      },
+      deep: true,
+    },
+    value: {
+      handler(v) {
+        if (v != this.html) {
+          this.html = v || "";
+        }
+      },
+      deep: true,
+      immediate: true,
+    },
+  },
+  methods: {
+    /** 编辑器创建完毕时的回调函数 */
+    onCreated(editor) {
+      this.editor = Object.seal(editor); // 一定要用 Object.seal() ,否则会报错
+      this.$nextTick(() => {
+        //   console.log(editor.getConfig())
+        //   const toolbar = DomEditor.getToolbar(this.editor)
+        //   const curToolbarConfig = toolbar.getConfig()
+        //   console.log('【 curToolbarConfig 】-39', curToolbarConfig)
+      });
+    },
+    /** 编辑器内容、选区变化时的回调函数 */
+    onChange(editor) {
+      if (this.html != this.$props.value) {
+        // console.log(this.html);
+        this.$emit("input", this.html == "<p><br></p>" ? "" : this.html);
+        this.$emit("onChange", editor);
+      }
+    },
+    /**
+     * 编辑器销毁时的回调函数
+     * 调用 editor.destroy() 即可销毁编辑器,详见 API
+     */
+    onDestroyed(editor) {
+      this.$emit("onDestroy", editor);
+    },
+    /** 编辑器 focus 时的回调函数 */
+    onFocus(editor) {
+      this.$emit("onFocus", editor);
+    },
+    /** 编辑器 blur 时的回调函数 */
+    onBlur(editor) {
+      this.$emit("onBlur", editor);
+    },
+    /** 文本长度达到指定长度 */
+    onMaxLength(editor) {
+      this.$emit("onMaxLength", editor);
+    },
+
+    /** 处理粘贴表格的时候生成一个换行 */
+    pasteTableLineBreaker() {
+      if (/<p><br><\/p><table/g.test(this.html)) {
+        let filteredHtml = this.html.replace(/<p><br><\/p><table/g, "<table");
+        filteredHtml = filteredHtml.replace(
+          /<\/table><p><br><\/p>/g,
+          "</table>"
+        );
+        this.editor.clear();
+        this.editor.dangerouslyInsertHtml(filteredHtml);
+        this.onChange(this.editor);
+      }
+    },
+  },
+  beforeDestroy() {
+    const editor = this.editor;
+    if (editor == null) return;
+    editor.destroy(); // 组件销毁时,及时销毁编辑器
+  },
+};
+</script>
+<style lang="scss">
+.rich-text-editor {
+  display: flex;
+  width: 100%;
+  height: 400px;
+  flex-direction: column;
+
+  .rich-text-editor-toolbar {
+    border-bottom: 1px solid #ccc;
+  }
+  .rich-text-editor-editor {
+    flex: 1;
+  }
+}
+</style>

+ 331 - 0
src/components/RichTextEditor/initData.js

@@ -0,0 +1,331 @@
+/** 上传图片配置 */
+export const initUploadImage = {
+  /** 上传地址 */
+  server: "/aew/file/upload",
+
+  /** form-data fieldName ,默认值 'wangeditor-uploaded-image' */
+  fieldName: "file",
+
+  /** 单个文件的最大体积限制,默认为 2M 单位kb */
+  maxFileSize: 50 * 1024 * 1024,
+
+  /** 自定义字段 超过最大体积限制的报错信息 */
+  overflowErrMsg: "文件大小不能超过 50 MB!",
+
+  /** 最多可上传几个文件,默认为 100 */
+  // maxNumberOfFiles: 1,
+
+  /**
+   * 选择文件时的类型限制,默认为 ['image/*']
+   * 如不想限制,则设置为 []
+   */
+  allowedFileTypes: ["image/*"],
+
+  /**
+   * 自定义上传参数,例如传递验证的 token 等。
+   * 参数会被添加到 formData 中,一起上传到服务端。
+   */
+  meta: {
+    // Authorization:'Bearer 3139591d-e1a9-4c08-a202-f37c2544a2dd'
+  },
+
+  /** 将 meta 拼接到 url 参数中,默认 false */
+  metaWithUrl: false,
+
+  /** 跨域是否传递 cookie ,默认为 false */
+  withCredentials: false,
+
+  /** 超时时间,默认为 10 秒 */
+  timeout: 60 * 1000,
+
+  /** 小于该值就插入 base64 格式(而不上传),默认为 0b 单位kb */
+  base64LimitSize: 50,
+
+  /** 上传之前触发 */
+  // onBeforeUpload(file) {
+  //   const f = Object.values(file)[0];
+  //   if (f.size > initUploadImage.base64LimitSize) {
+  //     that.$message.error(initUploadImage.overflowErrMsg);
+  //   }
+  //   console.log("上传文件", f);
+  //   // 1. return file 或者 new 一个 file ,接下来将上传
+  //   // 2. return false ,不上传这个 file
+  //   return false;
+  // },
+
+  /** 上传错误,或者触发 timeout 超时 */
+  // onError(file, err, res) {
+  //   console.log(file);
+  //   console.log(`${file.name} 上传出错`, err, res);
+  //   if (file.size > initUploadImage.base64LimitSize) {
+  //     that.$message.error(initUploadImage.overflowErrMsg);
+  //   }
+  // },
+
+  /** 上传进度的回调函数 */
+  onProgress(progress) {
+    // progress 是 0-100 的数字
+    console.log("当前已上传:", progress, "%");
+  },
+
+  /** 单个文件上传成功之后 */
+  onSuccess(file, res) {
+    console.log(`${file.name} 上传成功`, res);
+  },
+
+  // /** 上传失败 */
+  // onFailed(file, res) {
+  //   console.log(`${file.name} 上传失败`, res);
+  // },
+};
+
+/** 编辑器配置 */
+export const initEditorConfig = {
+  /** 配置编辑器 placeholder */
+  placeholder: "请输入内容...",
+
+  /**
+   * 配置编辑器是否只读,默认为 false
+   * 只读状态可通过 editor.enable() 和 editor.disable() 切换,详见 API 。
+   */
+  readOnly: false,
+
+  /** 配置编辑器默认是否 focus ,默认为 true */
+  autoFocus: true,
+
+  /**
+   * 配置编辑器是否支持滚动,默认为 true 。
+   * 注意,此时不要固定 editor-container 的高度,设置一个 min-height 即可。
+   */
+  scroll: true,
+
+  /**
+   * 配置编辑器的文本长度
+   * 无特殊需求,请慎用 maxLength ,这可能会导致编辑器内容过多时,编辑卡顿。
+   */
+  // maxLength: 0,
+
+  /**
+   * 配置编辑器的 hoverbar 菜单。
+   * 通过 editor.getConfig().hoverbarKeys 可查看当前的 hoverbarKeys
+   *
+   * TIP:
+   * createEditor 时设置 mode: 'simple' 可隐藏选中文本时的 hoverbar 。
+   */
+  hoverbarKeys: {
+    link: {
+      menuKeys: ["editLink", "unLink", "viewLink"],
+    },
+    image: {
+      menuKeys: [
+        "imageWidth30",
+        "imageWidth50",
+        "imageWidth100",
+        "editImage",
+        "viewImageLink",
+        "deleteImage",
+      ],
+    },
+    pre: {
+      menuKeys: ["enter", "codeBlock", "codeSelectLang"],
+    },
+    table: {
+      menuKeys: [
+        "enter",
+        "tableHeader",
+        "tableFullWidth",
+        "insertTableRow",
+        "deleteTableRow",
+        "insertTableCol",
+        "deleteTableCol",
+        "deleteTable",
+      ],
+    },
+    divider: {
+      menuKeys: ["enter"],
+    },
+    video: {
+      menuKeys: ["enter", "editVideoSize"],
+    },
+    text: {
+      menuKeys: [
+        "headerSelect",
+        "insertLink",
+        "bulletedList",
+        "|",
+        "bold",
+        "through",
+        "color",
+        // "bgColor",
+        "clearStyle",
+      ],
+    },
+  },
+
+  /** 菜单相关配置 */
+  MENU_CONF: {
+    uploadImage: initUploadImage,
+  },
+
+  /** 用于第三方插件做扩展配置,如 mention 插件 */
+  // EXTEND_CONF: {},
+
+  /** 自定义字段 */
+  style: {},
+};
+
+/** 工具栏配置 */
+export const initToolbarConfig = {
+  /** 配置工具栏,显示哪些菜单,以及菜单的排序、分组。 */
+  // toolbarKeys: mode === 'default' ? toolbarDefaultKeys : toolbarSimpleKeys
+  /**
+   * 如果仅仅想排除掉某些菜单,其他都保留,可以使用 excludeKeys 来配置。
+   * 可通过 toolbar.getConfig().toolbarKeys 查看工具栏的默认配置
+   */
+  excludeKeys: [
+    "bgColor", //  背景色
+    "todo", // 代办
+    "group-video", // 视频
+    "codeBlock", // 代码块
+    "undo", // 撤销
+    "redo", // 重做
+    "fullScreen", // 全屏
+    "insertVideo", // 插入视频
+  ],
+  /** 可以在当前 toolbarKeys 的基础上继续插入新菜单,如自定义扩展的菜单 */
+  insertKeys: {
+    index: 0,
+    keys: [],
+  },
+  /** 将菜单弹出的 modal 添加到 body 下,并自定义 modal 的定位和其他样式 */
+  modalAppendToBody: false,
+
+  /** 自定义字段 */
+  style: {},
+};
+
+// 工具栏默认配置
+const toolbarDefaultKeys = [
+  "headerSelect", // 标题 H1~H5 正文
+  "blockquote", // 引用
+  "|",
+  "bold", // 加粗
+  "underline", // 下划线
+  "italic", // 斜体
+  {
+    key: "group-more-style",
+    title: "更多",
+    iconSvg:
+      '<svg viewBox="0 0 1024 1024"><path d="M204.8 505.6m-76.8 0a76.8 76.8 0 1 0 153.6 0 76.8 76.8 0 1 0-153.6 0Z"></path><path d="M505.6 505.6m-76.8 0a76.8 76.8 0 1 0 153.6 0 76.8 76.8 0 1 0-153.6 0Z"></path><path d="M806.4 505.6m-76.8 0a76.8 76.8 0 1 0 153.6 0 76.8 76.8 0 1 0-153.6 0Z"></path></svg>',
+    menuKeys: [
+      "through", // 删除线
+      "code", // 行内代码
+      "sup", // 上标
+      "sub", // 下标
+      "clearStyle", // 清楚格式
+    ],
+  },
+  "color", //  文字颜色
+  "bgColor", //  背景色
+  "|",
+  "fontSize", // 字号
+  "fontFamily", // 字体
+  "lineHeight", // 行高
+  "|",
+  "bulletedList", // 无序列表
+  "numberedList", // 有序列表
+  "todo", // 代办
+  {
+    key: "group-justify",
+    title: "对齐",
+    iconSvg:
+      '<svg viewBox="0 0 1024 1024"><path d="M768 793.6v102.4H51.2v-102.4h716.8z m204.8-230.4v102.4H51.2v-102.4h921.6z m-204.8-230.4v102.4H51.2v-102.4h716.8zM972.8 102.4v102.4H51.2V102.4h921.6z"></path></svg>',
+    menuKeys: [
+      "justifyLeft", // 左对齐
+      "justifyRight", // 右对齐
+      "justifyCenter", // 居中对齐
+      "justifyJustify", // 两端对齐
+    ],
+  },
+  {
+    key: "group-indent",
+    title: "缩进",
+    iconSvg:
+      '<svg viewBox="0 0 1024 1024"><path d="M0 64h1024v128H0z m384 192h640v128H384z m0 192h640v128H384z m0 192h640v128H384zM0 832h1024v128H0z m0-128V320l256 192z"></path></svg>',
+    menuKeys: [
+      "indent", // 添加缩进
+      "delIndent", // 减少缩进
+    ],
+  },
+  "|",
+  "emotion", // 表情
+  "insertLink", // 插入链接
+  {
+    key: "group-image",
+    title: "图片",
+    iconSvg:
+      '<svg viewBox="0 0 1024 1024"><path d="M959.877 128l0.123 0.123v767.775l-0.123 0.122H64.102l-0.122-0.122V128.123l0.122-0.123h895.775zM960 64H64C28.795 64 0 92.795 0 128v768c0 35.205 28.795 64 64 64h896c35.205 0 64-28.795 64-64V128c0-35.205-28.795-64-64-64zM832 288.01c0 53.023-42.988 96.01-96.01 96.01s-96.01-42.987-96.01-96.01S682.967 192 735.99 192 832 234.988 832 288.01zM896 832H128V704l224.01-384 256 320h64l224.01-192z"></path></svg>',
+    menuKeys: [
+      "insertImage", // 网络图片
+      "uploadImage", // 上传图片
+    ],
+  },
+  {
+    key: "group-video",
+    title: "视频",
+    iconSvg:
+      '<svg viewBox="0 0 1024 1024"><path d="M981.184 160.096C837.568 139.456 678.848 128 512 128S186.432 139.456 42.816 160.096C15.296 267.808 0 386.848 0 512s15.264 244.16 42.816 351.904C186.464 884.544 345.152 896 512 896s325.568-11.456 469.184-32.096C1008.704 756.192 1024 637.152 1024 512s-15.264-244.16-42.816-351.904zM384 704V320l320 192-320 192z"></path></svg>',
+    menuKeys: [
+      "insertVideo", // 插入视频
+      "uploadVideo", // 上传视频
+    ],
+  },
+  "insertTable", // 插入表格
+  "codeBlock", // 代码块
+  "divider", // 分割线
+  "|",
+  "undo", // 撤销
+  "redo", // 重做
+  "|",
+  "fullScreen", // 全屏
+];
+
+// 工具栏默认配置
+const toolbarSimpleKeys = [
+  "blockquote",
+  "header1",
+  "header2",
+  "header3",
+  "|",
+  "bold",
+  "underline",
+  "italic",
+  "through",
+  "color",
+  "bgColor",
+  "clearStyle",
+  "|",
+  "bulletedList",
+  "numberedList",
+  "todo",
+  "justifyLeft",
+  "justifyRight",
+  "justifyCenter",
+  "|",
+  "insertLink",
+  {
+    key: "group-image",
+    title: "图片",
+    iconSvg:
+      '<svg viewBox="0 0 1024 1024"><path d="M959.877 128l0.123 0.123v767.775l-0.123 0.122H64.102l-0.122-0.122V128.123l0.122-0.123h895.775zM960 64H64C28.795 64 0 92.795 0 128v768c0 35.205 28.795 64 64 64h896c35.205 0 64-28.795 64-64V128c0-35.205-28.795-64-64-64zM832 288.01c0 53.023-42.988 96.01-96.01 96.01s-96.01-42.987-96.01-96.01S682.967 192 735.99 192 832 234.988 832 288.01zM896 832H128V704l224.01-384 256 320h64l224.01-192z"></path></svg>',
+    menuKeys: ["insertImage", "uploadImage"],
+  },
+  "insertVideo",
+  "insertTable",
+  "codeBlock",
+  "|",
+  "undo",
+  "redo",
+  "|",
+  "fullScreen",
+];

+ 4 - 0
src/components/index.js

@@ -1,7 +1,11 @@
 import BrowserTypePrompt from "./BrowserTypePrompt/index.vue";
 import ExpireModel from "./ExpireModel/index.vue";
+import RichTextEditor from "./RichTextEditor/index.vue";
+import RichTextOption from "./RichTextEditor/initData.js";
 
 export {
   BrowserTypePrompt,
   ExpireModel,
+  RichTextEditor,
+  RichTextOption,
 };

+ 14 - 2
src/index.js

@@ -12,7 +12,12 @@ import { checkToken } from "./mixins/index.js";
 import setupPlugins from "./plugin/index.js";
 
 // 引入自定义组件集合
-import { BrowserTypePrompt, ExpireModel } from "./components/index.js";
+import {
+  BrowserTypePrompt,
+  ExpireModel,
+  RichTextEditor,
+  RichTextOption,
+} from "./components/index.js";
 
 // 引入登录加密功能
 import { loginEncrypt } from "./func/loginEncryption/index.js";
@@ -29,7 +34,14 @@ export {
 // 默认导出所有模块,可用于 Vue 应用的全局注册或配置
 export default {
   mixins: { checkToken },
-  components: { BrowserTypePrompt, ExpireModel },
+  components: {
+    BrowserTypePrompt,
+    ExpireModel,
+    RichText: {
+      Editor: RichTextEditor,
+      initOption: RichTextOption,
+    },
+  },
   setupPlugins,
   func: { loginEncrypt },
 };