在 Vue 3 项目开发中,选择一个既能满足复杂排版需求,又具备良好扩展性的富文本编辑器是关键。TinyMCE 作为行业老牌王者,其 6.x 版本与 Vue 3 (Composition API) 的结合堪称完美。
本文将为您提供一份全功能封装模板,包含:插件全量引入、中文汉化、Base64 图片自动上传、代码高亮及 SEO 优化配置。
为什么 TinyMCE 是 Vue 3 的首选?
- 生态成熟:官方提供
@tinymce/tinymce-vue组件,深度适配 Vue 响应式系统。 - 功能全面:从表格编辑、媒体嵌入到代码预览,开源版已涵盖 90% 的需求。
- 高度可控:通过
init配置项,你可以精确控制生成的 HTML 结构,这对于 SEO(搜索引擎优化) 至关重要。
核心实现:Vue 3 + TypeScript 封装组件
以下代码实现了编辑器的按需加载与本地化配置。
1. 安装基础依赖
在项目中执行:
Bash
npm install @tinymce/tinymce-vue tinymce tinymce-i18n
2. 完整组件代码
你可以将此代码保存为 TinymceEditor.vue。
代码段
<template>
<div class="tinymce-editor-wrapper">
<Editor
v-model="editorContent"
:init="editorInit"
:disabled="disabled"
license-key="gpl"
@update:model-value="handleChange"
/>
</div>
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import Editor from '@tinymce/tinymce-vue'
// 核心
import 'tinymce/tinymce'
import 'tinymce/themes/silver'
import 'tinymce/icons/default'
import 'tinymce/models/dom'
// 加载所有开源插件
import 'tinymce/plugins/advlist' // 高级列表
import 'tinymce/plugins/anchor' // 锚点
import 'tinymce/plugins/autolink' // 自动链接
import 'tinymce/plugins/autoresize' // 自动调整高度
import 'tinymce/plugins/autosave' // 自动保存
import 'tinymce/plugins/charmap' // 字符映射
import 'tinymce/plugins/code' // 代码
import 'tinymce/plugins/codesample' // 代码示例
import 'tinymce/plugins/directionality' // 文本方向
import 'tinymce/plugins/emoticons' // 表情符号
import 'tinymce/plugins/emoticons/js/emojis' // 表情符号-表情列表
import 'tinymce/plugins/fullscreen' // 全屏
import 'tinymce/plugins/help' // 帮助
import 'tinymce/plugins/image' // 图片
import 'tinymce/plugins/importcss' // 导入 CSS
import 'tinymce/plugins/insertdatetime' // 插入日期时间
import 'tinymce/plugins/link' // 链接
import 'tinymce/plugins/lists' // 列表
import 'tinymce/plugins/media' // 媒体
import 'tinymce/plugins/nonbreaking' // 非断行空格
import 'tinymce/plugins/pagebreak' // 分页符
import 'tinymce/plugins/preview' // 预览
import 'tinymce/plugins/quickbars' // 快速工具栏
import 'tinymce/plugins/save' // 保存
import 'tinymce/plugins/searchreplace' // 搜索替换
import 'tinymce/plugins/table' // 表格
import 'tinymce/plugins/help/js/i18n/keynav/zh_CN.js' // 帮助-键盘导航中文
import 'tinymce/plugins/visualblocks' // 可视化块
import 'tinymce/plugins/visualchars' // 可视化字符
import 'tinymce/plugins/wordcount' // 字数统计
import 'tinymce/plugins/advlist' // 高级列表
import 'tinymce/plugins/autolink' // 自动链接
import 'tinymce/plugins/code' // 代码
import 'tinymce/plugins/image' // 图片
import 'tinymce/plugins/link' // 链接
import 'tinymce/plugins/lists' // 列表
import 'tinymce/plugins/table' // 表格
import 'tinymce/plugins/wordcount' // 字数统计
// 样式
import 'tinymce/skins/ui/oxide/skin.min.css'
import 'tinymce-i18n/langs/zh_CN'
import contentUiCss from 'tinymce/skins/ui/oxide/content.css?raw'
const props = withDefaults(
defineProps<{
modelValue?: string
height?: number | string
maxHeight?: number | string
disabled?: boolean
placeholder?: string
}>(),
{
modelValue: '',
height: 500,
disabled: false,
placeholder: '请输入内容...',
key: 'tinymce-editor'
}
)
const emit = defineEmits<{
'update:modelValue': [value: string]
change: [value: string]
}>()
const editorContent = ref(props.modelValue)
// 外部值同步
watch(
() => props.modelValue,
(val) => {
if (val !== editorContent.value) {
editorContent.value = val || ''
}
},
{ immediate: true }
)
const handleChange = (value: string) => {
emit('update:modelValue', value)
emit('change', value)
}
const editorInit = computed(() => ({
height: Number(props.height) || 500,
width: '100%',
language: 'zh_CN', // 语言:中文
image_caption: true, // 图片标题
quickbars_selection_toolbar: 'bold italic | quicklink h2 h3 blockquote quickimage quicktable', // 快速工具栏:选中文本/表格时浮动出现的快捷按钮
noneditable_class: 'mceNonEditable', // 非可编辑区域的类名,用于标识内容区域
toolbar_mode: 'wrap', // 工具栏模式:滑动显示
// UI
menubar: false, // 禁用菜单栏
branding: false, // 禁用品牌标识
promotion: false, // 禁用推广提示
statusbar: true, // 显示状态栏
// 自动保存:每 30 秒触发一次,仅当内容有变化时
autosave_ask_before_unload: true,
autosave_interval: '30s',
autosave_prefix: '{path}{query}-{id}-',
autosave_restore_when_empty: false,
autosave_retention: '2m',
image_advtab: true,
// 已加载插件列表,按需启用;如需新增插件,请在上方 import 并在 plugins 字符串中追加
plugins: [
'advlist',
'anchor',
'autolink',
'autoresize',
'autosave',
'charmap',
'code',
'codesample',
'directionality',
'emoticons',
'fullscreen',
'help',
'image',
'importcss',
'insertdatetime',
'link',
'lists',
'media',
'nonbreaking',
'pagebreak',
'preview',
'quickbars',
'save',
'searchreplace',
'table',
'visualblocks',
'visualchars',
'wordcount'
],
// 工具栏按钮配置
toolbar:
'undo redo | blocks fontfamily fontsize | bold italic underline strikethrough | align numlist bullist | link image | table media | lineheight outdent indent| forecolor backcolor removeformat | charmap emoticons | code fullscreen preview ',
// 粘贴时自动清理格式,保留基本样式
paste_webkit_styles: 'color font-size font-weight font-style',
paste_data_images: true, // 允许粘贴 base64 图片
// 表格默认样式:无边框、紧凑模式
table_default_attributes: { class: 'tinymce-table' },
table_default_styles: { 'border-collapse': 'collapse', width: '100%' },
// 代码块高亮主题:与主流代码编辑器保持一致
codesample_languages: [
{ text: 'HTML/XML', value: 'markup' },
{ text: 'JavaScript', value: 'javascript' },
{ text: 'CSS', value: 'css' },
{ text: 'TypeScript', value: 'typescript' },
{ text: 'Vue', value: 'vue' },
{ text: 'Python', value: 'python' },
{ text: 'Java', value: 'java' },
{ text: 'SQL', value: 'sql' },
{ text: 'Shell', value: 'shell' }
],
// 全屏时保留顶部工具栏,避免遮挡
toolbar_sticky: true,
toolbar_sticky_offset: 60,
// 允许拖拽调整高度
resize: true,
min_height: Number(props.height) || 500,
max_height: Number(props.maxHeight) || 0,
// 自定义快捷键:与主流编辑器保持一致
custom_shortcuts: {
'meta+s': 'save', // Cmd/Ctrl + S 触发保存事件
'meta+shift+s': 'codesample' // Cmd/Ctrl + Shift + S 插入代码块
},
// 禁用不可见字符提示,减少视觉干扰
visualchars_default_state: false,
// 字体大小下拉选项
font_size_formats: '12px 14px 16px 18px 20px 24px 30px 36px',
// 行高下拉选项
line_height_formats: '1 1.2 1.4 1.6 1.8 2 2.5 3',
// 颜色预设:与系统主题保持一致
color_map: [
'000000',
'Black',
'333333',
'Dark gray',
'5A5A5A',
'Gray',
'888888',
'Light gray',
'CCCCCC',
'Silver',
'FFFFFF',
'White',
'FF4D4F',
'Red',
'FFA940',
'Orange',
'FADB14',
'Yellow',
'73D13D',
'Green',
'40A9FF',
'Blue',
'9254DE',
'Purple'
],
// 字体,显示全部
font_family_formats:
'宋体=宋体;黑体=黑体;微软雅黑=微软雅黑;仿宋=仿宋;楷体=楷体;幼圆=幼圆;隶书=隶书;Arial=Arial;Arial Black=Arial Black;Comic Sans MS=Comic Sans MS;Courier New=Courier New;Georgia=Georgia;Helvetica=Helvetica;Impact=Impact;Lucida Console=Lucida Console;Lucida Sans Unicode=Lucida Sans Unicode;Palatino Linotype=Palatino Linotype;Tahoma=Tahoma;Times New Roman=Times New Roman;Trebuchet MS=Trebuchet MS;Verdana=Verdana;Wingdings=Wingdings;',
// 图片上传失败后回退为 base64,保证用户感知一致
images_upload_error_handler: (message: any) => {
console.warn('[TinyMCE] 图片上传失败,已回退为 base64 模式:', message)
},
// 编辑器初始化完成后的回调
setup: (editor: any) => {
editor.on('init', () => {
// 自动聚焦到编辑器
editor.focus()
})
// 监听保存快捷键
editor.addShortcut('meta+s', '保存内容', () => {
emit('change', editorContent.value)
})
},
placeholder: props.placeholder, // 占位符
object_resizing: 'img', // 图片调整大小:仅允许水平调整
content_style: `${contentUiCss}
body {
font-family: '微软雅黑', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial;
font-size: 14px;
line-height: 1.6;
color: #333;
}
p {
margin: 0 0 1em;
}
`,
/**
* 本地 Base64 上传
*/
images_upload_handler: (blobInfo: any) =>
new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result as string)
reader.onerror = () => reject('Image read failed')
reader.readAsDataURL(blobInfo.blob())
})
}))
</script>
<style scoped>
.tinymce-editor-wrapper {
width: 100%;
}
:deep(.tox-tinymce) {
border-radius: 6px;
border: 1px solid var(--el-border-color);
}
:deep(.tox-edit-area__iframe) {
background-color: #fff;
}
</style>