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-vars
    
    import '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">
    		<Editor
    			v-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-form
    			ref="formDataRef"
    			:model="formData"
    			:rules="rules"
    			label-width="120px"
    			class="demo-formData"
    			status-icon
    		>
    			<el-form-item label="短信内容" prop="region" style="width: 100%">
    				<Editor
    					v-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
<?php

namespace app\controllers;

use Yii;
use app\models\TinymceImg; // 链接tinymce_img数据库的文件,里面也继承了一些封装的基类,如getDbAll()、create()
use app\controllers\BaseController; //封装一些基类,如response

class 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();
            }
        }
    }

}

你可能感兴趣的:(vue,前端,php)