This commit is contained in:
262
src/components/Tinymce/index.tsx
Normal file
262
src/components/Tinymce/index.tsx
Normal file
@@ -0,0 +1,262 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user