Vue3后台权限系统整合CMS之富文本编辑器 tinymce

TinyMCE 是一款开源、可高度定制的富文本编辑器,广泛用于 Web 应用中(如 CMS 系统、博客平台、论坛、表单编辑器等),支持用户通过可视化界面编辑文本、插入图片、表格、链接、媒体等内容,最终输出标准化的 HTML 代码。

  • 丰富的功能与扩展‌:内置超过 50 个插件(如表格、代码高亮、图像上传等),并开放 400 多个 API 接口,便于深度集成与功能定制。 ‌13
  • 现代兼容性‌:自 v5 版本起对 Web 标准支持优秀,界面设计符合现代审美,并提供浅色(oxide)和深色(oxide-dark)主题。

Vue3后台权限管理系统整合 tinymce。

首先看演示:https://www.eleadmin.cn

看完演示直接上代码

封装vue组件

复制代码
<template>
  <main>
    <editor v-model="myValue" :init="init" :disabled="disabled" :id="tinymceId"></editor>
  </main>
</template>

<script setup>
import { reactive, ref, onMounted, watch } from 'vue'
import axios from 'axios'
import { getToken } from '@/utils/auth'
import tinymce from 'tinymce/tinymce'
// import 'tinymce/skins/content/default/content.css'
import Editor from '@tinymce/tinymce-vue'
import 'tinymce/themes/silver'
import 'tinymce/themes/silver/theme'
import 'tinymce/icons/default'
import 'tinymce/models/dom'

import 'tinymce/icons/default/icons'
import 'tinymce/plugins/table'
import 'tinymce/plugins/lists'
import 'tinymce/plugins/image'
import 'tinymce/plugins/wordcount'
import 'tinymce/plugins/fullscreen'
import 'tinymce/plugins/preview'
import 'tinymce/plugins/searchreplace'
import 'tinymce/plugins/autolink'
import 'tinymce/plugins/advlist'
import 'tinymce/plugins/code'
import 'tinymce/plugins/anchor'
import 'tinymce/plugins/autoresize'
import 'tinymce/plugins/autosave'
import 'tinymce/plugins/charmap'
import 'tinymce/plugins/codesample'
import 'tinymce/plugins/directionality'
import 'tinymce/plugins/importcss'
import 'tinymce/plugins/insertdatetime'
import 'tinymce/plugins/link'
import 'tinymce/plugins/media'
import 'tinymce/plugins/nonbreaking'
import 'tinymce/plugins/pagebreak'
import 'tinymce/plugins/quickbars'
import 'tinymce/plugins/save'
import 'tinymce/plugins/visualblocks'
import 'tinymce/plugins/visualchars'

