vue3+wangEditor5/vue-quill自定义上传音频+视频

一.各种编辑器分析

Quill

这是另一个常用的富文本编辑器,它提供了许多可定制的功能和事件,并且也有一2个官方的 Vue 3 组件

wangEditor5

wangEditor5用在Vue3中自定义扩展音频、视频、图片菜单;并扩展音频元素节点,保证音频节点的插入、读取、回写功能正常;支持动态修改尺寸

二. vue-quill

官网地址

(一)安装

npm install @vueup/vue-quill@alpha --save

(二)使用

Editor/index.vue

<template>
  <div class="editor">
    <el-upload
      class="avatar-uploader-editor"
      action="#"
      :before-upload="beforeAvatarUpload"
      accept=".jpg, .png, .gif, .jpeg"
      :http-request="handleFileChange"
      :show-file-list="false"
    >
      <el-button type="default" style="display: none; font-size: 14px"
        ><el-icon><UploadFilled /></el-icon>上传图片</el-button
      >
    </el-upload>
    <quill-editor
      ref="editorRef"
      v-model:content="content"
      contentType="html"
      @textChange="e => $emit('update:modelValue', content)"
      @blur="changeQuillEditor"
      :options="options"
      :style="styles"
    />
  </div>
</template>

<script setup>
import {
    
     QuillEditor, Quill } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'
const {
    
     proxy } = getCurrentInstance()
const props = defineProps({
    
    
  /* 编辑器的内容 */
  modelValue: {
    
    
    type: String
  },
  /* 高度 */
  height: {
    
    
    type: Number,
    default: null
  },
  /* 最小高度 */
  minHeight: {
    
    
    type: Number,
    default: null
  },
  /* 只读 */
  readOnly: {
    
    
    type: Boolean,
    default: false
  }
})

const options = ref({
    
    
  theme: 'snow',
  bounds: document.body,
  debug: 'warn',
  modules: {
    
    
    // 工具栏配置
    toolbar: {
    
    
      container: [
        ['bold', 'italic', 'underline', 'strike'], // 加粗 斜体 下划线 删除线
        ['blockquote', 'code-block'], // 引用  代码块
        [{
    
     list: 'ordered' }, {
    
     list: 'bullet' }], // 有序、无序列表
        [{
    
     indent: '-1' }, {
    
     indent: '+1' }], // 缩进
        [{
    
     size: ['small', false, 'large', 'huge'] }], // 字体大小
        [{
    
     header: [1, 2, 3, 4, 5, 6, false] }], // 标题
        [{
    
     color: [] }, {
    
     background: [] }], // 字体颜色、字体背景颜色
        [{
    
     align: [] }], // 对齐方式
        ['clean'], // 清除文本格式
        ['image', 'video'] // 链接、图片、视频
      ],
      handlers: {
    
    
        image: function (value) {
    
    
          if (value) {
    
    
            if (props.readOnly) {
    
    
              return false
            }
            // 触发input框选择图片文件
            document.querySelector('.avatar-uploader-editor input').click()
          } else {
    
    
            Quill.format('image', false)
          }
        }
      }
    }
  },
  placeholder: props.readOnly ? '' : '请输入内容',
  readOnly: props.readOnly
  // theme: 'snow'
})
const styles = computed(() => {
    
    
  let style = {
    
    }
  if (props.minHeight) {
    
    
    style.minHeight = `${
    
    props.minHeight}px`
  }
  if (props.height) {
    
    
    style.height = `${
    
    props.height}px`
  }
  return style
})
/**** 上传图片 start */
const editorRef = ref(null)
/**文件上传 限制条件
 *
 * @param {*} rawFile
 */
function beforeAvatarUpload(rawFile) {
    
    
  if (rawFile.size / 1024 / 1024 > 5) {
    
    
    proxy.$modal.msgError('单个文件不能超过5MB!')
    return false
  }
  let quill = toRaw(editorRef.value).getQuill()
  // 把图片转成base64
  getBase64(rawFile, url => {
    
    
    let length = quill.selection.savedRange.index
    // 插入图片,res为服务器返回的图片链接地址
    quill.insertEmbed(length, 'image', url)
  })
}
//工具函数
const getBase64 = (img, callback) => {
    
    
  const reader = new FileReader()
  reader.addEventListener('load', () => callback(reader.result))
  reader.readAsDataURL(img)
}
// 
async function handleFileChange(params) {
    
    
  let formData = new FormData()
  formData.append('file', params.file)
}
/**** 上传图片 end */
const content = ref('')
watch(
  () => props.modelValue,
  v => {
    
    
    if (v !== content.value) {
    
    
      content.value = v === undefined ? '<p></p>' : v
    }
  },
  {
    
     immediate: true }
)
/** 鼠标移开 */
const emit = defineEmits(['myClick'])
const changeQuillEditor = () => {
    
    
  emit('myClick', content.value)
}
</script>

<style>
.editor,
.ql-toolbar {
    
    
  white-space: pre-wrap !important;
  line-height: normal !important;
}
.quill-img {
    
    
  display: none;
}
.ql-snow .ql-tooltip[data-mode='link']::before {
    
    
  content: '请输入链接地址:';
}
.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
    
    
  border-right: 0px;
  content: '保存';
  padding-right: 0px;
}

.ql-snow .ql-tooltip[data-mode='video']::before {
    
    
  content: '请输入视频地址:';
}

.ql-snow .ql-picker.ql-size .ql-picker-label::before,
.ql-snow .ql-picker.ql-size .ql-picker-item::before {
    
    
  content: '14px';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='small']::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='small']::before {
    
    
  content: '10px';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='large']::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='large']::before {
    
    
  content: '18px';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='huge']::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before {
    
    
  content: '32px';
}

.ql-snow .ql-picker.ql-header .ql-picker-label::before,
.ql-snow .ql-picker.ql-header .ql-picker-item::before {
    
    
  content: '文本';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='1']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='1']::before {
    
    
  content: '标题1';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='2']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='2']::before {
    
    
  content: '标题2';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='3']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='3']::before {
    
    
  content: '标题3';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='4']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='4']::before {
    
    
  content: '标题4';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='5']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='5']::before {
    
    
  content: '标题5';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='6']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before {
    
    
  content: '标题6';
}

