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

news2024/12/23 22:13:26

一.各种编辑器分析

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'] // 链接、图片、视频
      ],
      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>

(四)参考

  • vue3使用vueup/vue-quill富文本、并限制输入字数

三. 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()
 - 

(三) 配置

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

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


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

import MyModalMenu from './MyModalMenu'
const MenusList = [
  {
    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 style="border: 1px solid #ccc">
    <Toolbar
      style="border-bottom: 1px solid #ccc"
      :editor="editorRef"
      :defaultConfig="toolbarConfig"
      :mode="mode"
    />
    <Editor
      style="height: 500px; overflow-y: hidden"
      v-model="valueHtml"
      :defaultConfig="editorConfig"
      :mode="mode"
      @onCreated="handleCreated"
    />
  </div>
</template>
<script setup name="MyEditor">
import '@wangeditor/editor/dist/css/style.css' // 引入 css
import { onBeforeUnmount, ref, shallowRef, onMounted } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-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
  }
})
// 编辑器内容 html
const valueHtml = ref(null)
/******************************************* 配置 start *****************/
// 编辑器实例,必须用 shallowRef
const editorRef = shallowRef()
const mode = ref('default')
/**** 工具栏配置 */
const toolbarConfig = {}
/**** 编辑器配置 */
const editorConfig = {
  placeholder: '请输入内容1...',
  MENU_CONF: {
    // 上传本地图片
    uploadImage: {
    /*** 插件自带上传功能*/
      // server: 'http://192.168.3.56:81/dev-api/file/attachFile/attachUpload',
      // fieldName: 'file',
      // maxFileSize: 1 * 1024 * 1024, // 1M
      // allowedFileTypes: ['.jpg, .png, .gif, .jpeg'],
      // base64LimitSize: 5 * 1024,
      // maxNumberOfFiles: 1,
      // headers: {
      //   Authorization:  'Bearer ' +'eyJhbGciOiJIUzUxMi....A'
      // },
      // // 单个文件上传失败
      // onFailed(file, res) {
      //   console.log(`${file.name} 上传失败`, res)
      // },
      // onSuccess(file, res) {
      //   console.log(`${file.name} 上传成功`, res)
      // },
      // // 上传错误,或者触发 timeout 超时
      // onError(file, err, res) {
      //   proxy.$modal.msgError(res)
      //   console.log(res)
      // }
      /**自定义上传功能
       *
       * @param {*} file 文件
       * @param {*} insertFn 输入到编辑器
       */
      async customUpload(file, insertFn) {
        console.log(editorRef.value.getConfig(), '---')
        if (file.size / 1024 / 1024 > 5) {
          proxy.$modal.msgError('单个文件不能超过5MB!')
          return false
        }
        // 把图片转成base64
        getBase64(file, url => {
          insertFn(url)
        })
      }
    },
    // 上传本地视频
    uploadVideo: {
      maxNumberOfFiles: 0,
      customUpload(file, insertFn) {
        const url = 'https://www.runoob.com/try/demo_source/movie.ogg'
        insertFn(url)
      }
    }
  }
}
//工具函数 图片转出base64
const getBase64 = (img, callback) => {
  const reader = new FileReader()
  reader.addEventListener('load', () => callback(reader.result))
  reader.readAsDataURL(img)
}
//创建实例触发的事件
const handleCreated = editor => {
  editorRef.value = editor // 记录 editor 实例,重要!
}
/******************************************* 配置 end *****************/

// 组件销毁时,也及时销毁编辑器
onBeforeUnmount(() => {
  const editor = editorRef.value
  if (editor == null) return
  editor.destroy()
})

watch(
  () => props.modelValue,
  v => {
    if (v !== valueHtml.value) {
      valueHtml.value = v === undefined ? '<p></p>' : v
    }
  },
  { immediate: true }
)
</script>

(六) 注意点

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

