TinyMCE + Vue 使用

1. 简介

TinyMCE 是一个强大的富文本编辑器,tinymce-vue 是官方提供的 Vue 组件封装,可轻松集成到 Vue 2 / Vue 3 项目中。

本文使用的依赖版本如下

"vue": "3.5.13",
"@tinymce/tinymce-vue": "^6.3.0",
"tinymce": "^7.9.1",

文档参考: https://www.tiny.cloud/docs/tinymce/7/dialog/

2. 安装

npm install tinymce @tinymce/tinymce-vue

如果需要本地部署 TinyMCE,可下载 tinymce 包并放入 public/tinymce


3. 基本使用(Vue3)

<template>
  <Editor
    v-model="content"
    :init="editorInit"
  />
</template>

<script setup>
import { ref } from 'vue'
import Editor from '@tinymce/tinymce-vue'

const content = ref('')

const editorInit = {
  height: 400,
  menubar: false,
  plugins: 'lists link image table code help wordcount',
  toolbar:
    'undo redo | formatselect | bold italic | alignleft aligncenter alignright | bullist numlist | image table | code',
  language: 'zh-Hans',
}
</script>

4. 使用本地 TinyMCE 资源

如果你希望离线使用 TinyMCE:

将 TinyMCE 拷贝到 public

public/
  tinymce/
    tinymce.min.js
    skins/
    plugins/
    themes/

image


5. 配置中文语言包

下载语言包(如 zh-Hans.js)放到:

public/tinymce/langs/zh-Hans.js

配置:

const editorInit = {
  language: 'zh-Hans',
  language_url: '/tinymce/langs/zh-Hans.js',
}

6. 上传图片(自定义图片上传)

TinyMCE 支持自定义图片上传处理:

const editorInit = {
  images_upload_handler: (blobInfo, success, failure) => {
    const formData = new FormData()
    formData.append('file', blobInfo.blob())

    fetch('/api/upload', { method: 'POST', body: formData })
      .then(res => res.json())
      .then(data => success(data.url))
      .catch(() => failure('上传失败'))
  },
}

7. 设置内容样式(content_css)

可以让编辑区内容使用自己的样式:

const editorInit = {
  content_style: `body { font-size: 14px; line-height: 1.6; }`,
}

如果需要引用外部 css:

content_css: '/css/editor-content.css'

8. 禁止自动换行、粘贴过滤、格式控制

8.1 粘贴过滤

让粘贴纯文本:

paste_as_text: true

8.2 禁止换行生成 div 替换为 p

forced_root_block: 'p'

9. 获取编辑器实例

有时需要直接操作 editor:

<Editor
  v-model="content"
  :init="editorInit"
  @init="onEditorInit"
/>
let editorInstance = null
const onEditorInit = (evt, editor) => {
  editorInstance = editor
}

调用方法:

editorInstance.execCommand('Bold')
console.log(editorInstance.getContent())

10. 常见插件清单

插件 功能
lists 列表
link 超链接
image 图片
table 表格
code 查看源代码
wordcount 字数统计
fullscreen 全屏模式
preview 预览

11. 常见问题

11.1 TinyMCE 不显示

通常是 base_url 配置不正确。检查:

/tinymce/tinymce.min.js 是否存在

11.2 语言包未加载

确保:

/tinymce/langs/zh-Hans.js 可访问

并配置:

language_url: '/tinymce/langs/zh-Hans.js'

11.3 图片上传返回 URL 但不显示

返回格式必须是纯 URL 字符串,不是 JSON:

success(data.url)

12. 参考完整配置示例

image

点击查看TMEditor组件代码
<template>
  <div class="tm-editor">
    <editor v-model="content" :init="init" :id="tinymceId"></editor>
  </div>
</template>

<script setup>
// 引入 Vue 相关方法
import { reactive, watch, ref, nextTick, onMounted } from "vue";

// 文件上传接口
import { commonUpload } from "^/api/common/uploadFile";

// 引入 TinyMCE 核心与插件
import tinymce from "tinymce/tinymce";
import "tinymce/icons/default/icons";
import "tinymce/themes/silver/theme";
import "tinymce/models/dom";
import Editor from "@tinymce/tinymce-vue";