.ql-snow .ql-picker.ql-font .ql-picker-label::before,
.ql-snow .ql-picker.ql-font .ql-picker-item::before {
    
    
  content: '标准字体';
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='serif']::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='serif']::before {
    
    
  content: '衬线字体';
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='monospace']::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before {
    
    
  content: '等宽字体';
}
</style>

在这里插入图片描述

(三)功能注意点

  • 通过按钮上传图片,图片不能到指定位置
    解决:
 let quill = toRaw(editorRef.value).getQuill()
  let length = quill.selection.savedRange.index
    // 插入图片,res为服务器返回的图片链接地址
  quill.insertEmbed(length, 'image', url)
  • 清除编辑器的表单验证
<template>
  <div class="myEditor">
      <el-form
        ref="formRef"
        :model="formModel"
        :rules="rules"
        label-width="100px"
      >
        <editor
          v-model="formModel.detail"
          :min-height="192"
          @myClick="changeText"
          :readOnly="formModel.id ? true : false"
          class="w-100%"
        />
      </el-form>
  </div>
</template>

<script setup name="myEditor">
/** 清除编辑器的表单验证 */
const changeText = text => {
    
    
  if ((text && text == '<p></p>') || text == '<p><br></p>') {
    
    
    formModel.value.detail = null
    proxy.$refs['formRef'].validateField('detail')
    return false
  }
  if (formModel.value.detail) {
    
    
    proxy.$refs['formRef'].clearValidate('detail') // clearValidate()取消验证方法
  }
}
</script>

(四)参考

三. wangeditor5

官网地址

(一) 安装

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

(二) 常见api

wangEditor 提供了丰富的 API ,可以进行任何编辑器操作。可参考文档

  const editor = editorRef.value
 - 插入内容文本:
   editor.insertText(' 222 ')
 - 插入节点:
 import {
    
     SlateTransforms } from '@wangeditor/editor'
  const node2 = [
      {
    
    
        type: 'video',
        src: 'https://www.runoob.com/try/demo_source/horse.mp3',
        children: [{
    
     text: 'bbb' }]
      }
    ]
    SlateTransforms.insertNodes(editor, node2)
 - 获取所有已注册的菜单
   editor.getAllMenuKeys();
 - 获取html
 	editor.getHtml()
 - 获取所有配置参数
   editor.getConfig()
 - 获取鼠标以上所有配置
   editorRef.value.getConfig().hoverbarKeys

(三) 配置

1. 编辑器配置

支持 readOnly autoFocus maxLength 等配置,可参考文档

// 编辑器配置
const editorConfig = {
    
    
     placeholder: '请输入内容...',
     readOnly: props.readonly,
     autoFocus: false,
     scroll: true,
    // 可继续其他配置...
    MENU_CONF: {
    
     /* 菜单配置 */ }
}

请注意,该文档中的所有回调函数,都不能以配置的形式传入,如 onCreated onChange onDestroyed 等。这些回调函数必须以 Vue 事件的方式传入

<Editor
    :editorId="editorId"
    :defaultConfig="editorConfig"
    :defaultContent="defaultContent"
    :defaultHtml="defaultHtml"
    style="height: 500px"
    
    <!-- 回调函数,以 Vue 事件形式 -->
    @onCreated="handleCreated"
    @onChange="handleChange"
    @onDestroyed="handleDestroyed"
    @onFocus="handleFocus"
    @onBlur="handleBlur"
    @customAlert="customAlert"
    @customPaste="customPaste"
  />

2.工具栏配置

修改工具栏的菜单,如隐藏某些菜单,重新排序分组,就可以使用该配置。支持 toolbarKeys 和 excludeKeys,可参考文档

/ 工具栏配置
const toolbarConfig = {
    
    
  toolbarKeys: [ /* 显示哪些菜单,如何排序、分组 */
    'undo', // 撤销
    'enter', // 回车
    'bulletedList', // 无序列表
    'numberedList',// 有序列表
    'insertTable',// 插入table
    // 菜单组,包含多个菜单
    {
    
    
      key: 'group-more-style', // 必填,要以 group 开头
      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', 'materialImage'] // 下级菜单 key ,必填
    }],
  excludeKeys: [ /* 隐藏哪些菜单 */],
}
<Toolbar
    :editorId="editorId"
    :defaultConfig="toolbarConfig" <!-- 传入配置 -->
    style="border-bottom: 1px solid #ccc"
/>

3.菜单配置

对某个菜单进行配置,例如配置颜色、字体、字号,配置上传图片的 API 地址等,可以使用菜单配置。具体参考文档

const editorConfig = computed(() => {
    
    
  return Object.assign({
    
    
    placeholder: '请输入内容...',
    readOnly: props.readonly,
    autoFocus: false,
    scroll: true,
	bgColor:{
    
    
		colors: ['#000', '#333', '#666']
	},
    MENU_CONF: {
    
    
      // 上传本地图片
      uploadImage: {
    
    
        /**
         *
         * @param {*} file 文件
         * @param {*} insertFn 输入到编辑器
         */
        async customUpload(file, insertFn) {
    
    
          if (file.size / 1024 / 1024 > 5) {
    
    
            customAlert('单个文件不能超过5MB!', 'warning')
            return false
          }
          // 把图片转成base64
          getBase64(file, url => {
    
    
            insertFn(url)
          })
        }
      },
      // 上传本地视频
      uploadVideo: {
    
    
        async customUpload(file, insertFn) {
    
    
          const url = 'https://www.runoob.com/try/demo_source/movie.ogg'
          insertFn(url)
        }
      }
    }
  })
})

(四) 自定义扩展新功能

1. 注册新菜单

ButtonMenu

MyButtonMenu.js

