You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

220 lines
7.5 KiB
TypeScript

3 months ago
import React, { useCallback, useEffect, useRef } from 'react';
import { Editor } from '@tinymce/tinymce-react';
import { defaultConfig, wordPasteProcessor } from './config';
import { message } from 'antd';
interface SimpleImagePasteEditorProps {
value?: string;
onChange?: (value: string) => void;
height?: number;
placeholder?: string;
disabled?: boolean;
className?: string;
// 图片上传配置
imageUploadUrl?: string;
imageUploadHandler?: (blobInfo: any, progress: any, failure: any) => Promise<string>;
// 是否启用图片粘贴自动上传
enableImagePaste?: boolean;
// 图片上传进度回调
onImageUploadProgress?: (progress: number) => void;
// 图片上传完成回调
onImageUploadComplete?: (url: string) => void;
// 图片上传失败回调
onImageUploadError?: (error: string) => void;
}
const SimpleImagePasteEditor: React.FC<SimpleImagePasteEditorProps> = ({
value = '',
onChange,
height = 400,
placeholder = '请输入内容...',
disabled = false,
className = '',
imageUploadUrl,
imageUploadHandler,
enableImagePaste = true,
onImageUploadProgress,
onImageUploadComplete,
onImageUploadError,
}) => {
const editorRef = useRef<any>(null);
const handleEditorChange = useCallback(
(content: string) => {
if (onChange) {
onChange(content);
}
},
[onChange],
);
const handleEditorInit = useCallback((evt: any, editor: any) => {
editorRef.current = editor;
}, []);
useEffect(() => {
if (editorRef.current && value !== editorRef.current.getContent()) {
editorRef.current.setContent(value);
}
}, [value]);
// 自定义图片上传处理函数
const customImageUploadHandler = useCallback((blobInfo: any, progress: any, failure: any): Promise<string> => {
if (imageUploadHandler) {
return imageUploadHandler(blobInfo, progress, failure);
}
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const fd = new FormData();
xhr.upload.addEventListener('progress', (e) => {
const progressPercent = (e.loaded / e.total) * 100;
progress(progressPercent);
onImageUploadProgress?.(progressPercent);
});
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (response.retcode) {
onImageUploadComplete?.(response.data);
resolve(response.data);
} else {
const errorMsg = '上传失败';
onImageUploadError?.(errorMsg);
reject(errorMsg);
}
} catch (e) {
const errorMsg = '响应解析失败';
onImageUploadError?.(errorMsg);
reject(errorMsg);
}
} else {
const errorMsg = '上传失败';
onImageUploadError?.(errorMsg);
reject(errorMsg);
}
});
xhr.addEventListener('error', () => {
const errorMsg = '网络错误';
onImageUploadError?.(errorMsg);
reject(errorMsg);
});
fd.append('file', blobInfo.blob(), blobInfo.filename());
xhr.open('POST', imageUploadUrl || process.env.BASE_URL + '/oss/imgUpload');
xhr.send(fd);
});
}, [imageUploadHandler, imageUploadUrl, onImageUploadProgress, onImageUploadComplete, onImageUploadError]);
// 处理粘贴事件,自动上传图片
const handlePastePreProcess = useCallback((e: any) => {
// 先处理Word粘贴
e.content = wordPasteProcessor(e.content);
if (!enableImagePaste) return;
// 检查并移除本地图片链接
3 months ago
// const localImgRegex = /<img[^>]*(src|data-image-src)=["']file:\/\/\/[^"']+["'][^>]*>/gi;
// if (localImgRegex.test(e.content)) {
// e.content = e.content.replace(localImgRegex, '');
// setTimeout(() => {
// message.warning('检测到本地图片链接,网页无法读取本地图片。请直接复制图片文件或截图粘贴。');
// }, 0);
// }
3 months ago
// 检查base64图片并上传原有逻辑
const imgRegex = /<img[^>]*src="data:image\/[^"]*"[^>]*>/gi;
const hasDataImages = imgRegex.test(e.content);
if (hasDataImages) {
const imgMatches = e.content.match(/<img[^>]*src="(data:image\/[^"]*)"[^>]*>/gi);
if (imgMatches) {
const uploadPromises = imgMatches.map(async (imgTag: string) => {
const srcMatch = imgTag.match(/src="(data:image\/[^"]*)"/);
if (srcMatch) {
const dataUrl = srcMatch[1];
try {
const response = await fetch(dataUrl);
const blob = await response.blob();
const blobInfo = {
blob: () => blob,
filename: () => `pasted-image-${Date.now()}.png`,
};
const uploadedUrl = await customImageUploadHandler(
blobInfo,
(progress: number) => { console.log('图片上传进度:', progress); },
(error: string) => { console.error('图片上传失败:', error); }
);
return e.content.replace(dataUrl, uploadedUrl);
} catch (error) {
console.error('图片处理失败:', error);
return e.content;
}
}
return e.content;
});
Promise.all(uploadPromises).then((results) => {
if (results.length > 0) {
e.content = results[0];
}
});
}
}
}, [enableImagePaste, customImageUploadHandler]);
// setup增强优先处理剪贴板图片文件
const initConfig = {
...defaultConfig,
height,
placeholder,
setup: (editor: any) => {
editor.on('PastePreProcess', handlePastePreProcess);
// 监听原生paste事件优先处理clipboardData图片
editor.on('paste', async (event: ClipboardEvent) => {
if (!enableImagePaste) return;
if (!event.clipboardData) return;
const items = event.clipboardData.items;
console.log(items)
for (let i = 0; i < items.length; i++) {
const item = items[i];
console.log(item)
if (item.kind === 'file' && item.type.startsWith('image/')) {
event.preventDefault();
const file = item.getAsFile();
if (file) {
const blobInfo = {
blob: () => file,
filename: () => `clipboard-image-${Date.now()}.${file.type.split('/')[1] || 'png'}`,
};
try {
const uploadedUrl = await customImageUploadHandler(
blobInfo,
(progress: number) => { console.log('图片上传进度:', progress); },
(error: string) => { console.error('图片上传失败:', error); }
);
editor.insertContent(`<img src="${uploadedUrl}" />`);
message.success('图片粘贴上传成功');
} catch (err) {
message.error('图片粘贴上传失败');
}
}
}
}
})
},
images_upload_handler: customImageUploadHandler,
paste_data_images: true,
images_file_types: 'jpeg,jpg,png,gif,webp',
images_upload_max_size: 5 * 1024 * 1024,
};
return (
<div className={className}>
<Editor
apiKey="acps5w8zgbvzyh1vn9y63oh6wa89n43ujdvswrz1ckjx8pi6"
onInit={handleEditorInit}
value={value}
onEditorChange={handleEditorChange}
disabled={disabled}
init={initConfig}
/>
</div>
);
};
export default SimpleImagePasteEditor;