// TinyMCE 插件
import "tinymce/themes/silver";
import "tinymce/plugins/image";
import "tinymce/plugins/table";
import "tinymce/plugins/lists"; // 列表插件
import "tinymce/plugins/wordcount"; // 字数统计
import "tinymce/plugins/preview"; // 预览
import "tinymce/plugins/emoticons"; // emoji 表情
import "tinymce/plugins/emoticons/js/emojis.js"; // 表情库
import "tinymce/plugins/code"; // 查看源码
import "tinymce/plugins/link"; // 超链接
import "tinymce/plugins/advlist"; // 高级列表
import "tinymce/plugins/codesample"; // 代码示例
import "tinymce/plugins/autoresize"; // 自动高度
import "tinymce/plugins/quickbars"; // 快捷工具栏
import "tinymce/plugins/nonbreaking"; // 不间断空格
import "tinymce/plugins/searchreplace"; // 查找替换
import "tinymce/plugins/autolink"; // 自动生成链接
import "tinymce/plugins/directionality"; // 文字方向
import "tinymce/plugins/visualblocks"; // 显示区块范围
import "tinymce/plugins/visualchars"; // 显示不可见字符
import "tinymce/plugins/charmap"; // 特殊符号
import "tinymce/plugins/insertdatetime"; // 插入日期时间
import "tinymce/plugins/importcss"; // 引入外部 CSS
import "tinymce/plugins/anchor"; // 锚点
import "tinymce/plugins/fullscreen"; // 全屏

const emits = defineEmits(["update:modelValue"]);

// 接收父组件传入的配置
const props = defineProps({
  modelValue: { type: String, default: "" },
  editable_root: { type: Boolean, default: true },
  plugins: {
    type: [String, Array],
    default:
      "image link importcss autoresize searchreplace autolink directionality code visualblocks visualchars fullscreen codesample table charmap nonbreaking anchor insertdatetime advlist lists wordcount charmap quickbars emoticons",
  },
  toolbar: {
    type: [String, Array, Boolean],
    default:
      "undo redo variable bold italic underline strikethrough ltr rtl | align numlist bullist | table | lineheight outdent indent| forecolor backcolor removeformat | blocks fontfamily fontsize | charmap emoticons | anchor codesample | image link | fullscreen | insertdatetime | code wordcount",
  },
  readonly: { type: Boolean, default: false },
  minHeight: { type: Number, default: 300 },
  variableList: {
    type: Array,
    default: () => [
      { label: "用户名", value: "{{username}}" },
      { label: "用户邮箱", value: "{{email}}" },
      { label: "公司名称", value: "{{company}}" },
      { label: "当前时间", value: "{{time}}" },
      { label: "当天日期", value: "{{date}}" },
    ],
  },
});

// TinyMCE 组件 ID
const tinymceId = ref(
  "vue-tinymce-" + new Date().getTime() + Math.random().toFixed(3)
);

// 编辑器内容
const content = ref(props.modelValue);

