微前端乾坤vue3项目使用tinymce,通过npm,yarn,pnpm包安装成功,但是引用报错无法使用

问题一:tinymce通过npm安装成功后,无法引用,由于官方做了限制,并且部分功能收费了,所以需要本包放到public目录下(包放到尾部),在index.html的head下面手动引入。使用自托管包时需要明确声明 GPL 许可,避免被禁用:license_key: "gpl",也不行。

问题二: 乾坤子项目在index.html的head引入引入后,依然找不到,需要在主机座的ndex.html再次手动引入。

1.封装的文件放到components下面:YEditor文件夹-index.vue

javascript 复制代码
<script setup>
import { uploadFileApi } from "@/api/file";
import { ElMessage } from "element-plus";
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
import initOptions from "./config/options";
import initPlugins from "./config/plugins";
import initToolbar from "./config/toolbar";
import { getFileAccessHttpUrl } from "@/api/manage";
const uploadRef = ref();
const emit = defineEmits(["update:modelValue"]);
const props = defineProps({
    /**
     * 文本内容
     */
    modelValue: {
        type: String,
        default: "",
    },
    /**
     * 高度
     */
    height: {
        type: Number,
        default: 600,
    },
    /**
     * 插件
     */
    plugins: {
        type: [Array, String],
        default: () => [...initPlugins],
    },
    /**
     * 工具栏
     */
    toolbar: {
        type: [Array, String],
        default: () => [...initToolbar],
    },
    /**
     * 选项
     */
    options: {
        type: Object,
        default: () => ({}),
    },
    // 允许配置 TinyMCE 资源路径
    baseURL: {
        type: String,
        default: "tinymce",
    },
    suffix: {
        type: String,
        default: ".min",
    },
});

// 使用 crypto.randomUUID 生成唯一 ID(若环境不支持,可降级为 nanoid 或其他方案)
const editorId = `winter_${crypto?.randomUUID?.().replace(/-/g, "") ?? Math.ceil(Math.random() * 1000000)}`;

let hasInit = false;

const init = () => {
    // 如果已存在实例,先销毁,避免重复初始化
    const existing = window.tinymce?.get(editorId);
    if (existing) existing.destroy();

    // 使用包内模块,不再依赖 baseURL/suffix 的静态资源路径

    // 包装用户可能传入的回调,避免覆盖内部逻辑
    const userInitCb = props.options?.init_instance_callback;
    const userImgHandler = props.options?.images_upload_handler;

    window.tinymce.init({
        // 默认 → 用户 → 组件强制项(选择器/高度等)
        ...initOptions,
        ...props.options,
        // 使用自托管包时需要明确声明 GPL 许可,避免被禁用
        license_key: "gpl",
        // 使用安装的语言包
        language: "zh-Hans",
        toolbar: props.toolbar,
        plugins: props.plugins,
        selector: `#${editorId}`,
        min_height: props.options?.min_height ?? props.height,
        max_height: props.options?.max_height ?? props.height,
        // 不需要 external_plugins,当通过 ESM 方式引入时交由打包器处理
        async images_upload_handler(blobInfo) {
            if (typeof userImgHandler === "function") {
                return userImgHandler.call(this, blobInfo);
            }
            const file = new File([blobInfo.blob()], "cut.jpg");
            const formData = new FormData();
            formData.append("file", file);
            const res = await uploadFileApi(formData);
            return res.result.fileUrl;
        },
        init_instance_callback(editor) {
            if (!hasInit) {
                editor.setContent(props.modelValue || "");
            }
            hasInit = true;
            editor.on("NodeChange Change KeyUp SetContent", () => {
                emit("update:modelValue", editor.getContent());
            });
        },
        // 富文本默认交互
        file_picker_callback: (cb, _value, meta) => {
            if (meta.filetype === "media") {
                const input = document.createElement("input");
                input.type = "file";
                input.accept = ".mp4,.avi,.mov,.wmv,.flv,.webm";
                input.style.display = "none";
                document.body.appendChild(input);

                input.onchange = async (e) => {
                    const file = e.target.files[0];
                    const validTypes = ["video/mp4", "video/avi", "video/mov", "video/wmv", "video/flv", "video/webm"];
                    if (!file || !validTypes.includes(file.type) || file.size > 50 * 1024 * 1024) {
                        ElMessage({
                            message: !file ? "请选择文件" : !validTypes.includes(file.type) ? "请选择有效的视频格式 (mp4, avi, mov, wmv, flv, webm)" : "视频文件不能大于50M",
                            type: "warning",
                            placement: "top",
                        });
                        document.body.removeChild(input);
                        return;
                    }
                    try {
                        ElMessage({ message: "视频上传中,请稍候...", type: "info", placement: "top" });
                        const formData = new FormData();
                        formData.append("file", file);
                        const res = await uploadFileApi(formData);
                        if (res?.result?.fileUrl) {
                            cb(res.result.fileUrl, {
                                source: res.result.fileUrl,
                                poster: "",
                                width: "100%",
                                height: "auto",
                            });
                            ElMessage({ message: "视频上传成功", type: "success", placement: "top" });
                        } else throw new Error();
                    } catch {
                        ElMessage({ message: "视频上传失败,请重试", type: "error", placement: "top" });
                    }
                    document.body.removeChild(input);
                };

                input.click();
            }
        },
        // 富文本自定义交互
        setup(editor) {
            editor.ui.registry.addButton("imgbtn", {
                text: "",
                icon: "image",
                tooltip: "插入图片",
                onAction: () => {
                    uploadRef.value.click();
                },
            });
        },
    });
};

