先上最终效果图:
一、tinymce初始化的一些配置
1、首先需要引入下面两个插件
"@tinymce/tinymce-vue": "^3.2.0"
"tinymce": "^5.6.2"
注:tinymce 6 与tinymce 5 在配置属性名发生过改变,详细参考官网
附上官网地址:https://www.tiny.cloud/docs/
至于这里为什么用5,不用6,因为6在火狐76版本中兼容性存在一定问题,富文本初始化有问题,所以这里采用5
2、当下载好插件后,需要到node_modules中找到tinymce文件夹复制出来放到vue项目的public文件夹中
3、需要汉化的还需下载一个语言包,将插件翻译成中文,下载地址如下
官网地址:Language Packages | Trusted Rich Text Editor | TinyMCE
将下载好的语言包,放置在vue的public文件夹中,然后在代码中引入进来
下面是代码及引入的一些插件和一些基础配置
data() {
return {
//初始化配置
myValue: this.value,
init: {
selector: "#" + this.tinymceId,
language_url: "/tinymce/langs/zh-Hans.js", //汉化路径是自定义的,一般放在public或static里面
language: "zh-Hans",
skin_url: "/tinymce/skins/ui/oxide", //皮肤:浅色
// skin_url: "/tinymce/skins/ui/oxide-dark", //皮肤:暗色
plugins: this.plugins, //插件
toolbar: this.toolbar,
// font_size_formats: '12px 14px 16px 18px 24px 36px 48px', // 字体大小
// font_family_formats: '微软雅黑=\'微软雅黑\';宋体=\'宋体\';黑体=\'黑体\';仿宋=\'仿宋\';楷体=\'楷体\';隶书=\'隶书\';幼圆=\'幼圆\';Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;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;sans-serif',
fontsize_formats: '12pt 14pt 16pt 18pt 24pt 36pt 48pt', // 字体大小
font_formats: '微软雅黑=\'微软雅黑\';宋体=\'宋体\';黑体=\'黑体\';仿宋=\'仿宋\';楷体=\'楷体\';隶书=\'隶书\';幼圆=\'幼圆\';Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;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;sans-serif',
plugin_preview_width: 375, // 预览宽度
plugin_preview_height: 668,
toolbar_location: "/",
content_style: "img {max-width:100%;}", // 直接自定义可编辑区域的css样式
// toolbar_drawer: "sliding",
menubar: this.menubar, //隐藏菜单栏
statusbar: this.statusbar, // 最下方的元素路径和字数统计那一栏是否显示
elementpath: this.elementpath, //元素路径是否显示
branding: false, // 隐藏品牌标识
paste_data_images: this.paste_data_images, // 设置为“true”即允许粘贴图像,而将其设置为“false”则不允许粘贴图像。
resize: this.resize, //编辑器宽高是否可变,false-否,true-高可变,'both'-宽高均可,注意引号
height: this.height, //高度
paste_webkit_styles: "all",
paste_merge_formats: true,
nonbreaking_force_tab: false,
paste_auto_cleanup_on_paste: false,
placeholder: this.placeholder,
//链接打开方式
default_link_target: "_blank",
};
},
配置好以上内容,页面如下效果
4.图片上传功能
data() {
return {
//初始化配置
myValue: this.value,
init: {
init_instance_callback: editor => {
editor.on("paste", evt => {
// 监听粘贴事件
this.onPaste(evt);
});
},
};
},
图片上传并替换Url
methods: {
onPaste(event, success, failure) {
let self = this;
const rtf = event.clipboardData.getData('text/rtf');
// 提取图片信息
const hexStrings = this.extractImageDataFromRtf(rtf);
// 获取base64图片数据
const base64Images = hexStrings.map((hexObj) => {
return this.convertHexToBase64(hexObj.hex);
})
// 调用上传方法
for (let i = 0; i < base64Images.length; i++) {
this.picUpload(`data:image/png;base64,${base64Images[i]}`,base64Images.length, i)
}
},
/**
* data: 图片Base64 编码
* len : 上传图片数量
*/
async picUpload(data, len, i) {
++this.uploadNum
let file = this.base64toFile(data);
var formData = new FormData();
formData.append('file', file);
await axios({
ContentType:'multipart/form-data',
url: '/api/file/Uploader',
method: 'post',
data: formData
}).then(res => {
this.uploadPicList.push({ url: res.data.data.url, index: i })
if (this.uploadNum === len) {
setTimeout(() => {
this.setResponseUrl(res.data.data.url)
},1000)
}
})
},
}
附上完整代码
<template>
<div class="tinymce-editor">
<Editor
:init="init"
:tinymceId="tinymceId"
:disabled="disabled"
v-model="myValue"
@input="tinymcechange"
></Editor>
</div>
</template>
<script>
import tinymce from "tinymce/tinymce"; //tinymce默认hidden,不引入不显示
import Editor from "@tinymce/tinymce-vue"; //编辑器引入
import "tinymce/themes/silver/theme"; //编辑器主题
// import "tinymce/icons/default"; //引入编辑器图标icon,不引入则不显示对应图标
// 引入编辑器插件(基本免费插件都在这儿了)
// import "tinymce/plugins/advlist"; //高级列表
// import "tinymce/plugins/autolink"; //自动链接
// import "tinymce/plugins/link"; //超链接
// import "tinymce/plugins/image"; //插入编辑图片
// import "tinymce/plugins/lists"; //列表插件
// import "tinymce/plugins/charmap"; //特殊字符
// import "tinymce/plugins/media"; //插入编辑媒体/
// import "tinymce/plugins/wordcount"; // 字数统计
// import "tinymce/plugins/colorpicker"; //选择颜色插件
// import "tinymce/plugins/textcolor"; //文本颜色插件
// import "tinymce/plugins/print"; //打印
import "tinymce/plugins/preview"; //预览
// import "tinymce/plugins/paste";
import "tinymce/plugins/fullscreen";
// import "tinymce/plugins/searchreplace";
// import "tinymce/plugins/save";
// import "tinymce/plugins/autosave";
// import "tinymce/plugins/imagetools";
// import "tinymce/plugins/table";
// import "tinymce/plugins/codesample";
// import "tinymce/plugins/hr";
// import "tinymce/plugins/emoticons";
// import "tinymce/plugins/anchor";
// import "tinymce/plugins/directionality";
// import "tinymce/plugins/pagebreak";
// import "tinymce/plugins/quickbars";
// import "tinymce/plugins/nonbreaking";
// import "tinymce/plugins/visualblocks";
// import "tinymce/plugins/visualchars";
// import "tinymce/plugins/powerpaste"; //粘贴图片
// import "tinymce/plugins/emoticons/js/emojis";
// import "tinymce/plugins/uploadImg"; //这里需要将插件引入
// import "tinymce/plugins/autoresize"; //编辑器高度自适应,注:plugins里引入此插件时,Init里设置的height将失效
import store from "@/store";
// import api from "@/api/";
import axios from "axios";
// import "tinymce/plugins/powerpaste/";
export default {
components: {
Editor
},
props: {
reportIndex: {
type: Number,
default: null
},
//内容
value: {
type: String,
default: ""
},
// 用于区分单个tinymce
tinymceId: {
type: String,
default: "tinymce"
},
tid: {
type: String,
default: 'my-tinymce-' + new Date().getTime() + parseInt(Math.random(100))
},
disabled: {
type: Boolean,
default: false
},
//插件
plugins: {
type: [String, Array],
default:"fullscreen preview"
//"preview searchreplace autolink directionality visualblocks visualchars fullscreen image link media template code codesample table charmap hr nonbreaking insertdatetime advlist lists wordcount imagetools textpattern autosave bdmap autoresize lineheight"
},
//工具栏
toolbar: {
type: [String, Array],
default: ()=>{
return ['removeformat undo redo | underline strikethrough code bold italic | blockquote | bullist numlist | outdent indent | forecolor | h2 p media link | alignleft aligncenter alignright | fullscreen preview | fontsizeselect | fontselect | blocks fontfamily fontsize | lineheight']
}
},
// 隐藏菜单栏
menubar: {
type: Boolean,
default: false
},
// 最下方的元素路径和字数统计那一栏是否显示
statusbar: {
type: Boolean,
default: false
},
// 元素路径是否显示
elementpath: {
type: Boolean,
default: false
},
// 设置为“true”即允许粘贴图像,而将其设置为“false”则不允许粘贴图像。
paste_data_images: {
type: Boolean,
default: true
},
// 编辑器宽高是否可变,false-否,true-高可变,'both'-宽高均可,注意引号
resize: {
type: [Boolean, String],
default: "both"
},
// 高度
height: {
type: [Number, String],
default: 400
},
// 插入word文档需要该属性
paste_convert_word_fake_lists: {
type: Boolean,
default: true
},
placeholder: {
type: String,
default: "在这里输入内容"
},
// 双向绑定并触发change事件
model: {
prop: "value",
event: "tinymcechange"
},
// 上传图片地址
imgUploadUrl: {
type: String,
default: ""
}
},
data() {
return {
//初始化配置
myValue: this.value,
init: {
selector: "#" + this.tinymceId,
language_url: "/tinymce/langs/zh-Hans.js", //汉化路径是自定义的,一般放在public或static里面
language: "zh-Hans",
skin_url: "/tinymce/skins/ui/oxide", //皮肤:浅色
// skin_url: "/tinymce/skins/ui/oxide-dark", //皮肤:暗色
plugins: this.plugins, //插件
toolbar: this.toolbar,
// line_height_formats:[1,1.5,2,3,4],
// font_size_formats: '12px 14px 16px 18px 24px 36px 48px', // 字体大小
// font_family_formats: '微软雅黑=\'微软雅黑\';宋体=\'宋体\';黑体=\'黑体\';仿宋=\'仿宋\';楷体=\'楷体\';隶书=\'隶书\';幼圆=\'幼圆\';Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;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;sans-serif',
fontsize_formats: '12pt 14pt 16pt 18pt 24pt 36pt 48pt', // 字体大小
font_formats: '微软雅黑=\'微软雅黑\';宋体=\'宋体\';黑体=\'黑体\';仿宋=\'仿宋\';楷体=\'楷体\';隶书=\'隶书\';幼圆=\'幼圆\';Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;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;sans-serif',
plugin_preview_width: 375, // 预览宽度
plugin_preview_height: 668,
toolbar_location: "/",
content_style: "img {max-width:100%;}", // 直接自定义可编辑区域的css样式
// toolbar_drawer: "sliding",
menubar: this.menubar, //隐藏菜单栏
statusbar: this.statusbar, // 最下方的元素路径和字数统计那一栏是否显示
elementpath: this.elementpath, //元素路径是否显示
branding: false, // 隐藏品牌标识
paste_data_images: this.paste_data_images, // 设置为“true”即允许粘贴图像,而将其设置为“false”则不允许粘贴图像。
resize: this.resize, //编辑器宽高是否可变,false-否,true-高可变,'both'-宽高均可,注意引号
height: this.height, //高度
// paste_retain_style_properties: "all",
// paste_word_valid_elements: "*[*]", //word需要它
// paste_convert_word_fake_lists: this.paste_convert_word_fake_lists, // 插入word文档需要该属性
paste_webkit_styles: "all",
paste_merge_formats: true,
nonbreaking_force_tab: false,
paste_auto_cleanup_on_paste: false,
placeholder: this.placeholder,
//链接打开方式
default_link_target: "_blank",
// 粘贴内容时的回调
paste_postprocess: (plugin, args) => {
},
// 图片上传三个参数,图片数据,成功时的回调函数,失败时的回调函数
images_upload_handler: (blobInfo, success, failure) => {
this.handleImgUpload(blobInfo, success, failure);
},
init_instance_callback: editor => {
if (this.value) {
editor.setContent(this.value);
}
this.hasInit = true;
editor.on("NodeChange Change KeyUp SetContent", (e) => {
this.hasChange = true;
// const val = editor.getContent().replace(/<p><img\s?src="data.*?<\/p>/g, '')
this.$emit("input", editor.getContent());
});
editor.on("paste", evt => {
// 监听粘贴事件
this.onPaste(evt);
});
},
// 自定义工具栏功能
setup: function(editor) {
editor.ui.registry.addButton("myimg", {
// text: "插入推荐的链接",
icon: "image",
onAction: () => {
}
});
}
},
uploadTimeOut: null,
removeFn: null,
uploadNum:0,// 记录上传次数
uploadPicList:[] // 图片URL list
};
},
mounted() {
tinymce.init({});
},
methods: {
onPaste(event, success, failure) {
let self = this;
const rtf = event.clipboardData.getData('text/rtf');
// 提取图片信息
const hexStrings = this.extractImageDataFromRtf(rtf);
// 获取base64图片数据
const base64Images = hexStrings.map((hexObj) => {
return this.convertHexToBase64(hexObj.hex);
})
// 调用上传接口
for (let i = 0; i < base64Images.length; i++) {
this.picUpload(`data:image/png;base64,${base64Images[i]}`,base64Images.length, i)
}
},
// change触发事件
tinymcechange(e) {
this.$emit("eventHandle", {
type: "change",
data: e,
reportIndex: this.reportIndex
});
},
//可以添加一些自己的自定义事件,如清空内容
clear() {
this.myValue = "";
},
handleImgUpload(blobInfo, success, failure) {
},
/**
*
* rtf中提取图片信息
* 利用正则从rtf内容中提取到图片的核心信息,得到数组。其中数组中保存的信息有
{
type: ‘’, //图片类型
hex: ‘’ // hex字符串
}
* @param {*} rtfData
*/
extractImageDataFromRtf(rtfData) {
if (!rtfData) {
return [];
}
const regexPictureHeader = /{\\pict[\s\S]+?({\\\*\\blipuid\s?[\da-fA-F]+)[\s}]*/
const regexPicture = new RegExp('(?:(' + regexPictureHeader.source + '))([\\da-fA-F\\s]+)\\}', 'g');
const images = rtfData.match(regexPicture);
const result = [];
if (images) {
for (const image of images) {
let imageType = false;
if (image.includes('\\pngblip')) {
imageType = 'image/png';
} else if (image.includes('\\jpegblip')) {
imageType = 'image/jpeg';
}
if (imageType) {
result.push({
hex: image.replace(regexPictureHeader, '').replace(/[^\da-fA-F]/g, ''),
type: imageType
});
}
}
}
return result;
},
// 讲hex格式转化为base64
convertHexToBase64(hexString) {
return btoa(hexString.match(/\w{2}/g).map(char => {
return String.fromCharCode(parseInt(char, 16));
}).join(''));
},
/**
* data: 图片Base64 编码
* len : 上传图片数量
*/
async picUpload(data, len, i) {
++this.uploadNum
let file = this.base64toFile(data);
var formData = new FormData();
formData.append('file', file);
await axios({
ContentType:'multipart/form-data',
url: '/api/file/Uploader',
method: 'post',
data: formData
}).then(res => {
this.uploadPicList.push({ url: res.data.data.url, index: i })
if (this.uploadNum === len) {
setTimeout(() => {
this.setResponseUrl(res.data.data.url)
},1000)
}
})
},
setResponseUrl() {
this.$nextTick(() => {
// 获取富文本中的所有img标签
const imgs = tinymce.activeEditor.dom.select("img");
// 循环所有img标签
imgs.forEach(async (item, i) => {
this.uploadPicList.forEach((it) => {
// 根据对相应顺序 替换url 图片地址
// if (i === it.index) tinymce.activeEditor.dom.setAttribs(item, { src: this.define.comUrl + it.url });
if (i === it.index) tinymce.activeEditor.dom.setAttrib(item, { src: this.define.comUrl + it.url });
})
});
})
},
base64toFile(dataurl, filename = 'file') {
let arr = dataurl.split(',')
let mime = arr[0].match(/:(.*?);/)[1]
let suffix = mime.split('/')[1]
let bstr = atob(arr[1])
let n = bstr.length
let u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
let file = new File([u8arr], `${filename}.${suffix}`, {
type: mime
})
return file
},
tinymceEditorId() {
return 'tinymce' + Date.parse(new Date())
}
},
watch: {
//监听内容变化
value(newValue) {
this.myValue = newValue == null ? "" : newValue;
},
myValue(newValue) {
if (this.triggerChange) {
this.$emit("change", newValue);
} else {
this.$emit("input", newValue);
}
}
}
};
</script>