class MyButtonMenu {
    
    
  constructor() {
    
    
    this.title = '按钮菜单';
    this.tag = 'button'
  }
  // 菜单是否需要激活(如选中加粗文本,“加粗”菜单会激活),用不到则返回 false
  isActive(editor) {
    
    
    return false
  }
  // 获取菜单执行时的 value ,用不到则返回空 字符串或 false
  getValue(editor) {
    
    
    return ''
  }
  // 菜单是否需要禁用(如选中 H1 ,“引用”菜单被禁用),用不到则返回 false
  isDisabled(editor) {
    
    
    return false;
  }
  //点击菜单时触发的函数
  exec(editor, value) {
    
    
    if (this.isDisabled(editor)) {
    
    
      return;
    }
    editor.emit('MyButtonMenuClick');
  }
}

export default MyButtonMenu
ModalMenu
MyModalMenu.js
class MyModalMenu {
    
    
  constructor() {
    
    
    this.title = '弹出框菜单';
    this.tag = 'button'
    this.showModal = true
    this.modalWidth = 300
    this.iconSvg = '<svg t="1688026657351" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4449" width="128" height="128"><path d="M609.28 481.28L481.28 399.36c-10.24-5.12-25.6-5.12-35.84 0-15.36 5.12-20.48 15.36-20.48 30.72v158.72c0 15.36 10.24 25.6 20.48 30.72 5.12 0 10.24 5.12 15.36 5.12 5.12 0 15.36 0 20.48-5.12l128-81.92c10.24-5.12 15.36-15.36 15.36-30.72 0-5.12-5.12-15.36-15.36-25.6zM476.16 563.2V455.68L563.2 512l-87.04 51.2z m0 0" p-id="4450" fill="#515151"></path><path d="M824.32 737.28h-51.2v-409.6c0-40.96-35.84-76.8-76.8-76.8h-409.6v-51.2c0-15.36-10.24-25.6-25.6-25.6s-25.6 10.24-25.6 25.6v51.2h-51.2c-15.36 0-25.6 10.24-25.6 25.6s10.24 25.6 25.6 25.6h51.2v409.6c0 40.96 35.84 76.8 76.8 76.8h409.6v51.2c0 15.36 10.24 25.6 25.6 25.6s25.6-10.24 25.6-25.6v-51.2h51.2c15.36 0 25.6-10.24 25.6-25.6s-10.24-25.6-25.6-25.6z m-506.88 0c-15.36 0-25.6-10.24-25.6-25.6v-409.6h409.6c15.36 0 25.6 10.24 25.6 25.6v409.6h-409.6z m0 0" p-id="4451" fill="#515151"></path></svg>'
  }
  // 菜单是否需要激活(如选中加粗文本,“加粗”菜单会激活),用不到则返回 false
  isActive(editor) {
    
    
    return false
  }
  // 获取菜单执行时的 value ,用不到则返回空 字符串或 false
  getValue(editor) {
    
    
    return ''
  }
  // 菜单是否需要禁用(如选中 H1 ,“引用”菜单被禁用),用不到则返回 false
  isDisabled(editor) {
    
    
    return false;
  }
  //点击菜单时触发的函数
  exec(editor, value) {
    
    
    if (this.isDisabled(editor)) {
    
    
      return;
    }
    editor.emit('setModelClick');
  }
  // 弹出框 modal 的定位:1. 返回某一个 SlateNode; 2. 返回 null (根据当前选区自动定位)
  getModalPositionNode(editor) {
    
    
    return null // modal 依据选区定位
  }
  // 定义 modal 内部的 DOM Element
  getModalContentElem(editor) {
    
    
    //这里面的内容除了编辑器里面的函数其它全是html+css动态创建标签
    const parentDiv = document.createElement("div");
    parentDiv.className = 'CssClass0';//设置css样式
    parentDiv.style.cssText = "color:#333";
    const updatabutton1 = document.createElement("button");
    const h2 = document.createElement("h2");
    const updatabutton = document.createElement("button");
    const file1 = document.createElement("input");
    file1.style.cssText = "display:block;";
    h2.innerText = "大文件上传";
    updatabutton.innerText = "插入";
    updatabutton1.innerText = "上传";
    parentDiv.appendChild(h2);
    parentDiv.appendChild(file1);
    parentDiv.appendChild(updatabutton);
    parentDiv.appendChild(updatabutton1);
    //点击后删除事件
    function uploadBtnEvent() {
    
    
      editor.focus();//先获得焦点,再插入,就能成功
      editor.dangerouslyInsertHtml("<a href='#'>百度</a>", true);;
      editor.hidePanelOrModal();
    }
    //添加事件
    updatabutton.addEventListener('click', uploadBtnEvent)

    return parentDiv
  }
}

export default MyModalMenu
SelectMenu

MySelectMenu.js
class MySelectMenu {
    
    
  constructor() {
    
    
    this.title = 'audio'
    this.tag = 'select'
    this.width = 60
  }
  getOptions(editor) {
    
    
    const options = [
      {
    
     value: 'beijing', text: '北京', styleForRenderMenuList: {
    
     'font-size': '32px', 'font-weight': 'bold' } },
      {
    
     value: 'shanghai', text: '上海', selected: true },
      {
    
     value: 'shenzhen', text: '深圳' }
    ]
    return options
  }

  getValue(editor) {
    
    
    return 'shanghai' // 匹配 options 其中一个 value
  }
  isActive(editor) {
    
    
    return false // or true
  }
  isDisabled(editor) {
    
    
    return false // or true
  }
  exec(editor, value) {
    
    
    editor.insertText(value) // value 即 this.getValue(editor) 的返回值
    editor.insertText(' ')
  }
}

