Vue3使用tinymce的配置和坑

一、安装依赖

    1. 下载安装依赖
yarn add tinymce -S;
yarn add @tinymce/tinymce-vue -S;
    1. 下载中文语言包和静态文件
    • 2.1 这一步网上都有,直接去官网下载语言包,将文件放在public/tinymce/langs中,这样打包后静态文件会被复制到打包后的目录中
    • 2.2 还可以添加主题UI等样式,public/tinymce/skins,可以在github找一些相关项目复制下来,可选项

二、 封装组件

    1. tinymce 引入功能
    // component/Editor/js/importTinymce.js
    // 引入node_modules里的tinymce相关文件文件
    // eslint-disable-next-line no-unused-varsimport 'tinymce/themes/silver' // 编辑器主题,不引入则报错
    import 'tinymce/icons/default' // 引入编辑器图标icon,不引入则不显示对应图标
    // import 'tinymce/skins/ui/oxide/content.css'
    // import 'tinymce/skins/content/default/content.css'
    // // 引入编辑器插件(基本免费插件都在这儿了)
    import 'tinymce/plugins/advlist' // 高级列表
    import 'tinymce/plugins/anchor' // 锚点
    import 'tinymce/plugins/autolink' // 自动链接
    import 'tinymce/plugins/autoresize' // 编辑器高度自适应,注:plugins里引入此插件时,Init里设置的height将失效
    import 'tinymce/plugins/autosave' // 自动存稿
    import 'tinymce/plugins/charmap' // 特殊字符
    import 'tinymce/plugins/code' // 编辑源码
    import 'tinymce/plugins/codesample' // 代码示例
    import 'tinymce/plugins/directionality' // 文字方向
    // import 'tinymce/plugins/emoticons' // 表情import 'tinymce/plugins/fullpage/index' // 文档属性
    import 'tinymce/plugins/fullscreen' // 全屏
    import 'tinymce/plugins/help' // 帮助
    import 'tinymce/plugins/hr' // 水平分割线
    import 'tinymce/plugins/image' // 插入编辑图片
    import 'tinymce/plugins/importcss' // 引入css
    import 'tinymce/plugins/insertdatetime' // 插入日期时间
    import 'tinymce/plugins/link' // 超链接
    import 'tinymce/plugins/lists' // 列表插件
    import 'tinymce/plugins/media' // 插入编辑媒体
    import 'tinymce/plugins/nonbreaking' // 插入不间断空格
    import 'tinymce/plugins/pagebreak' // 插入分页符
    import 'tinymce/plugins/paste' // 粘贴插件
    import 'tinymce/plugins/preview'// 预览
    // import 'tinymce/plugins/print'// 打印
    import 'tinymce/plugins/quickbars' // 快速工具栏
    import 'tinymce/plugins/save' // 保存
    import 'tinymce/plugins/searchreplace' // 查找替换
    // import 'tinymce/plugins/spellchecker'  //拼写检查,暂未加入汉化,不建议使用
    // import 'tinymce/plugins/tabfocus' // 切入切出,按tab键切出编辑器,切入页面其他输入框中
    import 'tinymce/plugins/table' // 表格
    import 'tinymce/plugins/template' // 内容模板
    import 'tinymce/plugins/textcolor' // 文字颜色
    import 'tinymce/plugins/textpattern' // 快速排版
    import 'tinymce/plugins/toc' // 目录生成器
    import 'tinymce/plugins/visualblocks' // 显示元素范围
    import 'tinymce/plugins/visualchars' // 显示不可见字符
    import 'tinymce/plugins/wordcount' // 字数统计
    
    1. tinymce 配置项
    // component/Editor/js/config.js
    //  导入插件
    const buttonPlugins = 'emoticons preview searchreplace autolink directionality visualblocks visualchars fullscreen image link media code codesample table charmap hr pagebreak nonbreaking anchor insertdatetime advlist lists wordcount textpattern autosave paste';// 导入工具栏
    const toolbar = 'fullscreen undo redo restoredraft | cut copy paste pastetext | forecolor backcolor bold italic underline strikethrough link anchor \ table image | alignleft aligncenter alignright alignjustify outdent indent | \ styleselect formatselect fontselect fontsizeselect | bullist numlist | blockquote subscript superscript removeformat | media charmap emoticons hr pagebreak insertdatetime print preview | code selectall searchreplace visualblocks | indent2em lineheight formatpainter axupimgs';// 初始化配置
    export const init = {selector: '#js_tinymce_editor',height: 550,width: '100%',cleanup: true,language_url: './tinymce/langs/zh-Hans.js', // 引入语言包文件language: 'zh-Hans', // 语言类型content_css: true,skin_url: './tinymce/skins/ui/oxide', // 皮肤:浅色// skin_url: '/tinymce/skins/ui/oxide-dark',//皮肤:暗色plugins: buttonPlugins, // 插件配置toolbar: toolbar, // 工具栏配置,设为false则隐藏body_class: 'panel-body',object_resizing: false, // 图片和表格是否开启在编辑器内部缩放// menubar: false, // 菜单栏配置,设为false则隐藏,不配置则默认显示全部菜单,也可自定义配置--查看 http://tinymce.ax-z.cn/configure/editor-appearance.php --搜索“自定义菜单”//emoticons_database_url: './tinymce/emoticons/js/emojis.js',fontsize_formats: '12px 14px 16px 18px 20px 22px 24px 28px 32px 36px 48px 56px 72px', // 字体大小nonbreaking_force_tab: true, // 此选项允许您在用户按下键盘tab键时强制TinyMCE插入三个实体font_formats: '微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方=PingFang SC,Microsoft YaHei,sans-serif;宋体=simsun,serif;仿宋体=FangSong,serif;黑体=SimHei,sans-serif;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;', // 字体样式 微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif, 宋体=simsun,serif,仿宋体=FangSong,黑体=SimHei,Arial=arial,lineheight_formats: '0.5 0.8 1 1.2 1.5 1.75 2 2.5 3 4 5', // 行高配置,也可配置成"12px 14px 16px 20px"这种形式branding: false, // tiny技术支持信息是否显示resize: false, // 编辑器宽高是否可变,false-否,true-高可变,'both'-宽高均可,注意引号// statusbar: false,  //最下方的元素路径和字数统计那一栏是否显示elementpath: false, // 元素路径是否显示content_style: 'p {margin-block-start: 0; margin-block-end: 0; color: #606D81; font-size: 14px;}; table { border: 1px}', // 直接自定义可编辑区域的css样式// content_css: './tinymce/index.css', // 以css文件方式自定义可编辑区域的css样式,css文件需自己创建并引入paste_data_images: true, // 图片是否可粘贴images_upload_handler: (blobInfo, success, failure) => {// 需要在setup里面重新写,传入 url},imagetools_toolbar: 'editimage', // 图片控制的工具栏
    }
    
    1. tinymce 封装组件
    // components/Editor/index
    <template><div class="tinymce-box"><Editorv-model="editorContent":init="initData":disabled="isDisabled"ref="editorRef"id="js_tinymce_editor"></Editor></div>
    </template><script setup lang="ts">import Editor from '@tinymce/tinymce-vue';import tinymce from 'tinymce/tinymce'; // tinymce默认hidden,不引入则不显示编辑器import { formatTime } from '@/utils/';import { uploadImage } from '@/api/tinymce';const props = defineProps({modelValue: {type: String,default: '',},});// 导入配置文件import './js/importTinymce';import { init } from './js/config';import axios from 'axios';onMounted(() => {// 配置信息tinymce.init({});});const isDisabled = ref(true);// 初始化配置const initData = ref(Object.assign({}, init, {init_instance_callback: (editor: any) => {// 初始化编辑器实例时要执行的函数isDisabled.value = false;},// 图片上传images_upload_handler: async (blobInfo: any, success: any, failure: any) => {var form = new FormData();form.append('file', blobInfo.blob());const config = {headers: {'Content-Type': 'multipart/form-data',},};axios.post('/my-api/vueadmin/image/upload', form, config).then((res) => {const data = res.data.data;if (res.data.code === '00000') {success(data.imgUrl);} else {failure('上传失败!');}}).catch((err) => {failure('上传失败!');});},}));const editorContent = ref(props.modelValue);const emit = defineEmits(['update:modelValue']);// 实时监听内容变化,并同步到父组件watch(() => editorContent.value,(n) => {debounce(() => {emit('update:modelValue', editorContent.value);});});const timeout = ref();// 防抖const debounce = (fn: any, wait = 400) => {if (timeout.value) {clearTimeout(timeout.value);fn();}timeout.value = setTimeout(fn, wait);};
    </script><style lang="scss" scoped>.tinymce-box {width: 100%;}
    </style>