const destroy = () => {
    const tinymce = window.tinymce?.get(editorId);
    if (tinymce) {
        tinymce.destroy();
    }
};

const setContent = (value) => {
    const editor = window.tinymce?.get(editorId);
    if (editor) {
        editor.setContent(value);
    }
};

const getContent = () => {
    const editor = window.tinymce?.get(editorId);
    return editor ? editor.getContent() : "";
};

const onFileChange = async (e) => {
    const file = e.target.files[0];
    if (!file) return;

    const validTypes = ["image/jpeg", "image/png", "image/bmp", "image/svg+xml"];
    if (!validTypes.includes(file.type)) {
        ElMessage({
            message: "请选择有效的图片格式",
            type: "warning",
            placement: "top",
        });
        return;
    }

    const size = file.size;
    if (size > 2 * 1024 * 1024) {
        ElMessage({
            message: "图片资源不能大于2M",
            type: "warning",
            placement: "top",
        });
        return;
    }

    const formData = new FormData();
    formData.append("file", file);
    // const img = await fileToBase64(file)
    // window.tinymce.get(id.value).insertContent(`<img class="wscnph" src="${img}" >`)
    const res = await uploadFileApi(formData);
    window.tinymce.get(editorId).insertContent(`<img style="max-width: 100%" class="wscnph" src="${getFileAccessHttpUrl(res?.result?.fileUrl)}" >`);
};

// 使用更安全的 hash 比较方式(可选)
const getHash = (str) => {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
        const char = str.charCodeAt(i);
        hash = (hash << 5) - hash + char;
        hash |= 0; // Convert to 32bit integer
    }
    return hash;
};

watch(
    () => props.modelValue,
    (val) => {
        if (!hasInit) return;
        if (typeof val !== "string") val = "";

        const valHash = getHash(val);
        const contentHash = getHash(getContent());
        if (valHash !== contentHash) {
            nextTick(() => {
                setContent(val || "");
            });
        }
    },
    { flush: "post" },
);

onMounted(() => {
    init();
});

onUnmounted(() => {
    destroy();
});
</script>

<template>
    <div class="editor-container">
        <textarea :id="editorId" class="tinymce-textarea" />
        <input ref="uploadRef" type="file" hidden accept=".jpg,.jpeg,.png,.bmp,.svg" @change="onFileChange" />
    </div>
</template>

<style scoped>
.editor-container {
    width: 100%;
}
</style>

2.YEditor文件夹->config->options.ts

javascript 复制代码
import plugins from "./plugins";
import toolbar from "./toolbar";
export default {
    language: "zh-Hans",
    toolbar,
    plugins,
    toolbar_sticky: true,
    statusbar: false, // 隐藏底部状态栏
    menubar: false,
    min_height: 600,
    max_height: 600,
    body_class: "panel-body ",
    object_resizing: false,
    content_style: "body { font-family:Helvetica,Arial,sans-serif; font-size:16px }",
    end_container_on_empty_block: true,
    powerpaste_word_import: "clean",
    // document_base_url: ,
    code_dialog_height: 450,
    code_dialog_width: 1000,
    // imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],
    default_link_target: "_blank",
    link_title: false,
    nonbreaking_force_tab: true,
    relative_urls: false,
    convert_urls: false,
    fontsize_formats: "8px 10px 12px 14px 16px 18px 20px 22px 24px 26px 28px 30px 32px 34px 36px 38px 40px 42px 44px 46px 48px 50px 52px 54px 56px 58px 60px", // 可选的字体大小列表
};

3.YEditor文件夹->config->plugins.ts