export default MySelectMenu
DropPanelMenu
AudioMenu.js
class AudioMenu {
    
    
  constructor() {
    
    
    this.title = 'Audio'
    this.tag = 'button'
    this.showDropPanel = true
  }
  getValue(editor) {
    
    
    return ''
  }
  isActive(editor) {
    
    
    return false // or true
  }
  isDisabled(editor) {
    
    
    return false // or true
  }
  exec(editor, value) {
    
    
    // do nothing 什么都不用做
  }
  getPanelContentElem(editor) {
    
    
    //这里面的内容除了编辑器里面的函数其它全是html+css动态创建标签
    const parentDiv = document.createElement("div");
    parentDiv.className = 'dropPanelSelStyle';//设置css样式
    const btn1 = createButton("网络音频", '<svg t="1688026336282" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1516" width="128" height="128"><path d="M876.15 960h-728.3C66.33 960 0 894.1 0 813.11V209.89C0 128.9 66.33 63 147.85 63h728.29C957.67 63 1024 128.9 1024 209.89V813.1c0 81-66.33 146.9-147.85 146.9z m-728.3-822.56c-40.21 0-72.93 32.5-72.93 72.45V813.1c0 39.95 32.71 72.45 72.93 72.45h728.29c40.21 0 72.93-32.5 72.93-72.45V209.89c0-39.95-32.71-72.45-72.93-72.45H147.85z m0 0" p-id="1517"></path><path d="M693 327.37v271.77c0 34.99-35.51 67.91-79.05 73.22-43.55 5.32-79.06-16.98-79.06-51.97 0-34.99 35.51-67.91 79.06-73.23 29.67-3.76 46.13 5.84 46.13 5.84V398c0-16.46-18.01-10.63-18.01-10.63l-162.91 50.94s-18.53 6.87-18.53 22.29v187.53c0 34.99-32.4 67.39-75.95 73.74-43.54 6.35-79.05-15.42-79.05-50.41 0-35.12 34.99-68.56 79.05-74.91 29.81-4.28 43.03 4.8 43.03 4.8V407.07c0-22.29 18.02-45.62 40.3-52.1l184.16-56.24c22.3-6.35 40.31 6.35 40.83 28.64z" p-id="1518" data-spm-anchor-id="a313x.7781069.0.i0" class="selected" fill="#2c2c2c"></path></svg>', () => {
    
    
      editor.emit('audioNetworkClick');
    }, {
    
     "class": "btn" });

    const btn2 = createButton("上传音频", '<svg t="1688026632239" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4018" width="128" height="128"><path d="M465.49 363.13c0-21.44 17.33-38.82 38.54-38.82 21.47 0 38.8 17.38 38.8 38.82v440.98c0 21.47-17.33 38.54-38.8 38.54-21.21 0-38.54-17.07-38.54-38.54V363.13z m352.87 303.89c0-21.44 17.1-39.08 38.54-39.08 21.47 0 39.1 17.64 39.1 39.08v89.22h89.18c21.47 0 38.82 17.33 38.82 38.8 0 21.16-17.35 38.54-38.82 38.54H896v89.46c0 21.46-17.63 38.54-39.1 38.54-21.44 0-38.54-17.08-38.54-38.54v-89.46h-89.71c-21.19 0-38.57-17.38-38.57-38.54 0-21.47 17.38-38.8 38.57-38.8h89.71v-89.22zM288.23 537.39c0-21.5 17.33-38.82 39.05-38.82 21.49 0 38.54 17.32 38.54 38.82v266.72c0 21.47-17.05 38.54-38.54 38.54-21.72 0-39.05-17.07-39.05-38.54V537.39zM38.82 961.58C17.35 961.58 0 944.5 0 923.04c0-21.47 17.35-38.54 38.82-38.54h555.76c21.47 0 38.82 17.07 38.82 38.54 0 21.46-17.35 38.54-38.82 38.54H38.82z m72.66-663.13c0-21.19 17.63-38.54 39.1-38.54 20.91 0 38.54 17.35 38.54 38.54v505.66c0 21.47-17.63 38.54-38.54 38.54-21.47 0-39.1-17.07-39.1-38.54V298.45z m708-80.67c0-21.47 17.61-39.07 38.52-39.07 21.49 0 39.1 17.61 39.1 39.07v309.96c0 20.91-17.61 38.54-39.1 38.54-20.91 0-38.52-17.63-38.52-38.54V217.78zM642.47 96.14c0-21.47 17.35-38.54 38.54-38.54 21.75 0 38.82 17.07 38.82 38.54v462.44c0 21.47-17.07 38.83-38.82 38.83-21.19 0-38.54-17.35-38.54-38.83V96.14z" fill="#515151" p-id="4019"></path></svg>', () => {
    
    
      editor.emit('audioLocalClick');
    }, {
    
     "class": "btn" });

    const btn3 = createButton("素材上传", '<svg t="1688026657351" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4449" width="128" height="128"><path d="M609.28 481.28L481.28 399.36c-10.24-5.12-25.6-5.12-35.84 0-15.36 5.12-20.48 15.36-20.48 30.72v158.72c0 15.36 10.24 25.6 20.48 30.72 5.12 0 10.24 5.12 15.36 5.12 5.12 0 15.36 0 20.48-5.12l128-81.92c10.24-5.12 15.36-15.36 15.36-30.72 0-5.12-5.12-15.36-15.36-25.6zM476.16 563.2V455.68L563.2 512l-87.04 51.2z m0 0" p-id="4450" fill="#515151"></path><path d="M824.32 737.28h-51.2v-409.6c0-40.96-35.84-76.8-76.8-76.8h-409.6v-51.2c0-15.36-10.24-25.6-25.6-25.6s-25.6 10.24-25.6 25.6v51.2h-51.2c-15.36 0-25.6 10.24-25.6 25.6s10.24 25.6 25.6 25.6h51.2v409.6c0 40.96 35.84 76.8 76.8 76.8h409.6v51.2c0 15.36 10.24 25.6 25.6 25.6s25.6-10.24 25.6-25.6v-51.2h51.2c15.36 0 25.6-10.24 25.6-25.6s-10.24-25.6-25.6-25.6z m-506.88 0c-15.36 0-25.6-10.24-25.6-25.6v-409.6h409.6c15.36 0 25.6 10.24 25.6 25.6v409.6h-409.6z m0 0" p-id="4451" fill="#515151"></path></svg>', () => {
    
    
      editor.emit('materialClick', 'audio');
    }, {
    
     "class": "btn" });

    parentDiv.appendChild(btn1);
    parentDiv.appendChild(btn2);
    parentDiv.appendChild(btn3);

    return parentDiv
  }
}
// 创建按钮
function createButton(text, svgText, method, attribute) {
    
    
  var button = document.createElement("button");
  button.innerHTML += svgText + '<span class="title">' + text + '</span>'
  button.addEventListener("click", method);
  // 添加额外属性
  for (var key in attribute) {
    
    
    button.setAttribute(key, attribute[key]);
  }
  return button;
}

