安装:npm install --save "@tinymce/tinymce-vue@^5"

文档:https://www.tiny.cloud/docs/tinymce/latest/vue-cloud/

下载语言包放在静态资源里进行引入:

import "@/assets/tinymce/langs/zh_CN.js"; //下载后的语言包

封装组件:

 <template>
  <div>
    <Editor
      v-model="myValue"
      class="custom-edit"
      :init="init"
      :disabled="disabled"
      :placeholder="placeholder"
      :id="tinymceId"
      :noEidtclass="noEidtclass"
    ></Editor>
  </div>
</template>

基础配置:

<script setup lang="ts">
    import { reactive, ref, onMounted, watch } from "vue";
    import { apiImgaUpload } from "@/api/emails";
    import { ElMessage, ElMessageBox } from "element-plus";
    import type {ApiResponse } from "@/hooks/type";
    import { AxiosResponse } from "axios";
    import tinymce from "tinymce/tinymce";
    import Editor from "@tinymce/tinymce-vue";
    import "tinymce/themes/silver";
    import "tinymce/themes/silver/theme";
    import "tinymce/models/dom";
    import "tinymce/icons/default";
    import "tinymce/icons/default/icons";
    // 引入编辑器插件
    import "tinymce/plugins/code"; //编辑源码
    import "@/assets/tinymce/plugins/image"; //插入编辑图片、
    // import "@/assets/tinymce/plugins/codeSelect"; //编辑源代码选项
    import "tinymce/plugins/media"; //插入视频
    import "tinymce/plugins/link"; //超链接
    import "tinymce/plugins/preview"; //预览
    // import "tinymce/plugins/template"; //模板
    import "tinymce/plugins/table"; //表格
    import "tinymce/plugins/pagebreak"; //分页
    import "tinymce/plugins/emoticons"; // emoji表情
    import 'tinymce/plugins/emoticons/js/emojis';
    import "tinymce/plugins/lists"; //列
    import "tinymce/plugins/advlist"; //列
    import "tinymce/plugins/fullscreen"; 
    import "tinymce/plugins/autoresize"; // 自动调整编辑器大小
    import "tinymce/plugins/quickbars"; //快速工具条
    import "tinymce/plugins/wordcount"; // 字数统计插件
    import "@/assets/tinymce/langs/zh_CN.js"; //下载后的语言包
    import "@/assets/tinymce/skins/content/default/content.css";
    const emits = defineEmits(["update:value"]);
    const props = defineProps({
    value: {
        type: String,
        default: "",
    },
    placeholder: {
        type: String,
        default: "",
    },
    minHeight:{
        type: Number,
        default: 500,
    },
    height: {
        type: Number,
        default: 500,
    },
    disabled: {
        type: Boolean,
        default: false,
    },
    isDelectContent:{
        type:Boolean,
        default:false
    },
    
    plugins: {
        type: [String, Array],
        default:
        "code image media link preview table quickbars pagebreak lists advlist emoticons fullscreen quickbars wordcount",
    },
    toolbar: {
        type: [String, Array],
        default:
        "undo redo | blocks fontfamily fontsize  | headings formatselect alignleft aligncenter alignright alignjustify | link unlink | image | forecolor backcolor | bold italic underline strikethrough | indent outdent | numlist bullist | superscript subscript | hr emoticons fullscreen | customButton " 
        // "undo redo codesample bold italic underline strikethrough link alignleft aligncenter alignright alignjustify \
        // bullist numlist outdent indent removeformat forecolor backcolor |formatselect fontselect fontsizeselect | \
        // blocks fontfamily fontsize pagebreak lists image media table template preview | code selectall",
    },
    editRoot:{
        type:Boolean,
        default:true
    },//根节点是否可编辑
    placeholderCustom:{
        type:String,
        default:''
    },
    noEidtclass:{
        type:String,
        default:''
    },
    templates: {
        type: Array,
        default: () => [],
    },
    options: {
        type: Object,
        default: () => {},
    },
    contentCss:{
        type:String,
        default:''
    },
    selectTemplateList: {
        type: Array,
        default: () => [],
    },
    isExpandStatus:{
        type:Boolean,
        default:false,//高度变化的状态
    }
    });
    //用于接收外部传递进来的富文本
    const myValue = ref(props.value);

    const tinymceId = ref(
    "vue-tinymce-" + +new Date() + ((Math.random() * 1000).toFixed(0) + "")
    );
    const codeContentType = ref(null)
    const init = reactive({
    selector: "#" + tinymceId.value, //富文本编辑器的id,
    language_url: "/tinymce/langs/zh_CN.js", // 语言包的路径,具体路径看自己的项目,文档后面附上中文js文件
    language: "zh_CN", //语言
    skin_url: "/manage/tinymce/skins/ui/oxide", // skin路径,具体路径看自己的项目 /manage
    content_css: "/manage/tinymce/skins/content/default/content.css",
    icons_url: '/manage/tinymce/icon/customIcon.js',  // 指向你的自定义图标JS文件
    icons: 'customIcons',  // 使用自定义图标集
    content_style: 'div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, abbr, address, cite, code, del, dfn, em, img, ins, kbd, q, samp, small, strong, sub, sup, var, b, i, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, figcaption, figure, footer, header, hgroup, menu, nav, section, summary, time, mark, audio, video {margin: 0;padding: 0;border: 0;}ul,ol{padding-left: 20px;}body{max-width:600px;box-sizing: border-box;} .tox .tox-form__group{margin-bottom:14px}',//img{max-width: 100%;height:auto}
    menubar: false, //顶部菜单栏显示
    statusbar: true, // 底部的状态栏
    plugins: props.plugins,
    toolbar: props.toolbar,
    toolbar_mode: 'wrap',//"sliding" 工具栏的展示形态
    font_formats:
        "Arial=arial,helvetica,sans-serif; 宋体=SimSun; 微软雅黑=Microsoft Yahei; Impact=impact,chicago;", //字体
    font_size_formats: "12px 14px 16px 18px 22px 24px 36px 72px", //文字大小
    height: window.innerHeight - 290, //编辑器高度
    autoresize_min_height: window.innerHeight - 290,
    placeholder: props.placeholderCustom,
    branding: false, //是否禁用“Powered by TinyMCE”
    promotion: false,//禁用升级提醒
    // image_dimensions: false, //去除宽高属性
    paste_webkit_styles: "all",
    paste_merge_formats: true,
    nonbreaking_force_tab: false,
    paste_auto_cleanup_on_paste: false,
    file_picker_types: "file",
    resize: true,
    elementpath: false,
    quickbars_selection_toolbar:
        "forecolor backcolor bold italic underline strikethrough link",
    quickbars_image_toolbar: "alignleft aligncenter alignright | rotateleft rotateright | imageoptions",
    editimage_toolbar:
        "rotateleft rotateright | flipv fliph | editimage imageoptions",
    quickbars_insert_toolbar: false,
    // image_class_list: [   //图像的预定义类列表
    //   { title: 'None', value: '' },
    //   { title: 'No border', value: 'img_no_border' },
    //   { title: 'Green border', value: 'img_green_border' },
    //   { title: 'Blue border', value: 'img_blue_border' },
    //   { title: 'Red border', value: 'img_red_border' }
    // ],
    object_resizing:true,
    autoresize_bottom_margin: 20,
    // autoresize_overflow_padding: 16,
    min_height: props.minHeight,
    image_caption: false,
    image_description:false,//去掉图片描述
    image_advtab: false,//图片高级选项
    convert_urls: false,
    a_plugin_option:true,
    invalid_elements: 'iframe',
    // editable_root: false,
    // editable_class: 'mceEditable',//可编辑区域的类名
    noneditable_class: props.noEidtclass,//不可编辑区域的类名
    editable_root: props.editRoot,
    editable_class: 'editable',

    images_upload_handler: function (blobInfo,success,failure, progress) {
        
        return new Promise((resolve, reject) => {
        // 限制图片大小为 20MB
        const maxSize = 20 * 1024 * 1024;
        if (blobInfo.blob().size > maxSize) {
            // ElMessage.success('Image size exceeds the maximum allowed size of 5MB');
            reject("图像大小不能超过20MB");
            return false;
        }
        const data = new FormData();
        data.append("file", blobInfo.blob(), blobInfo.filename());
        apiImgaUpload(data)
            .then((res: AxiosResponse & ApiResponse) => {
            //   resolve(res?.data);
            if (res?.code == '00') {
                ElMessage.success('上传成功')
                resolve(res?.data);
            } else {
                // ElMessage.error('上传失败!'+res?.msg)
                reject('上传失败!'+res?.msg);
            }
            })
            .catch(() => {
            reject("图片上传失败!");
            });
        });
    },
    
    setup: function (editor) {
        editor.on("init", function () {
        this.getBody().style.fontSize = "14px";
        });
    },
    ...props.options,
    });
    const editorContent = ref('')

    //监听外部传递进来的的数据变化
    watch(
    () => props.value,
    () => {
        myValue.value = props.value;
        emits("update:value", myValue.value);
    }
    );

    watch(
    () => props.isExpandStatus,
    (val) => {
        var element = document.querySelector('.tox-tinymce');

    if(val){
        element.style.height = window.innerHeight - 191 +'px'
            
    }else{
        element.style.height = window.innerHeight - 283 +'px'
    }
    
    }
    );

    //监听富文本中的数据变化
    watch(
    () => myValue.value,
    () => {
        emits("update:value", myValue.value);
    }
    );
    watch(
    () => editorContent.value,
    () => {
        console.log('editorContent.value',editorContent.value);
        
        emits("update:value", editorContent.value);
    }
    )
    //在onMounted中初始化编辑器
    onMounted(() => {
    tinymce.init({
    
    });
    });
