TinyMce的使用

tinymce中文官网

tinymce英文官网

安装

注:可以不用下载安装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回调函数中的逻辑

为了能在点击上传图标时选择文件,我们可以先创建一个typefileinput框,设置它的accept属性为我们需要上传的文件类型

其次,触发它的点击事件,以供我们选择文件

然后,我们需要为input框添加change事件。在change事件中拿到我们需要上传的file文件,并将它上传到阿里云服务器上,就能获取到该file文件在阿里云服务器上的地址url

紧接着,我们可以根据file.type, 判断用户上传的文件类型,封装不同的方法。传入fileurl作为不同函数方法的参数, 对不同情况进行分类讨论

最后,我们使用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编辑器的资源样式地址访问错误

解决方法:

  1. 下载rollup-plugin-copy,使用该插件将public里面的文件指定你想要的打包目录中去,但这样打包会产生一个问题就是该插件的操作只是一个拷贝作用,源文件依旧存在,这样就导致了打包体积增加资源浪费
  2. 将public文件创建vite打包资源输出目录("assetsDir: 'static'")同名的文件夹static,将tinymce文件夹放入staic文件夹中,在vite打包的过程中由于同名文件static会自动合并
  3. 通过官网注册获取密钥实现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;
}
相关推荐
To_OC5 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
vipbic6 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy6 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志6 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble7 小时前
c端系统,其实更像一个信息展示平台
前端
李姆斯8 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜9 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司9 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能
kaiking_g9 小时前
vue项目中,静态导入 vs 动态导入 详解
前端·javascript·vue.js
江华森9 小时前
Web 开发基础:HTTP 与 REST
前端·网络协议·http