export default AudioMenu

在这里插入图片描述

editImage(hoverbarKeys 上的菜单)–图片可以设置宽高
import {
    
    
  IModalMenu,
  IDomEditor,
  DomEditor,
  genModalInputElems,
  genModalButtonElems,
  t
} from '@wangeditor/core'
import {
    
     Node, Range } from 'slate'
import {
    
     Dom7Array } from 'dom7'
import {
    
     updateImageNode } from '../utils/helper'
import $ from '../utils/dom'
import {
    
     genRandomStr, getWH } from '../utils/index'
import {
    
     ImageElement, ImageStyle } from '../utils/custom-types'
/**
 * 生成唯一的 DOM ID
 */
function genDomID(): string {
    
    
  return genRandomStr('w-e-edit-image')
}
class EditImage{
    
    
  private $content: Dom7Array | null = null
  private readonly srcInputId = genDomID()
  private readonly hrefInputId = genDomID()
  private readonly widthId = genDomID()
  private readonly heightId = genDomID()
  private readonly buttonId = genDomID()
  constructor() {
    
    
    this.title = '修改图片'
    this.tag = 'button'
    this.showModal = true
    this.modalWidth = 300
    this.iconSvg =
      '<svg t="1693205321165" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4106" width="32" height="32"><path d="M358.165868 554.624796c-0.533143 0.680499-1.066285 1.391696-1.303692 2.251274l-41.104163 150.700257c-2.400676 8.772804 0.059352 18.226107 6.550183 24.892947 4.860704 4.742001 11.261485 7.350408 18.077727 7.350408 2.252297 0 4.504594-0.267083 6.727215-0.860601l149.630902-40.808428c0.23843 0 0.357134 0.207731 0.534166 0.207731 1.718131 0 3.408633-0.62217 4.683672-1.927909l400.113747-400.054395c11.883655-11.897981 18.404162-28.109198 18.404162-45.74281 0-19.989263-8.476045-39.963177-23.324218-54.767348l-37.786605-37.844933c-14.81645-14.848173-34.822087-23.338544-54.797024-23.338544-17.631566 0-33.842783 6.520507-45.75816 18.388812L358.758362 553.232077C358.344946 553.615816 358.462626 554.179658 358.165868 554.624796M862.924953 257.19778l-39.742143 39.71349-64.428382-65.451688 39.180348-39.179324c6.193049-6.222725 18.194384-5.318122 25.308409 1.822508l37.813211 37.845956c3.943822 3.941775 6.195096 9.18622 6.195096 14.372336C867.223862 250.574942 865.710392 254.42769 862.924953 257.19778M429.322487 560.907896l288.712541-288.728914 64.459081 65.49569L494.314711 625.838721 429.322487 560.907896zM376.718409 677.970032l20.863167-76.580143 55.656601 55.657624L376.718409 677.970032z" fill="#595959" p-id="4107"></path><path d="M888.265084 415.735539c-15.144932 0-27.562752 12.313443-27.620058 27.665083l0 372.98283c0 19.559475-15.885805 35.444257-35.475979 35.444257L194.220958 851.827709c-19.559475 0-35.504632-15.883759-35.504632-35.444257L158.716326 207.602222c0-19.575848 15.945157-35.474956 35.504632-35.474956l406.367171 0c15.231913 0 27.592428-12.371772 27.592428-27.606755 0-15.202237-12.360516-27.590382-27.592428-27.590382L190.013123 116.930129c-47.684022 0-86.49291 38.779212-86.49291 86.49291L103.520213 820.59231c0 47.713698 38.808888 86.47756 86.49291 86.47756l639.334083 0c47.715745 0 86.509283-38.763862 86.509283-86.47756L915.856489 443.222567C915.794068 428.048983 903.408993 415.735539 888.265084 415.735539" fill="#595959" p-id="4108"></path></svg>'
  }
  // 菜单是否需要激活(如选中加粗文本,“加粗”菜单会激活),用不到则返回 false
  isActive(editor) {
    
    
    return false
  }
  // 获取当前选择的图片节点信息
  getImageNode(editor: IDomEditor): Node | null {
    
    
    return DomEditor.getSelectedNodeByType(editor, 'image')
  }
  // 获取菜单执行时的 value ,用不到则返回空 字符串或 false
  getValue(editor) {
    
    
    return ''
  }
  // 菜单是否需要禁用(如选中 H1 ,“引用”菜单被禁用),用不到则返回 false
  isDisabled(editor) {
    
    
    const {
    
     selection } = editor
    if (selection == null) return true
    if (!Range.isCollapsed(selection)) return true // 选区非折叠,禁用

    const imageNode = DomEditor.getSelectedNodeByType(editor, 'image')

    // 未匹配到 image node 则禁用
    if (imageNode == null) return true
    return false
  }
  //点击菜单时触发的函数
  exec(editor, value) {
    
    
    // 点击菜单时,弹出 modal 之前,不需要执行其他代码
    // 此处空着即可
  }
  // 弹出框 modal 的定位:1. 返回某一个 SlateNode; 2. 返回 null (根据当前选区自动定位)
  getModalPositionNode(editor: IDomEditor): Node | null {
    
    
    return this.getImageNode(editor)
  }
  // 定义 modal 内部的 DOM Element
  getModalContentElem(editor: IDomEditor) {
    
    
    const {
    
     srcInputId, altInputId, hrefInputId, widthId, heightId, buttonId } =
      this
    const selectedImageNode = this.getImageNode(editor)
    if (selectedImageNode == null) {
    
    
      throw new Error('Not found selected image node')
    }
    // 获取 input button elem
    const [srcContainerElem, inputSrcElem] = genModalInputElems(
      t('图片地址'),
      srcInputId
    )
    const $inputSrc = $(inputSrcElem)
    const [hrefContainerElem, inputHrefElem] = genModalInputElems(
      t('图片连接'),
      hrefInputId
    )
    const $inputHref = $(inputHrefElem)
    const [wContainerElem, inputWElem] = genModalInputElems(
      t('图片宽度'),
      widthId
    )
    const $inputW = $(inputWElem)
    const [hContainerElem, inputHElem] = genModalInputElems(
      t('图片高度'),
      heightId
    )
    const $inputH = $(inputHElem)
    const [buttonContainerElem] = genModalButtonElems(buttonId, t('确定'))
    if (this.$content == null) {
    
    
      // 第一次渲染
      const $content = $('<div></div>')

      // 绑定事件(第一次渲染时绑定,不要重复绑定)
      $content.on('click', `#${
      
      buttonId}`, e => {
    
    
        e.preventDefault()
        const src = $content.find(`#${
      
      srcInputId}`).val()
        const href = $content.find(`#${
      
      hrefInputId}`).val()
        const width = $content.find(`#${
      
      widthId}`).val()
        const height = $content.find(`#${
      
      heightId}`).val()
        const style = {
    
    
          width: width + 'px',
          height: height + 'px'
        }
        this.updateImage(editor, src, alt, href, style)
        editor.hidePanelOrModal() // 隐藏 modal
      })

      // 记录属性,重要
      this.$content = $content
    }
    const $content = this.$content
    $content.empty() // 先清空内容

    // append inputs and button
    $content.append(srcContainerElem)
    $content.append(hrefContainerElem)
    $content.append(wContainerElem)
    $content.append(hContainerElem)
    $content.append(buttonContainerElem)
    // 设置 input val
    const {
    
     src, alt = '', href = '' } = selectedImageNode as ImageElement

    $inputSrc.val(src)
    // $inputAlt.val(alt)
    $inputHref.val(href)
    const {
    
     width, height } = getWH(editor.getHtml(), src)
    const w = width ? width.replace('px', '') : width
    $inputW.val(w)
    const h = height ? height.replace('px', '') : height
    $inputH.val(h)

    // focus 一个 input(异步,此时 DOM 尚未渲染)
    setTimeout(() => {
    
    
      $inputSrc.focus()
    })
    return $content[0]
  }
  private updateImage(
    editor: IDomEditor,
    src: string,
    alt = '',
    href = '',
    style: ImageStyle = {
    
    }
  ) {
    
    
    if (!src) return

    // 还原选区
    editor.restoreSelection()

    if (this.isDisabled(editor)) return

    // 修改图片信息
    updateImageNode(editor, src, alt, href, style)
  }
}

