Deng
Deng
在vue3.0项目中使用Tinymce富文本编辑器 | odjBlog
    欢迎来到odjBlog的博客!

在vue3.0项目中使用Tinymce富文本编辑器

技术总结及问题解决 odjbin 1年前 (2024-10-27) 71次浏览 0个评论

TinyMCE 简介

  • TinyMCE 是一款易用、且功能强大的所见即所得的富文本编辑器
  • 刚开始使用官网的写法, 发现安装插件都是放在云端的, 初始化加载太慢, 我就选择了本地部署, 然后解决了这个 tinymce 初始化加载太慢的问题, 从而在 vue3 项目中集成 tinymce 富文本编辑器

演示效果

版本说明

  • "vue": "^3.2.47",
  • "@tinymce/tinymce-vue": "^4.0.7", 支持 vue3.0 版本, 但不支持 2.0 版本
  • "tinymce": "^5.0.4",

安装

命令行操作

npm install --save "@tinymce/tinymce-vue@^4"
npm install --save "tinymce"

创建组件 Tinymce.vue

  • 在 src/components 目录创建组件 Tinymce.vue

注意

  • 编辑器本身是英文编辑器,所以还需要下载本地化文件(下载这个)
    语言包 |可信富文本编辑器 |TinyMCE

  • /langs/zh_CN.js : 这是汉化路径, 将下载的 zh_CN.js 放于项目根目录 public/langs/里面

 skin_url: '/skins/ui/oxide', //皮肤
 content_css: '/skins/content/default/content.css',
  • 将/node_modules/tinymce 里面的 skins 样式文件复制到 public 里面, 用于赋值 skin_url 和 content_css

附件上传

附件上传位置

附件上传函数

 file_picker_callback: (callback, value, meta) => {
    // 使用案例 http://tinymce.ax-z.cn/general/upload-images.php
    // meta.filetype  //根据这个判断点击的是什么 file image media
    let filetype; //限制文件的上传类型,需要什么就添加什么的后缀
    if (meta.filetype == "image") {
      filetype = ".jpg, .jpeg, .png, .gif, .ico, .svg";
    } else if (meta.filetype == "media") {
      filetype = ".mp3, .mp4, .avi, .mov";
    } else {
      filetype =
          ".pdf, .txt, .zip, .rar, .7z, .doc, .docx, .xls, .xlsx, .ppt, .pptx, .mp3, .mp4, .jpg, .jpeg, .png, .gif, .ico, .svg";
    }
    let inputElem = document.createElement("input"); //创建文件选择
    inputElem.setAttribute("type", "file");
    inputElem.setAttribute("accept", filetype);
    inputElem.click();
    inputElem.onchange = () => {
      let file = inputElem.files[0]; //获取文件信息
      let xhr, formData;
      xhr = new XMLHttpRequest();
      xhr.withCredentials = false;
      xhr.open('POST', url);//url 是后端附件上传接口
      xhr.setRequestHeader(
        'Authorization',getToken()
      )//设置头部 token
      let loading= ElLoading.service({
        lock: true,
        text: '文件上传中...',
        background: 'rgba(0, 0, 0, 0.7)',
      })
      xhr.onload = function() {
        let res;
        res = JSON.parse(xhr.responseText);
          ElNotification({
            title: '提示',
            message: res.code === 1000 ? '上传成功' : '上传失败',
            type: res.code === 1000 ? 'success' : 'error'
          })
        loading.close()
        const fileUrl = res.data.url;
        callback(fileUrl+'?fileName='+res.data.originalFilename, {text:file.name ,title: file.name });
      };
      formData = new FormData();
      formData.append('file', file, file.name );
      xhr.send(formData);
    };
  },

注:

  • 附件上传回调,第一个参数是文件地址 ( 携带 fileName 参数, 用于文件下载时显示原文件名 );第二个参数是 Object, 分别是 text ( 设置显示的文本信息 ),和 title ( 标题 )
callback(fileUrl+'?fileName='+res.data.originalFilename, {text:file.name ,title: file.name });

附件上传成功后显示效果

附件下载

  • 点击文件名方可下载

富文本编辑器的内容详情展示

<div class="article-a" v-html="formData.articleContent" @click="clickHandle"></div>
  • 为 div 设置 class="article-a" 属性, 可以为刚刚上传的附件名设置颜色
