news 2026/4/2 19:30:38

Vue3 + TypeScript 封装 UEditor 富文本编辑器:一站式解决图片上传与格式控制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue3 + TypeScript 封装 UEditor 富文本编辑器:一站式解决图片上传与格式控制

UEditor(百度富文本编辑器)作为经典的富文本解决方案,在企业级项目中仍有广泛应用,但原生 UEditor 与 Vue3 + TypeScript 结合时存在配置繁琐、图片上传逻辑不统一、样式控制难等问题。本文基于你提供的代码,详细讲解如何封装一个高扩展性、易维护的 Vue3 + TS 版 UEditor 组件,重点解决单 / 多图上传统一处理、图片尺寸控制、内容双向绑定、生命周期管理等核心痛点。

组件核心功能概览

该组件基于 Vue3<script setup lang="ts">语法封装,兼容 UEditor 原生功能的同时,做了大量优化,核心功能包括:

  1. 基础富文本编辑(文本格式化、表格、列表等原生功能)
  2. 单图 / 多图上传统一处理(自定义上传接口、文件大小 / 格式限制)
  3. 图片尺寸精细化控制(禁用自动缩放、自定义固定宽度、移除干扰样式)
  4. 双向数据绑定(v-model 适配 Vue3 语法,防抖优化输入体验)
  5. 完善的事件监听(上传成功 / 失败、内容变化、失焦等)
  6. 生命周期自动管理(组件卸载时销毁实例,避免内存泄漏)
  7. TypeScript 类型兼容(声明全局变量,避免类型报错)

前置准备