export default EditImage

在这里插入图片描述

上面定义几种模式的菜单后,注册菜单到wangEditor ,再插入菜单到工具栏


import {
    
     Boot } from "@wangeditor/editor"
/*** 自定义扩展菜单工具栏功能 */
import MyButtonMenu from "./MyButtonMenu";
import MySelectMenu from "./MySelectMenu";
import AudioMenu from './AudioMenu'
import EditImagefrom './EditImage'

import MyModalMenu from './MyModalMenu'

const MenusList = [
  {
    
    
    key: 'EditImage',
    class: EditImage,
    title: '修改图片',
    iseparate: false, //是否单独一行
    index: 24 // 菜单要在工具栏显示的位置
  },
  {
    
    
    key: 'MyButtonMenu',
    class: MyButtonMenu,
    title: '按钮菜单',
    iseparate: false,//是否单独一行
    index: 24 // 菜单要在工具栏显示的位置
  },
  {
    
    
    key: 'MyModalMenu',
    class: MyModalMenu,
    title: '弹出框菜单',
    iseparate: false,//是否单独一行
    index: 25 // 菜单要在工具栏显示的位置
  },
  {
    
    
    key: 'MySelectMenu',
    class: MySelectMenu,
    title: '下拉框选择',
    iseparate: false,//是否单独一行
    index: 26
  },
  {
    
    
    key: 'AudioMenu',
    class: AudioMenu,
    title: '上传音频',
    iseparate: false,//是否单独一行
    index: 27
  },
]
/**
 * 自定义扩展菜单工具栏
 * @param {*} editor 编辑器
 * @param {*} toolbarConfig  工具栏
 */
const registerMenu = function (editor, toolbarConfig) {
    
    
  const allRegisterMenu = editor.getAllMenuKeys(); // 获取所有已注册的菜单
  let keys = [];
  for (let item of MenusList) {
    
    
    if (allRegisterMenu.indexOf(item.key) < 0) {
    
     // 1.如果未注册,则注册
      const menuObj = {
    
    
        key: item.key,
        factory() {
    
    
          return new item.class()
        }
      }
      Boot.registerMenu(menuObj);
    }
    if (item.iseparate) {
    
    
      //如果是单行的则注册在toolbar
      keys.push(item.key)
    }

  }
  //2. 插入菜单到工具栏
  toolbarConfig.insertKeys = {
    
    
    index: MenusList[0].index,
    keys: keys
  }
}

export default registerMenu

创建编辑器时 注册

import registerMenu from './toolbars/index'

const initMediaMenuEvent = () => {
    
    
  const editor = editorRef.value
  editor.on('MyButtonMenuClick', () => {
    
    
		console.log('按钮菜单')
  })
  /*** 音频模块 start */
  editor.on('audioNetworkClick', () => {
    
    
    audioNetwork.value.isVisible = true
    editor.hidePanelOrModal()
  })
  editor.on('audioLocalClick', () => {
    
    
    // 本地音频
    document.querySelector('.avatar-uploader-editor input').click() //触发input框选择图片文件
  })
  /**
   *素材音频以及素材上传
   * type 类型
   */
  editor.on('materialClick', type => {
    
    
    // 素材音频
    materialParam.value.isVisible = true
    materialParam.value.type = type
    switch (type) {
    
    
      case 'audio':
        editor.insertBreak() //换行
        materialParam.value.title = '请选择音频'
        break
      case 'image':
        materialParam.value.title = '请选择图片'
        break
      case 'video':
        editor.insertBreak() //换行
        materialParam.value.title = '请选择视频'
        break
    }
    editor.hidePanelOrModal()
  })
}