.article-a {
  a {
    color: #409eff !important;
    &:hover {
      text-decoration: underline !important;
    }
  }
}
  • 为 div 设置 @click="clickHandle" , 绑定点击事件 ( 原文件名下载 )
const clickHandle = (e) => {
  if (e.target.nodeName == "A") {
    e.preventDefault()
    let url = e.target.href.split('?')[0];
    const searchParams = new URLSearchParams(e.target.href.split('?')[1])
    const fileId = searchParams.get('fileId');
    const fileName = searchParams.get('fileName');
    let item = {
      url,
      fileName
    }
    download(item)
  }
}
const download = (row) => {
  const x = new window.XMLHttpRequest();
  x.open('GET', row.url, true);
  x.responseType = 'blob';
  x.onload = () => {
    const url = window.URL.createObjectURL(x.response);
    const a = document.createElement('a');
    a.href = url;
    a.download = row.fileName;
    a.click();
  };
  x.send();
}

完整代码

<template>
  <div class="tinymce-boxz">
    <Editor v-model="content" :init="init"/>
  </div>
</template>

<script setup>
import tinymce from 'tinymce'
import Editor from "@tinymce/tinymce-vue";
import {defineProps} from "vue";
import {ElMessage} from "element-plus";
import {getToken} from '@/utils/auth'
import axios from "axios";

import 'tinymce/themes/silver'
import 'tinymce/icons/default/icons'
import 'tinymce/plugins/preview'
import 'tinymce/plugins/searchreplace'
import 'tinymce/plugins/autolink'
import 'tinymce/plugins/directionality'
import 'tinymce/plugins/visualblocks'
import 'tinymce/plugins/visualchars'
import 'tinymce/plugins/advlist'
import 'tinymce/plugins/fullscreen'
import 'tinymce/plugins/image'
import 'tinymce/plugins/link'
import 'tinymce/plugins/media'
import 'tinymce/plugins/template'
import 'tinymce/plugins/code'
import 'tinymce/plugins/codesample'
import 'tinymce/plugins/table'
import 'tinymce/plugins/pagebreak'
import 'tinymce/plugins/nonbreaking'
import 'tinymce/plugins/anchor'
import 'tinymce/plugins/insertdatetime'
import 'tinymce/plugins/lists'
import 'tinymce/plugins/wordcount'
import 'tinymce/plugins/autosave'