三、引用组件

    1. 封装好Tinymce组件后,可以开始应用
      注意:如果Tinymce引入的页面是弹窗之类的,存在其自身弹窗层级问题,导致其自身层级低于项目弹窗的层级,这里可以通过加添或修改public/tinymce/skins自定义的样式来覆盖
    // views/page/edit/index.js
    <!-- @format --><template><el-card><el-formref="formDataRef":model="formData":rules="rules"label-width="120px"class="demo-formData"status-icon><el-form-item label="短信内容" prop="region" style="width: 100%"><Editorv-if="editDataLoading"v-model="formData.textarea"style="width: 100%"></Editor></el-form-item></el-form><div class="btn-box"><el-button type="primary" @click="submitForm(formDataRef)"> 保存 </el-button><el-button @click="resetForm(formDataRef)">重置</el-button></div></el-card>
    </template><script setup lang="ts">import Editor from '@/components/Editor/index.vue';import type { FormInstance, FormRules } from 'element-plus';import { IFormData } from '@/api/sms/types';import { getSmsInfo, saveSms } from '@/api/sms/';const propsData = defineProps({drawerData: {type: Object,default: () => {id: '';},},});const emit = defineEmits(['closeDrawerHandle']);const editDataLoading = ref(false);const initHandle = async () => {if (propsData.drawerData?.id) {const { code, data } = await getSmsInfo(propsData.drawerData.id);if (code === '00000') {console.log(data);formData.textarea = data.textarea;formData.id = data.id;}}nextTick(() => {editDataLoading.value = true;});};initHandle();const formDataRef = ref<FormInstance>();const formData = reactive<IFormData>({textarea: '',id: '',});const rules = reactive<FormRules<IFormData>>({textarea: [{required: true,message: '请输入富文本',trigger: 'blur',},],});const submitForm = (formEl: FormInstance | undefined) => {if (!formEl) return;formEl.validate(async (valid, fields) => {if (valid) {console.log(formData);const { code, data } = await saveSms(formData);if (code === '00000') {ElMessage.success(data.msg || '保存成功!');emit('closeDrawerHandle', true);}} else {console.log(fields);ElMessage.error('保存失败!');}});};const resetForm = (formEl: FormInstance | undefined) => {if (!formEl) return;formEl.resetFields();};
    </script><style scoped></style>
    

