tinymce中文官网
tinymce英文官网
安装
- npm i @tinymce/tinymce-vue tinymce -S
- 下载中文包www.tiny.cloud/get-tiny/la...
注:可以不用下载安装tinymce文件包,采用官网注册申请的api-key值通过cdn的方式获取在线样式内容
下载完成后,在public文件夹里面创建static文件夹,再在里面创建tinymce文件夹,把node_module中的tinymce文件下的skins文件夹,以及解压后的汉化包拷贝到创建的tinymce文件夹中(无法直接从node_module中访问)
使用
typescript
<script lang="ts" setup>
import { ref, reactive, onMounted, onUnmounted, computed } from 'vue';
import { ElMessage, ElLoading } from 'element-plus';
import Editor from '@tinymce/tinymce-vue';
//引入tinymce开启本地模式
import tinymce from 'tinymce/tinymce'; //tinymce核心文件
//引入图标和主题等
import 'tinymce/themes/silver/theme';
import 'tinymce/icons/default/icons';
import 'tinymce/models/dom';
//引入插件
import 'tinymce/plugins/codesample';
import 'tinymce/plugins/lists';
import 'tinymce/plugins/advlist';
import 'tinymce/plugins/link';
import 'tinymce/plugins/autolink';
import 'tinymce/plugins/charmap';
import 'tinymce/plugins/fullscreen';
import 'tinymce/plugins/preview';
import 'tinymce/plugins/code';
import 'tinymce/plugins/searchreplace';
import 'tinymce/plugins/table';
import 'tinymce/plugins/visualblocks';
import 'tinymce/plugins/wordcount';
import 'tinymce/plugins/insertdatetime';
import 'tinymce/plugins/image';
import { cos as COS, uploadData as UploadData } from '@/utils/upload'; //图片上传接口
onMounted(() => {
cos.value = COS;
uploadData.value = UploadData;
tinymce.init({});
});
onUnmounted(() => {
tinymce.remove();
});
const cos = ref();
const uploadData = ref();
const prop = defineProps({
modelValue: {
type: String,
default: ''
},
height: {
type: Number,
default: 400
}
});
const emit = defineEmits<{
(e: 'update:modelValue', value: any): void;
}>();
const contentValue = computed({
get() {
return prop.modelValue;
},
set(value) {
emit('update:modelValue', value);
}
});
const editorHeight = computed(() => prop.height);
const init = reactive({
selector: '#editor',
language_url: 'static/tinymce/langs/zh-Hans.js', // 语言包
language: 'zh-Hans', // 语言类型
skin_url: 'static/tinymce/skins/ui/oxide', // 皮肤类型
toolbar: [
'bold italic hr | fontsize blocks | forecolor backcolor align | blockquote removeformat | subscript superscript | bullist table insertdatetime | link charmap wordcount searchreplace code | codesample visualblocks image fullscreen preview | undo redo'
],
height: editorHeight.value,
menubar: false, // 清空上方不需要的菜单栏
statusbar: false,
plugins:
'codesample lists advlist link autolink charmap fullscreen preview code searchreplace table visualblocks wordcount insertdatetime image',
placeholder: '请输入内容吧...',
branding: false, //tiny技术支持信息是否显示
resize: true, //编辑器宽高是否可变,false-否,true-高可变,'both'-宽高均可,注意引号
elementpath: false, //元素路径是否显示
content_css: 'static/tinymce/skins/content/default/content.css', // 自动导入样式会报404错误需要手动操作,
// 编辑区内容样式
content_style:
'body{font-size:12pt;font-family:Microsoft YaHei,微软雅黑,宋体,Arial,Helvetica,sans-serif;line-height:1.5}img {max-width:100%;}',
paste_data_images: true, //图片是否可粘贴
// 图片自定义上传方式
images_upload_handler: (blobInfo: any) =>
new Promise((resolve, reject) => {
cos.value
.uploadImg(blobInfo.blob())
.then((data: any) => {
resolve('https://' + data.Location);
})
.catch((err: any) => {
reject(err);
});
}),
//可上传文件类型
file_picker_types: 'file image media',
// 文件上传
file_picker_callback: (callback: any) => {
const fileBtn = document.createElement('input');
fileBtn.type = 'file';
fileBtn.style.position = 'fixed';
fileBtn.style.left = '0';
fileBtn.style.top = '0';
fileBtn.style.opacity = '0';
document.body.appendChild(fileBtn);
fileBtn.click();
fileBtn.addEventListener('change', () => {
const loading = ElLoading.service({
lock: true,
text: 'Loading',
background: 'rgba(0, 0, 0, 0.7)'
});
const fileList: any = fileBtn.files;
let file: any = undefined;
if (fileList.length) {
file = fileList[0];
} else {
ElMessage.error('请选择需要上传的文件');
return;
}
new Promise(() => {
cos.value
.uploadImg(file)
.then((data: any) => {
loading.close();
callback('https://' + data.Location, { text: file.name });
})
.catch(() => {
loading.close();
ElMessage.error('该文件无法上传');
});
});
});
}
});
</script>
<template>
<div id="sample">
<Editor
id="editor"
api-key="rmwp2ztcxsgkocdb77x4gaub5tmz7k1jpsx39xvvg2lnjzu1"
v-model="contentValue"
:init="init"
/>
</div>
</template>
<style lang="scss" scoped>
#sample {
width: 100%;
}
</style>
代码编写中遇到的问题
问题:中文官网和tinymce6之前版本提供的自定义上传图片的方式适用于vue2版本,并不能在vue3中使用,所以无法覆盖编辑器本身的上传图片的功能,还是优先选择编辑器自带的上传图片方式把图片转换为base64编码
typescript
// 官网vue2
images_upload_handler: function (blobInfo, succFun, failFun) {
var xhr, formData;
var file = blobInfo.blob();//转化为易于理解的file对象
xhr = new XMLHttpRequest();
xhr.withCredentials = false;
xhr.open('POST', '/demo/upimg.php');
xhr.onload = function() {
var json;
if (xhr.status != 200) {
failFun('HTTP Error: ' + xhr.status);
return;
}
json = JSON.parse(xhr.responseText);
if (!json || typeof json.location != 'string') {
failFun('Invalid JSON: ' + xhr.responseText);
return;
}
succFun(json.location);
};
formData = new FormData();
formData.append('file', file, file.name );//此处与源文档不一样
xhr.send(formData);
}
// vue3
images_upload_handler: (blobInfo: any) =>
new Promise((resolve, reject) => {
cos.value
.uploadImg(blobInfo.blob())
.then((data: any) => {
resolve('https://' + data.Location);
})
.catch((err: any) => {
reject(err);
});
})
注意:由于官网并没有提供上传文件的插件,但提供了上传文件的回调函数和自定义插件的实现方式
通过上传文件回调的方式实现文件的上传
在tinymce
编辑器初始化时,添加选择文件的回调函数file_picker_callback
添加回调函数,link
插件下的模态框就会多出一个上传svg
图标。
在不写函数逻辑的情况下,此时点击上传图标是没有任何反应的,因为未触发file_picker_callback
回调函数中的逻辑
为了能在点击上传图标时选择文件,我们可以先创建一个type
为file
的input
框,设置它的accept
属性为我们需要上传的文件类型
其次,触发它的点击事件,以供我们选择文件
然后,我们需要为input
框添加change
事件。在change
事件中拿到我们需要上传的file
文件,并将它上传到阿里云服务器上,就能获取到该file
文件在阿里云服务器上的地址url
紧接着,我们可以根据file.type
, 判断用户上传的文件类型,封装不同的方法。传入file
和url
作为不同函数方法的参数, 对不同情况进行分类讨论
最后,我们使用file_picker_callback
提供的callback
方法,调用回调函数即可。callback
方法有两个作用:a. 文件上传完成后,在插入/编辑链接模态框
回显上传的文件信息 。其中,url
为插入/编辑链接模态框
回显的地址信息,text
为插入/
编辑链接模态框
回显的显示文字信息。b. 点击保存
按钮后,会在TinyMCE编辑器
中展示一个指向上传文件的链接。
自定义插件的方式可参考tinymce爱好者魔改插件(适用于vue2,tinymce版本4以下)
问题: 由于webpack和vite的打包方式不同导致tinymce无法正常访问样式资源代码
-
webpack会将所有的文件都进行打包,包括public里面的文件
-
vite在不进行任何配置的情况下,会将除开public的所有引用到资源打包编译添加哈希值至assets文件夹中(非引用文件以及行内样式图片未被打包编译资源会被treeSharp直接忽略不打包),vite只会对public文件夹进行不打包处理,public文件夹内所有文件会移至dist中,与vite.config.ts打包后的静态目录"assetsDir: 'static'"同级,如下图
-
当由于rewrite.conf配置文件规定了资源的访问方式,这就导致了正常情况下,tinymce编辑器的资源样式地址访问错误
解决方法:
- 下载rollup-plugin-copy,使用该插件将public里面的文件指定你想要的打包目录中去,但这样打包会产生一个问题就是该插件的操作只是一个拷贝作用,源文件依旧存在,这样就导致了打包体积增加资源浪费
- 将public文件创建vite打包资源输出目录("assetsDir: 'static'")同名的文件夹static,将tinymce文件夹放入staic文件夹中,在vite打包的过程中由于同名文件static会自动合并
- 通过官网注册获取密钥实现cdn的方式获取
还有注意的一点是网上所有的教程资源地址都是以斜杠开头'/'的路径地址(如:'/static/tinymce/langs/zh-Hans.js'),这样就导致资源的访问指向了网站的根目录(tiku.zuoyebang.cc/static/tiny...),而不会带上我们需要的网站制定的域名'/fe',所以在实际开发中tinymce的资源访问路径不能以斜杠开头'/'(如'static/tinymce/skins/ui/oxide'),至于为什么本地运行的环境加上'/'也能正常访问是因为在本地运行的时候,根目录就是我所编辑的项目目录,资源能够正常访问
问题: 弹窗中使用tinymce编辑器,tinymce的所有自带弹窗无法展示
全局样式文件里设置
css
.tox-tinymce-aux {
z-index: 9999 !important;
}