实际开发中,富文本编辑器使用的总结
背景
应用于各种类型的文章的发布,先后使用tinyMce、UEditor、wangeditor。最终选择了wangeditor。因为tinyMce太重,UEditor不支持图片粘贴的前端配置,还需要静态引入UE资源,而且UE也没在维护。
各种富文本编辑器的使用
一、wangeditor(Vue2)
1、安装
yarn add @wangeditor/editor @wangeditor/editor-for-vue
2、组件封装
局部导入,不需要在mian.js/main.ts里面操作
<template>
<div>
<Toolbar
style="border-bottom: 1px solid #ccc"
:editor="editor"
:defaultConfig="toolbarConfig"
:mode="mode"
/>
<Editor
style="height: 700px; overflow-y: hidden"
v-model="msg"
:defaultConfig="editorConfig"
:mode="mode"
@onCreated="onCreated"
@customPaste="customPaste"
/>
</div>
</template>
<script>
import {
Editor, Toolbar } from '@wangeditor/editor-for-vue'
import {
uploadArticleOrVideo,uploadVideo} from '@/api/content/content.js';
export default {
components: {
Editor, Toolbar },
props : ['ifDisabled','content','height'],
data() {
return {
editor: null,
msg: '',
toolbarConfig: {
excludeKeys: ['fullScreen']
},
file: null,
editorConfig: {
placeholder: '请输入内容...',
readOnly: false,
scroll: false,
MENU_CONF: {
uploadImage: {
customUpload: this.autoUp,
},
uploadVideo: {
customUpload: this.uploadVideo
}
},
},
mode: 'default'
}
},
watch : {
content : {
handler() {
this.msg = this.content;
},
immediate:true
},
msg() {
this.$emit('getEditContent',this.msg)
}
},
methods: {
async autoUp(file, insertFn) {
let resultUrl = await this.customRequestImg(file);
insertFn(resultUrl);
},
async uploadVideo(file,insertFn) {
let {
url,poster} = await this.customRequestVideo(file);
insertFn(url,poster)
},
// 自定义上传图片
async customRequestImg(file) {
const formData = new FormData();
formData.append('file',file);
let {
resData } = await uploadArticleOrVideo(formData);
return resData.url;
},
// 自定义上传视频
async customRequestVideo(file) {
const loadMsg = this.$message.loading("上传中...", 0);
const formData = new FormData();
formData.append('file',file);
let {
resData } = await uploadVideo(formData);
setTimeout(loadMsg, 0); // 关闭loading,确保await完,才关闭loading
this.$message.success('上传成功');
return {
url: resData.url: // 视频地址
poster: resData.coverUrl // 视频封面
}
},
onCreated(editor) {
this.editor = Object.seal(editor) // 一定要用 Object.seal() ,否则会报错
},
// @customPaste="customPaste" 自定义粘贴
async customPaste(editor,event) {
const items = (event.clipboardData || window.clipboardData).items;
let file = null; // 保存image类型的file对象
if (!items || items.length === 0) {
this.$message.error("当前浏览器不支持本地粘贴");
return;
}
// 搜索剪切板items
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf("image") !== -1) {
file = items[i].getAsFile();
break;
}
}
if (file) {
// 说明粘贴的是图片
// 禁止粘贴图片的默认行为 wangeditor默认可以粘贴word里面的图片,但不能粘贴微信、qq里面的图片
event.preventDefault()
let imgUrl = await this.customRequestImg(file);
// 插入图片节点
let imgNode = {
type: 'image',src:imgUrl,children: [{
text: '' }]} // 节点数据格式不能变
editor.insertNode(imgNode)
// 禁止粘贴图片的默认行为
return false
}
}
},
beforeDestroy() {
const editor = this.editor
if (editor == null) return
editor.destroy() // 组件销毁时,及时销毁编辑器
},
created() {
this.editorConfig.readOnly = this.ifDisabled;
}
}
</script>
<style src="@wangeditor/editor/dist/css/style.css"></style>
3、父组件使用
import MyEditor from '../MyEditor.vue';
components : {
MyEditor,
},
<MyEditor @getEditContent="getEditContent" :content="currentData.content" :ifDisabled="false"/>
二、UEditor(vue2)
1、安装
npm i vue-ueditor-wrap -S
2、在public目录下,引入UE静态资源
3、组件封装
<template>
<div>
<vue-ueditor-wrap
v-model="msg"
:config="editorConfig"
@beforeInit="addImageButton"
></vue-ueditor-wrap>
<a-modal
title="上传图片"
:visible="ifShowUploadImgDialog"
@cancel="ifShowUploadImgDialog = false"
>
<a-form-model style="display: flex" >
<a-form-item style="display: flex">
<template slot="label">
<span style="font-size: 14px;color: #595959;">图片</span>
</template>
<a-upload
:show-upload-list="false"
:customRequest="customRequestImg"
:multiple="false"
list-type="picture-card"
>
<img v-if="apiDataUrl" style="width: 102px;height:102px;object-fit:cover;" :src="apiDataUrl" alt="avatar" />
<div v-else>
<a-icon type="plus" />
<div class="ant-upload-text">
上传图片
</div>
</div>
</a-upload>
</a-form-item>
<div style="margin-left: 100px;">
<a-form-item style="display: flex">
<template slot="label">
<span style="font-size: 14px;color: #595959;">宽</span>
</template>
<a-input-number :min="0" addon-after="px" v-model="file.w"/>
</a-form-item>
<a-form-item style="display: flex">
<template slot="label">
<span style="font-size: 14px;color: #595959;">高</span>
</template>
<a-input-number :min="0" v-model="file.h">
</a-input-number>
</a-form-item>
</div>
</a-form-model>
<template template slot="footer">
<a-button type="default" @click="ifShowUploadImgDialog = false">取消</a-button>
<a-button type="primary" @click="sureUpImgFn">确定</a-button>
</template>
</a-modal>
<a-modal
title="上传视频"
width="520px"
:visible="ifShowUploadVideoDialog"
@cancel="ifShowUploadVideoDialog = false"
>
<a-form-model :label-col="labelCol" :wrapper-col="wrapperCol">
<a-form-item >
<template slot="label">
<span style="font-size: 14px;color: #595959;">视频</span>
</template>
<a-upload
accept=".mp4,.ogg,.webm"
:customRequest="customRequestVideo"
:multiple="false"
>
<a-button> <a-icon type="upload" /> 上传视频 </a-button>
</a-upload>
</a-form-item>
<a-form-item >
<template slot="label">
<span style="font-size: 14px;color: #595959;">宽</span>
</template>
<a-input-number :min="0" addon-after="px" v-model="file.w"/>
</a-form-item>
<a-form-item>
<template slot="label">
<span style="font-size: 14px;color: #595959;">高</span>
</template>
<a-input-number :min="0" v-model="file.h">
</a-input-number>
</a-form-item>
</a-form-model>
<template template slot="footer">
<a-button type="default" @click="ifShowUploadVideoDialog = false">取消</a-button>
<a-button type="primary" @click="sureUpVideoFn">确定</a-button>
</template>
</a-modal>
</div>
</template>
<script>
import VueUeditorWrap from 'vue-ueditor-wrap';
import {uploadArticleOrVideo,uploadVideo} from '@/api/content/content.js';
export default {
name: "MyEditor",
props : ['ifDisabled','content','height'],
data() {
return {
editorConfig: {
autoHeightEnabled: false, //编译器不自动被内容撑高
initialFrameHeight: 650, //初始容器高度
initialFrameWith: "100%",
readonly : false,
UEDITOR_HOME_URL: "./UE/", //UEditor资源文件的存放路径
allowDivTransToP: false, // 阻止DIV 自动过滤为P标签
// serverUrl: 'http://192.168.43.89:3000/ueconfig' // //ueditor.szcloudplus.com/cos
},
msg : '',
editorInstance : {},
ifShowUploadImgDialog : false,
ifShowUploadVideoDialog : false,
editor : null,
apiDataUrl : null,
labelCol: { span: 5},
wrapperCol: { span: 15 },
file : {
w : 100,
h : 100
}
};
},
components : {
VueUeditorWrap
},
watch : {
content : {
handler() {
this.msg = this.content;
},
immediate:true
},
msg() {
this.$emit('getEditContent',this.msg)
}
},
methods : {
readyFn(editorInstance) {
this.editorInstance=editorInstance
},
// 自定义工具按钮
addImageButton(editorId) {
let that = this;
window.UE.registerUI("autoimg", (editor, uname) => {
const btnImg = new window.UE.ui.Button({
name: 'my-img',
title: '上传图片',
onclick: function(){
that.editor = editor;
that.ifShowUploadImgDialog = true;
}
});
return btnImg
});
window.UE.registerUI("autovideo", (editor, uname) => {
const btnVideo = new window.UE.ui.Button({
name: 'my-video',
title: '上传视频',
onclick: function(){
that.editor = editor;
that.ifShowUploadVideoDialog = true;
}
});
return btnVideo
});
},
customRequestImg(data) {
const formData = new FormData();
formData.append('file',data.file);
uploadArticleOrVideo(formData).then(res=>{
if(res.data.success) {
this.apiDataUrl = window.customizeConfig.proposed === "NJ" ? window.customizeConfig.minioUrl + JSON.parse(res.data.data).url :
JSON.parse(res.data.data).url;
} else {
this.$message.error('上传失败')
}
})
},
customRequestVideo(data) {
const formData = new FormData();
formData.append('file',data.file);
uploadVideo(formData).then(res=>{
data.onSuccess(res, data.file);
if(res.data.success) {
this.apiDataUrl = window.customizeConfig.proposed === "NJ" ? window.customizeConfig.minioUrl + JSON.parse(res.data.data).url :
JSON.parse(res.data.data).url;
} else {
this.$message.error('上传失败')
}
})
},
sureUpImgFn() {
if(this.apiDataUrl) {
this.editor.execCommand('insertimage', {
src:this.apiDataUrl,
_src: this.apiDataUrl,
height : this.file.h,
width : this.file.w
});
this.ifShowUploadImgDialog = false;
} else {
this.$message.warning("暂无上传文件")
}
},
sureUpVideoFn() {
if(this.apiDataUrl) {
if(this.file.h && this.file.w) {
this.editor.execCommand('insertHTML',`<video height=${this.file.h} width=${this.file.w} src="${this.apiDataUrl}" autoplay loop controls="controls"></video>`);
} else {
this.editor.execCommand('insertHTML',`<video src="${this.apiDataUrl}" autoplay loop controls="controls"></video>`);
}
this.ifShowUploadVideoDialog = false;
} else {
this.$message.warning("暂无上传文件")
}
}
},
created() {
this.editorConfig.readonly = this.ifDisabled;
}
};
</script>
<style>
.tox-notifications-container {
display: none;
}
.tox-statusbar {
display: none !important;
}
.edui-button.edui-for-my-img .edui-button-wrap .edui-button-body .edui-icon {
background-image: url("~@/assets/img/ue/upImg.png") !important;
background-size: contain;
}
.edui-button.edui-for-my-video .edui-button-wrap .edui-button-body .edui-icon {
background-image: url("~@/assets/img/ue/upVideo.png") !important;
background-size: contain;
}
textarea {
display:none;
}
</style>