環(huán)境:VUE2.x + Tinymce + Tinymce-Vue + element
安裝tinymce-vue
npm i @tinymce/tinymce-vue@3.2.8
注意:因為tinymce-vue4.x版本旨在支持Vue3

image.png
安裝tinymce
npm i tinymce@5.7.1
復制icons、skins
安裝完成后,在 public文件夾 下創(chuàng)建 tinymce文件夾,然后在node_modules下找到 tinymce,注意不是 @tinymce,復制 icons、skins 文件夾到 public/tinymce 下
下載中文語言包
下載地址:https://www.tiny.cloud/get-tiny/language-packages/,下載 zh_CN,下載后,把解壓后的 langs文件夾 放到 public/tinymce 下

image.png
tinymceEditor組件
在 components文件夾 下TinymceEditor.vue文件,復制以下代碼
<template>
<div>
<Editor v-model="myValue" :id="tinymceId" :init="init" :disabled="disabled"></Editor>
<input type="file" hidden :id="'file-' + tinymceId"/>
</div>
</template>
<script>
import tinymce from 'tinymce/tinymce'
import Editor from '@tinymce/tinymce-vue'
import 'tinymce/themes/mobile/theme'
import 'tinymce/themes/silver/theme'
import 'tinymce/icons/default/icons' ////解決了icons.js 報錯Unexpected token '<'
import 'tinymce/plugins/advlist'
import 'tinymce/plugins/autolink'
import 'tinymce/plugins/lists'
import 'tinymce/plugins/link'
import 'tinymce/plugins/image'
import 'tinymce/plugins/charmap'
import 'tinymce/plugins/print'
import 'tinymce/plugins/preview'
import 'tinymce/plugins/anchor'
import 'tinymce/plugins/searchreplace'
import 'tinymce/plugins/visualblocks'
import 'tinymce/plugins/code'
import 'tinymce/plugins/fullscreen'
import 'tinymce/plugins/code'
import 'tinymce/plugins/insertdatetime'
import 'tinymce/plugins/media'
import 'tinymce/plugins/table'
import 'tinymce/plugins/paste'
import 'tinymce/plugins/help'
import 'tinymce/plugins/wordcount'
export default {
name: 'TinymceEditor',
components: {Editor},
props: {
id: {
type: String,
default: function () {
return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
}
},
value: {
type: String,
default: ''
},
height: {
type: [Number, String],
required: false,
default: 200
},
width: {
type: [Number, String],
required: false,
default: 'auto'
},
disabled: {
type: Boolean,
default: false
}
},
data() {
return {
tinymceId: this.id,
myValue: this.value,
oldValue: '',
init: {
selector: `#${this.id}`,
content_style: "p {margin: 0; border:0; padding: 0;}",
language_url: '/tinymce/langs/zh_CN.js',
language: 'zh_CN',
skin_url: '/tinymce/skins/ui/oxide',
height: this.height,
body_class: 'panel-body ',
object_resizing: true,//是否允許調(diào)整圖像大小.
branding: false, //隱藏右下角由TINY驅(qū)動
contextmenu_never_use_native: true, //防止瀏覽器上下文菜單出現(xiàn)在編輯器中
elementpath: false, //隱藏底欄的元素路徑(隱藏右下角元素顯示)
toolbar: 'code | undo redo | lineheight| fontselect fontsizeselect formatselect | bold italic forecolor backcolor | \
alignleft aligncenter alignright alignjustify | \
bullist numlist outdent indent | removeformat | imageUpload | help',
menubar: false,
plugins: ['advlist autolink lists link image charmap print preview anchor',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste help wordcount'],
paste_retain_style_properties: 'all', //從Word中復制,保留所有樣式
//paste_word_valid_elements: '*[*]', // 該設(shè)置在自word粘貼時,允許指定元素和屬性保存在過濾結(jié)果中,要使用此功能,paste_enable_default_filters 要設(shè)置為true
paste_data_images: true, // 粘貼的同時能把內(nèi)容里的圖片自動上傳,非常強力的功能
paste_convert_word_fake_lists: false, // 設(shè)為false可禁用復制word中的列表內(nèi)容時,轉(zhuǎn)換為html的UL或OL格式。
paste_webkit_styles: 'all',
paste_merge_formats: true, //啟用PowerPaste插件的合并格式功能,例如:<b>abc <b>bold</b> 123</b>成為<b>abc bold 123</b>
end_container_on_empty_block: true, //如果在空的內(nèi)部塊元素中按下Enter鍵,則可以使用此選項拆分當前容器塊元素。
powerpaste_word_import: 'clean', //保留標題,表格和列表等內(nèi)容的結(jié)構(gòu),但刪除內(nèi)聯(lián)樣式和類。這樣就產(chǎn)生了使用站點CSS樣式表的簡單內(nèi)容,同時保留了原始文檔的語義結(jié)構(gòu)。
advlist_bullet_styles: 'default,circle,disc,square',
advlist_number_styles: 'default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman',
default_link_target: '_blank', //默認鏈接是當前窗口打開,你也可以通過此參數(shù)將其變?yōu)開blank新窗口打開。
link_title: false,
nonbreaking_force_tab: true, // 按tab鍵插入三個 。一直按一直插。
images_upload_handler: this.images_upload_handler, //該images_upload_handler選項允許你指定被用來替代TinyMCE的默認的JavaScript和定制邏輯上傳處理函數(shù)的函數(shù)。
setup: this.setup, //,您可以指定在渲染TinyMCE編輯器實例之前將執(zhí)行的回調(diào)。
convert_urls: false
}
}
},
watch: {
value(newValue) {
this.myValue = newValue
},
myValue(newValue) {
this.$emit('input', newValue)
}
},
mounted() {
tinymce.init({})
},
methods: {
images_upload_handler(blobInfo, success, failure, progress) {
//this.oldValue = this.value;
let fd = new FormData();
fd.append('imageFile', blobInfo.blob(), blobInfo.filename());
this.$http.post('/editor/uploadImg', fd).then(res => {
if (res.data.code == 200) {
success(res.data.data);
} else {
//這樣寫是因為復制進來的圖片進入這方法時就以base64存在(即在this.oldValue以base64存在),
//如果上傳失敗this.oldValue中已存在圖片,所以setContent是不能回滾
this.$message.error("圖片上傳失敗");
success("");
// failure('上傳失敗!');
// this.setContent(this.oldValue);
}
}).catch(res => {
this.$message.error("圖片上傳失敗");
success("");
// failure('上傳失??!');
// this.setContent(this.oldValue);
})
},
setup(editor) {
//注冊一個圖片按鈕
editor.ui.registry.addButton('imageUpload', {
tooltip: '圖片',
icon: 'image',
onAction: () => {
//點擊按鈕后執(zhí)行
this.oldValue = this.value;
let self = this;
let input = document.getElementById("file-" + this.tinymceId);
input.click();
input.onchange = function () {
let file = input.files[0];
let fd = new FormData();
fd.append('imageFile', file);
self.$http.post('/editor/uploadImg', fd).then(res => {
if (res.data.code == 200) {
tinymce.get(self.tinymceId).insertContent("<img src='" + res.data.data + "'>");
input.value = '';
} else {
self.setContent(self.oldValue);
}
}).catch(res => {
self.setContent(self.oldValue);
})
}
}
});
},
}
}
</script>
<style lang="stylus" scoped>
.tinymce-container {
position: relative;
line-height: normal;
}
.tinymce-container {
::v-deep {
.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>
<style>
.oe-editor-wrap{
position: relative;
}
.oe-editor-del-btn{
position: absolute;
top:0;
right:0;
z-index:3010;
padding:0 2px 1px;
border-left:1px solid #DCDFE6;
}
.oe-editor-del-btn img{
height:36px;
}
.tox .tox-dialog-wrap__backdrop {
z-index: 3000;
background-color: rgba(0, 0, 0, .6);
}
.tox .tox-dialog {
z-index: 3001;
}
/*調(diào)整堆疊順序,讓下拉列表等顯示*/
.tox-tinymce-aux {
z-index: 3002!important;
}
.tox-tinymce{
border: 1px solid #DCDFE6!important;
}
.tox .tox-statusbar{
border-top: 1px solid #DCDFE6!important;
}
.tox:not([dir=rtl]) .tox-toolbar__group:not(:last-of-type){
border-right: 1px solid #DCDFE6!important;
}
.tox .tox-toolbar, .tox .tox-toolbar__overflow, .tox .tox-toolbar__primary{
background: none!important;
border-bottom:1px solid #DCDFE6;
}
.tox .tox-tbtn svg{
fill:#7b8397!important;
}
</style>
使用
import editor from "@/components/TinymceEditor";
<editor v-model="record.content"></editor>

image.png
不引用@tinymce/tinymce-vue的tinymce組件
<template>
<div>
<textarea :id="tinymceId"></textarea>
<input type="file" hidden :id="'file-' + tinymceId"/>
<div class="oe-editor-del-btn" v-if="showDel">
<img src="@/assets/editor_del.png" alt="">
</div>
</div>
</template>
<script>
import tinymce from 'tinymce/tinymce'
import 'tinymce/themes/mobile/theme'
import 'tinymce/themes/silver/theme'
import 'tinymce/icons/default/icons' ////解決了icons.js 報錯Unexpected token '<'
import 'tinymce/plugins/advlist'
import 'tinymce/plugins/autolink'
import 'tinymce/plugins/lists'
import 'tinymce/plugins/link'
import 'tinymce/plugins/image'
import 'tinymce/plugins/charmap'
import 'tinymce/plugins/print'
import 'tinymce/plugins/preview'
import 'tinymce/plugins/anchor'
import 'tinymce/plugins/searchreplace'
import 'tinymce/plugins/visualblocks'
import 'tinymce/plugins/code'
import 'tinymce/plugins/fullscreen'
import 'tinymce/plugins/insertdatetime'
import 'tinymce/plugins/media'
import 'tinymce/plugins/table'
import 'tinymce/plugins/paste'
import 'tinymce/plugins/help'
import 'tinymce/plugins/wordcount'
//import 'tinymce/plugins/imagetools'
import {editorUploadImg} from "@/api/public";
const INIT = 0;
const INPUT = 1;
const CHANGED = 2;
export default {
name: "Tinymce",
props: {
id: {
type: String,
default: function () {
return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
}
},
value: {
type: String,
default: ''
},
height: {
type: [Number, String],
required: false,
default: 200
},
width: {
type: [Number, String],
required: false,
default: 'auto'
},
readonly: {
type: Boolean,
default: false
},
showDel: {
type: Boolean,
default: () => {
return false
}
}
},
data() {
return {
tinymceId: this.id,
oldValue: '',
status: CHANGED,
init: {
selector: `#${this.id}`,
content_style: "p {margin: 0; border:0; padding: 0;}",
language_url: '/tinymce/langs/zh_CN.js',
language: 'zh_CN',
skin_url: '/tinymce/skins/ui/oxide',
height: this.height,
body_class: 'panel-body ',
object_resizing: true, //是否允許調(diào)整圖像大小.
branding: false, //隱藏右下角由TINY驅(qū)動
contextmenu_never_use_native: true, //防止瀏覽器上下文菜單出現(xiàn)在編輯器中
elementpath: false, //隱藏底欄的元素路徑(隱藏右下角元素顯示)
toolbar: 'code | undo redo | lineheight| fontselect fontsizeselect formatselect | bold italic forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | removeformat | imageUpload | help',
menubar: false,
plugins: ['advlist autolink lists link image charmap print preview anchor',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste help wordcount'],
paste_retain_style_properties: 'all', //從Word中復制,保留所有樣式
paste_word_valid_elements: '*[*]', // 該設(shè)置在自word粘貼時,允許指定元素和屬性保存在過濾結(jié)果中,要使用此功能,paste_enable_default_filters 要設(shè)置為true
paste_data_images: true, // 粘貼的同時能把內(nèi)容里的圖片自動上傳,非常強力的功能
paste_convert_word_fake_lists: false, // 設(shè)為false可禁用復制word中的列表內(nèi)容時,轉(zhuǎn)換為html的UL或OL格式。
paste_webkit_styles: 'all',
paste_merge_formats: true, //啟用PowerPaste插件的合并格式功能,例如:<b>abc <b>bold</b> 123</b>成為<b>abc bold 123</b>
end_container_on_empty_block: true, //如果在空的內(nèi)部塊元素中按下Enter鍵,則可以使用此選項拆分當前容器塊元素。
advlist_bullet_styles: 'default,circle,disc,square',
advlist_number_styles: 'default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman',
default_link_target: '_blank', //默認鏈接是當前窗口打開,你也可以通過此參數(shù)將其變?yōu)開blank新窗口打開。
link_title: false,
nonbreaking_force_tab: true, // 按tab鍵插入三個 。一直按一直插。
images_upload_handler: this.images_upload_handler, //該images_upload_handler選項允許你指定被用來替代TinyMCE的默認的JavaScript和定制邏輯上傳處理函數(shù)的函數(shù)。
setup: this.setup, //,您可以指定在渲染TinyMCE編輯器實例之前將執(zhí)行的回調(diào)。
convert_urls: false,
readonly: this.readonly,
init_instance_callback : this.init_instance_callback,
// lineheight_val:
// '1 1.1 1.2 1.3 1.35 1.4 1.5 1.55 1.6 1.75 1.8 1.9 1.95 2 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 3 3.1 3.2 3.3 3.4 4 5',
// fontsize_formats: '8pt 10pt 11pt 12pt 13pt 14pt 15pt 16pt 17pt 18pt 24pt 36pt',
// font_formats:
// "微軟雅黑='微軟雅黑';宋體='宋體';黑體='黑體';仿宋='仿宋';楷體='楷體';隸書='隸書';幼圓='幼圓';Andale Mono=andale mono,times;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier;Georgia=georgia,palatino;Helvetica=helvetica;Impact=impact,chicago;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco;Times New Roman=times new roman,times;Trebuchet MS=trebuchet ms,geneva;Verdana=verdana,geneva;Webdings=webdings;Wingdings=wingdings",
}
}
},
watch: {
value(newValue) {
if(this.status === CHANGED || this.status === INIT) return this.status = INPUT;
if (!newValue) {
tinymce.get(this.tinymceId).setContent('');
} else {
tinymce.get(this.tinymceId).setContent(newValue);
}
}
},
mounted() {
tinymce.init(this.init);
},
methods: {
images_upload_handler(blobInfo, success, failure, progress) {
// this.oldValue = this.value;
const fd = new FormData();
fd.append('imageFile', blobInfo.blob(), blobInfo.filename());
editorUploadImg(fd).then(res => {
if (res && res.code === 1) {
success(process.env.VUE_APP_BASE_API + res.data);
} else {
this.$message.error("圖片上傳失敗");
success("");
// failure('上傳失?。?);
// this.setContent(this.oldValue);
}
}).catch(() => {
this.$message.error("圖片上傳失敗");
success("");
// failure('上傳失?。?);
// this.setContent(this.oldValue);
});
},
setup(editor) {
editor.on('init', (e) => {
editor.setContent(this.value);
editor.on('input change undo redo execCommand', () => {
// 只在用戶輸入導致事件相應時才更新value數(shù)據(jù),不然的話會導致光標跳到首位
if(this.status === INPUT || this.status === INIT) return this.status = CHANGED;
this.$emit('input', editor.getContent());
});
})
//注冊一個圖片按鈕
editor.ui.registry.addButton('imageUpload', {
tooltip: '圖片',
icon: 'image',
onAction: () => {
//點擊按鈕后執(zhí)行
this.oldValue = this.value;
const self = this;
const input = document.getElementById("file-" + this.tinymceId);
input.click();
input.onchange = function () {
const file = input.files[0];
const fd = new FormData();
fd.append('imageFile', file);
editorUploadImg(fd).then(res => {
if (res && res.code === 1) {
tinymce.get(self.tinymceId).insertContent(`<img src='${process.env.VUE_APP_BASE_API + res.data}' alt=""/>`);
input.value = '';
} else {
self.setContent(self.oldValue);
}
}).catch(() => {self.setContent(self.oldValue);});
}
}
});
},
init_instance_callback(editor) {
// console.log("ID為: " + editor.id + " 的編輯器已初始化完成.");
// editor.on('change', (e) => {
// this.$emit('change', e.level.content)
// })
// editor.on('input', (e) => {
// this.$emit('input', e.target.innerHTML)
// });
}
}
}
</script>
<style lang="scss" scoped>
.tinymce-container {
position: relative;
line-height: normal;
}
.tinymce-container {
::v-deep {
.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>
<style lang="scss">
.oe-editor-wrap{
position: relative;
}
.oe-editor-del-btn{
position: absolute;
top:0;
right:0;
z-index:3010;
padding:0 2px 1px;
border-left:1px solid #DCDFE6;
}
.oe-editor-del-btn img{
height:36px;
}
.tox .tox-dialog-wrap__backdrop {
z-index: 3000;
background-color: rgba(0, 0, 0, .6);
}
.tox .tox-dialog {
z-index: 3001;
}
/*調(diào)整堆疊順序,讓下拉列表等顯示*/
.tox-tinymce-aux {
z-index: 3002!important;
}
.tox-tinymce{
border: 1px solid #DCDFE6!important;
}
.tox .tox-statusbar{
border-top: 1px solid #DCDFE6!important;
}
.tox:not([dir=rtl]) .tox-toolbar__group:not(:last-of-type){
border-right: 1px solid #DCDFE6!important;
}
.tox .tox-toolbar, .tox .tox-toolbar__overflow, .tox .tox-toolbar__primary{
background: none!important;
border-bottom:1px solid #DCDFE6;
}
.tox .tox-tbtn svg{
fill:#7b8397!important;
}
</style>