(七) 功能点不足

  • customUpload自定义上传跟插件自带的上传不能一起使用;插件自定义上传插件必须填写server
  • 插件自带上传功能中maxNumberOfFiles 没有效果
  • 网络上传图片和视频 只有该文件允许访问 才可以上传成功【但是WangEditor并没有报错提示,只是编辑器内容展示不了】
  • 不兼容音频插入
    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)

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

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

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

在这里插入图片描述

wangEditor5+vue3编辑器

(九) 参考

  • wangEditor5在Vue3中的自定义图片+视频+音频菜单
  • vue3+ts+wangEditor5菜单栏添加自定义图标按钮
  • wangEditor富文本框的使用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/717984.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数据结构与算法篇】之时间复杂度与空间复杂度

【数据结构与算法篇】之时间复杂度与空间复杂度 一、时间复杂度1.1时间复杂度的定义1.2 常见的时间复杂度的计算1.2.1 常数时间复杂度&#xff08; O ( 1 ) ) O(1)) O(1))1.2.2 线性时间复杂度&#xff08; O ( N ) O(N) O(N)&#xff09;1.2.3 对数时间复杂度&#xff08; O (…

蓝桥杯专题-试题版含答案-【荷兰国旗问题】【正三角形的外接圆面积】【比较字母大小】【车牌号】

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例点击跳转>软考全系列点击跳转>蓝桥系列 &#x1f449;关于作者 专注于Android/Unity和各种游…

SSTI模板注入

目录 1、原理简述 2、常用payload及相关脚本 &#xff08;1&#xff09;.__class__ &#xff08;2&#xff09;.__class__.__base__ &#xff08;3&#xff09;.__class__.__base__.__subclasses__() &#xff08;4&#xff09;.__class__.__base__.__subclass…

【周末闲谈】浅谈“AI+算力”

随着人工智能技术的飞速发展&#xff0c;“AI算力”的结合应用已成为科技行业的热点话题&#xff0c;甚至诞生出“AI算力最强龙头“的网络热门等式。该组合不仅可以提高计算效率&#xff0c;还可以为各行各业带来更强大的数据处理和分析能力&#xff0c;从而推动创新和增长。 文…

ue4:Dota总结—HUD篇

1.绘制ui&#xff1a; DrawMoney&#xff1a; DrawPower&#xff1a; 点击ui响应事件&#xff1a; 点击响应显示对应的模型&#xff1a; 点击ui拖动模型跟随鼠标移动&#xff1a; 显示ui&#xff1a;PlayerContrler&#xff1a;

【JAVA】Java 开发环境配置(WIndows篇)

个人主页&#xff1a;【&#x1f60a;个人主页】 系列专栏&#xff1a;【初始JAVA】 文章目录 前言下载JDK配置环境变量JAVA_HOME 设置PATH设置CLASSPATH 设置变量设置参数 前言 在前篇中我们介绍了JAVA语言的诞生与发展&#xff0c;现在是时候去学习使用他们了。 下载JDK 首先…

常微分方程(ODE)求解方法总结(续)

常微分方程&#xff08;ODE&#xff09;求解方法总结&#xff08;续&#xff09; 1 隐式方法2 多步法2.1 二阶方法2.1.1 非自启动修恩方法2.2 开型和闭型积分公式2.3 高阶多步法 1 隐式方法 常微分方程&#xff08;ODE&#xff09;求解方法总结 里面介绍了我称为“正常思路”的…

Jira UI Locations及注意事项总结

issue view ui locations : https://developer.atlassian.com/server/jira/platform/issue-view-ui-locations/#issue-operations-bar-locations1.问题操作栏Issue Operations Bar Locations模块分为两部分: opsbar-operationsflopsbar-transitions两个location.共同定义了问题…

【力扣】111、二叉树的最小深度

111、二叉树的最小深度 给定一个二叉树&#xff0c;找出其最小深度。 最小深度是从根节点到最近叶子节点的最短路径上的节点数量。 说明&#xff1a;叶子节点是指没有子节点的节点。 // var minDepth function(root){if(!root) return 0;const stack [ [ root ,1] ];//…