//创建实例触发的事件
const handleCreated = editor => {
    
    
  editorRef.value = editor // 记录 editor 实例,重要!
  registerMenu(editor, toolbarConfig) // 注册自定义菜单
  initMediaMenuEvent() // 注册自定义菜单点击事件
}

注意

  • 必须在创建编辑器之前注册。
  • 全局只能注册一次,不要重复注册

(五) 使用

业务

<template>
  <div>
    <MyEditor
      v-model="activeNewsItem.content"
      :setEditorToolbar="editorConfig"
      @previewPage="previewPageClick"
    />
  </div>
</template>
<script setup name="ProjectList">
import MyEditor from '@/components/MyEditor'
const activeNewsItem = ref({
    
    
  content: '3'
})
const editorConfig = ref([])
/**
 * 预览页面
 */
const previewPageClick = () => {
    
    
  console.log('预览 获取html', activeNewsItem.value.content)
}
</script>

MyEditor组件

MyEditor/index.vye
<template>
  <div>
    <!-- 编辑器 -->
    <div class="wangeditorStyle border-1 border-solid border-#ccc z-99">
      <Toolbar
        style="border-bottom: 1px solid #ccc"
        :editor="editorRef"
        :editorId="editorId"
        :defaultConfig="toolbarConfig"
        :mode="mode"
      />
      <Editor
        :style="styles"
        v-model="valueHtml"
        :defaultConfig="editorConfig"
        :mode="mode"
        :editorId="editorId"
        @onCreated="handleCreated"
        @onChange="handleChange"
        @onBlur="handleBlur"
      />
    </div>
    <!-- 上传本地文件 start -->
    <el-upload
      class="avatar-uploader-editor"
      action="#"
      :before-upload="beforeAvatarUpload"
      accept=".mp3, .wma, .Aac, .ogg, .mpc"
      :show-file-list="false"
    >
      <el-button type="default" style="display: none; font-size: 14px"
        ><el-icon><UploadFilled /></el-icon>上传</el-button
      >
    </el-upload>
    <!-- 网络音频 -->
    <!-- 素材上传 -->
  </div>
</template>
<script setup name="MyEditor">
import '@wangeditor/editor/dist/css/style.css' // 引入 css
import {
    
     onBeforeUnmount, ref, shallowRef, onMounted, computed } from 'vue'
import {
    
     Editor, Toolbar } from '@wangeditor/editor-for-vue'
import {
    
     SlateTransforms } from '@wangeditor/editor'
import registerMenu from './toolbars/index'
import setToolbarKeys from './utils/setConfig'
import {
    
     getBase64,getSelection } from './utils/index'
import {
    
     isNumber } from '@/utils/validate'
/*** 自定义插件 */
import customAlert from './plugin/customAlert'
// 业务组件
import NetworkModel from './editorComponents/NetworkModel.vue'
import MaterialModel from './editorComponents/materialModel.vue'
const {
    
     proxy } = getCurrentInstance()
const props = defineProps({
    
    
  /* 编辑器的内容 */
  modelValue: {
    
    
    type: String
  },
  /*** 编辑器id */
  editorId: {
    
    
    type: String,
    default: 'wangeEditor-1'
  },
  /**配置项 */
  setEditorToolbar: {
    
    
    type: Array,
    default: () => undefined
  },
  /* 高度 */
  height: {
    
    
    type: Number,
    default: 500
  },
  /* 只读 */
  readonly: {
    
    
    type: Boolean,
    default: false
  }
})
const emit = defineEmits(['change', 'update:modelValue', 'previewPage'])
// 编辑器内容 html
const valueHtml = ref(null)
/******************************************* 配置 start *****************/
// 编辑器实例,必须用 shallowRef
const editorRef = shallowRef()
const mode = ref('default')
/**** 工具栏配置 */
const toolbarConfig = {
    
    
  toolbarKeys:
    props.setEditorToolbar && props.setEditorToolbar.length > 0
      ? props.setEditorToolbar
      : setToolbarKeys
}
/**** 编辑器配置 */
const editorConfig = computed(() => {
    
    
  return Object.assign({
    
    
    placeholder: '请输入内容...',
    readOnly: props.readonly,
    autoFocus: false,
    scroll: true,
    MENU_CONF: {
    
    
      // 上传本地图片
      uploadImage: {
    
    
        /**
         *
         * @param {*} file 文件
         * @param {*} insertFn 输入到编辑器
         */
        async customUpload(file, insertFn) {
    
    
          if (file.size / 1024 / 1024 > 5) {
    
    
            customAlert('单个文件不能超过5MB!', 'warning')
            return false
          }
          // 把图片转成base64
          getBase64(file, url => {
    
    
            insertFn(url)
          })
        }
      },
      // 上传本地视频
      uploadVideo: {
    
    
        async customUpload(file, insertFn) {
    
    
          const url = 'https://www.runoob.com/try/demo_source/movie.ogg'
          insertFn(url)
        }
      }
    }
  })
})
/**** 自定义扩展新功能 */
//注册自定义菜单点击事件 事件监听
const audioNetwork = ref({
    
    })
const materialParam = ref({
    
    })
const initMediaMenuEvent = () => {
    
    
  const editor = editorRef.value
  /*** 音频模块 start */
  editor.on('audioNetworkClick', () => {
    
    
    audioNetwork.value.isVisible = true
    editor.hidePanelOrModal()
  })
  editor.on('audioLocalClick', () => {
    
    
    // 本地音频
    document.querySelector('.avatar-uploader-editor input').click() //触发input框选择图片文件
  })
  /**
   *素材音频以及素材上传
   * type 类型
   */
  editor.on('materialClick', type => {
    
    
    // 素材音频....
  })
  /**预览页面 */
  editor.on('PreviewPageClick', () => {
    
    
    emit('previewPage', 'previewPage')
  })
}
/**
 * 弹出框 确定回调事件
 * @param {*} row 获取的参数
 */
