tinymce本地化部署方案

前提说明

  • vue3
  • tinymce 7.2.1
  • tinymce-vue 6.0.1

准备工作

安装tinymce和tinymce-vue

npm install tinymce@7.2.1 tinymce-vue@6.0.1 --save

复制代码到public

为什么不直接引用node_modules的文件 Tiny does not recommend bundling tinymce and tinymce-vue with a module loader. Bundling these packages can be complex and error prone.

如果一定想用node_moudule 文件参考上面的链接说明即可。可以忽略复制文件这一步。

复制文件只保留min.js相关即可。可以根据自己的需求保留哪些文件即可。也可以存放在自己的cdn,使用远程链接。

public

新建组件

TinyMceEditor.vue

<script setup lang="ts">
import { ref, defineProps, computed } from 'vue'
import Editor from '@tinymce/tinymce-vue'
import type { RawEditorOptions } from 'tinymce'
import { getUploadParams, fileUpload } from '@/components/KzUpload/src/api'

defineOptions({
  name: 'BaseEditor'
})

interface ImageConfig {
  maxSize: number
  allowType: string[]
}

const props = defineProps<{
  /** 配置 */
  config: RawEditorOptions
  /** 值 */
  modelValue: string
  /** 图片限制 */
  imageConfig?: ImageConfig
}>()

const editorRef = ref()

// 图片上传自定义逻辑
/** 文档:https://www.tiny.cloud/docs/tinymce/latest/upload-images/ */
type UploadFn = RawEditorOptions['images_upload_handler']
const imageUploadHandler: UploadFn = async function (blobInfo, _) {
  return new Promise(async (resolve, reject) => {
    const { maxSize = 10 * 1024 * 1024, allowType = ['image/png', 'image/jpg', 'image/jpeg'] } =
      props.imageConfig || {}
    if (blobInfo.blob().size > maxSize) {
      const msg = `图片太大了. 最大支持 ${maxSize / 1024 / 1024}MB`
      console.error(msg, blobInfo.blob().size)
      reject({ message: msg, remove: true })
      return
    }
    if (!allowType.includes(blobInfo.blob().type)) {
      const msg = `图片只支持${allowType.map((item) => item.replace('image/', '').split('、'))}格式`
      reject({
        message: msg,
        remove: true
      })
      console.error(msg, blobInfo.blob().type)
      return
    }
    // 自己上传图片的逻辑开始
    let baseUrl = import.meta.env.VITE_BASE_URL
    const { data } = await getUploadParams({
      baseUrl,
      appCode: 'App',
      originalFilename: blobInfo.filename()
    })
    const res = await fileUpload(blobInfo.blob(), data, [], () => {})
    // 自己上传图片的逻辑结束
    resolve(res.data.downloadLocation)
    return res.data.downloadLocation
  })
}

// 双向绑定v-model
const modelContent = defineModel({ default: '' })

const defaultConfig = ref<RawEditorOptions>({
  // 如果不设置license_key为gpl,console会一直提示一个模式问题
  license_key: 'gpl',
  // 编辑器的高度
  height: 300,
  // 移除tinymce右上角升级提示
  promotion: false,
  skin: 'QZD',
  // 移除tinymce右下角品牌提示
  branding: false,
  // 设置语言包路径
  language_url: `/libs/tinymce/langs/zh_CN.js`,
  // 设置语言
  language: 'zh_CN',
  menubar: false,
  // 配置插件列表
  plugins:
    'preview importcss searchreplace autolink save directionality code visualblocks visualchars fullscreen image link table charmap pagebreak nonbreaking anchor insertdatetime advlist lists wordcount help charmap quickbars emoticons accordion',
  // 配置工具栏
  toolbar: [
    'undo redo | blocks fontfamily fontsize | bold italic underline strikethrough | lineheight align numlist bullist | link image | table | forecolor backcolor removeformat | charmap emoticons | code fullscreen preview'
  ],
  setup: (editor) => {
    editor.on('PreInit', function () {
      editor.editorUpload.addFilter(function (image) {
        const maxSize = 10 * 1024
        const imageSize = image.width * image.height
        return imageSize < maxSize
      })
    })
  },
  // 内容默认样式
  content_style: '',
  // 配置图片上传功能
  images_upload_handler: imageUploadHandler
})

