import { Editor } from '@tinymce/tinymce-react'; import { message } from 'antd'; import React, { useCallback, useState } from 'react'; import type { Editor as TinyMCEEditor } from 'tinymce'; export interface RichEditorProps { value?: string; onChange?: (content: string) => void; height?: number; placeholder?: string; disabled?: boolean; uploadConfig?: { action: string; headers?: Record; maxSize?: number; acceptTypes?: string[]; data?: Record; }; showWordCount?: boolean; maxWords?: number; } const RichEditor: React.FC = ({ // value = "", // onChange, // height = 400, // placeholder = "请输入内容...", disabled = false, // showWordCount = true, // maxWords, uploadConfig = { action: '/api/upload/image', maxSize: 5, acceptTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], }, }) => { const [uploading, setUploading] = useState(false); const uploadFile = useCallback( async (file: File): Promise => { return new Promise((resolve, reject) => { const formData = new FormData(); formData.append('file', file); if (uploadConfig.data) { Object.keys(uploadConfig.data).forEach((key) => { formData.append(key, uploadConfig.data?.[key]); }); } const xhr = new XMLHttpRequest(); xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { const percent = Math.round((e.loaded / e.total) * 100); console.log(`上传进度: ${percent}%`); } }); xhr.addEventListener('load', () => { if (xhr.status === 200) { try { const response = JSON.parse(xhr.responseText); if (response.code === 200 && response.data?.url) { resolve(response.data.url); } else { reject(response.message || '上传失败'); } } catch (_error) { reject('响应解析失败'); } } else { reject(`上传失败: ${xhr.status}`); } }); xhr.addEventListener('error', () => { reject('网络错误'); }); if (uploadConfig.headers) { Object.keys(uploadConfig.headers).forEach((key) => { xhr.setRequestHeader(key, uploadConfig.headers![key]); }); } xhr.open('POST', uploadConfig.action); xhr.send(formData); }); }, [uploadConfig], ); const handleImageUpload = useCallback( (blobInfo: any, _: (percent: number) => void): Promise => { return new Promise((resolve, reject) => { try { setUploading(true); const file = blobInfo.blob(); uploadFile(file).then((url) => { resolve(url); }); message.success('图片上传成功'); } catch (error) { reject(error); message.error(typeof error === 'string' ? error : '上传失败'); } finally { setUploading(false); } }); }, [uploadFile], ); const handleCustomUpload = useCallback( (editor: TinyMCEEditor) => { const input = document.createElement('input'); input.type = 'file'; input.accept = uploadConfig.acceptTypes?.join(',') || 'image/*'; input.multiple = true; const handleFileChange = async (event: Event) => { const target = event.target as HTMLInputElement; const files = Array.from(target.files || []); if (files.length === 0) return; setUploading(true); try { for (const file of files) { const isValidType = uploadConfig.acceptTypes?.includes(file.type); if (!isValidType) { message.error(`文件 ${file.name} 类型不支持`); continue; } const isValidSize = file.size / 1024 / 1024 < (uploadConfig.maxSize || 5); if (!isValidSize) { message.error( `文件 ${file.name} 大小超过 ${uploadConfig.maxSize || 5}MB`, ); continue; } try { const url = await uploadFile(file); const imgHtml = `${file.name}`; editor.insertContent(imgHtml); message.success(`${file.name} 上传成功`); } catch (error) { message.error(`${file.name} 上传失败: ${error}`); } } } finally { setUploading(false); } }; input.addEventListener('change', handleFileChange); input.click(); }, [uploadConfig, uploadFile], ); return ( Promise) => any }, ) => respondWith.string(() => Promise.reject('See docs to implement AI Assistant'), ), language: 'zh-CN', // 其他配置 convert_urls: false, remove_script_host: false, uploadcare_public_key: '0ad3671d77f59c5756dd', setup: (editor: TinyMCEEditor) => { // 注册自定义上传按钮 editor.ui.registry.addButton('customupload', { text: uploading ? '上传中...' : '上传', icon: 'upload', tooltip: '上传图片(支持多选)', enabled: !disabled && !uploading, onAction: () => { handleCustomUpload(editor); }, }); }, // 图片上传配置 images_upload_handler: handleImageUpload, // 性能配置 browser_spellcheck: true, contextmenu: 'link image table', }} initialValue="Welcome to TinyMCE!" /> ); }; export default React.memo(RichEditor);