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

这篇具有很好参考价值的文章主要介绍了vue3+wangEditor5/vue-quill自定义上传音频+视频。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一.各种编辑器分析

Quill

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

wangEditor5

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

ueditor

UEditor是由百度web前端研发部开发所见即所得富文本web编辑器,具有轻量,可定制,注重用户体验等特点,开源基于MIT协议,允许自由使用和修改代码。

二. 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>

vue3+wangEditor5/vue-quill自定义上传音频+视频,Vue+Element+axios+store(vuex),vue.js,vue-quill,wangEditor5,编辑器,富文本,vue3,富文本上传视频+音频

(三)功能注意点

  • 通过按钮上传图片,图片不能到指定位置
    解决:
 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()
 - 获取鼠标以上所有配置
   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

vue3+wangEditor5/vue-quill自定义上传音频+视频,Vue+Element+axios+store(vuex),vue.js,vue-quill,wangEditor5,编辑器,富文本,vue3,富文本上传视频+音频

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

vue3+wangEditor5/vue-quill自定义上传音频+视频,Vue+Element+axios+store(vuex),vue.js,vue-quill,wangEditor5,编辑器,富文本,vue3,富文本上传视频+音频

上面定义几种模式的菜单后,注册菜单到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 不行【我照着官网弄,但是没效果,具体不知道为啥 ,后期在研究】

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

vue3+wangEditor5/vue-quill自定义上传音频+视频,Vue+Element+axios+store(vuex),vue.js,vue-quill,wangEditor5,编辑器,富文本,vue3,富文本上传视频+音频

wangEditor5+vue3编辑器

(九) 参考

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

四 UEditor

有已经封装好vue3的相关的组件:vue-ueditor-wrap
vue2

(一)安装

  1. 安装组件
# vue-ueditor-wrap v3 仅支持 Vue 3
npm i vue-ueditor-wrap@3.x -S
# or
yarn add vue-ueditor-wrap@3.x
  1. 需要相关静态资源做辅助
    UEditor 并不支持通过 npm 的方式来安装,vue-ueditor-wrap 也只是一个 Vue 组件,组件本身并不是 UEditor 的 Vue 版。
    vue3+wangEditor5/vue-quill自定义上传音频+视频,Vue+Element+axios+store(vuex),vue.js,vue-quill,wangEditor5,编辑器,富文本,vue3,富文本上传视频+音频下载放到public文件下,然后命名为UEditor
    vue3+wangEditor5/vue-quill自定义上传音频+视频,Vue+Element+axios+store(vuex),vue.js,vue-quill,wangEditor5,编辑器,富文本,vue3,富文本上传视频+音频

(二)使用

全局安装

import {VueUeditorWrap} from "vue-ueditor-wrap";
app.use(VueUeditorWrap)

或者页面组件里使用

import {VueUeditorWrap} from "vue-ueditor-wrap";

组件使用:

<template>
    <div>
        <vue-ueditor-wrap editor-id="editor" v-model="content" :config="myConfig"  />
    </div>
</template>
<script setup>
import {VueUeditorWrap} from "vue-ueditor-wrap";
let content = ref('e + UEditor + v-model双向绑定')
let myConfig = {
    // 编辑器不自动被内容撑高
    autoHeightEnabled: false,
    // 初始容器高度
    initialFrameHeight: 450,
    // 初始容器宽度
    initialFrameWidth: 800,
    // 上传文件接口(这个地址是我为了方便各位体验文件上传功能搭建的临时接口,请勿在生产环境使用!!!)
    serverUrl: '/api/upload/index',
    // UEditor 资源文件的存放路径,如果你使用的是 vue-cli 生成的项目,通常不需要设置该选项,vue-ueditor-wrap 会自动处理常见的情况
    UEDITOR_HOME_URL: '/UEditor/',
}
</script>

vue3+wangEditor5/vue-quill自定义上传音频+视频,Vue+Element+axios+store(vuex),vue.js,vue-quill,wangEditor5,编辑器,富文本,vue3,富文本上传视频+音频

参考

vue3+springboot集成百度编辑器UEditor-Plus解决图片上传问题文章来源地址https://www.toymoban.com/news/detail-521621.html