// 初始化配置
const initConfig = computed(() => {
  return {
    ...defaultConfig.value,
    ...props.config,
    setup: (editor: any) => {
      props.config?.setup?.(editor)
      defaultConfig.value?.setup?.(editor)
    }
  }
})

defineExpose({
  editorRef
})
</script>

<template>
  <Editor
    ref="editorRef"
    :init="initConfig"
    :tinymceScriptSrc="/libs/tinymce/tinymce.min.js"
    v-model="modelContent"
  />
</template>

<style>
.tox-tinymce {
  border: none !important;
}

.tox-shadowhost.tox-fullscreen,
.tox.tox-tinymce.tox-fullscreen {
  z-index: 2004 !important;
}
</style>

常见问题

如何做语言本地化?

上方的组件已经做了本地化操作。主要配置就是lang字段。可以去下载文档,language-packages, 如果需要自定义修改下载的文件即可。

如何添加自定义字体?

// TinyMceEditor.vue 文件中
const defaultConfig = {
  ...otherConfig,
    font_size_formats:
      '初号=56px 小初=48px 一号=34px 小一=32px 二号=29px 小二=24px 三号=21px 小三=20px 四号=18px 小四=16px 五号=14px 小五=12px 六号=10px',
    content_style: `@import url('your-cdn-domain/front-cdn/fonts/style/tinymce.css?v1');body { font-family:  SimHei; font-size: 14px; }`,
    font_family_formats:
      '黑体=SimHei;宋体=SimSun;微软雅黑=Microsoft YaHei,Helvetica,sans;Arial=Arial,Helvetica,sans-serif;Times New Roman=Times New Roman,Times;',
}
/* your-cdn-domain/front-cdn/fonts/style/tinymce.css?v1 */
@font-face {
    font-family: 'SimSun';
    src: url('../songti/Songti.ttc');
}

@font-face {
    font-family: 'Microsoft Yahei';
    src: url('../yahei/Microsoft-YaHei-Regular.ttc');
    font-weight: 400;
}

@font-face {
    font-family: 'Microsoft Yahei';
    src: url('../yahei/Microsoft-YaHei-Bold.ttc');
    font-weight: 700;
}

@font-face {
    font-family: 'SimHei';
    src: url('../heiti/STHeiti Light.ttc');
}
<!-- TinyMceEditor.vue 文件中 编辑器页面也要导入用于页面展示 -->
<style lang="scss">
@import url('your-cdn-domain/file-resource/dev/front-cdn/fonts/style/tinymce.css?v1');
</style>

如何添加自定义icon?

tinymce 的多套皮肤,是需要付费的,如果觉得不好看就自己改tinymce/icons/default/icons.min.js文件

如果只有个别自己新增的icon,可以自己使用api 添加,(只支持svg文件。)

editor.ui.registry.addIcon('triangleUp', '<svg height="24" width="24"><path d="M12 0 L24 24 L0 24 Z" /></svg>');

a 标签的href属性被编辑器自动修改了

具体原因尚未定位到,目前推测是link插件内部做了某些格式化导致的,具体表现为:

<a href="https://www.baidu.com?id=1&search=2&tag=3">https://www.baidu.com?id=1&search=2</a>

当以上内容通过insertContent方法插入到编辑器中时,再编辑器中回车操作时,当前内容会变成

<a href="https://www.baidu.com?id=1&search=2">https://www.baidu.com?id=1&search=2</a>

编辑器会自动将href更新为https://www.baidu.com?id=1&search=2, 也就是保持跟a标签内容一致,从而导致属性丢失。

最终解决方案是,将a标签的内容变成更href毫无关联的内容,就不会替换。

<a href="https://www.baidu.com?id=1&search=2&tag=3">百度搜索</a>

后续可以跟进此issue After insertContent 'a' tag, the href parameter is lost after pressing enter.