doum
2025-09-11 7298d5354963a88643a543b51b90192dc9fc934c
admin/src/components/common/RichEditor.vue
@@ -1,158 +1,318 @@
<template>
  <div style="border: 1px solid #ccc;">
    <Toolbar
      style="border-bottom: 1px solid #ccc"
      :editor="editor"
      :defaultConfig="toolbarConfig"
      :mode="mode"
    />
    <Editor
      style="height: 300px; overflow-y: hidden;"
      :value="content.content"
      :mode="mode"
      :defaultConfig="editorConfig"
      @onCreated="onCreated"
      @onChange="onChange"
      @input="html=$event"
    />
  <div :style="styleEditor">
    <Toolbar style="border-bottom: 1px solid #ccc" :editor="editor" :defaultConfig="toolbarConfig" :mode="mode" />
    <Editor style="min-height: 80px; overflow-y: hidden;" v-model="html" :defaultConfig="editorConfig" :mode="mode"
            @onCreated="onCreated" @onChange="onChange" />
  </div>
</template>
<style src="@wangeditor/editor/dist/css/style.css"></style>
<script>
import Vue from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
export default {
  name: 'RichEditor',
  components: { Editor, Toolbar },
import { Loading } from 'element-ui';
let loadingInstance = null
export default Vue.extend({
  props: {
    content: {
      type: Object,
      default: () => {}
    }
    richData: { // 父组件传递的数据
      type: String,
      default: ''
    },
    styleEditor: '',
    readonly: false, // 是否可以输入
  },
  name:'RichEditor',
  components: { Editor, Toolbar },
  data() {
    return {
      editor: null,
      html: '<p><br></p>',
      toolbarConfig: {
        toolbarKeys: [
          "headerSelect",
          "blockquote",
          "|",
          "bold",
          "underline",
          "italic",
      html: '',
      toolbarConfig: { // 工具栏配置
        toolbarKeys: this.readonly ? ["fullScreen"]: [ // 显示指定的菜单项
          "bold", // 粗体
          "underline", // 下划线
          "italic", // 斜体
          "through", // 删除线
          "code", // 行内代码
          "sub", // 下标
          "sup", // 上标
          "clearStyle", // 清除格式
          "color", // 字体颜色
          "bgColor", // 背景色
          "fontSize", // 字号
          "fontFamily", // 字体
          "indent", // 增加缩进
          "delIndent", // 减少缩进
          "justifyLeft", // 左对齐
          "justifyRight", // 右对齐
          "justifyCenter", // 居中对齐
          "justifyJustify", // 两端对齐
          "lineHeight", // 行高
          // "viewImageLink", // 查看链接
          "divider", // 分割线
          "emotion", // 表情
          "insertLink", // 插入链接
          // "editLink", // 修改链接
          // "unLink", // 取消链接
          // "viewLink", // 查看链接
          "codeBlock", // 代码块
          "blockquote", // 引用
          "headerSelect", // 标题
          // "header1", // 标题1
          // "header2", // 标题2
          // "header3", // 标题3
          // "header4", // 标题4
          // "header5", // 标题5
          // "todo", // 待办
          "redo", // 重做
          "undo", // 撤销
          // "enter", // 回车
          // "bulletedList", // 无序列表
          // "numberedList", // 有序列表
          // "codeSelectLang" // 选择语言
          // 表格功能分组
          /* {
             key: 'table-style', // 必填,要以 group 开头
             title: '表格', // 必填
             // iconSvg: '<svg>....</svg>', // 可选
             menuKeys: [
               "insertTable", // 插入表格
               "deleteTable", // 删除表格
               "insertTableRow", // 插入行
               "deleteTableRow", // 删除行
               "insertTableCol", // 插入列
               "deleteTableCol", // 删除列
               "tableHeader", // 表头
               "tableFullWidth", // 宽度自适应
             ] // 下级菜单 key ,必填
           },*/
          // 上传图片分组
          {
              "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"
              ]
            key: 'img-style', // 必填,要以 group 开头
            title: '图片', // 必填
            // iconSvg: '<svg>....</svg>', // 可选
            menuKeys: [
              "uploadImage", // 上传图片
              "insertImage", // 网络图片
              "deleteImage", // 删除图片
              "editImage", // 编辑图片
              "imageWidth30", // 图片宽度相对于编辑器宽度的百分比30
              "imageWidth50", // 图片宽度相对于编辑器宽度的百分比50
              "imageWidth100", // 图片宽度相对于编辑器宽度的百分比100
            ] // 下级菜单 key ,必填
          },
          "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: 'video-style', // 必填,要以 group 开头
            title: '视频', // 必填
            // iconSvg: '<svg>....</svg>', // 可选
            menuKeys: [
              "insertVideo", // 插入网络视频
              "uploadVideo", // 上传视频
              "editVideoSize", // 修改视频尺寸
            ] // 下级菜单 key ,必填
          },
          {
              "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"
              ]
          },
          "insertTable",
          "codeBlock",
          "divider",
          "|",
          "undo",
          "redo",
          "|",
          "fullScreen"
        ]
      },
      editorConfig: {
        placeholder: '请输入内容...',
        MENU_CONF: {
          uploadImage: {
            // server: '/api/upload',
            name: 'file',
            server: process.env.VUE_APP_API_PREFIX + '/public/uploadLocal',
            meta: {
              folder: 'shop'
            },
            onBeforeUpload(file) {    // JS 语法
              // file 选中的文件,格式如 { key: file }
              // debugger
              return file
            },
            onSuccess(file, res) {
                console.log(`${file.name} 上传成功`, res)
            },
            onError(file, err, res) {
              console.log(`${file.name} 上传出错`, err, res)
            },
          },
          "fullScreen", // 全屏
        ],
        excludeKeys: [ // 隐藏指定的菜单项
          // 'headerSelect',
          // 'video-style'
          // 排除菜单组,写菜单组 key 的值即可
        ],
      },
      editorConfig: { // 编辑器配置
        placeholder: '请输入内容...',
        readOnly: this.readonly, // 是否只读,默认false
        autoFocus: false, // 是否自动focus,默认为true
        scroll: true, // 配置编辑器是否支持滚动,默认为 true 。注意,此时不要固定 editor-container 的高度,设置一个 min-height 即可。
        maxLength: 20000, // 最大限制,避免内容过多卡顿
        MENU_CONF: {
          // 图片上传
          uploadImage: {
            server: process.env.VUE_APP_API_PREFIX + '/visitsAdmin/cloudService/public/upload?folder=richeditor',
            fieldName: 'file',
            // 单个文件的最大体积限制,默认为 2M
            maxFileSize: 10 * 1024 * 1024, // 10M
            // 最多可上传几个文件,默认为 100
            maxNumberOfFiles: 10,
            // 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []
            allowedFileTypes: ['image/*'],
            // 自定义上传参数,例如传递验证的 token 等。参数会被添加到 formData 中,一起上传到服务端。
            meta: {
            },
            // 将 meta 拼接到 url 参数中,默认 false
            metaWithUrl: false,
            // 自定义增加 http  header
            // headers: { Authorization: "Bearer " + getToken() },
            // 跨域是否传递 cookie ,默认为 false
            withCredentials: true,
            // 超时时间,默认为 10 秒
            timeout: 10 * 1000, //10 秒
            // 上传前
            onBeforeUpload(files) {
              loadingInstance = Loading.service({
                lock: true,
                text: '上传中...',
                spinner: 'el-icon-loading',
                background: 'rgba(0, 0, 0, 0.7)'
              });
              return files;
            },
            // 自定义插入图片
            customInsert(res, insertFn) {
              console.log(res);
              // 因为自定义插入导致onSuccess与onFailed回调函数不起作用,自己手动处理
              // 先关闭等待的Message
              loadingInstance = Loading.service({
                lock: true,
                text: '上传中...',
                spinner: 'el-icon-loading',
                background: 'rgba(0, 0, 0, 0.7)'
              }).close();
              if (res.code === 200) {
                // Message.success({
                //     message: `${res.data.originalName} 上传成功`
                // });
              } else {
                // Message.error({
                //     message: `${res.data.originalName} 上传失败,请重新尝试`
                // });
              }
              insertFn(res.data.url, res.data.originname, res.data.imgname);
            },
            // 单个文件上传成功之后
            onSuccess(file, res) {
              console.log(`${file.originalFilename} 上传成功`, res);
            },
            // 单个文件上传失败
            onFailed(file, res) {
              console.log(`${file.originalFilename} 上传失败`, res);
              loadingInstance.close();
            },
            // 上传进度的回调函数
            onProgress(progress) {
              console.log('progress', progress);
              // progress 是 0-100 的数字
            },
            // 上传错误,或者触发 timeout 超时
            onError(file, err, res) {
              loadingInstance.close();
              console.log(`${file.originalFilename} 上传出错`, err, res);
            }
          },
          // 视频上传
          uploadVideo: {
            fieldName: 'file',
            server: process.env.VUE_APP_API_PREFIX + '/public/upload?folder=richeditor',
            // 单个文件的最大体积限制,默认为 10M
            maxFileSize: 50 * 1024 * 1024, // 50M
            // 最多可上传几个文件,默认为 5
            maxNumberOfFiles: 3,
            // 选择文件时的类型限制,默认为 ['video/*'] 。如不想限制,则设置为 []
            allowedFileTypes: ['video/*'],
            // 自定义上传参数,例如传递验证的 token 等。参数会被添加到 formData 中,一起上传到服务端。
            meta: {
              // token: 'xxx',
              // otherKey: 'yyy'
            },
            // 将 meta 拼接到 url 参数中,默认 false
            metaWithUrl: false,
            // 自定义增加 http  header
            headers: {
              // Authorization: "Bearer " + getToken()
              // otherKey: 'xxx'
            },
            // 跨域是否传递 cookie ,默认为 false
            withCredentials: true,
            // 超时时间,默认为 30 秒
            timeout: 1000 * 1000, // 1000 秒,
            // 上传之前触发
            onBeforeUpload(file) {
              return file;
            },
            // 自定义插入视频
            customInsert(res, insertFn) {
              // 因为自定义插入导致onSuccess与onFailed回调函数不起作用,自己手动处理
              // 先关闭等待的Message
              // Message.closeAll();
              if (res.code === 200) {
                // Message.success({
                //     message: `${res.data.originalName} 上传成功`
                // });
              } else {
                // Message.error({
                //     message: `${res.data.originalName} 上传失败,请重新尝试`
                // });
              }
              insertFn(res.data.url, res.data.url);
            },
            // 上传进度的回调函数
            onProgress(progress) {
              console.log(progress);
              // onProgress(progress) {       // JS 语法
              // progress 是 0-100 的数字
            },
            // // 单个文件上传成功之后
            // onSuccess(file, res) {
            //   console.log(`${file.name} 上传成功`, res);
            //   this.successMsg(file);
            // },
            // // 单个文件上传失败
            // onFailed(file, res) {
            //   console.log(`${file.name} 上传失败`, res);
            //   this.errorMsg(file);
            // },
            // 上传错误,或者触发 timeout 超时
            onError(file, err, res) {
              console.log(`${file.name} 上传出错`, err, res);
              // Notification.error({
              //     title: '错误',
              //     message: `${file.name} 上传失败,请重新尝试`
              // });
            }
          }
        }
      },
      mode: 'default', // or 'simple'
    }
  },
  watch: {
    richData: function (value) {
      this.html = value
    },
    readonly: function (value) {
      this.readonly = value
    },
    styleEditor: function (value) {
      this.styleEditor = value
    },
  },
  mounted() {
    // 需要在编辑器创建完毕后在赋值
    this.$nextTick(()=>{
      this.html = this.richData
    })
  },
  methods: {
    // 编辑器创建完毕时的回调函数
    onCreated(editor) {
      this.editor = Object.seal(editor) // 一定要用 Object.seal() ,否则会报错
    },
    // 编辑器内容、选区变化时的回调函数
    onChange(editor) {
      this.$emit('getWangedditor', editor.getHtml())
      console.log("onChange", editor.getHtml()); // onChange 时获取编辑器最新内容
    },
  },
  beforeDestroy() {
    // 编辑器销毁时的回调函数。调用 editor.destroy() 即可销毁编辑器
    const editor = this.editor
    if (editor == null) return
    editor.destroy() // 组件销毁时,及时销毁编辑器
  },
  methods: {
    onCreated (editor) {
      this.editor = Object.seal(editor)
    },
    onChange (editor) {
      console.log(this.html);
      // debugger
      if (!this.html||this.content.content==this.html) {
        return
      }
      this.$emit('edit', this.html)
    },
  },
}
  }
})
</script>
<style src="@wangeditor/editor/dist/css/style.css"></style>
<style lang="scss">
</style>>