const emit = defineEmits(['update:value'])
const props = defineProps({
  //默认值
  value: {
    type: String,
    default: "",
  },
  imageUrl: {
    type: String,
    default: "",
  },
  fileUrl: {
    type: String,
    default: "",
  },
  plugins: {
    type: [String, Array],
    default:
        "preview searchreplace autolink directionality visualblocks visualchars fullscreen image link media template code codesample table pagebreak nonbreaking anchor insertdatetime advlist lists wordcount autosave",
  },
  toolbar: {
    type: [String, Array],
    default: [
      "fullscreen undo redo restoredraft | cut copy paste pastetext | forecolor backcolor bold italic underline strikethrough link anchor | alignleft aligncenter alignright alignjustify outdent indent | bullist numlist | blockquote subscript superscript removeformat ",
      "styleselect formatselect fontselect fontsizeselect |  table image axupimgs media  pagebreak insertdatetime  selectall visualblocks searchreplace | code preview | indent2em lineheight formatpainter",
    ],
  },
  fontFormats: {
    type: [String, Array],
    default: "微软雅黑=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;"
  }
})
const content = ref(props.value);
const imgUrl = ref();
const init = reactive({
  language_url: '/langs/zh_CN.js', //汉化路径是自定义的
  skin_url: '/skins/ui/oxide', //皮肤
  content_css: '/skins/content/default/content.css',
  language: 'zh_CN',
  placeholder: "在这里输入文字", //textarea 中的提示信息
  min_width: 320,
  min_height: 220,
  height: 500, //注:引入 autoresize 插件时,此属性失效
  resize: "both", //编辑器宽高是否可变,false-否,true-高可变,'both'-宽高均可,注意引号
  promotion: false,
  branding: false, //tiny 技术支持信息是否显示
  statusbar: false,  //最下方的元素路径和字数统计那一栏是否显示
  elementpath: false, //元素路径是否显示
  font_formats: props.fontFormats,  //字体样式
  plugins: props.plugins, //插件配置 axupimgs indent2em
  toolbar: props.toolbar, //工具栏配置,设为 false 则隐藏
  menubar: "file edit view format table tools", //菜单栏配置,设为 false 则隐藏,不配置则默认显示全部菜单,也可自定义配置--查看 http://tinymce.ax-z.cn/configure/editor-appearance.php --搜索“自定义菜单”
  // images_upload_url: '/apib/api-upload/uploadimg',  //后端处理程序的 url,建议直接自定义上传函数 image_upload_handler,这个就可以不用了
  // images_upload_base_path: '/demo',  //相对基本路径--关于图片上传建议查看--http://tinymce.ax-z.cn/general/upload-images.php
  paste_data_images: true, //图片是否可粘贴
  file_picker_types: "file image media", //file image media 分别对应三个类型文件的上传:link 插件,image 和 axupimgs 插件,media 插件。想屏蔽某个插件的上传就去掉对应的参数
  // 文件上传处理函数
  setup: function (editor) {
    editor.on('change', function (e) {
      tinymce.activeEditor.save();//执行自动保存
    });
  },
  //此处为图片上传处理函数
  images_upload_handler: (blobInfo, success) => {
    let formData = new FormData()
    formData.append('file', blobInfo.blob())
    //上传图片接口 上传成功后返回图片地址,用于显示在富文本中
    uploadFile(formData, props.imageUrl, success)
  },
  file_picker_callback: (callback, value, meta) => {
    // 使用案例 http://tinymce.ax-z.cn/general/upload-images.php
    // meta.filetype  //根据这个判断点击的是什么 file image media
    let filetype; //限制文件的上传类型,需要什么就添加什么的后缀
    if (meta.filetype == "image") {
      filetype = ".jpg, .jpeg, .png, .gif, .ico, .svg";
    } else if (meta.filetype == "media") {
      filetype = ".mp3, .mp4, .avi, .mov";
    } else {
      filetype =
          ".pdf, .txt, .zip, .rar, .7z, .doc, .docx, .xls, .xlsx, .ppt, .pptx, .mp3, .mp4, .jpg, .jpeg, .png, .gif, .ico, .svg";
    }
    let inputElem = document.createElement("input"); //创建文件选择
    inputElem.setAttribute("type", "file");
    inputElem.setAttribute("accept", filetype);
    inputElem.click();
    inputElem.onchange = () => {
      let file = inputElem.files[0]; //获取文件信息
      let reader = new FileReader();
      reader.readAsDataURL(file);
      reader.onload = function () {
        let id = "blobid" + new Date().getTime();
        let blobCache = tinymce.activeEditor.editorUpload.blobCache;
        let base64 = reader.result.split(",")[1];
        let blobInfo = blobCache.create(id, file, base64);
        blobCache.add(blobInfo);
        callback(blobInfo.blobUri(), {alt: file.name});
      };
    };
  },
});
//内容有变化,就更新内容,将值返回给父组件
watch(() => {
  emit("update:value", content.value);
});
//图片上传函数
const uploadFile = (formData, url,success) => {
  axios.post(
      'http://192.168.xx.xx:xxxx' + url,//上传接口 完整 url
      formData,
      {
        headers: {
          'Content-Type': 'multipart/form-data',
          Authorization: getToken()
        }
      }
  ).then(res => {
    // if (res.status !== 200) {
    //   ElMessage.error("上传失败!")
    // }
    let data = res.data
    if (data.code !== 1000) {
      ElMessage.error(data.msg)
    }else {
      success(data.data.url)
    }
  })
}
</script>
<style scoped>
.tinymce-boxz > textarea {
  display: none;
}
</style>
<style>
/* 隐藏 apikey 没有绑定当前域名的提示 */
.tox-notifications-container .tox-notification--warning {
  display: none !important;
}

.tox.tox-tinymce {
  max-width: 100%;
}
</style>

页面上引用

  • /notice/file 上传文件接口
<Tinymce image-url="/notice/file" file-url="/notice/file" v-model:value="form.noticeContent"/>
喜欢 (0)
[]
分享 (0)
发表我的评论
取消评论
表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
已稳定运行:3年255天3小时47分