到了这里,关于vue3+wangEditor5/vue-quill自定义上传音频+视频的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • vue2+wangEditor5富文本编辑器(图片视频上传)并加锚链接

    官网:https://www.wangeditor.com/v5/installation.html#npm 1、安装使用 安装 在main.js中引入样式 在使用编辑器的页面引入js 模板 js 到这一步编辑完就可以正常显示了 2、上传图片、视频 1)上传到后台接口的可直接按照文档这个配置就行接口返回格式也要可文档上一致 2)自定义上传(一

    2024年02月12日
    浏览(51)
  • vue2+wangEditor5富文本编辑器

    1、安装使用 安装 yarn add @wangeditor/editor # 或者 npm install @wangeditor/editor --save yarn add @wangeditor/editor-for-vue # 或者 npm install @wangeditor/editor-for-vue --save  在main.js中引入样式 import \\\'@wangeditor/editor/dist/css/style.css\\\'   在使用编辑器的页面引入js  import { Editor, Toolbar } from \\\"@wangeditor/editor-fo

    2024年01月22日
    浏览(50)
  • Vue中使用wangEditor实现自定义上传图片和视频

    之前没用过wangEditor真是一脸懵,做自己没做过的东西总是心里没谱,既然项目已经结束了那来总结一下吧 看着官网的demo发现简单了好多 官网地址:快速开始 | wangEditor 这里可以选自己的框架,我用的vue2.0 然后按照demo示例一步步开整!

    2024年02月12日
    浏览(40)
  • [Vue3] Wangeditor富文本实现将粘贴中包含的图片自动上传后台,并替换src

    前言 吐槽: 还以为他们富文本把文字写好, 在一个个上传图片, 组成一片文章. 那就没有办法了, 只能修改下代码增加下自动上传图片功能. 后来写到一半才发现html 有个属性好像可以让跨域的图片 显示出来. meta name=\\\"referrer\\\" content=\\\"no-referrer\\\" / 以为这样就可以不用写了, 但是老大

    2024年02月04日
    浏览(47)
  • wangEditor 5上传图片自定义 html 样式

    由于官方文档是针对 TS 进行说明的,且并未详细提及到上传图片后对渲染的处理,这里仅针对 JS 的处理方法进行描述 通过覆写该文件后进行注册即可:https://github.com/wangeditor-team/wangEditor/blob/master/packages/basic-modules/src/modules/image/elem-to-html.ts HTML 部分 JS 部分 以上是根据文档创

    2024年02月11日
    浏览(32)
  • Vue +vue-quill-editor+ Element UI使用富文本编辑器,上传图片,上传视频

    如果你们有问题,可以发评论提问,我看见一定回复!!!!! 一、基本使用 1、下载vue-quill-editor组件 2、引入· 富文本组件 方式一:全局引入 (在 main.js 文件中) 方式二:按需引入 (在 单个组件 中引用) 3、工具栏相关配置 4、设置工具栏中文提示 5、修改vue-quill-editor字体

    2024年02月08日
    浏览(61)
  • Vue3使用wangEditor

    那么这一节我们在编辑公司信息的弹窗中使用富文本插件wangEditor官网 案例内效果: npm install @wangeditor/editor --save npm install @wangeditor/editor-for-vue@next --save 文档部分: 代码部分: 后端 需要注意的是,在35集《完成轮播图以及公司介绍接口》中 我们的参数是 set_value ,类型是 v

    2024年02月13日
    浏览(37)
  • Vue3使用富文本框(wangeditor)

    毕业涉及中使用到了富文本框,所以学习使用了wangeditor富文本框,现进行总结 1.安装 2.配置wangeditor组件(src/components/wangeditor.vue) 3.父组件中

    2024年02月15日
    浏览(40)
  • vue3 + fastapi 实现选择目录所有文件自定义上传到服务器

    大家好,我是yma16,本文分享关于vue3 + fastapi 实现选择目录文件上传到服务器指定位置。 vue3系列相关文章: 前端vue2、vue3去掉url路由“ # ”号——nginx配置 csdn新星计划vue3+ts+antd赛道——利用inscode搭建vue3(ts)+antd前端模板 认识vite_vue3 初始化项目到打包 python_selenuim获取csdn新星

    2024年02月08日
    浏览(47)
  • vue3 wangeditor/editor富文本使用和编辑

    第一步:安装 第二步:使用 最终效果图: 第四:工具栏配置 进入官方demo:https://www.wangeditor.com/demo/index.html 打开之后,按F12在控制台输入 toolbar.getConfig()  查看工具栏的默认配置。  如果有不需要的工具栏项,可以在  toolbarConfig.excludeKeys 中配置

    2024年01月21日
    浏览(50)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包