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" />
相关推荐
liangshanbo12152 小时前
Speculation Rules API
前端·javascript·html
石国旺2 小时前
前端javascript在线生成excel,word模板-通用场景(免费)
前端·javascript·excel
Jenna的海糖2 小时前
Vue 项目首屏加载速度优化
前端·javascript·vue.js
前端梭哈攻城狮2 小时前
js计算精度溢出,自定义加减乘除类
前端·javascript·算法
北辰alk2 小时前
React JSX 内联条件渲染完全指南:四招让你的UI动态又灵活
前端
前端小巷子2 小时前
最长递增子序列:从经典算法到 Vue3 运行时核心优化
前端·vue.js·面试
zayyo2 小时前
深入解读 SourceMap:如何实现代码反解与调试
前端
龙在天2 小时前
以为 Hooks 是银弹,结果是新坑
前端
wayhome在哪2 小时前
前端高频考题(css)
前端·css·面试
wayhome在哪3 小时前
前端高频考题(html)
前端·面试·html