// TinyMCE 初始化配置
const init = reactive({
  setup(editor) {
    editor.ui.registry.addIcon(
      "preset-variable",
      `
      <svg t="1765359636723" class="icon" viewBox="0 0 1170 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6120"
          width="18" height="18">
          <path
              d="M285.96419 0a60.099048 60.099048 0 1 1 0 120.198095c-29.45219 0-47.567238 10.654476-51.882666 38.619429l-0.536381 4.315428-0.463238 9.947429v203.337143c0 38.278095-16.091429 70.485333-43.081143 98.401524l-5.168762 5.193142-10.410667 9.386667 10.410667 9.411048c24.966095 24.137143 41.667048 51.46819 46.713905 83.382857l0.902095 6.899809 0.633905 13.287619v248.539429l0.536381 9.947429c2.56 26.209524 15.920762 38.692571 38.424381 41.984l4.071619 0.487619 9.849904 0.463238a60.099048 60.099048 0 1 1 0 120.198095c-87.576381 0-162.133333-51.419429-171.983238-150.357333l-0.560762-6.826667-0.53638-15.896381V602.38019c0-5.071238-10.118095-18.16381-30.915048-32.621714a241.712762 241.712762 0 0 0-44.27581-24.600381 60.099048 60.099048 0 0 1 0-111.518476 238.592 238.592 0 0 0 44.373334-24.576c16.335238-11.459048 26.038857-21.942857 29.403428-28.281905l0.780191-1.633524 0.633905-2.706285V173.080381C112.88381 59.294476 191.951238 0 285.96419 0z m598.454858 0c87.576381 0 162.108952 51.419429 171.958857 150.357333l0.585143 6.826667 0.536381 15.896381v203.337143c0 5.071238 10.020571 18.18819 30.817523 32.646095 13.848381 9.752381 28.696381 17.968762 44.27581 24.576a60.099048 60.099048 0 0 1 0 111.518476c-15.579429 6.607238-30.427429 14.872381-44.27581 24.600381-16.335238 11.434667-26.038857 21.942857-29.427809 28.281905l-0.75581 1.633524-0.633904 2.706285v248.539429l-0.463239 15.823238c-7.119238 103.472762-83.041524 157.257143-172.617142 157.257143a60.099048 60.099048 0 1 1 0-120.198095c29.45219 0 47.567238-10.654476 51.882666-38.619429l0.536381-4.315428 0.463238-9.947429V602.38019c0-38.278095 16.091429-70.460952 43.081143-98.401523l5.168762-5.168762 10.313143-9.411048-10.313143-9.313524c-24.966095-24.210286-41.667048-51.565714-46.713905-83.456l-0.902095-6.92419-0.633905-13.263238V173.080381c0-38.692571-18.895238-52.882286-52.882285-52.882286a60.099048 60.099048 0 1 1 0-120.198095zM462.506667 243.809524c59.733333 0 113.590857 37.254095 135.801904 94.061714l8.143239 20.748191 23.966476-30.622477A217.673143 217.673143 0 0 1 801.767619 243.809524H804.571429C844.946286 243.809524 877.714286 277.308952 877.714286 318.659048c0 41.252571-32.768 74.752-73.167238 74.752h-2.779429a72.655238 72.655238 0 0 0-57.148952 28.038095l-75.776 96.816762 39.009523 99.474285H731.428571c40.399238 0 73.167238 33.499429 73.167239 74.849524 0 41.252571-32.768 74.776381-73.167239 74.776381h-23.600761a146.334476 146.334476 0 0 1-135.875048-94.061714l-8.118857-20.748191-23.966476 30.598096a217.6 217.6 0 0 1-171.300572 84.211809h-2.82819c-40.399238 0-73.167238-33.52381-73.167238-74.776381 0-41.276952 32.768-74.849524 73.167238-74.849524h2.82819c22.186667 0 43.178667-10.288762 57.100191-28.038095l75.776-96.792381-38.936381-99.474285H438.857143c-40.399238 0-73.167238-33.52381-73.167238-74.800762C365.689905 277.308952 398.457905 243.809524 438.857143 243.809524h23.649524z"
              p-id="6121"></path>
      </svg>
      `
    );
    // 注册 toolbar 按钮 参考:https://www.tiny.cloud/docs/tinymce/7/apis/tinymce.editor.ui.registry/
    editor.ui.registry.addButton("variable", {
      icon: "preset-variable",
      tooltip: "预设变量",
      onAction: () => {
        editor.windowManager.open({
          title: "选择变量",
          size: "normal",
          body: {
            type: "panel",
            items: [
              {
                type: "input",
                name: "search",
                label: "搜索变量",
              },
              {
                type: "htmlpanel",
                name: "list",
                html: `
                  <div id="variable-list"
                       style="max-height:260px; overflow:auto; padding:4px;">
                  </div>
                `,
              },
            ],
          },
          // buttons: [{ type: "cancel", text: "关闭" }],
          initialData: { search: "" },

          // 输入搜索自动过滤
          onChange(api, details) {
            if (details.name === "search") {
              const keyword = api.getData().search.toLowerCase();
              renderList(keyword);
            }
          },

          // 点击变量插入内容
          // onAction(api, details) {
          //   if (details.name.startsWith("select-variable-")) {
          //     const index = Number(
          //       details.name.replace("select-variable-", "")
          //     );
          //     const variable = props.variableList[index];
          //     editor.insertContent(`{{${variable.value}}}`);
          //     api.close();
          //   }
          // },
        });
        // 初次打开初始化数据
        setTimeout(() => {
          renderList("");
        }, 0);
      },
    });

    // 变量列表渲染
    function renderList(keyword) {
      const container = document.getElementById("variable-list");
      if (!container) return;

      const key = (keyword || "").trim().toLowerCase(); // 关键:处理 null 和空字符串

      // 空搜索 → 返回全部
      const filtered = key
        ? props.variableList.filter(
            (v) =>
              v.label.toLowerCase().includes(key) ||
              v.value.toLowerCase().includes(key)
          )
        : props.variableList;

      container.innerHTML = filtered
        .map((v) => {
          const index = props.variableList.indexOf(v);
          return `
        <div class="variable-item"
             data-index="${index}"
             style="padding:6px; cursor:pointer; border-bottom:1px solid #eee;">
          <b>${v.label}</b>
          <span style="color:#888;">${v.value}</span>
        </div>
      `;
        })
        .join("");

      // 绑定点击事件
      const items = container.querySelectorAll(".variable-item");
      items.forEach((item) => {
        item.onclick = () => {
          const index = item.getAttribute("data-index");
          const variable = props.variableList[index];
          editor.insertContent(`${variable.value}`);
          editor.windowManager.close();
        };
      });
    }
  },
  selector: "#" + tinymceId.value,
  language_url: "/tinymce/langs/zh_CN.js", // 中文语言包
  language: "zh_CN",
  skin_url: "/tinymce/skins/ui/oxide", // 皮肤
  editable_root: props.editable_root, // 可编辑区域
  height: 300,
  max_height: 500,
  min_height: props.minHeight,
  branding: false, // 关闭“Powered by TinyMCE”
  promotion: false, // 隐藏升级提示
  menubar: false, // 菜单栏
  paste_data_images: false, // 禁止粘贴图片
  image_dimensions: true, // 图片宽高属性
  automatic_uploads: true,
  plugins: props.plugins,
  toolbar: props.toolbar,
  convert_urls: false, // 不自动转换图片路径
  link_default_target: "_blank", // 默认新窗口打开链接
  quickbars_insert_toolbar: false, // 禁用光标插入工具栏
  quickbars_selection_toolbar: false, // 禁用选中文本工具栏
  quickbars_image_toolbar:
    "alignleft aligncenter alignright | rotateleft rotateright | imageoptions", // 图片快捷工具栏
  editimage_toolbar:
    "rotateleft rotateright | flipv fliph | editimage imageoptions", // 图片编辑
  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", // 字体大小
  image_caption: true,
  noneditable_class: "mceNonEditable", // 不可编辑区域的 class
  toolbar_mode: "sliding", // 工具栏模式
  content_style:
    "body { font-family:Helvetica,Arial,sans-serif; font-size:16px } p { margin:3px; line-height:24px; } table { border-collapse: collapse !important; width: 100% !important; } th, tr { border: 1px solid #ddd; padding: 8px; text-align: left;} th { background-color: #f2f2f2; font-weight: bold;} img {object-fit: cover; }", // 内容样式
  image_advtab: true, // 开启图片高级设置
  importcss_append: true, // 引入外部 CSS
  paste_webkit_styles: "all", // 保留粘贴样式
  paste_merge_formats: true,
  nonbreaking_force_tab: false,
  paste_auto_cleanup_on_paste: false,
  file_picker_types: "file", // 允许文件上传
  quickbars_selection_toolbar:
    "bold italic | quicklink h2 h3 blockquote quickimage quicktable",
  autoresize_bottom_margin: 20, // 自动高度底部留白
  content_css: "/tinymce/skins/content/default/content.css", // 内容区域样式
  statusbar: false, // 关闭底部状态栏
  placeholder: "请输入内容",
  lineheight_formats: "16px", // 行高

  // 图片上传处理
  images_upload_handler: (blobInfo, success, progress) =>
    new Promise((resolve, reject) => {
      let errorMsg = "上传失败,请联系管理员";
      const formData = new FormData();
      formData.append("file", blobInfo.blob());
      commonUpload(formData)
        .then((res) => {
          if (res.code == "200") {
            resolve(res.url);
            success(res.url);
            return;
          } else {
            reject(errorMsg);
          }
        })
        .catch((error) => {
          // console.log(error);
          reject(errorMsg);
        });
    }),
});

