162 lines
4.2 KiB
TypeScript
162 lines
4.2 KiB
TypeScript
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||
import { message, theme } from 'antd';
|
||
import { Editor, Toolbar } from '@wangeditor/editor-for-react';
|
||
import type { IDomEditor, IEditorConfig, IToolbarConfig } from '@wangeditor/editor';
|
||
import '@wangeditor/editor/dist/css/style.css';
|
||
import { uploadFile } from '@/api/system/sysFile';
|
||
import type { ISysFileInfo } from '@/domain/iSysFile';
|
||
import { useTranslation } from 'react-i18next';
|
||
import type { RichTextEditorProps } from './typings';
|
||
|
||
/**
|
||
* 富文本编辑器(wangEditor v5)
|
||
* - 自定义图片上传:复用系统文件上传接口,校验参照 ImageUploader
|
||
* - 隐藏视频上传、表情、附件等复杂功能,仅保留基础排版与图片
|
||
*/
|
||
const RichTextEditor: React.FC<RichTextEditorProps> = ({
|
||
value,
|
||
onChange,
|
||
disabled = false,
|
||
height = 400,
|
||
placeholder,
|
||
groupId = 0,
|
||
maxSize = 5,
|
||
}) => {
|
||
const { t } = useTranslation();
|
||
const { token } = theme.useToken();
|
||
const [editor, setEditor] = useState<IDomEditor | null>(null);
|
||
|
||
// 自定义图片上传(校验逻辑参照 ImageUploader 组件)
|
||
const customUpload = useCallback(
|
||
async (file: File, insertFn: (src: string, alt: string, href: string) => void) => {
|
||
// 1. 文件类型校验
|
||
if (!file.type.startsWith('image/')) {
|
||
message.error(t('xin.form.richText.error.notImage'));
|
||
return;
|
||
}
|
||
|
||
// 2. 文件大小校验
|
||
if (file.size / 1024 / 1024 > maxSize) {
|
||
message.error(t('xin.form.richText.error.sizeExceeded', { maxSize }));
|
||
return;
|
||
}
|
||
|
||
// 3. 调用系统文件上传接口
|
||
try {
|
||
const res = await uploadFile(file, groupId);
|
||
const info = res.data.data as ISysFileInfo;
|
||
insertFn(info.preview_url || info.file_url || '', info.file_name || '', '');
|
||
} catch {
|
||
message.error(t('xin.form.richText.error.uploadFailed'));
|
||
}
|
||
},
|
||
[t, groupId, maxSize]
|
||
);
|
||
|
||
// 工具栏配置:只保留基础排版 + 图片,隐藏视频/表情/附件等复杂功能
|
||
const toolbarConfig: Partial<IToolbarConfig> = useMemo(
|
||
() => ({
|
||
toolbarKeys: [
|
||
'headerSelect',
|
||
'blockquote',
|
||
'|',
|
||
'bold',
|
||
'underline',
|
||
'italic',
|
||
'through',
|
||
'|',
|
||
'color',
|
||
'bgColor',
|
||
'|',
|
||
'bulletedList',
|
||
'numberedList',
|
||
'todo',
|
||
'|',
|
||
'justifyLeft',
|
||
'justifyCenter',
|
||
'justifyRight',
|
||
'|',
|
||
'insertLink',
|
||
'uploadImage',
|
||
'insertTable',
|
||
'|',
|
||
'undo',
|
||
'redo',
|
||
'fullScreen',
|
||
],
|
||
}),
|
||
[]
|
||
);
|
||
|
||
// 编辑器配置
|
||
const editorConfig: Partial<IEditorConfig> = useMemo(
|
||
() => ({
|
||
placeholder,
|
||
MENU_CONF: {
|
||
uploadImage: {
|
||
// 自定义上传:覆盖默认的服务端上传方式
|
||
customUpload,
|
||
allowedFileTypes: ['image/*'],
|
||
maxFileSize: maxSize * 1024 * 1024,
|
||
// 禁用 base64 插入,粘贴/拖拽图片一律走自定义上传
|
||
base64LimitSize: 0,
|
||
},
|
||
},
|
||
}),
|
||
[placeholder, customUpload, maxSize]
|
||
);
|
||
|
||
// 禁用/启用编辑器
|
||
useEffect(() => {
|
||
if (editor == null) return;
|
||
if (disabled) {
|
||
editor.disable();
|
||
} else {
|
||
editor.enable();
|
||
}
|
||
}, [editor, disabled]);
|
||
|
||
// 组件卸载时销毁编辑器实例
|
||
useEffect(
|
||
() => () => {
|
||
if (editor == null) return;
|
||
editor.destroy();
|
||
setEditor(null);
|
||
},
|
||
[editor]
|
||
);
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
border: `1px solid ${token.colorBorder}`,
|
||
borderRadius: token.borderRadius,
|
||
overflow: 'hidden',
|
||
}}
|
||
>
|
||
{!disabled && (
|
||
<Toolbar
|
||
editor={editor}
|
||
defaultConfig={toolbarConfig}
|
||
mode="default"
|
||
style={{ borderBottom: `1px solid ${token.colorBorder}` }}
|
||
/>
|
||
)}
|
||
<Editor
|
||
defaultConfig={editorConfig}
|
||
value={value}
|
||
onCreated={setEditor}
|
||
onChange={(ed) => onChange?.(ed.getHtml())}
|
||
mode="default"
|
||
style={{
|
||
height,
|
||
overflowY: 'hidden',
|
||
background: disabled ? token.colorBgLayout : undefined,
|
||
}}
|
||
/>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default RichTextEditor;
|