优化FormBuilder

优化Upload,新增fileType字段,自动限制上传的文件
master
LiuJiaNan 2025-11-01 15:21:51 +08:00
parent 320e27802d
commit 72ae11aec3
7 changed files with 195 additions and 91 deletions

View File

@ -1,4 +1,4 @@
import type { FormInstance, FormProps } from "antd/es/form"; import type { FormProps } from "antd/es/form";
import type { Gutter } from "antd/es/grid/row"; import type { Gutter } from "antd/es/grid/row";
import type { FC, ReactNode } from "react"; import type { FC, ReactNode } from "react";
import type { FormOption, FormValues } from "./FormItemsRenderer"; import type { FormOption, FormValues } from "./FormItemsRenderer";
@ -6,7 +6,7 @@ import type { FormOption, FormValues } from "./FormItemsRenderer";
/** /**
* FormBuilder * FormBuilder
*/ */
export interface FormBuilderProps extends Omit<FormProps, "form"> { export interface FormBuilderProps extends FormProps {
/** 表单初始值 */ /** 表单初始值 */
values?: FormValues; values?: FormValues;
/** 表单配置项数组 */ /** 表单配置项数组 */
@ -15,24 +15,22 @@ export interface FormBuilderProps extends Omit<FormProps, "form"> {
gutter?: Gutter | [Gutter, Gutter]; gutter?: Gutter | [Gutter, Gutter];
/** 占据栅格列数,默认 12 */ /** 占据栅格列数,默认 12 */
span?: number | string; span?: number | string;
/** 表单实例(通过 Form.useForm() 创建) */
form?: FormInstance;
/** 自动生成必填规则,默认 true */ /** 自动生成必填规则,默认 true */
useAutoGenerateRequired?: boolean; useAutoGenerateRequired?: boolean;
/** 表单提交回调 */
onFinish?: (values: FormValues) => void;
/** 是否显示操作按钮区域,默认 true */ /** 是否显示操作按钮区域,默认 true */
showActionButtons?: boolean; showActionButtons?: boolean;
/** 提交按钮文字,默认为"提交" */ /** 提交按钮文字,默认为"提交" */
submitButtonText?: string; submitButtonText?: string;
/** 重置按钮文字,默认为"重置" */ /** 取消按钮文字,默认为"取消" */
resetButtonText?: string; cancelButtonText?: string;
/** 是否显示提交按钮,默认 true */ /** 是否显示提交按钮,默认 true */
showSubmitButton?: boolean; showSubmitButton?: boolean;
/** 是否显示重置按钮,默认 true */ /** 是否显示取消按钮,默认 true */
showResetButton?: boolean; showCancelButton?: boolean;
/** 自定义操作按钮组 */ /** 自定义操作按钮组 */
customActionButtons?: ReactNode; customActionButtons?: ReactNode;
/** 额外操作按钮组 */
extraActionButtons?: ReactNode;
} }
/** /**

View File

@ -11,23 +11,19 @@ const FormBuilder = (props) => {
gutter = 24, gutter = 24,
span = 12, span = 12,
labelCol = { span: 4 }, labelCol = { span: 4 },
onFinish,
useAutoGenerateRequired = true, useAutoGenerateRequired = true,
showActionButtons = true, showActionButtons = true,
submitButtonText = "提交", submitButtonText = "提交",
resetButtonText = "重置", cancelButtonText = "取消",
showSubmitButton = true, showSubmitButton = true,
showResetButton = true, showCancelButton = true,
customActionButtons, customActionButtons,
form: externalForm, extraActionButtons,
...restProps ...restProps
} = props; } = props;
const [internalForm] = Form.useForm(); const handleCancel = () => {
const form = externalForm || internalForm; window.history.back();
const handleReset = () => {
form.resetFields();
}; };
return ( return (
@ -35,9 +31,7 @@ const FormBuilder = (props) => {
labelCol={labelCol} labelCol={labelCol}
scrollToFirstError scrollToFirstError
wrapperCol={{ span: 24 - labelCol.span }} wrapperCol={{ span: 24 - labelCol.span }}
onFinish={onFinish}
initialValues={values} initialValues={values}
form={form}
style={{ width: `calc(100% - ${gutter * 2}px)`, margin: `0 auto` }} style={{ width: `calc(100% - ${gutter * 2}px)`, margin: `0 auto` }}
{...restProps} {...restProps}
> >
@ -60,11 +54,12 @@ const FormBuilder = (props) => {
{submitButtonText} {submitButtonText}
</Button> </Button>
)} )}
{showResetButton && ( {showCancelButton && (
<Button onClick={handleReset} style={{ marginRight: 8 }}> <Button onClick={handleCancel} style={{ marginRight: 8 }}>
{resetButtonText} {cancelButtonText}
</Button> </Button>
)} )}
{extraActionButtons}
</Space> </Space>
)} )}
</Col> </Col>

View File

@ -27,7 +27,7 @@ const FormItemsRenderer = ({
collapse = false, collapse = false,
useAutoGenerateRequired = true, useAutoGenerateRequired = true,
initialValues, initialValues,
}) => { }) => {
const form = Form.useFormInstance(); const form = Form.useFormInstance();
// 获取表单值,优先使用 initialValues // 获取表单值,优先使用 initialValues

View File

@ -75,7 +75,7 @@ const ImportFile = (props) => {
> >
{children && typeof children === "function" ? children({ form }) : children} {children && typeof children === "function" ? children({ form }) : children}
<Form.Item label="附件" name="file" rules={[{ required: true, message: "附件不能为空" }]}> <Form.Item label="附件" name="file" rules={[{ required: true, message: "附件不能为空" }]}>
<Upload accept=".xls,.xlsx" listType="text" /> <Upload accept=".xls,.xlsx" listType="text" maxCount={1} />
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>

View File

@ -11,12 +11,14 @@ export interface UploadProps extends Omit<AntUploadProps, "fileList"> {
ratio?: `${number}*${number}`; ratio?: `${number}*${number}`;
/** 是否显示提示,默认 true */ /** 是否显示提示,默认 true */
showTip?: boolean; showTip?: boolean;
/** 文件大小限制单位MB,默认 0不限制 */ /** 文件大小限制单位MB */
size?: number; size?: number;
/** 自定义提示内容 */ /** 自定义提示内容 */
tipContent?: ReactNode; tipContent?: ReactNode;
/** listType 为 text 时上传按钮文本,默认 "点击选择文件上传" */ /** listType 为 text 时上传按钮文本 */
uploadButtonText?: string; uploadButtonText?: string;
/** 要上传的文件类型,默认为 image */
fileType?: "image" | "video" | "document";
} }
/** /**
@ -26,3 +28,7 @@ export interface UploadProps extends Omit<AntUploadProps, "fileList"> {
declare const Upload: FC<UploadProps>; declare const Upload: FC<UploadProps>;
export default Upload; export default Upload;
// 视频数量默认1个且只支持mp4格式单个文件大小默认100M
// 文件数量默认4个且只支持pdf、doc、docx格式
// 图片数量默认4个且只支持jpg、jpeg、png格式

View File

@ -1,4 +1,4 @@
import { PlusOutlined, UploadOutlined } from "@ant-design/icons"; import { PlusOutlined, UploadOutlined, VideoCameraAddOutlined } from "@ant-design/icons";
import { Upload as AntUpload, Button, message, Modal } from "antd"; import { Upload as AntUpload, Button, message, Modal } from "antd";
import { useState } from "react"; import { useState } from "react";
@ -10,15 +10,16 @@ const Upload = (props) => {
value = [], value = [],
onChange, onChange,
onPreview, onPreview,
maxCount = 1, maxCount: externalMaxCount,
listType = "picture-card", listType: externalListType,
accept = ["picture-card", "picture-circle", "picture"].includes(listType) ? ".jpg,.jpeg,.png" : "", accept: externalAccept,
ratio = "", ratio = "",
showTip = true, showTip = true,
multiple = true, multiple = true,
size = 0, size: externalSize,
tipContent, tipContent,
uploadButtonText = "点击选择文件上传", uploadButtonText: externalUploadButtonText,
fileType: externalFileType,
formValues, formValues,
...restProps ...restProps
} = props; } = props;
@ -26,6 +27,90 @@ const Upload = (props) => {
const [previewVisible, setPreviewVisible] = useState(false); const [previewVisible, setPreviewVisible] = useState(false);
const [previewImage, setPreviewImage] = useState(""); const [previewImage, setPreviewImage] = useState("");
// 预设的文件格式
const imageAccept = ".jpg,.jpeg,.png";
const documentAccept = ".pdf,.doc,.docx";
const videoAccept = ".mp4";
// 根据accept自动判断文件类型
const getAutoFileType = () => {
if (externalAccept) {
if (externalAccept === "*")
return "document";
const acceptList = externalAccept.split(",");
if (acceptList.some(format => videoAccept.split(",").includes(format)))
return "video";
if (acceptList.some(format => documentAccept.split(",").includes(format)))
return "document";
if (acceptList.some(format => imageAccept.split(",").includes(format)))
return "image";
return "document";
}
return "image";
};
const fileType = externalFileType || getAutoFileType();
// 文件类型判断
const isImageType = fileType === "image";
const isVideoType = fileType === "video";
const isDocumentType = fileType === "document";
// 获取listType
const getListType = () => {
if (externalListType)
return externalListType;
if (externalAccept === "*")
return "text";
if (fileType === "image")
return "picture-card";
return "text";
};
const listType = getListType();
// 获取文件格式
const getAccept = () => {
if (externalAccept)
return externalAccept === "*" ? "" : externalAccept;
if (isImageType)
return imageAccept;
if (isVideoType)
return videoAccept;
if (isDocumentType)
return documentAccept;
return imageAccept;
};
const accept = getAccept();
// 获取默认上传数量
const getMaxCount = () => {
if (externalMaxCount)
return externalMaxCount;
if (isVideoType)
return 1;
if (isImageType)
return 4;
if (isDocumentType)
return 4;
return 1;
};
const maxCount = getMaxCount();
// 获取默认文件大小
const getSize = () => {
if (externalSize)
return externalSize;
if (isVideoType)
return 100;
return 0;
};
const size = getSize();
// 上传按钮文字
const uploadButtonText = externalUploadButtonText || (isVideoType ? "上传视频" : "上传附件");
// 文件格式提示
const acceptTip = accept.replace(/\./g, "").split(",").join("、");
// 生成提示信息 // 生成提示信息
const getTipText = () => { const getTipText = () => {
if (tipContent) if (tipContent)
@ -34,10 +119,7 @@ const Upload = (props) => {
const tips = [ const tips = [
`最多上传${maxCount}个文件`, `最多上传${maxCount}个文件`,
accept accept
? `并且只能上传${accept ? `并且只能上传${acceptTip}格式的文件`
.replace(/\./g, "")
.split(",")
.join("、")}格式的文件`
: "可以上传任意格式的文件", : "可以上传任意格式的文件",
size ? `文件大小不能超过${size}M` : "", size ? `文件大小不能超过${size}M` : "",
ratio ? `只能上传${ratio}分辨率的图片` : "", ratio ? `只能上传${ratio}分辨率的图片` : "",
@ -62,7 +144,7 @@ const Upload = (props) => {
// 验证文件格式 // 验证文件格式
if (acceptList.length > 0 && !acceptList.includes(suffix)) { if (acceptList.length > 0 && !acceptList.includes(suffix)) {
message.warning(`只能上传${accept}格式的文件`); message.warning(`只能上传${acceptTip}格式的文件`);
return; return;
} }
@ -107,7 +189,7 @@ const Upload = (props) => {
// 预览文件 // 预览文件
const handlePreview = (file) => { const handlePreview = (file) => {
if (["picture-card", "picture-circle", "picture"].includes(listType)) { if (isImageType) {
setPreviewImage(file.url || file.thumbUrl); setPreviewImage(file.url || file.thumbUrl);
setPreviewVisible(true); setPreviewVisible(true);
} }
@ -121,14 +203,19 @@ const Upload = (props) => {
// 上传按钮 // 上传按钮
const uploadButton const uploadButton
= ["picture-card", "picture-circle", "picture"].includes(listType) = isImageType
? ( ? (
<div> <div>
<PlusOutlined style={{ fontSize: 32 }} /> <PlusOutlined style={{ fontSize: 32 }} />
</div> </div>
) )
: ( : (
<Button type="primary" icon={<UploadOutlined />}>{uploadButtonText}</Button> <Button
type="primary"
icon={isVideoType ? <VideoCameraAddOutlined /> : <UploadOutlined />}
>
{uploadButtonText}
</Button>
); );
return ( return (
@ -148,9 +235,9 @@ const Upload = (props) => {
</AntUpload> </AntUpload>
{ {
showTip showTip
? (tipContent || getTipText()) && ( ? (getTipText()) && (
<div style={{ marginTop: 10, color: "#ff4d4f" }}> <div style={{ marginTop: 10, color: "#ff4d4f" }}>
{tipContent || getTipText()} {getTipText()}
</div> </div>
) )
: null : null

20
utils/index.d.ts vendored
View File

@ -31,6 +31,24 @@ type DataType
| "HTMLDocument" | "HTMLDocument"
| string; // 允许其他可能的类型 | string; // 允许其他可能的类型
// 定义 getFileSuffix 函数可能返回的常见类型
type FileSuffix
= | "jpg"
| "jpeg"
| "png"
| "mp4"
| "mp3"
| "pdf"
| "doc"
| "docx"
| "xls"
| "xlsx"
| "txt"
| "zip"
| "rar"
| "tar"
| string; // 允许其他可能的类型
// 为 findCharIndex 函数定义接口类型 // 为 findCharIndex 函数定义接口类型
interface FindCharIndexOptions { interface FindCharIndexOptions {
/** 查找的字符串 */ /** 查找的字符串 */
@ -186,7 +204,7 @@ export function paging<T>(options: PagingOptions): T[];
/** /**
* *
*/ */
export function getFileSuffix(name: string): string; export function getFileSuffix(name: string): FileSuffix;
/** /**
* *