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

View File

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

View File

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

View File

@ -75,7 +75,7 @@ const ImportFile = (props) => {
>
{children && typeof children === "function" ? children({ form }) : children}
<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>
</Modal>

View File

@ -11,12 +11,14 @@ export interface UploadProps extends Omit<AntUploadProps, "fileList"> {
ratio?: `${number}*${number}`;
/** 是否显示提示,默认 true */
showTip?: boolean;
/** 文件大小限制单位MB,默认 0不限制 */
/** 文件大小限制单位MB */
size?: number;
/** 自定义提示内容 */
tipContent?: ReactNode;
/** listType 为 text 时上传按钮文本,默认 "点击选择文件上传" */
/** listType 为 text 时上传按钮文本 */
uploadButtonText?: string;
/** 要上传的文件类型,默认为 image */
fileType?: "image" | "video" | "document";
}
/**
@ -26,3 +28,7 @@ export interface UploadProps extends Omit<AntUploadProps, "fileList"> {
declare const Upload: FC<UploadProps>;
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 { useState } from "react";
@ -10,15 +10,16 @@ const Upload = (props) => {
value = [],
onChange,
onPreview,
maxCount = 1,
listType = "picture-card",
accept = ["picture-card", "picture-circle", "picture"].includes(listType) ? ".jpg,.jpeg,.png" : "",
maxCount: externalMaxCount,
listType: externalListType,
accept: externalAccept,
ratio = "",
showTip = true,
multiple = true,
size = 0,
size: externalSize,
tipContent,
uploadButtonText = "点击选择文件上传",
uploadButtonText: externalUploadButtonText,
fileType: externalFileType,
formValues,
...restProps
} = props;
@ -26,6 +27,90 @@ const Upload = (props) => {
const [previewVisible, setPreviewVisible] = useState(false);
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 = () => {
if (tipContent)
@ -34,10 +119,7 @@ const Upload = (props) => {
const tips = [
`最多上传${maxCount}个文件`,
accept
? `并且只能上传${accept
.replace(/\./g, "")
.split(",")
.join("、")}格式的文件`
? `并且只能上传${acceptTip}格式的文件`
: "可以上传任意格式的文件",
size ? `文件大小不能超过${size}M` : "",
ratio ? `只能上传${ratio}分辨率的图片` : "",
@ -62,7 +144,7 @@ const Upload = (props) => {
// 验证文件格式
if (acceptList.length > 0 && !acceptList.includes(suffix)) {
message.warning(`只能上传${accept}格式的文件`);
message.warning(`只能上传${acceptTip}格式的文件`);
return;
}
@ -107,7 +189,7 @@ const Upload = (props) => {
// 预览文件
const handlePreview = (file) => {
if (["picture-card", "picture-circle", "picture"].includes(listType)) {
if (isImageType) {
setPreviewImage(file.url || file.thumbUrl);
setPreviewVisible(true);
}
@ -121,14 +203,19 @@ const Upload = (props) => {
// 上传按钮
const uploadButton
= ["picture-card", "picture-circle", "picture"].includes(listType)
= isImageType
? (
<div>
<PlusOutlined style={{ fontSize: 32 }} />
</div>
)
: (
<Button type="primary" icon={<UploadOutlined />}>{uploadButtonText}</Button>
<Button
type="primary"
icon={isVideoType ? <VideoCameraAddOutlined /> : <UploadOutlined />}
>
{uploadButtonText}
</Button>
);
return (
@ -148,9 +235,9 @@ const Upload = (props) => {
</AntUpload>
{
showTip
? (tipContent || getTipText()) && (
? (getTipText()) && (
<div style={{ marginTop: 10, color: "#ff4d4f" }}>
{tipContent || getTipText()}
{getTipText()}
</div>
)
: null

20
utils/index.d.ts vendored
View File

@ -31,6 +31,24 @@ type DataType
| "HTMLDocument"
| string; // 允许其他可能的类型
// 定义 getFileSuffix 函数可能返回的常见类型
type FileSuffix
= | "jpg"
| "jpeg"
| "png"
| "mp4"
| "mp3"
| "pdf"
| "doc"
| "docx"
| "xls"
| "xlsx"
| "txt"
| "zip"
| "rar"
| "tar"
| string; // 允许其他可能的类型
// 为 findCharIndex 函数定义接口类型
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;
/**
*