这是另一个常用的富文本编辑器,它提供了许多可定制的功能和事件,并且也有一2个官方的 Vue 3 组件
wangEditor5用在Vue3中自定义扩展音频、视频、图片菜单;并扩展音频元素节点,保证音频节点的插入、读取、回写功能正常;支持动态修改尺寸
官网地址
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 ? '' : 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 == '') || text == '
') {
formModel.value.detail = null
proxy.$refs['formRef'].validateField('detail')
return false
}
if (formModel.value.detail) {
proxy.$refs['formRef'].clearValidate('detail') // clearValidate()取消验证方法
}
}
</script>
官网地址
yarn add @wangeditor/editor
或者 npm install @wangeditor/editor --save
yarn add @wangeditor/editor-for-vue@next
或者 npm install @wangeditor/editor-for-vue@next --save
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
支持 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"