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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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;
// 检查并移除本地图片链接
// const localImgRegex = /<img[^>]*(src|data-image-src)=["']file:\/\/\/[^"']+["'][^>]*>/gi;
// if (localImgRegex.test(e.content)) {
// e.content = e.content.replace(localImgRegex, '');
// setTimeout(() => {
// message.warning('检测到本地图片链接,网页无法读取本地图片。请直接复制图片文件或截图粘贴。');
// }, 0);
// }
// 检查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;