Files
xin-procurement/web/components/XinFormField/RichTextEditor/index.tsx
T

162 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;