1. 环境依赖

  • Vue3 + TypeScript 项目(Vite/CLI 均可)
  • UEditor 静态资源包(放置在/public/UEditor目录,包含ueditor.all.jsdialogs等文件夹)
  • 后端上传接口(需兼容 UEditor 上传参数规范,默认字段名upfile

2. 资源引入

在项目入口文件(如main.ts)或 HTML 中引入 UEditor 核心脚本(也可通过组件内动态加载):

<!-- index.html 中引入 --> <script src="/public/UEditor/ueditor.config.js"></script> <script src="/public/UEditor/ueditor.all.min.js"></script>

3. 接口配置

确保代码中dataurl指向正确的后端域名,且multiImageUploadUrl配置的上传接口符合 UEditor 响应规范:

// 上传成功响应示例 { "state": "SUCCESS", "url": "/upload/image/20260203/123456.png", "title": "123456.png", "original": "test.png" } // 多图上传成功响应(数组格式) [ {"state":"SUCCESS","url":"/upload/1.png"}, {"state":"SUCCESS","url":"/upload/2.png"} ]

功能详解与使用方法

1. 组件基础使用(父组件引入)

这是最核心的使用方式,通过v-model绑定编辑器内容,监听上传事件:

<template> <div class="editor-wrapper" style="width: 100%;"> <!-- UEditor 富文本组件 --> <UEditor v-model="content" :img-fixed-width="80" :img-resizable="false" :multi-image-max-count="9" :multi-image-max-size="5 * 1024" @multi-image-upload-success="handleUploadSuccess" @multi-image-upload-error="handleUploadError" @blur="handleEditorBlur" /> </div> </template> <script setup lang="ts"> import { ref } from 'vue'; import UEditor from './components/UEditor.vue'; // 绑定编辑器内容 const content = ref<string>(''); // 多图上传成功回调 const handleUploadSuccess = (images: any[]) => { console.log('图片上传成功:', images); // images 数组包含每张图片的 url/title/original 等信息 }; // 上传失败回调 const handleUploadError = (error: { message: string; data?: any }) => { console.error('图片上传失败:', error.message); // 可在此处提示用户(如 ElMessage.error(error.message)) }; // 编辑器失焦回调 const handleEditorBlur = () => { console.log('编辑器失焦,当前内容:', content.value); }; </script>

2. 核心 Props 配置说明

组件提供了丰富的可配置参数,满足不同业务场景需求:

参数名类型默认值说明
modelValueString''双向绑定的编辑器内容(v-model 绑定)
imgResizableBooleanfalse是否允许图片缩放(禁用后无法拖动调整尺寸)
imgFixedWidthNumber/String100图片固定宽度(数字 = 百分比,字符串支持 px/%,如 '500px')
multiImageMaxCountNumber9多图上传最大数量
multiImageMaxSizeNumber5*1024单张图片最大尺寸(KB),默认 5MB
multiImageAllowFilesArray['.png','.jpg','.jpeg','.gif','.bmp']允许上传的图片格式
multiImageUploadUrlString'/pc/common/ueditor'图片上传接口地址
multiImageFieldNameString'upfile'上传文件的表单字段名

3. 图片上传功能(单图 / 多图统一处理)

组件重写了 UEditor 原生的图片插入逻辑,解决了单图 / 多图上传行为不一致的问题:

3.1 单图上传使用
  1. 点击编辑器工具栏的「图片」按钮 → 选择「本地上传」
  2. 选择单张图片,点击「上传」,组件会自动:
    • 校验图片格式 / 大小(超出限制触发multi-image-upload-error事件)
    • 调用配置的上传接口,上传成功后插入图片到光标位置
    • 移除图片内联 style 属性,标记data-single="true"便于识别
    • 触发multi-image-upload-success事件,返回图片信息
3.2 多图上传使用
  1. 点击编辑器工具栏的「图片」按钮 → 选择「多图上传」
  2. 选择多张图片(最多multiImageMaxCount张),点击「开始上传」
  3. 组件会:
    • 批量校验图片,过滤不符合格式 / 大小的图片
    • 批量上传成功后,将所有图片插入到光标位置(图片间自动加空格分隔)
    • 标记图片data-multi="true",统一控制样式
    • 若部分图片上传失败,会触发multi-image-upload-error事件,返回失败详情

4. 图片样式控制(核心优化点)

组件通过「移除内联样式 + CSS 变量 + MutationObserver 监听」实现图片样式统一:

4.1 固定图片宽度

组件会自动将img-fixed-width转换为 CSS 变量--img-fixed-width,你只需在全局样式中添加:

/* 全局样式(非 scoped) */ .ueditor-content img { width: var(--img-fixed-width) !important; height: auto !important; // 宽高比自适应 border: none !important; float: none !important; // 禁用浮动 }
  • img-fixed-width="80",则图片宽度为 80%;
  • img-fixed-width="500px",则图片宽度为 500px;
  • 禁用图片自动缩放(imgResizable=false)后,用户无法拖动调整图片尺寸。
4.2 自动修正图片样式

组件通过MutationObserver监听编辑器内图片插入行为,确保:

  • 移除 UEditor 自动添加的style属性(避免干扰自定义样式);
  • 为所有图片标记data-single/data-multi属性,便于区分来源;
  • 多次延迟修正(100ms/300ms/800ms),覆盖 UEditor 后续的样式修改逻辑。

5. 内容双向绑定(防抖优化)

组件对contentChange事件做了防抖处理,避免用户输入时频繁触发父组件更新:

  • 用户打字 / 粘贴时,200ms 无操作后才更新v-model
  • 非用户输入(如代码调用setContent)时,立即更新;
  • 监听父组件modelValue变化,同步更新编辑器内容,并保留光标位置。

6. 高级用法:调用组件暴露的方法

组件通过defineExpose暴露了核心实例和方法,支持外部手动控制:

<template> <UEditor ref="editorRef" v-model="content" /> <button @click="fixImageSize">强制修正图片尺寸</button> <button @click="getEditorContent">获取编辑器内容</button> </template> <script setup lang="ts"> import { ref } from 'vue'; import UEditor from './components/UEditor.vue'; const editorRef = ref<any>(null); const content = ref(''); // 强制修正所有图片尺寸(如手动修改内容后调用) const fixImageSize = () => { editorRef.value.forceImageFixedSize(); }; // 直接获取 UEditor 实例,调用原生方法 const getEditorContent = () => { // 获取纯文本内容(原生方法) const plainText = editorRef.value.ueditorInstance.getContentTxt(); console.log('纯文本内容:', plainText); // 禁用编辑器(原生方法) // editorRef.value.ueditorInstance.setDisabled(); }; </script>

7. 生命周期与资源清理

组件内置了完善的生命周期管理,无需手动处理:

  • onMounted:等待 DOM 渲染完成后初始化 UEditor,避免容器未挂载;
  • onBeforeUnmount:销毁 UEditor 实例、断开 MutationObserver 监听、清空引用,避免内存泄漏;
  • 监听error事件,捕获初始化 / 上传过程中的错误,便于排查问题。

关键优化点解析

1. 重写图片插入方法

原生 UEditor 的insertImage方法对多图支持不佳,组件重写后:

  • 数组格式的 URL 视为多图,逐个插入并添加分隔空格;
  • 单图插入后保留光标在图片后方,提升编辑体验;
  • 为图片添加自定义属性(data-single/data-multi),便于后续样式控制。

2. 防抖的内容更新逻辑

通过isUserTyping标记用户输入状态,结合 200ms 防抖:

  • 避免用户每输入一个字符就触发一次v-model更新,提升性能;
  • 失焦后立即重置输入状态,确保内容最终同步。

3. 多重图片尺寸修正

通过「初始化修正 + 插入后延迟修正 + MutationObserver 监听修正」:

  • 覆盖 UEditor 原生的样式自动添加逻辑;
  • 确保所有图片(包括初始化时的已有图片、上传后的新图片)都符合样式规范。

总结

  • 该组件基于 Vue3 + TS 封装,兼容 UEditor 原生功能,解决了图片上传、样式控制、双向绑定等核心痛点;
  • 使用时只需通过 Props 配置上传参数、图片尺寸,通过事件监听上传状态,开箱即用;
  • 内置完善的生命周期管理和异常处理,保证了组件的稳定性和可维护性;
  • 支持高级扩展(如调用原生 UEditor 方法、自定义图片样式),满足复杂业务场景需求。

源码

<template> <!-- 编辑器容器,通过ref获取DOM元素供UEditor挂载 --> <div ref="editor"></div> </template> <script setup lang="ts"> // 导入Vue核心API:响应式引用、生命周期钩子、监听、DOM更新后回调、计算属性 import { ref, onMounted, onBeforeUnmount, watch, nextTick, computed } from 'vue'; //导入域名 import { dataurl } from '/@/api/url/index'; // 声明UEditor全局变量(避免TS类型报错,实际由外部脚本引入) declare const UE: any; // 确保window上存在loader对象(用于动态加载UEditor相关资源,保持原有逻辑兼容) if (!window.loader) { window.loader = { // loader.load方法:加载指定URL资源,成功后执行回调,失败执行错误回调 load: (urls: string[], callback: () => void, errorCallback: (err: any) => void) => { const filteredUrls = urls; // 筛选后的资源URL数组(此处未做筛选,保留原逻辑) // 若没有需要加载的资源,直接执行成功回调 if (filteredUrls.length === 0) { callback(); return; } // 单个脚本加载函数:创建script标签并返回Promise const loadScript = (url: string) => { return new Promise((resolve, reject) => { const script = document.createElement('script'); // 创建script元素 script.src = url; // 设置脚本URL script.onload = resolve; // 加载成功触发resolve script.onerror = (err) => { // 加载失败触发reject并打印警告 console.warn(`资源加载失败: ${dataurl}`, err); reject(err); }; document.head.appendChild(script); // 将script标签添加到页面头部 }); }; // 并行加载所有资源,全部成功后执行回调,任意失败触发错误回调 Promise.all(filteredUrls.map(loadScript)).then(callback).catch(errorCallback); } }; } // 定义组件Props(外部传入的配置项和绑定值) const props = defineProps({ // 双向绑定的编辑器内容(默认空字符串) modelValue: { type: String, default: '' }, // 图片是否可缩放(默认不可缩放) imgResizable: { type: Boolean, default: false }, // 图片固定宽度(支持数字/字符串,默认100,数字将转为百分比) imgFixedWidth: { type: [Number, String], default: 100 }, // 多图上传最大数量(默认9张) multiImageMaxCount: { type: Number, default: 9 }, // 单张图片最大尺寸(默认5MB,单位KB) multiImageMaxSize: { type: Number, default: 5 * 1024 }, // 允许上传的图片格式(默认常见图片格式数组) multiImageAllowFiles: { type: Array, default: () => ['.png', '.jpg', '.jpeg', '.gif', '.bmp'] }, // 多图上传接口地址(默认后端UEditor上传接口) multiImageUploadUrl: { type: String, default: '图片上传接口' }, // 上传文件的字段名(默认upfile,与后端接口约定) multiImageFieldName: { type: String, default: 'upfile' }, }); // 定义组件事件(向父组件传递状态和数据) const emit = defineEmits([ 'update:modelValue', // 内容更新事件(用于v-model双向绑定) 'blur', // 编辑器失焦事件 'multi-image-upload-success', // 多图上传成功事件 'multi-image-upload-error' // 多图上传失败事件 ]); // 编辑器容器DOM引用(用于挂载UEditor实例) const editor = ref<HTMLElement | null>(null); // UEditor实例对象(存储初始化后的编辑器实例) let ueditorInstance: any = null; // 图片尺寸修正锁(防止重复执行修正逻辑) let isFixingImages = false; // 图片插入监听者(用于监听编辑器内图片插入操作) let imageInsertObserver: MutationObserver | null = null; // 内容更新锁(防止modelValue监听与编辑器内部更新冲突) let isContentUpdating = false; // 计算属性:处理图片固定宽度(统一格式,数字转为百分比,字符串直接使用) const fixedWidth = computed(() => { if (typeof props.imgFixedWidth === 'number') { return `${props.imgFixedWidth}%`; // 数字类型默认按百分比处理(如100 → "100%") } // 字符串类型直接返回(支持px/%等自定义单位,如"500px"、"80%") return props.imgFixedWidth as string; }); // 动态更新CSS变量(让全局样式能获取到最新的图片宽度配置) const updateCssVariable = () => { // 向根元素添加--img-fixed-width变量,供全局样式使用 document.documentElement.style.setProperty('--img-fixed-width', fixedWidth.value); }; // 生命周期钩子:组件挂载完成后执行 onMounted(() => { updateCssVariable(); // 初始化CSS变量(设置初始图片宽度) nextTick(() => { // 等待DOM更新完成后初始化UEditor(确保editor容器已渲染) initUEditor(); }); }); // 生命周期钩子:组件卸载前执行(清理资源) onBeforeUnmount(() => { destroyUEditor(); // 销毁UEditor实例 if (imageInsertObserver) { imageInsertObserver.disconnect(); // 断开MutationObserver监听 } }); // 初始化UEditor编辑器 const initUEditor = () => { // 校验依赖:编辑器容器不存在或UEditor未加载则终止 if (!editor.value || !UE) return; // 配置UEditor全局参数(影响所有编辑器实例) if (UE.config) { UE.config.UEDITOR_HOME_URL = '/public/UEditor'; // UEditor资源根路径(需确保对应目录存在) UE.config.imageAutoSize = false; // 禁用图片自动调整尺寸 UE.config.imageScale = false; // 禁用图片缩放 UE.config.retainImageSize = false; // 不保留图片原始尺寸 UE.config.initialFrameHeight = 200; // 编辑器初始高度(200px) // 图片管理(多图浏览)相关配置 UE.config.imageManagerActionName = 'listimage'; // 图片管理接口动作名 UE.config.imageManagerListPath = '/upload/image/'; // 图片存储路径 UE.config.imageManagerUrlPrefix = ''; // 图片URL前缀(为空则使用相对路径) UE.config.imageManagerInsertAlign = 'none'; // 插入图片时不设置对齐方式 UE.config.imageManagerAllowFiles = props.multiImageAllowFiles; // 图片管理允许的文件格式 UE.config.imageManagerPageSize = 30; // 图片管理分页大小(每页30张) // 核心配置:统一单图/多图上传参数 UE.config.imageMultiUpload = true; // 启用多图上传 UE.config.imageActionName = 'uploadimage'; // 图片上传接口动作名 UE.config.imageFieldName = props.multiImageFieldName; // 上传文件字段名 UE.config.imageMaxSize = props.multiImageMaxSize; // 单张图片最大尺寸 UE.config.imageAllowFiles = props.multiImageAllowFiles; // 允许上传的图片格式 UE.config.imageCompressEnable = true; // 启用图片压缩 UE.config.imageCompressBorder = 1200; // 图片压缩最大边长(超过则压缩) UE.config.imageCompressQuality = 0.8; // 图片压缩质量(0.8为80%) UE.config.imageInsertAlign = 'none'; // 插入图片时不设置对齐方式 UE.config.imageUrlPrefix = ''; // 图片URL前缀(为空则使用相对路径) // 关键配置:多图上传弹窗路径(指定UEditor内置弹窗页面) UE.config.imagePopupUrl = `${UE.config.UEDITOR_HOME_URL}/dialogs/image/image.html`; UE.config.imageManagerPopupUrl = `${UE.config.UEDITOR_HOME_URL}/dialogs/image/imageManager.html`; // 上传接口地址(统一使用多图上传接口,避免重复配置) UE.config.serverUrl = props.multiImageUploadUrl; // 关键修复:禁用UEditor默认的图片自动尺寸和浮动功能 UE.config.imageEnableAutoSize = false; UE.config.imageEnableFloat = false; } // 创建UEditor实例(绑定到editor容器,传入实例专属配置) ueditorInstance = UE.getEditor(editor.value, { initialFrameWidth: '100%', // 编辑器宽度100%自适应 initialFrameHeight: 200, // 编辑器初始高度200px autoHeightEnabled: false, // 禁用自动高度调整 serverUrl: props.multiImageUploadUrl, // 上传接口地址(实例级覆盖) // 单图上传配置(与多图统一,确保参数一致) imageFieldName: props.multiImageFieldName, imageActionName: 'uploadimage', imageAllowFiles: props.multiImageAllowFiles, imageMaxSize: props.multiImageMaxSize, imageCompressEnable: true, imageCompressBorder: 1200, imageCompressQuality: 0.8, enableScaleImage: props.imgResizable, // 是否允许图片缩放(由props控制) enableFloatImage: false, // 禁用图片浮动 retainImageSize: false, // 不保留图片原始尺寸 enableCodeHighlight: false, // 禁用代码高亮 enableAutoSave: false, // 禁用自动保存 imageEnableAutoSize: false, // 再次禁用自动尺寸(确保生效) // 多图上传增强配置 imageMultiUpload: true, // 启用多图上传 imageManagerActionName: 'listimage', // 图片管理接口动作名 imageManagerListPath: '/pc/common/ueditor', // 图片管理接口路径 imageManagerAllowFiles: props.multiImageAllowFiles, // 允许的图片格式 imageManagerMaxCount: props.multiImageMaxCount, // 多图上传最大数量 enableCORS: false, // 禁用跨域处理(根据后端配置调整) timeout: 60000, // 上传超时时间(60秒) }); // UEditor实例初始化完成后的回调(确保编辑器已就绪) ueditorInstance.ready(() => { // 初始化编辑器内容(将props.modelValue传入编辑器) ueditorInstance.setContent(props.modelValue); // ========== 内容变化监听逻辑(优化用户输入体验) ========== let contentChangeTimer: number | null = null; // 内容变化防抖计时器 let isUserTyping = false; // 是否正在用户输入 let lastUserActionTime = 0; // 最后一次用户操作时间 // 监听键盘按下事件(标记用户正在输入) ueditorInstance.addListener('keydown', () => { isUserTyping = true; lastUserActionTime = Date.now(); }); // 监听键盘松开事件(更新最后操作时间) ueditorInstance.addListener('keyup', () => { isUserTyping = true; lastUserActionTime = Date.now(); }); // 监听鼠标松开事件(更新最后操作时间,如粘贴、点击等操作) ueditorInstance.addListener('mouseup', () => { lastUserActionTime = Date.now(); }); // 监听编辑器内容变化事件(触发v-model更新) ueditorInstance.addListener('contentChange', () => { if (isContentUpdating) return; // 内容更新中则跳过(避免循环触发) // 若用户正在输入,使用防抖延迟更新(避免频繁触发父组件事件) if (isUserTyping) { if (contentChangeTimer) { window.clearTimeout(contentChangeTimer); // 清除之前的计时器 } // 200ms防抖:用户停止输入200ms后再更新内容 contentChangeTimer = window.setTimeout(() => { emit('update:modelValue', ueditorInstance.getContent()); // 向父组件发送内容更新事件 // 300ms后检查是否仍无用户操作,重置输入状态 setTimeout(() => { if (Date.now() - lastUserActionTime > 300) { isUserTyping = false; } }, 300); }, 200); } else { // 非用户输入(如代码触发的内容变化),立即更新 emit('update:modelValue', ueditorInstance.getContent()); } }); // 监听编辑器失焦事件(向父组件发送blur事件) ueditorInstance.addListener('blur', () => { isUserTyping = false; // 失焦后重置输入状态 emit('blur'); }); // ========== 重写图片插入方法(优化单图/多图插入逻辑) ========== const originalInsertImage = ueditorInstance.insertImage; // 保存原始插入图片方法 // 重写insertImage:统一处理单图/多图插入,保留宽高属性,优化光标位置 ueditorInstance.insertImage = function ( url: string | string[], alt = '', href = '', width = '', height = '', border = 0, align = '' ) { const currentRange = this.selection.getRange(); // 获取当前光标选区 if (Array.isArray(url)) { // 多图插入逻辑 url.forEach((imgUrl, index) => { const img = this.document.createElement('img'); // 创建img元素 img.src = imgUrl; // 设置图片URL img.alt = alt; // 设置图片alt属性 img.setAttribute('data-multi', 'true'); // 标记为多图上传的图片 // 保留传入的宽高属性(若有) if (width) img.width = width; if (height) img.height = height; currentRange.insertNode(img); // 将图片插入光标位置 // 图片之间插入空格(最后一张图片后不插入) if (index < url.length - 1) { const space = this.document.createTextNode(' '); currentRange.insertNode(space); currentRange.setStartAfter(space); // 光标移到空格后 currentRange.setEndAfter(space); } }); // 光标移到最后一张图片后面 const lastImg = currentRange.document.querySelector('img[data-multi]:last-of-type'); if (lastImg) { currentRange.setStartAfter(lastImg); currentRange.setEndAfter(lastImg); } } else { // 单图插入逻辑 const img = this.document.createElement('img'); // 创建img元素 img.src = url; // 设置图片URL img.alt = alt; // 设置图片alt属性 img.setAttribute('data-single', 'true'); // 标记为单图上传的图片 // 保留传入的宽高属性(若有) if (width) img.width = width; if (height) img.height = height; currentRange.insertNode(img); // 将图片插入光标位置 } currentRange.collapse(false); // 光标折叠到插入内容后面 this.selection.setRange(currentRange); // 恢复光标位置 this.focus(); // 编辑器重新获取焦点 // 延迟触发内容变化事件(确保DOM已更新) setTimeout(() => { this.fireEvent('contentChange'); }, 50); }; // 重写insertImages方法(适配多图上传插件的调用逻辑) ueditorInstance.setOpt('insertImages', function (images: any[]) { const imageUrls = images.map(img => img.url || img.src); // 提取图片URL数组 return ueditorInstance.insertImage.call(this, imageUrls); // 调用重写后的insertImage }); // ========== 监听图片插入(使用MutationObserver确保所有图片都被处理) ========== if (ueditorInstance.document && ueditorInstance.document.body) { // 创建MutationObserver:监听编辑器内容区的DOM变化 imageInsertObserver = new MutationObserver((mutations) => { let hasNewImage = false; // 标记是否有新图片插入 mutations.forEach(mutation => { // 遍历所有新增的节点 if (mutation.addedNodes.length > 0) { Array.from(mutation.addedNodes).forEach(node => { const element = node as HTMLElement; // 查找新增节点中的图片(直接是img或包含img的元素) const imgs = element.tagName === 'IMG' ? [element] : Array.from(element.querySelectorAll('img')); if (imgs.length > 0) { hasNewImage = true; imgs.forEach(img => { // 标记未分类的图片为单图 if (!img.hasAttribute('data-multi') && !img.hasAttribute('data-single')) { img.setAttribute('data-single', 'true'); } // 移除内联style属性(避免UEditor默认样式干扰) if (img.hasAttribute('style')) { img.removeAttribute('style'); } }); } }); } }); // 有新图片插入时,执行图片尺寸修正(多重延迟确保生效) if (hasNewImage) { safeImageSizeFix(true); // 立即修正 setTimeout(() => safeImageSizeFix(true), 300); // 300ms后再次修正 setTimeout(() => safeImageSizeFix(true), 800); // 800ms后第三次修正 } }); // 启动监听:监听编辑器body的子节点变化、子树变化、属性变化 imageInsertObserver.observe(ueditorInstance.document.body, { childList: true, // 监听子节点增减 subtree: true, // 监听所有子树(包括嵌套元素) attributes: true, // 监听元素属性变化 attributeFilter: ['style', 'width', 'height'], // 只监听关键属性(优化性能) characterData: false // 不监听文本内容变化 }); } // ========== 监听单图上传完成事件(确保上传后图片尺寸正确) ========== ueditorInstance.addListener('afterUpload', function (type: string, result: any) { // 仅处理图片类型上传且上传成功的情况 if (type === 'image' && result && result.state === 'SUCCESS') { // 多重延迟修正(覆盖UEditor上传后的后续处理) setTimeout(() => safeImageSizeFix(true), 100); setTimeout(() => safeImageSizeFix(true), 500); setTimeout(() => safeImageSizeFix(true), 1000); } }); // ========== 监听单图上传插入事件(强化单图处理) ========== ueditorInstance.addListener('afterInsertImage', function (tid: string, imgs: any[]) { // 延迟200ms处理(确保图片已插入DOM) setTimeout(() => { if (imgs && imgs.length > 0) { imgs.forEach((imgInfo: any) => { const imgUrl = imgInfo.src || imgInfo.url; // 提取图片URL if (imgUrl) { const doc = ueditorInstance.document; // 同时查找编辑器主文档和iframe内的图片(避免遗漏) const iframeDoc = doc.querySelector('iframe')?.contentDocument || doc; const allImgs = [...doc.querySelectorAll('img'), ...iframeDoc.querySelectorAll('img')]; allImgs.forEach((img: HTMLImageElement) => { // 匹配刚上传的图片(通过URL模糊匹配) if ((img.src.includes(imgUrl) || img.src.includes(imgUrl.split('/').pop())) && !img.hasAttribute('data-single')) { img.setAttribute('data-single', 'true'); // 标记为单图 if (img.hasAttribute('style')) { img.removeAttribute('style'); // 移除内联样式 } } }); } }); safeImageSizeFix(true); // 修正图片尺寸 } }, 200); // 800ms后再次修正(确保覆盖UEditor的后续操作) setTimeout(() => { if (imgs && imgs.length > 0) { safeImageSizeFix(true); } }, 800); }); // ========== 多图上传事件监听(向父组件传递上传状态) ========== ueditorInstance.addListener('uploadSuccess', function (type: string, response: any) { if (type !== 'image') return; // 仅处理图片上传 try { // 解析上传响应(兼容字符串和对象类型) const result = typeof response === 'string' ? JSON.parse(response) : response; if (Array.isArray(result)) { // 多图上传响应(数组格式):分离成功和失败的图片 const successImages = result.filter((item: any) => item.state === 'SUCCESS'); const errorImages = result.filter((item: any) => item.state !== 'SUCCESS'); if (successImages.length > 0) { emit('multi-image-upload-success', successImages); // 发送成功事件 } if (errorImages.length > 0) { emit('multi-image-upload-error', { // 发送部分失败事件 message: `部分图片上传失败(${errorImages.length}张)`, data: errorImages }); } } else if (result && result.state === 'SUCCESS') { // 单图上传成功(对象格式) emit('multi-image-upload-success', [result]); } else { // 上传失败(响应格式正确但状态错误) emit('multi-image-upload-error', { message: result?.state || '多图上传失败', data: result }); } } catch (error) { // 响应解析失败(如JSON格式错误) emit('multi-image-upload-error', { message: '多图上传响应解析失败', error: error }); } }); // 监听多图上传失败事件 ueditorInstance.addListener('uploadError', function (type: string, xhr: XMLHttpRequest, error: any) { if (type !== 'image') return; // 仅处理图片上传 // 发送上传失败事件(包含状态码、响应内容等信息) emit('multi-image-upload-error', { message: `上传失败(状态码:${xhr.status})`, status: xhr.status, response: xhr.responseText, error: error }); }); // ========== 初始化时修正已有图片(确保页面加载的图片符合配置) ========== setTimeout(() => { safeImageSizeFix(true); }, 300); }); // 监听UEditor初始化错误事件 ueditorInstance.addListener('error', function (error: any) { console.error('UEditor初始化错误:', error); emit('multi-image-upload-error', { message: 'UEditor初始化失败', error: error }); }); }; /** * 安全的图片尺寸修正(强化版) * 核心逻辑:移除内联style属性,保留width/height属性,标记图片类型(单图/多图) * @param force 是否强制执行(忽略isFixingImages锁) */ const safeImageSizeFix = (force = false) => { // 校验依赖:编辑器实例或文档不存在则终止 if (!ueditorInstance || !ueditorInstance.document) return; // 非强制模式下,若正在修正则跳过(避免重复执行) if (!force && isFixingImages) return; isFixingImages = true; // 开启修正锁 try { const doc = ueditorInstance.document; // 同时获取主文档和iframe内的图片(覆盖所有可能的图片容器) const iframeDoc = doc.querySelector('iframe')?.contentDocument || doc; const bodyImg = doc.body ? doc.body.querySelectorAll('img') : []; const iframeImg = iframeDoc.body ? iframeDoc.body.querySelectorAll('img') : []; const imgElements = [...bodyImg, ...iframeImg]; // 合并所有图片元素 imgElements.forEach((img: HTMLImageElement) => { // 移除内联style属性(避免UEditor默认样式或用户手动设置的样式干扰) if (img.hasAttribute('style')) { img.removeAttribute('style'); } // 标记未分类的图片为单图(确保所有图片都有类型标记) if (!img.hasAttribute('data-multi') && !img.hasAttribute('data-single')) { img.setAttribute('data-single', 'true'); } }); } catch (error) { console.warn('安全修正图片尺寸时发生错误:', error); } finally { // 释放修正锁(强制模式延迟200ms释放,避免短时间内重复触发) if (!force) { isFixingImages = false; } else { setTimeout(() => isFixingImages = false, 200); } } }; // 销毁UEditor实例(组件卸载时清理资源) const destroyUEditor = () => { if (ueditorInstance) { try { UE.delEditor(editor.value); // 从UE全局移除编辑器 ueditorInstance.destroy(); // 销毁实例 ueditorInstance = null; // 清空实例引用 } catch (error) { console.warn('销毁UEditor时发生错误:', error); } } if (imageInsertObserver) { imageInsertObserver.disconnect(); // 断开MutationObserver监听 imageInsertObserver = null; // 清空监听者引用 } }; // ========== 监听modelValue变化(同步外部更新到编辑器) ========== watch( () => props.modelValue, // 监听props中的modelValue (newValue) => { // 编辑器未初始化或正在更新内容则跳过 if (!ueditorInstance || isContentUpdating) return; const currentContent = ueditorInstance.getContent(); // 获取编辑器当前内容 // 新值与当前内容不一致时才更新(避免不必要的操作) if (newValue !== currentContent) { isContentUpdating = true; // 开启内容更新锁 // 保存当前光标选区(更新后恢复,提升用户体验) const currentRange = ueditorInstance.selection.getRange(); const isRangeValid = currentRange && !currentRange.collapsed; // 判断选区是否有效 ueditorInstance.setContent(newValue); // 将新值设置到编辑器 // 延迟恢复选区和修正图片(确保DOM已更新) setTimeout(() => { try { if (isRangeValid && currentRange) { ueditorInstance.selection.setRange(currentRange); // 恢复光标选区 } ueditorInstance.focus(); // 编辑器获取焦点 safeImageSizeFix(true); // 修正图片尺寸(确保新内容中的图片符合配置) } catch (e) { // 恢复选区失败时,至少保持焦点并修正图片 ueditorInstance.focus(); safeImageSizeFix(true); } isContentUpdating = false; // 释放内容更新锁 }, 50); } } ); // 暴露组件方法供外部调用 defineExpose({ ueditorInstance, // 暴露UEditor实例(供外部直接操作) forceImageFixedSize: () => { // 暴露强制修正图片尺寸的方法 safeImageSizeFix(true); } }); </script> <style scoped lang="scss"> /* 组件样式(此处留空,可根据需求添加编辑器容器样式) */ </style>
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/30 20:48:09

ue 导出 fbx

目录 参数截图&#xff1a; 找到身体 Skeletal Mesh 导出fbx&#xff0c; 勾选参数&#xff1a; ✔ Level of DetailLOD 0 ✔ Collision❌ 关 ✔ Vertex Color✔ 开 ✔ Export Morph Targets✔ 开&#xff08;脸部表情需要&#xff09; ✔ Export Preview Mesh✔ ✔ For…

作者头像 李华
网站建设 2026/3/27 0:56:53

金属检测机的核心原理与技术指标解析

放在现代工业生产里&#xff0c;金属检测机是质量控制设备里不能少的&#xff0c;它核心的原理是基于电磁感应技术。产品含有金属异物&#xff0c;通过检测区域时&#xff0c;会让交变磁场有扰动&#xff0c;设备内部接收线圈捕捉信号变化&#xff0c;经过电路分析处理&#xf…

作者头像 李华
网站建设 2026/3/30 8:13:33

智慧城市、能源等优质学术会议分享!

​ ↑↑↑ 了解更多详细会议信息、投稿优惠 请添加会议老师 2026年可持续发展与城市规划国际学术会议&#xff08;SDUP 2026&#xff09; 2026 International Conference on Sustainable Development and Urban Planning ​ ↑↑↑ 了解更多详细会议信息、投稿优惠 请添…

作者头像 李华