const modelValOK = row => {
    
    
  const editor = editorRef.value
  let node = []
  let current_path = null
  switch (row.type) {
    
    
    case 'audioNetwork':
      // 网络音频
      current_path = getSelection()
      audioNetwork.value.isVisible = false
      if (!row.data.url) {
    
    
        return false
      }
      node.push({
    
    
        type: 'video',
        poster: row.data.poster || '',
        src: row.data.url,
        children: [{
    
     text: 'ddd' }] // 该字段必须要
      })
      SlateTransforms.insertNodes(editor, node, {
    
     at: [current_path] })
      break
    case 'image':
      // 素材图片
      materialParam.value.isVisible = false
      if (!row.data || row.data.length == 0) {
    
    
        return false
      }
      node = JSON.parse(JSON.stringify(row.data))
      node.map(value => {
    
    
        value.type = 'image'
        value.src = value.url
        value.children = [{
    
     text: value.name }] // 该字段必须要
      })
      SlateTransforms.insertNodes(editor, node)
      break
    default:
      current_path = getSelection()
      // 素材音频 视频
      materialParam.value.isVisible = false
      if (!row.data || row.data.length == 0) {
    
    
        editor.undo() //撤销
        return false
      }
      node = JSON.parse(JSON.stringify(row.data))
      node.map(value => {
    
    
        value.type = 'video'
        value.src = value.url
        value.children = [{
    
     text: value.name }] // 该字段必须要
      })
      SlateTransforms.insertNodes(editor, node, {
    
     at: [current_path] })
      break
  }
}

/*本地音频上传 限制条件
 *
 * @param {*} rawFile
 */
function beforeAvatarUpload(rawFile) {
    
    
  if (rawFile.size / 1024 / 1024 > 5) {
    
    
    proxy.$modal.msgError('单个文件不能超过5MB!')
    return false
  }
  const node = [
    {
    
    
      type: 'video',
      src: 'https://www.runoob.com/try/demo_source/horse.mp3',
      children: [{
    
     text: 'bbb' }]
    }
  ]
  SlateTransforms.insertNodes(editorRef.value, node)
}
//创建实例触发的事件
const handleCreated = editor => {
    
    
  editorRef.value = editor // 记录 editor 实例,重要!
  registerMenu(editor, toolbarConfig) // 注册自定义菜单
  initMediaMenuEvent() // 注册自定义菜单点击事件
}
// 回调函数 值发生改变触发
const handleChange = editor => {
    
    
  emit('change', editor)
}
const handleBlur = editor => {
    
    }

/******************************************* 配置 end *****************/
const styles = computed(() => {
    
    
  let style = {
    
    }
  if (props.height) {
    
    
    style.height = isNumber(props.height) ? `${
    
    props.height}px` : props.height
  }
  style.overflowY = 'hidden'
  return style
})
// 组件销毁时,也及时销毁编辑器
onBeforeUnmount(() => {
    
    
  const editor = editorRef.value
  if (editor == null) return
  editor.destroy() // 销毁,并移除 editor
})

watch(
  () => props.modelValue,
  v => {
    
    
    if (v === unref(valueHtml)) return
    valueHtml.value = v
  },
  {
    
     immediate: true }
)
// 监听
watch(
  () => valueHtml.value,
  val => {
    
    
    emit('update:modelValue', val)
  }
)
</script>
<style lang="scss">
.wangeditorStyle {
    
    
  // border: 1px solid #ccc;
  .dropPanelSelStyle {
    
    
    .btn {
    
    
      display: block;
      width: 100%;
      text-align: left;
      svg {
    
    
        vertical-align: text-bottom;
      }
    }
  }
}
</style>

(六) 注意点

  • editorRef 必须用 shallowRef
  • 组件销毁时,要及时销毁编辑器

(七) 功能点不足

  • customUpload自定义上传跟插件自带的上传不能一起使用;插件自定义上传插件必须填写server
  • 插件自带上传功能中maxNumberOfFiles 没有效果
  • 网络上传图片和视频 只有该文件允许访问 才可以上传成功【但是WangEditor并没有报错提示,只是编辑器内容展示不了】
  • 无查看源码功能(不能直接编辑html代码 和内容切换)
  • 无预览内容功能
    可以简单用一个预览的菜单按钮,然后document.write 出来
  /**预览页面 */
  editor.on('PreviewPageClick', () => {
    
    
    let printPages = window.open('', '_blank')
    printPages.document.write(unref(valueHtml))
  })
  • 不兼容音频插入
    i.懒人开发。
    其实在HTML中,音频文件也可以直接使用video标签播放,所以对于不想折腾的读者,可以直接使用video插入视频的方式来实现播放音频。

const node = [
{
type: ‘video’,
src: ‘https://www.runoob.com/try/demo_source/horse.mp3’,
children: [{ text: ‘bbb’ }]
}
]
SlateTransforms.insertNodes(editor, node)

用这种方式插入的话,在手机端预览这个内容(编辑器生成的内容)的话是有问题,ios不能识别video播放音频。所以需要在移动端预览页面 把音频的video改成audio 才可以

// 音频mp3 的video标签替换成audio 及type
var replacedContent = cnt.replace(
	/<video([\s\S]*?)<\/video>/g,
	function (match) {
    
    
	if (match.includes('.mp3')) {
    
    
	  return match
		.replace(/<video/g, '<audio')
		.replace(/type="video\/mp4"/g, 'type="audio/mpeg"')
	} else {
    
    
	  return match
	}
	}
)
dataInfos.value.content = replacedContent

ii. 费脑子开发
这里要注意,这边会涉及到wangEditor中ModalMenu、插件、新元素等方面的内容,具体可以参考官方文档。这边所涉及的源代码是在wangeditor的video源码的上做更改的。涉及多个文件。记得安装snabbdom.js这个包。

  • 插入视频(自定义如音频走视频的模式)SlateTransforms.insertNodes(editor, node) 找不到鼠标在编辑器内焦点 ,而是直接把节点插入到后面【我找了很多方式不行,目前是通过获取鼠标的上个节点位置,然后插入】
  • 自定义编辑器alert -customAlert 不行【我照着官网弄,但是没效果,具体不知道为啥 ,后期在研究】

(八) 集成后最终效果的演示视频

在这里插入图片描述

wangEditor5+vue3编辑器

(九) 参考

猜你喜欢

转载自blog.csdn.net/gao_xu_520/article/details/131453054