import React, { useEffect, useRef, forwardRef, useImperativeHandle, useCallback } from 'react';
interface UEditorProps {
value?: string;
onChange?: (content: string) => void;
style?: React.CSSProperties;
config?: Record<string, any>;
imgResizable?: boolean;
imgFixedWidth?: number | string;
multiImageMaxCount?: number;
multiImageMaxSize?: number;
multiImageAllowFiles?: string[];
multiImageUploadUrl?: string;
multiImageFieldName?: string;
onBlur?: () => void;
onMultiImageUploadSuccess?: (images: any[]) => void;
onMultiImageUploadError?: (error: {
message: string;
data?: any;
status?: number;
response?: string;
error?: any;
}) => void;
}
declare global {
interface Window {
UE: any;
loader?: {
load: (
urls: string[],
callback: () => void,
errorCallback: (err: any) => void
) => void;
};
}
}
const DEFAULT_CONFIG = {
imgResizable: false,
imgFixedWidth: 100,
multiImageMaxCount: 9,
multiImageMaxSize: 5 * 1024,
multiImageAllowFiles: ['.png', '.jpg', '.jpeg', '.gif', '.bmp'],
multiImageUploadUrl: '/pc/common/ueditor',
multiImageFieldName: 'upfile',
};
const UEditor = forwardRef((props: UEditorProps, ref) => {
const editorContainer = useRef<HTMLDivElement>(null);
const ueInstance = useRef<any>(null);
const isInitializing = useRef(false);
const isFixingImages = useRef(false);
const isContentUpdating = useRef(false);
const contentChangeTimerRef = useRef<NodeJS.Timeout | null>(null);
const {
value,
onChange,
style,
config = {},
imgResizable = DEFAULT_CONFIG.imgResizable,
imgFixedWidth = DEFAULT_CONFIG.imgFixedWidth,
multiImageMaxCount = DEFAULT_CONFIG.multiImageMaxCount,
multiImageMaxSize = DEFAULT_CONFIG.multiImageMaxSize,
multiImageAllowFiles = DEFAULT_CONFIG.multiImageAllowFiles,
multiImageUploadUrl = DEFAULT_CONFIG.multiImageUploadUrl,
multiImageFieldName = DEFAULT_CONFIG.multiImageFieldName,
onBlur,
onMultiImageUploadSuccess,
onMultiImageUploadError,
} = props;
// 关键修复:直接使用图片中显示的路径,不进行任何拼接
const UEDITOR_BASE_PATH = '/UEditor';
// 计算处理后的宽度值
const getFixedWidth = useCallback(() => {
if (typeof imgFixedWidth === 'number') {
return `${imgFixedWidth}%`;
}
return imgFixedWidth as string;
}, [imgFixedWidth]);
// 动态更新CSS变量
const updateCssVariable = useCallback(() => {
document.documentElement.style.setProperty('--img-fixed-width', getFixedWidth());
}, [getFixedWidth]);
// 核心修复:简化图片修正逻辑,只清理样式,不处理URL
const safeImageSizeFix = useCallback((force = false) => {
if (!ueInstance.current || !ueInstance.current.document || (!force && isFixingImages.current)) return;
isFixingImages.current = true;
try {
const doc = ueInstance.current.document;
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属性,保留width和height
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 {
setTimeout(() => {
isFixingImages.current = false;
}, force ? 200 : 0);
}
}, []);
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
getContent: () => ueInstance.current?.getContent() || '',
setContent: (content: string) => {
if (ueInstance.current && ueInstance.current.isReady) {
isContentUpdating.current = true;
ueInstance.current.setContent(content);
setTimeout(() => {
safeImageSizeFix(true);
isContentUpdating.current = false;
}, 50);
}
},
getPlainText: () => ueInstance.current?.getPlainTxt() || '',
forceImageFixedSize: () => safeImageSizeFix(true),
ueditorInstance: ueInstance.current,
}));
// 加载脚本
const loadScript = useCallback((src: string) => {
return new Promise((resolve, reject) => {
if (document.querySelector(`script[src="${src}"]`)) {
return resolve(true);
}
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = () => reject(new Error(`加载失败: ${src}`));
document.body.appendChild(script);
});
}, []);
// 初始化loader
const initLoader = useCallback(() => {
if (!window.loader) {
window.loader = {
load: (urls: string[], callback: () => void, errorCallback: (err: any) => void) => {
const filteredUrls = urls.filter(url =>
!document.querySelector(`script[src="${url}"]`)
);
if (filteredUrls.length === 0) {
callback();
return;
}
const loadScript = (url: string) => {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = url;
script.onload = resolve;
script.onerror = (err) => {
console.warn(`资源加载失败: ${url}`, err);
reject(err);
};
document.head.appendChild(script);
});
};
Promise.all(filteredUrls.map(loadScript))
.then(callback)
.catch(errorCallback);
}
};
}
}, []);
// 安全销毁编辑器
const safeDestroyEditor = useCallback(() => {
if (contentChangeTimerRef.current) {
clearTimeout(contentChangeTimerRef.current);
contentChangeTimerRef.current = null;
}
if (!ueInstance.current) return;
try {
const listeners = [
'contentChange', 'blur', 'afterUpload', 'afterInsertImage',
'uploadSuccess', 'uploadError', 'error', 'keydown',
'keyup', 'mouseup'
];
listeners.forEach(event => {
ueInstance.current.removeAllListeners?.(event);
});
if (window.UE?.instances[ueInstance.current.key]) {
window.UE.delEditor(ueInstance.current.key);
}
ueInstance.current = null;
} catch (error) {
console.warn('UEditor 销毁警告:', error);
}
}, []);
// 核心修复:简化图片插入方法,直接使用原始URL
const setupImageHandlers = useCallback(() => {
if (!ueInstance.current) return;
// 重写insertImage方法,直接使用原始URL
const originalInsertImage = ueInstance.current.insertImage;
ueInstance.current.insertImage = function(
url: string | string[],
alt = '',
href = '',
width = '',
height = '',
border = 0,
align = ''
) {
const currentRange = this.selection.getRange();
const targetUrls = Array.isArray(url) ? url : [url];
targetUrls.forEach((imgUrl, index) => {
const img = this.document.createElement('img');
// 直接使用原始URL,不进行任何处理
img.src = imgUrl;
img.alt = alt;
if (Array.isArray(url) && url.length > 1) {
img.setAttribute('data-multi', 'true');
} else {
img.setAttribute('data-single', 'true');
}
if (width) img.setAttribute('width', width.toString());
if (height) img.setAttribute('height', height.toString());
currentRange.insertNode(img);
if (index < targetUrls.length - 1) {
const space = this.document.createTextNode(' ');
currentRange.insertNode(space);
}
});
currentRange.collapse(false);
this.selection.setRange(currentRange);
this.focus();
setTimeout(() => {
this.fireEvent('contentChange');
safeImageSizeFix(true);
}, 50);
};
// 简化insertImages方法
ueInstance.current.setOpt('insertImages', function(images: any[]) {
const imageUrls = images.map(img => img.url || img.src);
return ueInstance.current.insertImage.call(this, imageUrls);
});
}, [safeImageSizeFix]);
// 初始化编辑器
const initEditor = useCallback(async () => {
if (isInitializing.current || !editorContainer.current) return;
isInitializing.current = true;
try {
initLoader();
// 直接加载UEditor资源,不进行路径处理
if (!window.UE) {
await Promise.all([
loadScript(`${UEDITOR_BASE_PATH}/ueditor.config.js`),
loadScript(`${UEDITOR_BASE_PATH}/ueditor.all.min.js`),
loadScript(`${UEDITOR_BASE_PATH}/lang/zh-cn/zh-cn.js`)
]);
}
if (window.UE && editorContainer.current && document.contains(editorContainer.current)) {
safeDestroyEditor();
if (!editorContainer.current.id) {
editorContainer.current.id = `ueditor-${Date.now()}`;
}
// 简化配置,禁用所有自动URL处理
const editorConfig = {
// 直接使用基础路径
UEDITOR_HOME_URL: UEDITOR_BASE_PATH + '/',
serverUrl: multiImageUploadUrl,
// 关键修复:禁用所有URL前缀和路径格式化
imageUrlPrefix: '',
imagePathFormat: '',
imageManagerUrlPrefix: '',
scrawlUrlPrefix: '',
snapscreenUrlPrefix: '',
catcherUrlPrefix: '',
videoUrlPrefix: '',
fileUrlPrefix: '',
imageManagerUrlPrefix: '',
// 图片上传配置
imageActionName: 'uploadimage',
imageFieldName: multiImageFieldName,
imageMaxSize: multiImageMaxSize * 1024,
imageAllowFiles: multiImageAllowFiles,
imageMultiUpload: true,
// 图片管理配置
imageManagerActionName: 'listimage',
imageManagerAllowFiles: multiImageAllowFiles,
imageManagerPageSize: 30,
// 编辑器基础配置
initialFrameWidth: '100%',
initialFrameHeight: 200,
autoHeightEnabled: false,
enableScaleImage: imgResizable,
// 禁用所有自动处理
autoTransWord: false,
autoClearEmptyNode: false,
enableAutoSave: false,
...config,
};
ueInstance.current = window.UE.getEditor(editorContainer.current.id, editorConfig);
ueInstance.current.ready(() => {
ueInstance.current.isReady = true;
if (value) {
ueInstance.current.setContent(value);
}
updateCssVariable();
setupImageHandlers();
// 内容变化监听
ueInstance.current.addListener('contentChange', () => {
if (!ueInstance.current?.isReady || isContentUpdating.current) return;
if (contentChangeTimerRef.current) {
clearTimeout(contentChangeTimerRef.current);
}
contentChangeTimerRef.current = setTimeout(() => {
const content = ueInstance.current.getContent();
onChange?.(content);
}, 200);
});
ueInstance.current.addListener('blur', () => {
onBlur?.();
});
// 图片上传成功监听
ueInstance.current.addListener('afterUpload', (type: string, result: any) => {
if (type === 'image' && result) {
setTimeout(() => safeImageSizeFix(true), 100);
}
});
// 多图上传事件
ueInstance.current.addListener('uploadSuccess', (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');
if (successImages.length > 0) {
onMultiImageUploadSuccess?.(successImages);
}
} else if (result?.state === 'SUCCESS') {
onMultiImageUploadSuccess?.([result]);
}
} catch (error) {
console.error('上传响应解析失败:', error);
}
});
// 初始修正
setTimeout(() => safeImageSizeFix(true), 300);
});
ueInstance.current.addListener('error', (error: any) => {
console.error('UEditor错误:', error);
});
}
} catch (error) {
console.error('UEditor初始化失败:', error);
} finally {
isInitializing.current = false;
}
}, [
UEDITOR_BASE_PATH,
initLoader,
loadScript,
safeDestroyEditor,
value,
multiImageUploadUrl,
multiImageFieldName,
multiImageMaxSize,
multiImageAllowFiles,
imgResizable,
config,
updateCssVariable,
setupImageHandlers,
onChange,
onBlur,
onMultiImageUploadSuccess,
safeImageSizeFix
]);
// 生命周期
useEffect(() => {
initEditor();
return () => safeDestroyEditor();
}, [initEditor, safeDestroyEditor]);
// 监听value变化
useEffect(() => {
if (!ueInstance.current?.isReady || value === undefined || isContentUpdating.current) return;
const currentContent = ueInstance.current.getContent();
if (value !== currentContent) {
isContentUpdating.current = true;
ueInstance.current.setContent(value);
setTimeout(() => {
safeImageSizeFix(true);
isContentUpdating.current = false;
}, 50);
}
}, [value, safeImageSizeFix]);
// 监听配置变化
useEffect(() => {
if (ueInstance.current?.isReady) {
updateCssVariable();
safeImageSizeFix(true);
}
}, [imgFixedWidth, imgResizable, updateCssVariable, safeImageSizeFix]);
return (
<div
ref={editorContainer}
style={{
...style,
minHeight: '300px',
}}
/>
);
});
export default UEditor;优化为什么会多出来一层src?
最新发布