选择 TinyMCE 免费版,原因是:
- 功能完整,免费版已经能满足 90% 的业务需求
- 插件丰富,支持代码块、表格、表情、分割线等
- 文档完善,社区活跃
不过 TinyMCE 的"开箱即用"体验并不完美:
- 加载时会白屏,没有友好的 loading 状态
- 默认 toolbar 太简陋,需要手动配置常用功能
- 字体 / 字号 / 行高 在免费版中默认没有,需要自己补齐
- 图片上传 需要自定义处理,否则只能用本地 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" />