本地虚拟机wamp存储富文本

    1. 在数据库中创建数据表
    DROP TABLE IF EXISTS `tinymce_content`;
    CREATE TABLE `tinymce_content`  (`id` bigint NOT NULL AUTO_INCREMENT,`textarea` longtext NOT NULL DEFAULT '' COMMENT '富文本内容',`create_time` date NOT NULL DEFAULT '' COMMENT '添加时间',PRIMARY KEY (`id`) USING BTREE,
    ) ENGINE = InnoDB AUTO_INCREMENT = 0 CHARACTER SET = utf8;
    
    1. 在数据库中创建图片数据表
    DROP TABLE IF EXISTS `tinymce_img`;
    CREATE TABLE `tinymce_img`  (`id` bigint NOT NULL AUTO_INCREMENT,`name` varchar(100) NOT NULL DEFAULT '' COMMENT '图片名称',`url` varchar(100) NOT NULL DEFAULT '' COMMENT '图片地址',`type` varchar(50) NOT NULL DEFAULT '' COMMENT '图片类型',`blob` mediumblob DEFAULT NULL COMMENT '图片内容(不建议将图片存入数据库)',`create_time` date NOT NULL DEFAULT '' COMMENT '添加时间',PRIMARY KEY (`id`) USING BTREE,
    ) ENGINE = InnoDB AUTO_INCREMENT = 0 CHARACTER SET = utf8;
    
    1. 创建好这两张表后,在富文本编辑器中便可以将上传、截图、图片链接源存在项目中,并将图片链接存在tinymce_img中,整个内容存在tinymce_content中
    1. 后端使用PHP框架Yii2,所以图片存在根目录的/web/static/静态目录中,访问地址:http://myproject.com/static/1705046024_finmart.png,主域名是通过wamp配置的代理host访问
    1. PHP存储代码
// /controllers/TinymceImgContent.php
<?phpnamespace app\controllers;use Yii;
use app\models\TinymceImg; // 链接tinymce_img数据库的文件,里面也继承了一些封装的基类,如getDbAll()、create()
use app\controllers\BaseController; //封装一些基类,如responseclass ImageController extends BaseController
{public function actionIndex(){return $this->render('index');}public function actionUpload(){$model = new Images();// $_FILES 获取文件信息$fileData = $_FILES['file'];// 上传文件的临时路径$tempFile = $fileData['tmp_name'];// 上传文件的类型$fileType = $fileData['type'];// 尺寸大小$size = $fileData['size'];// 名称$name = time() . '_' . $fileData["name"];// dd($fileData);// 是否上传的文件if (is_uploaded_file($tempFile)) {// 从临时文件中读取内容$img = file_get_contents($tempFile);// Yii::$app->getBasePath() 项目的根目录,设置绝对访问地址$saveLocalUrl = Yii::$app->getBasePath() . "/web/static/" . $name;$imgLink = 'http://' . $_SERVER['HTTP_HOST'] . "/static/$name";// move_uploaded_file 将文件存在本地web/static中move_uploaded_file($tempFile, $saveLocalUrl);$data = [// "img"  => $img, // 存储在数据库,不建议"date" => date("Y-m-d H:i:s"),"name" => $name,"url"  => $imgLink,"type" => $fileType,];$row = $model->create($data);if (isset($row)) {$res = ['imgUrl' => $imgLink];return $this->response($res);} else {$this->code = "00001";$this->msg = "图片保存失败";return $this->response();}}}}