</script>

自定义插件(例如添加一个按钮):

//添加按钮的弹窗
const dialogButtonConfig =  {
  title: '插入button',
  body: {
    type: 'panel',
    items: [
      {
        type: 'input',
        name: 'btnText',
        label: '文本内容'
      },
      {
        type: 'input',
        name: 'btnLink',
        label: '链接地址'
      },
      {
        type: 'colorinput', // component type
        name: 'colorBg', // identifier
        label: '背景颜色' // text for the label
      },
      {
        type: 'colorinput', // component type
        name: 'colorText', // identifier
        label: '文字颜色' // text for the label
      },
    ]
  },
  buttons: [
    {
      type: 'cancel',
      name: 'closeButton',
      text: '取消'
    },
    {
      type: 'submit',
      name: 'submitButton',
      text: '插入',
      buttonType: 'primary'
    }
  ],
  //设置默认值
  initialData: {
    colorBg: '#67C23A',
    colorText: '#ffffff',
  },
  onSubmit: (api) => {
    const data = api.getData();
    // const pet = data.isdog ? 'dog' : 'cat';
    if (data.btnLink.indexOf("http") !== 0){
      ElMessage.warning("链接必须以 http/https 开头")
      return false;
    }
    tinymce.activeEditor.execCommand('mceInsertContent', false, `<a style="border-radius:4px;background-color:${data.colorBg};color:${data.colorText};cursor: pointer;text-decoration: none;display: inline-flex;align-items: center;justify-content: center;padding: 10px 20px;line-height: 1.4;
     font-size: 14px;" href="${data.btnLink}" target="_blank">${data.btnText}</a>`);
    tinymce.activeEditor.execCommand('mceInsertContent', false, `<span> </span>`)

    api.close();
  }
};

在tinymce的setup里注册一个工具栏按钮:

setup: function (editor) {
    editor.ui.registry.addButton('customButton', {
      tooltip: '插入button',
      // icon: '',  //图标
      text:'插入button',
      onAction : () =>{
          editor.windowManager.open(dialogButtonConfig)
      }
    });
}

 将 customButton 配置到 toolbar 就会在工具栏看见自定义的组件

如果需要下拉菜单样式的组件中,则按照以下方法:

    editor.ui.registry.addMenuButton('inserPlaceholder', {
      text: '下拉菜单',
      fetch: (callback) => {
        const items = [
          {
            type: 'menuitem',
            text: '菜单1',
            onAction: () => editor.insertContent('<span></span>&nbsp;')
          },
          {
            type: 'menuitem',
            text: '菜单2',
            onAction: () => editor.insertContent('<span></span>&nbsp;')
          },
        ];
        callback(items)
      }
    })

使用组件:

<TinymceEditor :toolbar="toolbar" :value="tinyContent" @update:value="getContentInput" :placeholderCustom="'请输入正文内容'"/>

import TinymceEditor from '@/components/TinymceEditor.vue'

let tinyContent = ref('')
const getContentInput = (val) =>{
  tinyContent.value = val
}

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