TinyMCE 免费版也能很好用:Vue3 富文本编辑器最佳实践

选择 TinyMCE 免费版,原因是:

  • 功能完整,免费版已经能满足 90% 的业务需求
  • 插件丰富,支持代码块、表格、表情、分割线等
  • 文档完善,社区活跃

不过 TinyMCE 的"开箱即用"体验并不完美:

  1. 加载时会白屏,没有友好的 loading 状态
  2. 默认 toolbar 太简陋,需要手动配置常用功能
  3. 字体 / 字号 / 行高 在免费版中默认没有,需要自己补齐
  4. 图片上传 需要自定义处理,否则只能用本地 URL

废话不多说直接看封装免费版效果图

代码贴上(cv直接使用,very good)

Tinymce.vue

xml 复制代码
<template>
  <div class="tinymce-editor">
    <!-- 加载状态 -->
    <div v-if="loading" class="tinymce-loading" :style="{ height: `${props.contentHeight}px` }">
      <div class="loading-content">
        <div class="editor-icon">
          <i class="i-mdi:text-box-edit-outline"></i>
        </div>
        <div class="loading-spinner">
          <div class="spinner"></div>
        </div>
        <div class="loading-text">
          <h4>Initializing editor</h4>
          <p>Please wait, the editor is loading ..</p>
        </div>
      </div>
    </div>

    <!-- TinyMCE编辑器 -->
    <div v-show="!loading">
      <Editor
        api-key=""
        :init="editorConfig"
        :model-value="content"
        @update:model-value="emits('update:content', $event)"
      />
    </div>
  </div>
</template>
<script setup>
import Editor from '@tinymce/tinymce-vue'

// 加载状态
const loading = ref(true)

const props = defineProps({
  placeholder: {
    type: String,
    default: 'Enter content...',
  },
  content: {
    type: String,
    default: '',
  },
  contentHeight: {
    type: Number,
    default: 600,
  },
})

const emits = defineEmits(['update:content'])

// 字体列表配置
const fontFamilyFormats = [
  'Aptos=Aptos',
  'Aptos Display=Aptos Display',
  'Aptos Mono=Aptos Mono',
  'Aptos Narrow=Aptos Narrow',
  'Aptos Serif=Aptos Serif',
  'Arial=Arial',
  'Arial Black=Arial Black',
  'Calibri=Calibri',
  'Calibri Light=Calibri Light',
  'Cambria=Cambria',
  'Candara=Candara',
  'Century Gothic=Century Gothic',
  'Comic Sans MS=Comic Sans MS',
  'Consolas=Consolas',
  'Constantia=Constantia',
  'Corbel=Corbel',
  'Courier New=Courier New',
  'Franklin Gothic Book=Franklin Gothic Book',
  'Franklin Gothic Demi=Franklin Gothic Demi',
  'Franklin Gothic Medium=Franklin Gothic Medium',
  'Garamond=Garamond',
  'Georgia=Georgia',
  'Impact=Impact',
  'Lucida Console=Lucida Console',
  'Lucida Sans Unicode=Lucida Sans Unicode',
  'Palatino Linotype=Palatino Linotype',
  'Segoe UI=Segoe UI',
  'Sitka Heading=Sitka Heading',
  'Sitka Text=Sitka Text',
  'Tahoma=Tahoma',
  'Times=Times',
  'Times New Roman=Times New Roman',
  'Trebuchet MS=Trebuchet MS',
  'TW Cen MT=TW Cen MT',
  'Verdana=Verdana',
  '微软雅黑=Microsoft YaHei',
  '黑体=SimHei',
  '新宋体=NSimSun',
  '仿宋=FangSong',
  '隶书=LiSu',
  '楷体=KaiTi',
  '微軟正黑體=Microsoft JhengHei',
].join(';')

// 字号配置
const fontSizeFormats =
  '8px 9px 10px 11px 12px 13px 10pt 14px 16px 18px 20px 24px 26px 28px 36px 48px 72px'

// 行高配置
const lineHeightFormats = '1 1.2 1.5 1.75 2 2.5 3'