零拷贝详解

目录 一、什么是零拷贝 二、传统的IO执行流程 三、零拷贝相关的知识点回顾 1、内核空间&用户空间 2、用户态&内核态 3、上下文切换 4、虚拟内存 5、DMA技术 四、零拷贝实现的几种方式 1、mmapwrite实现的零拷贝 2、sendfile实现的零拷贝 3、sendfileDMA sc…

MySQL原理探索——23 MySQL是怎么保证数据不丢的

今天这篇文章&#xff0c;我会继续介绍在业务高峰期临时提升性能的方法。从文章标题“MySQL 是怎么保证数据不丢的”&#xff0c;你就可以看出来&#xff0c;今天我介绍的方法&#xff0c;跟数据的可靠性有关。 在前面文章&#xff0c;我都着重介绍了 WAL 机制&#xff08;你可…

ElementUI plus框架Table表格cell-style属性的使用

官方文档说明&#xff1a; 例&#xff1a;设置单元格文字居中 Object方式&#xff1a; function方式&#xff1a;

安全 --- http报文包详解及burp简单使用

HTTP HTTP&#xff08;超文本传输协议&#xff09;是今天所有web应用程序使用的通信协议。最初HTTP只是一个为了获取基本文本的静态资源而开发的简单协议&#xff0c;后来对其进行扩展和利用&#xff0c;使其发展为能够支持如今常见的复杂分布式应用程序。 &#xff08;1&…

PADS-LAYOUT菜单及工具使用

目录 1菜单栏 1.1文件菜单 1.2编辑菜单 1.3查看菜单 1.4设置菜单 1.5工具菜单 1.6帮助菜单 2工具栏 2.1标准工具栏 2.2绘图工具栏 2.3设计工具栏 2.4尺寸标注工具栏 2.5ECO工具栏 3系统配置 3.1全局选项 3.2设计选项 3.3栅格和捕获选项 3.4显示选项 3.5布线选…

【UnityDOTS 六】预制实例化成Entity

预制实例化成Entity 前言 从Authoring模式中&#xff0c;如何通过预制件来实例化一个对应的Entity对象到DOTS系统中。 一、Authoring模式与Runtime模式 Authoring创作模式&#xff1a;即我们熟悉的方便操作的创建预制的模式 Runtime模式&#xff1a;运行模式&#xff0c;即在…

Three.js教程:网格模型

推荐&#xff1a;将 NSDT场景编辑器 加到你的3D工具链 工具集&#xff1a; NSDT简石数字孪生 网格模型(三角形概念) 本节课给大家演示网格模型Mesh渲染自定义几何体BufferGeometry的顶点坐标,通过这样一个例子帮助大家建立**三角形(面)**的概念 三角形(面) 网格模型Mesh其实…

Spring Boot 中的 XA 事务

Spring Boot 中的 XA 事务 在现代化的应用程序开发中&#xff0c;事务管理是一个重要的话题。事务管理可以确保数据的一致性和完整性&#xff0c;同时也可以避免数据丢失和冲突等问题。在分布式环境中&#xff0c;XA 事务是一种常用的事务管理方式。在本文中&#xff0c;我们将…

基于麦克风阵列模块I2s6路slot数字音频信号的ADSP/STM32F4处理

hezkz17进数字音频系统研究开发交流答疑 1麦克风阵列 2 ADAU1452 DSP 输入接口 3 PCM数据算法处理

uni-app获取节点的相关信息

获取单个节点&#xff1a; selectorQuery.select(selector) 在当前页面下选择第一个匹配选择器 selector 的节点&#xff0c;返回一个 NodesRef 对象实例&#xff0c;可以用于获取节点信息。 selector 说明&#xff1a; selector 类似于 CSS 的选择器&#xff0c;但仅支持下列…