商品列表、分类管理、客户等级

This commit is contained in:
liu
2026-08-05 19:28:53 +08:00
parent dfa471bf22
commit 258df92e6c
36 changed files with 472 additions and 516 deletions
@@ -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;
}