// TinyMCE配置
const editorConfig = {
  height: props.contentHeight,
  placeholder: props.placeholder,
  toolbar_mode: 'sliding',
  plugins: [
    'anchor',
    'autolink',
    'charmap',
    'codesample',
    'emoticons',
    'image',
    'link',
    'lists',
    'media',
    'searchreplace',
    'table',
    'visualblocks',
    'wordcount',
    'lineheight',
    'code',
    'preview',
  ],
  toolbar: [
    'undo redo | blocks fontfamily fontsize lineheight | bold italic underline strikethrough superscript subscript | forecolor backcolor',
    'align | numlist bullist indent outdent | blockquote hr | codesample link image table| emoticons charmap searchreplace | removeformat code preview selectall',
  ],
  font_family_formats: fontFamilyFormats,
  font_size_formats: fontSizeFormats,
  lineheight_formats: lineHeightFormats,
  // 默认字体和字号
  content_style: 'body { font-family: Aptos; font-size: 10pt; line-height: 1; }',
  // 图片上传配置
  images_upload_handler: async (blobInfo, progress) => {
    const file = blobInfo.blob()
    const acceptType = ['image/jpeg', 'image/png', 'image/jpg']

    if (!acceptType.includes(file.type)) {
      throw new Error('Image format only accepts .png, .jpg, .jpeg')
    }

    const isLt5M = file.size / 1024 / 1024 < 5
    if (!isLt5M) {
      throw new Error('The size of the image cannot exceed 5MB!')
    }

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

    try {

      //自定义上传方法 ---
      const { data } = await uploadAttachment(formData, false)
      if (data) {
        return data.contentid
      }
    } catch (e) {
      console.log(e)
      throw new Error('Upload failed')
    } 
  },
  // 自动调整大小
  resize: true,
  // 菜单栏
  menubar: false,
  // 状态栏
  statusbar: false,
  // 语言
  language: 'en',
  // 初始化回调
  setup: (editor) => {
    editor.on('init', (editor) => {
      // 编辑器初始化完成,隐藏加载状态
      loading.value = false
    })
    editor.on('input change undo redo', () => {
      emits('update:content', editor.getContent())
    })
  },
}
</script>

<style scoped lang="scss">
.tinymce-editor {
  position: relative;
}

.tinymce-loading {
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  position: relative;
  overflow: hidden;

  &::before {
    content: '';
    position: absolute;
    top: 0;
    left: -100%;
    width: 100%;
    height: 100%;
    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
    animation: shimmer 2s infinite;
  }

  .loading-content {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 16px;
    z-index: 1;
  }

  .editor-icon {
    font-size: 48px;
    color: #3b82f6;
    animation: pulse 2s infinite;

    i {
      display: block;
    }
  }

  .loading-spinner {
    position: relative;

    .spinner {
      width: 32px;
      height: 32px;
      border: 3px solid #e2e8f0;
      border-top: 3px solid #3b82f6;
      border-radius: 50%;
      animation: spin 1s linear infinite;
    }
  }

  .loading-text {
    text-align: center;

    h4 {
      margin: 0 0 4px 0;
      font-size: 16px;
      font-weight: 600;
      color: #1e293b;
    }

    p {
      margin: 0;
      font-size: 14px;
      color: #64748b;
    }
  }
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

@keyframes pulse {
  0%,
  100% {
    opacity: 1;
    transform: scale(1);
  }
  50% {
    opacity: 0.7;
    transform: scale(1.05);
  }
}

@keyframes shimmer {
  0% {
    left: -100%;
  }
  100% {
    left: 100%;
  }
}
</style>

使用

ruby 复制代码
<Tinymce v-model:content="localEmailObj.content" :content-height="contentHeight" />
相关推荐
Qrun1 小时前
Windows11安装nvm管理node多版本
前端·vscode·react.js·ajax·npm·html5
中国lanwp1 小时前
全局 npm config 与多环境配置
前端·npm·node.js
JELEE.2 小时前
Django登录注册完整代码(图片、邮箱验证、加密)
前端·javascript·后端·python·django·bootstrap·jquery
TeleostNaCl4 小时前
解决 Chrome 无法访问网页但无痕模式下可以访问该网页 的问题
前端·网络·chrome·windows·经验分享
前端大卫5 小时前
为什么 React 中的 key 不能用索引?
前端
你的人类朋友5 小时前
【Node】手动归还主线程控制权:解决 Node.js 阻塞的一个思路
前端·后端·node.js
小李小李不讲道理7 小时前
「Ant Design 组件库探索」五:Tabs组件
前端·react.js·ant design
毕设十刻7 小时前
基于Vue的学分预警系统98k51(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js
mapbar_front8 小时前
在职场生存中如何做个不好惹的人
前端
牧杉-惊蛰8 小时前
纯flex布局来写瀑布流
前端·javascript·css