富文本是管理後臺一個核心的功能,但同時又是一個有不少坑的地方。在選擇富文本的過程當中我也走了很多的彎路,市面上常見的富文本都基本用過了,最終權衡了一下選擇了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開源項目的,具體代碼還在學習當中,歡迎提出寶貴意見