微前端乾坤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>
相关推荐
Luna-player4 小时前
npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本,解决方法
前端·npm·node.js
悢七4 小时前
windows npm打包无问题,但linux npm打包后部分样式缺失
linux·前端·npm
Mountain086 小时前
解决 Node.js 启动报错:digital envelope routines 错误全记录
javascript·npm·node.js
老程序员刘飞6 小时前
node.js 和npm 搭建项目基本流程
前端·npm·node.js
wangbing11257 小时前
开发指南139-VUE里的高级糖块
前端·javascript·vue.js
半桶水专家7 小时前
Vue 3 动态组件详解
前端·javascript·vue.js
我有一棵树7 小时前
避免 JS 报错阻塞 Vue 组件渲染:以 window.jsbridge 和 el-tooltip 为例
前端·javascript·vue.js
没有鸡汤吃不下饭8 小时前
解决前端项目中大数据复杂列表场景的完美方案
前端·javascript·vue.js
Yolo566Q8 小时前
空间数据采集与管理丨在 ArcGIS Pro 中利用模型构建器批处理多维数据
arcgis