// 监听只读模式变化
watch(
  () => props.readonly,
  (newValue) => {
    nextTick(() => {
      tinymce.activeEditor.mode.set(newValue ? "readonly" : "design");
      const iframeDom = document.querySelector("iframe");
      if (iframeDom) {
        iframeDom.contentWindow.document.body.style.margin = newValue
          ? 0
          : "16px";
      }
    });
  },
  { immediate: true }
);
// 监听内容变化
watch(
  () => content.value,
  (newVal) => {
    nextTick(emits("update:modelValue", newVal));
  }
);

// 监听外部 modelValue 变化
watch(
  () => props.modelValue,
  (newVal) => {
    nextTick(() => {
      if (newVal !== content.value) {
        content.value = newVal;
      }
    });
  }
);

// 初始化编辑器
onMounted(() => {
  tinymce.init({});
});
// 设置值
const setContent = (content) => {
  tinymce.activeEditor.setContent(content);
};
// 获取值
const getContent = () => {
  return tinymce.activeEditor.getContent();
};
defineExpose({ setContent, getContent });
</script>

<style lang="scss" scoped>
.tm-editor {
  width: 100%;
}
</style>
<style lang="scss">
/* 提升编辑器层级,避免被覆盖 */
.tox {
  z-index: 9999 !important;
}
</style>

使用如下:

<TMEditor
  ref="tmEditor"
  v-model="contentTemplate"
/>
posted @ 2025-12-11 09:12  槑孒  阅读(26)  评论(0)    收藏  举报