263 lines
7.5 KiB
TypeScript
263 lines
7.5 KiB
TypeScript
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<string, string>;
|
|
maxSize?: number;
|
|
acceptTypes?: string[];
|
|
data?: Record<string, any>;
|
|
};
|
|
showWordCount?: boolean;
|
|
maxWords?: number;
|
|
}
|
|
const RichEditor: React.FC<RichEditorProps> = ({
|
|
// 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<boolean>(false);
|
|
const uploadFile = useCallback(
|
|
async (file: File): Promise<string> => {
|
|
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<string> => {
|
|
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 = `<img src="${url}" alt="${file.name}" style="max-width: 100%; height: auto;" />`;
|
|
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 (
|
|
<Editor
|
|
apiKey="xt51qhzvj8q9mxzxytigtp0nu94v4rz7dska2dbqeilkxo99"
|
|
init={{
|
|
plugins: [
|
|
// Core editing features
|
|
'anchor',
|
|
'autolink',
|
|
'charmap',
|
|
'codesample',
|
|
'emoticons',
|
|
'link',
|
|
'lists',
|
|
'media',
|
|
'searchreplace',
|
|
'table',
|
|
'visualblocks',
|
|
'wordcount',
|
|
// Your account includes a free trial of TinyMCE premium features
|
|
// Try the most popular premium features until Oct 8, 2025:
|
|
'checklist',
|
|
'mediaembed',
|
|
'casechange',
|
|
'formatpainter',
|
|
'pageembed',
|
|
'a11ychecker',
|
|
'tinymcespellchecker',
|
|
'permanentpen',
|
|
'powerpaste',
|
|
'advtable',
|
|
'advcode',
|
|
'advtemplate',
|
|
'ai',
|
|
'uploadcare',
|
|
'mentions',
|
|
'tinycomments',
|
|
'tableofcontents',
|
|
'footnotes',
|
|
'mergetags',
|
|
'autocorrect',
|
|
'typography',
|
|
'inlinecss',
|
|
'markdown',
|
|
'importword',
|
|
'exportword',
|
|
'exportpdf',
|
|
],
|
|
menubar: false,
|
|
toolbar:
|
|
'undo redo | blocks | bold italic forecolor | ' +
|
|
'alignleft aligncenter | ' +
|
|
'alignright alignjustify | bullist numlist outdent indent customupload | ' +
|
|
'removeformat ',
|
|
tinycomments_mode: 'embedded',
|
|
tinycomments_author: 'Author name',
|
|
toolbar_mode: 'sliding',
|
|
mergetags_list: [
|
|
{ value: 'First.Name', title: 'First Name' },
|
|
{ value: 'Email', title: 'Email' },
|
|
],
|
|
ai_request: (
|
|
_: any,
|
|
respondWith: { string: (arg0: () => Promise<never>) => 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);
|