富文本是管理后台一个核心的功能,但同时又是一个有不少坑的地方。在选择富文本的过程当中我也走了很多的弯路,市面上常见的富文本都基本用过了,最终权衡了一下选择了Tinymce。css
这里在简述一下推荐使用 tinymce 的缘由:tinymce 是一家老牌作富文本的公司(这里也推荐 ckeditor,也是一家一直作富文本的公司,新版本很不错),它的产品经受了市场的承认,不论是文档仍是配置的自由度都很好。在使用富文本的时候有一点也很关键就是复制格式化,以前在用一款韩国人作的富文本 summernote 被它的格式化坑的死去活来,但 tinymce 的去格式化至关的好,它还有一些增值服务(付费插件),最好用的就是powerpaste,很是的强大,支持从 word 里面复制各类东西,并且还帮你顺带格式化了。富文本还有一点也很关键,就是拓展性。楼主用 tinymce 写了好几个插件,学习成本和容易度都不错,很方便拓展。最后一点就是文档很完善,基本你想获得的配置项,它都有。tinymce 也支持按需加载,你能够经过它官方的 build 页定制本身须要的 plugins。html
以上内容引自vue-element-admin做者官网前端
目前采用全局引用的方式。代码地址:static/tinymce (static 目录下的文件不会被打包), 在 index.html 中引入。并确保它的引入顺序在你的app.js以前!vue
在这个例子中,替换<textarea id='mytextarea'>
经过使选择器与TinyMCE的5.0编辑器实例'#mytextarea'来tinymce.init()。
<!DOCTYPE html>
<html>
<head>
<script src='https://cloud.tinymce.com/5/tinymce.min.js?apiKey=your_API_key'></script>
<script>
tinymce.init({
selector: '#mytextarea'
});
</script>
</head>
<body>
<h1>TinyMCE Quick Start Guide</h1>
<form method="post">
<textarea id="mytextarea">Hello, World!</textarea>
</form>
</body>
</html>
复制代码
因为目前使用 npm 安装 Tinymce 方法比较复杂并且还有一些问题(往后可能会采用该模式)且会大大增长编译的时间因此暂时不许备采用。因此这里直接去官网下载源文件保存在项目static文件夹中git
因为富文本不适合双向数据流,因此只会 watch 传入富文本的内容一次变化,以后传入内容的变化就不会再监听了,若是以后还有改变富文本内容的需求。程序员
能够经过 this.refs.xxx.setContent() 手动来设置。github
plugins.js文件的代码以下web
const plugins = ['advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textcolor textpattern visualblocks visualchars wordcount']
export default plugins
复制代码
toolbar.js文件的代码以下npm
const toolbar = ['searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample', 'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen']
export default toolbar
复制代码
index.vue组件的代码以下api
<template>
<div :class="{fullscreen:fullscreen}" class="tinymce-container editor-container">
<textarea :id="tinymceId" class="tinymce-textarea" />
<div class="editor-custom-btn-container">
<editorImage color="#1890ff" class="editor-upload-btn" @successCBK="imageSuccessCBK" />
</div>
</div>
</template>
<script>
// 导入图片上传的组件
import editorImage from './components/editorImage'
// 导入富文本插件
import plugins from './plugins'
// 导入富文本工具栏
import toolbar from './toolbar'
export default {
name: 'Tinymce',
components: { editorImage },
props: {
id: {
type: String,
default: function() {
return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
}
},
value: {
type: String,
default: ''
},
toolbar: {
type: Array,
required: false,
default() {
return []
}
},
menubar: {
type: String,
default: 'file edit insert view format table'
},
height: {
type: Number,
required: false,
default: 360
}
},
data() {
return {
hasChange: false,
hasInit: false,
tinymceId: this.id,
fullscreen: false,
languageTypeList: {
'en': 'en',
'zh': 'zh_CN'
}
}
},
computed: {
language() {
return 'zh_CN'
}
},
watch: {
value(val) {
if (!this.hasChange && this.hasInit) {
this.$nextTick(() =>
window.tinymce.get(this.tinymceId).setContent(val || ''))
}
},
language() {
this.destroyTinymce()
this.$nextTick(() => this.initTinymce())
}
},
mounted() {
this.initTinymce()
},
activated() {
this.initTinymce()
},
deactivated() {
this.destroyTinymce()
},
destroyed() {
this.destroyTinymce()
},
methods: {
initTinymce() {
const _this = this
window.tinymce.init({
language: this.language,
selector: `#${this.tinymceId}`,
height: this.height,
body_class: 'panel-body ',
object_resizing: false,
toolbar: this.toolbar.length > 0 ? this.toolbar : toolbar,
menubar: this.menubar,
plugins: plugins,
end_container_on_empty_block: true,
powerpaste_word_import: 'clean',
code_dialog_height: 450,
code_dialog_width: 1000,
advlist_bullet_styles: 'square',
advlist_number_styles: 'default',
imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],
default_link_target: '_blank',
link_title: false,
nonbreaking_force_tab: true, // inserting nonbreaking space need Nonbreaking Space Plugin
init_instance_callback: editor => {
if (_this.value) {
editor.setContent(_this.value)
}
_this.hasInit = true
editor.on('NodeChange Change KeyUp SetContent', () => {
this.hasChange = true
this.$emit('input', editor.getContent())
})
},
setup(editor) {
editor.on('FullscreenStateChanged', (e) => {
_this.fullscreen = e.state
})
}
// 整合七牛上传
// images_dataimg_filter(img) {
// setTimeout(() => {
// const $image = $(img);
// $image.removeAttr('width');
// $image.removeAttr('height');
// if ($image[0].height && $image[0].width) {
// $image.attr('data-wscntype', 'image');
// $image.attr('data-wscnh', $image[0].height);
// $image.attr('data-wscnw', $image[0].width);
// $image.addClass('wscnph');
// }
// }, 0);
// return img
// },
// images_upload_handler(blobInfo, success, failure, progress) {
// progress(0);
// const token = _this.$store.getters.token;
// getToken(token).then(response => {
// const url = response.data.qiniu_url;
// const formData = new FormData();
// formData.append('token', response.data.qiniu_token);
// formData.append('key', response.data.qiniu_key);
// formData.append('file', blobInfo.blob(), url);
// upload(formData).then(() => {
// success(url);
// progress(100);
// })
// }).catch(err => {
// failure('出现未知问题,刷新页面,或者联系程序员')
// console.log(err);
// });
// },
})
},
destroyTinymce() {
const tinymce = window.tinymce.get(this.tinymceId)
if (this.fullscreen) {
tinymce.execCommand('mceFullScreen')
}
if (tinymce) {
tinymce.destroy()
}
},
setContent(value) {
window.tinymce.get(this.tinymceId).setContent(value)
},
getContent() {
window.tinymce.get(this.tinymceId).getContent()
},
imageSuccessCBK(arr) {
const _this = this
arr.forEach(v => {
window.tinymce.get(_this.tinymceId).insertContent(`<img class="wscnph" src="${v.url}" >`)
})
}
}
}
</script>
<style scoped>
.tinymce-container {
position: relative;
line-height: normal;
}
.tinymce-container>>>.mce-fullscreen {
z-index: 10000;
}
.tinymce-textarea {
visibility: hidden;
z-index: -1;
}
.editor-custom-btn-container {
position: absolute;
right: 4px;
top: 4px;
/*z-index: 2005;*/
}
.fullscreen .editor-custom-btn-container {
z-index: 10000;
position: fixed;
}
.editor-upload-btn {
display: inline-block;
}
</style>
复制代码
图片上传组件editorImage的代码以下
<template>
<div class="upload-container">
<el-button :style="{background:color,borderColor:color}" icon="el-icon-upload" size="mini" type="primary" @click=" dialogVisible=true">
上传图片
</el-button>
<el-dialog :visible.sync="dialogVisible">
<el-upload
:multiple="true"
:file-list="fileList"
:show-file-list="true"
:on-remove="handleRemove"
:on-success="handleSuccess"
:before-upload="beforeUpload"
class="editor-slide-upload"
action="https://httpbin.org/post"
list-type="picture-card"
>
<el-button size="small" type="primary">
点击上传
</el-button>
</el-upload>
<el-button @click="dialogVisible = false">
取 消
</el-button>
<el-button type="primary" @click="handleSubmit">
确 定
</el-button>
</el-dialog>
</div>
</template>
<script>
// import { getToken } from 'api/qiniu'
export default {
name: 'EditorSlideUpload',
props: {
color: {
type: String,
default: '#1890ff'
}
},
data() {
return {
dialogVisible: false,
listObj: {},
fileList: []
}
},
methods: {
checkAllSuccess() {
return Object.keys(this.listObj).every(item => this.listObj[item].hasSuccess)
},
handleSubmit() {
const arr = Object.keys(this.listObj).map(v => this.listObj[v])
if (!this.checkAllSuccess()) {
this.$message('请等待全部图片上传成功 或 出现了网络问题,请刷新页面从新上传!')
return
}
this.$emit('successCBK', arr)
this.listObj = {}
this.fileList = []
this.dialogVisible = false
},
handleSuccess(response, file) {
const uid = file.uid
const objKeyArr = Object.keys(this.listObj)
for (let i = 0, len = objKeyArr.length; i < len; i++) {
if (this.listObj[objKeyArr[i]].uid === uid) {
this.listObj[objKeyArr[i]].url = response.files.file
this.listObj[objKeyArr[i]].hasSuccess = true
return
}
}
},
handleRemove(file) {
const uid = file.uid
const objKeyArr = Object.keys(this.listObj)
for (let i = 0, len = objKeyArr.length; i < len; i++) {
if (this.listObj[objKeyArr[i]].uid === uid) {
delete this.listObj[objKeyArr[i]]
return
}
}
},
beforeUpload(file) {
const _self = this
const _URL = window.URL || window.webkitURL
const fileName = file.uid
this.listObj[fileName] = {}
return new Promise((resolve, reject) => {
const img = new Image()
img.src = _URL.createObjectURL(file)
img.onload = function() {
_self.listObj[fileName] = { hasSuccess: false, uid: file.uid, width: this.width, height: this.height }
}
resolve(true)
})
}
}
}
</script>
<style lang="scss" scoped>
.editor-slide-upload {
margin-bottom: 20px;
/deep/ .el-upload--picture-card {
width: 100%;
}
}
</style>
复制代码
以上代码直接复制到本地就可以直接使用
这里定义了一个add方法 在富文本内编辑内容后,点击提交按钮便可获取到postForm.content的内容(富文本编辑的内容)
当咱们把富文本的内容传递后台后,若是有二次编辑的需求,后台又将数据原封不动的传递给前端,如何将传递回来的数据放置在富文本中呢?
使用方式:
调用tinymce组件中的setContent()方法便可;
例如经过chage方法调用
change () {
this.$refs.Tinymce.setContent("'<h1>我是后台传递回来的数据,要放在富文本中二次编辑内容<h1>'")
}
复制代码
封装的代码都是看vue-element-admin开源项目的,具体代码还在学习当中,欢迎提出宝贵意见