javascript 复制代码
import plugins from "./plugins";
import toolbar from "./toolbar";
export default {
    language: "zh-Hans",
    toolbar,
    plugins,
    toolbar_sticky: true,
    statusbar: false, // 隐藏底部状态栏
    menubar: false,
    min_height: 600,
    max_height: 600,
    body_class: "panel-body ",
    object_resizing: false,
    content_style: "body { font-family:Helvetica,Arial,sans-serif; font-size:16px }",
    end_container_on_empty_block: true,
    powerpaste_word_import: "clean",
    // document_base_url: ,
    code_dialog_height: 450,
    code_dialog_width: 1000,
    // imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],
    default_link_target: "_blank",
    link_title: false,
    nonbreaking_force_tab: true,
    relative_urls: false,
    convert_urls: false,
    fontsize_formats: "8px 10px 12px 14px 16px 18px 20px 22px 24px 26px 28px 30px 32px 34px 36px 38px 40px 42px 44px 46px 48px 50px 52px 54px 56px 58px 60px", // 可选的字体大小列表
};

4.YEditor文件夹->config->toolbar.ts

javascript 复制代码
// toolbar.js
const toolbar = [
    "undo redo blocks fontSize bold italic underline strikethrough forecolor backcolor link hr   imgbtn table blockquote lineHeight alignleft aligncenter alignright alignjustify " +
        "bullist numlist outdent indent searchreplace fullscreen",
];

export default toolbar;

页面引用:

javascript 复制代码
<template>
  <el-card id="platformConfig" class="card" :body-style="{ padding: '20px' }">
    <el-form ref="formRef" :model="model" :rules="rules" label-width="120px">
      <el-form-item label="底部介绍:" prop="footIntroduction">
        <y-editor
          v-model="model.footIntroduction"
          :height="400"
        />
      </el-form-item>
      <div style="display: flex; justify-content: center">
        <el-button type="primary" :loading="loading" @click="handle">保存</el-button>
      </div>
    </el-form>
  </el-card>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useUserStore } from '@/store/modules/user'
import { ElMessage } from 'element-plus'
import YEditor from '@/components/YEditor/index.vue'
import { update, queryLastLogo } from '@/api/platform-config/index'

defineOptions({
  name: 'Platformintroduction'
})

// 响应式数据
const formRef = ref(null)
const loading = ref(false)

const model = reactive({
  footIntroduction: '',
})



// 自定义验证器
const validateRichText = (rule, value, callback) => {
  if (value) {
    // 去除HTML标签
    const textContent = value.replace(/<[^>]+>/g, '').trim()
    // 检查是否只包含空格或没有内容
    if (textContent.length === 0) {
      callback(new Error('内容不能为空'))
    } else {
      // 检查是否包含汉字
      const hasChineseChar = /[一-龥]/.test(textContent)
      if (!hasChineseChar) {
        callback(new Error('内容不能为空'))
      } else {
        callback()
      }
    }
  } else {
    callback()
  }
}

// 表单验证规则
const rules = {
  footIntroduction: [
    { required: true, message: '底部介绍不能为空', trigger: 'change' },
    { validator: validateRichText, trigger: 'change' }
  ]
}


// 暴露方法给父组件使用
defineExpose({
  query
})
</script>
相关推荐
技术钱13 分钟前
vue3基于 Vxe Table 实现可拖拽分组 + 动态求和的高级表格
javascript·vue.js
还是大剑师兰特14 分钟前
Vue3 + Element Plus 日期选择器:开始 / 结束时间,结束时间不超过今天
前端·javascript·vue.js
不会写DN15 分钟前
Js常用数组处理
开发语言·javascript·ecmascript
还是大剑师兰特16 分钟前
数组中有两个数据,将其变成字符串
开发语言·javascript·vue.js
Saga Two17 分钟前
Vue实现核心原理
前端·javascript·vue.js
技术钱17 分钟前
vue3实现时间根据系统时区转换对应的时间
javascript·vue.js
殷忆枫24 分钟前
基于STM32的ML307R连接Onenet平台
服务器·前端·javascript
Java 码农25 分钟前
vue cli 环境搭建
前端·javascript·vue.js
酉鬼女又兒30 分钟前
零基础入门前端JavaScript Object 对象完全指南:从基础到进阶(可用于备赛蓝桥杯Web应用开发赛道)
开发语言·前端·javascript·职场和发展·蓝桥杯
RPGMZ33 分钟前
RPGMakerMZ游戏引擎 地图角色顶部显示称号
javascript·游戏引擎·rpgmz·rpgmakermz