const emits = defineEmits(['update:modelValue', 'getContent'])
const props = defineProps({
  modelValue: {
    type: String,
    default: () => {
      return ''
    }
  },
  value: {
    type: String,
    default: () => {
      return ''
    }
  },
  baseUrl: {
    type: String,
    default: ''
  },
  disabled: {
    type: Boolean,
    default: false
  },
  fileSize: {
    type: Number,
    default: 5 // 默认5MB
  },
  plugins: {
    type: [String, Array],
    default: `lists 
    table 
    image 
    wordcount 
    fullscreen 
    preview 
    searchreplace 
    autolink 
    advlist 
    anchor 
    autosave 
    charmap 
    codesample 
    directionality 
    importcss 
    insertdatetime 
    link 
    media 
    nonbreaking 
    pagebreak 
    quickbars 
    save 
    visualblocks 
    visualchars 
    code 
    `
  }, //必填
  toolbar: {
    type: [String, Array],
    default: `code codesample bold italic underline alignleft aligncenter alignright alignjustify | 
      undo redo | 
      fontfamily fontsize blocks |
      formatselect | 
      forecolor backcolor | 
      bullist numlist outdent indent | 
      lists link unlink table code | 
      removeformat | 
      image media |
      lineheight |
      anchor blockquote orderedlist |
      wordcount preview |
      fullscreen
      `
  } //必填
})
//用于接收外部传递进来的富文本
const myValue = ref(props.modelValue || props.value)
const tinymceId = ref('vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + ''))
//定义一个对象 init初始化
const init = reactive({
  selector: '#' + tinymceId.value, //富文本编辑器的id,
  language_url: '/tinymce/langs/zh_CN.js', // 语言包的路径,具体路径看自己的项目,文档后面附上中文js文件
  language: 'zh_CN', //语言
  skin_url: '/tinymce/skins/ui/oxide', // skin路径,具体路径看自己的项目
  height: 500, //编辑器高度
  branding: false, //是否禁用"Powered by TinyMCE"
  menubar: true, //顶部菜单栏显示
  toolbar_mode: 'wrap', // 工具栏换行显示
  image_dimensions: false, //去除宽高属性
  image_title: true, //是否开启图片标题设置
  automatic_uploads: true, //自动上传
  images_upload_url: import.meta.env.VITE_APP_BASE_API + '/common/upload', //图片上传接口
  images_upload_base_path: import.meta.env.VITE_APP_BASE_API, //图片上传基础路径
  images_upload_credentials: true, //是否发送cookie
  plugins: props.plugins, //这里的数据是在props里面就定义好了的
  toolbar: props.toolbar, //这里的数据是在props里面就定义好了的
  font_family_formats:
    'Arial=arial,helvetica,sans-serif; 宋体=SimSun; 微软雅黑=Microsoft Yahei; Impact=impact,chicago;', //字体
  font_size_formats: '11px 12px 14px 16px 18px 24px 36px 48px 64px 72px', //文字大小
  // fontsize_formats: '8pt 10pt 12pt 14pt 18pt 24pt 36pt', // 第二步
  // paste_convert_word_fake_lists: false, // 插入word文档需要该属性
  paste_webkit_styles: 'all',
  paste_merge_formats: true,
  nonbreaking_force_tab: false,
  file_picker_types: 'file',
  promotion: false,
  content_css: '/tinymce/skins/content/default/content.css', //以css文件方式自定义可编辑区域的css样式,css文件需自己创建并引入
  //图片上传
  images_upload_handler: (blobInfo, progress) =>
    new Promise((resolve, reject) => {
      // 检查文件大小
      if (blobInfo.blob().size / 1024 / 1024 > props.fileSize) {
        reject({ message: `上传失败,图片大小请控制在 ${props.fileSize}M 以内`, remove: true })
        return
      }

      // 检查文件类型
      const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']
      if (!allowedTypes.includes(blobInfo.blob().type)) {
        reject({ message: '上传失败,只支持 JPG、PNG、GIF、WebP 格式的图片', remove: true })
        return
      }

      const formData = new FormData()
      formData.append('file', blobInfo.blob(), blobInfo.filename())

      // 获取认证token
      const token = getToken()

      const config = {
        headers: {
          'Content-Type': 'multipart/form-data',
          Authorization: token ? `Bearer ${token}` : ''
        }
      }

      // 使用项目的上传接口
      const uploadUrl = '/common/upload'

      axios
        .post(uploadUrl, formData, config)
        .then((res) => {
          if (res.data.code === 200) {
            // 返回完整的图片URL
            const imageUrl =  res.data.fileName
            resolve(imageUrl)
          } else {
            reject('上传失败: ' + (res.data.msg || '未知错误'))
          }
        })
        .catch((error) => {
          console.error('图片上传失败:', error)
          reject('上传失败: ' + (error.response?.data?.msg || error.message || '网络错误'))
        })
    }),
  // 文件选择器回调
  file_picker_callback: (callback, value, meta) => {
    // 创建文件输入元素
    const input = document.createElement('input')
    input.setAttribute('type', 'file')
    input.setAttribute('accept', 'image/*')

    input.onchange = function () {
      const file = this.files[0]
      if (!file) return

      // 检查文件大小
      if (file.size / 1024 / 1024 > props.fileSize) {
        alert(`图片大小不能超过 ${props.fileSize}MB`)
        return
      }

      // 检查文件类型
      const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']
      if (!allowedTypes.includes(file.type)) {
        alert('只支持 JPG、PNG、GIF、WebP 格式的图片')
        return
      }

      const formData = new FormData()
      formData.append('file', file)

      // 获取认证token
      const token = getToken()

      const config = {
        headers: {
          'Content-Type': 'multipart/form-data',
          Authorization: token ? `Bearer ${token}` : ''
        }
      }

      // 上传文件
      const uploadUrl = '/common/upload'

      axios
        .post(uploadUrl, formData, config)
        .then((res) => {
          if (res.data.code === 200) {
            const imageUrl = import.meta.env.VITE_APP_BASE_API + res.data.fileName
            callback(imageUrl, { alt: file.name })
          } else {
            alert('上传失败: ' + (res.data.msg || '未知错误'))
          }
        })
        .catch((error) => {
          console.error('文件上传失败:', error)
          alert('上传失败: ' + (error.response?.data?.msg || error.message || '网络错误'))
        })
    }

    input.click()
  }
})
//监听外部传递进来的的数据变化
watch(
  () => props.modelValue || props.value,
  (newVal) => {
    if (newVal !== myValue.value) {
      myValue.value = newVal
      emits('getContent', myValue.value)
      emits('update:modelValue', myValue.value)
    }
  }
)
//监听富文本中的数据变化
watch(
  () => myValue.value,
  (newVal) => {
    emits('getContent', newVal)
    emits('update:modelValue', newVal)
  }
)

</script>

页面引用组件

复制代码
<TEditor v-model="form.content" :min-height="300" style="width: 100%"></TEditor>

大家看演示效果,请移步:https://www.eleadmin.cn

有问题可以私聊哦。

相关推荐
打小就很皮...4 小时前
《在 React/Vue 项目中引入 Supademo 实现交互式新手指引》
前端·supademo·新手指引
C澒4 小时前
系统初始化成功率下降排查实践
前端·安全·运维开发
摘星编程5 小时前
React Native + OpenHarmony:自定义useFormik表单处理
javascript·react native·react.js
C澒5 小时前
面单打印服务的监控检查事项
前端·后端·安全·运维开发·交通物流
pas1365 小时前
39-mini-vue 实现解析 text 功能
前端·javascript·vue.js
qq_532453535 小时前
使用 GaussianSplats3D 在 Vue 3 中构建交互式 3D 高斯点云查看器
前端·vue.js·3d
Swift社区5 小时前
Flutter 路由系统,对比 RN / Web / iOS 有什么本质不同?
前端·flutter·ios
2601_949833396 小时前
flutter_for_openharmony口腔护理app实战+我的实现
开发语言·javascript·flutter
雾眠气泡水@6 小时前
前端:解决同一张图片由于页面大小不统一导致图片模糊
前端
开发者小天6 小时前
python中计算平均值
开发语言·前端·python