商品列表、分类管理、客户等级
This commit is contained in:
@@ -6,6 +6,7 @@ import type { UploadFile, UploadProps } from 'antd';
|
||||
import type { RcFile } from 'antd/es/upload';
|
||||
import type { ImageUploaderProps } from './typings';
|
||||
import type { ISysFileInfo } from '@/domain/iSysFile';
|
||||
import { getFileInfo } from '@/api/system/sysFile';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/**
|
||||
@@ -51,8 +52,29 @@ const ImageUploader: React.FC<ImageUploaderProps> = ({
|
||||
return;
|
||||
}
|
||||
const valueArray = Array.isArray(value) ? value : [value];
|
||||
const newFileList: UploadFile[] = valueArray.map(fileToList);
|
||||
setFileList(newFileList);
|
||||
// changeType='id' 时表单值为文件 id:拉取文件信息回显;
|
||||
if (changeType === 'id') {
|
||||
const ids = valueArray as number[];
|
||||
|
||||
const idSet = new Set(fileList.map(obj => Number(obj.uid)));
|
||||
|
||||
// 过滤出不在 Set 中的数字
|
||||
const missing = ids.filter(num => !idSet.has(Number(num)));
|
||||
|
||||
if (missing.length > 0) {
|
||||
const fetchers = missing.map((id) => getFileInfo(id));
|
||||
Promise.all(fetchers).then((resList) => {
|
||||
const files = resList
|
||||
.map((res) => res.data.data)
|
||||
.filter(i => !!i)
|
||||
.map(fileToList);
|
||||
setFileList([...files, ...fileList]);
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const newFileList: UploadFile[] = (valueArray as ISysFileInfo[]).map(fileToList);
|
||||
setFileList(newFileList);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
// 上传前校验
|
||||
@@ -106,8 +128,16 @@ const ImageUploader: React.FC<ImageUploaderProps> = ({
|
||||
|
||||
// 处理文件列表变化
|
||||
const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => {
|
||||
// 如果所有文件都被删除
|
||||
if (newFileList.length === 0) return;
|
||||
// 所有文件都被删除时,同步清空表单值,避免残留旧 id 被提交
|
||||
if (newFileList.length === 0) {
|
||||
setFileList([]);
|
||||
if (mode === 'single') {
|
||||
onChange?.(null);
|
||||
} else {
|
||||
onChange?.([]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
setFileList(newFileList);
|
||||
// 全部上传完成格式化图片列表
|
||||
if (newFileList.every((file) => file.status === 'done' || file.status === 'error')) {
|
||||
@@ -130,6 +160,7 @@ const ImageUploader: React.FC<ImageUploaderProps> = ({
|
||||
}
|
||||
// 上传失败的文件
|
||||
const errorFiles = newFileList.filter((file) => file.status === 'error');
|
||||
|
||||
setFileList([...uploadedFiles.map(fileToList), ...errorFiles]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface ImageUploaderProps {
|
||||
* - 单选模式:ISysFileInfo | null
|
||||
* - 多选模式:ISysFileInfo[]
|
||||
*/
|
||||
value?: ISysFileInfo | ISysFileInfo[] | null;
|
||||
value?: ISysFileInfo | ISysFileInfo[] | number | number[] | null;
|
||||
|
||||
/**
|
||||
* 赋值类型
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
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;
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 富文本编辑器组件属性
|
||||
*/
|
||||
export interface RichTextEditorProps {
|
||||
/**
|
||||
* 当前值(HTML 字符串)
|
||||
*/
|
||||
value?: string;
|
||||
|
||||
/**
|
||||
* 值变化回调
|
||||
* @param value HTML 字符串
|
||||
*/
|
||||
onChange?: (value?: string) => void;
|
||||
|
||||
/**
|
||||
* 是否禁用
|
||||
* @default false
|
||||
*/
|
||||
disabled?: boolean;
|
||||
|
||||
/**
|
||||
* 编辑器高度 (px)
|
||||
* @default 400
|
||||
*/
|
||||
height?: number;
|
||||
|
||||
/**
|
||||
* 占位符
|
||||
*/
|
||||
placeholder?: string;
|
||||
|
||||
/**
|
||||
* 上传文件分组 ID
|
||||
* @default 0
|
||||
*/
|
||||
groupId?: number;
|
||||
|
||||
/**
|
||||
* 图片大小限制 (MB)
|
||||
* @default 5
|
||||
*/
|
||||
maxSize?: number;
|
||||
}
|
||||
Reference in New Issue
Block a user