first commit

This commit is contained in:
liu
2026-07-13 15:23:29 +08:00
commit 50885a98c8
473 changed files with 33772 additions and 0 deletions
+320
View File
@@ -0,0 +1,320 @@
import React, { useCallback, useImperativeHandle, useMemo, useState } from 'react';
import {
Form,
Button,
Space,
Row,
Col,
Modal,
Drawer
} from 'antd';
import { useTranslation } from 'react-i18next';
import type { XinFormProps, XinFormRef, SubmitterButton } from './typings';
import type { FormItemProps } from 'antd';
import type { FormColumn } from '@/components/XinFormField/FieldRender/typings';
import FieldRender from '@/components/XinFormField/FieldRender';
import {pick} from "lodash";
/**
* XinForm - JSON 配置动态表单组件
*/
function XinForm<T extends Record<string, any> = any>(props: XinFormProps<T>) {
const {
columns,
layoutType = 'Form',
grid = false,
rowProps,
colProps = { span: 12 },
onFinish,
formRef,
form,
modalProps,
drawerProps,
trigger,
submitter,
...formProps
} = props;
const { t } = useTranslation();
const [xinForm] = Form.useForm<T>(form);
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
// 暴露表单方法
useImperativeHandle(formRef, (): XinFormRef => ({
...xinForm,
open: handleOpen,
close: handleClose,
isOpen: () => open,
setLoading: (loading: boolean) => setLoading(loading),
}));
// 表单提交处理
const handleFinish = useCallback(async (values: T) => {
if (!onFinish) return;
try {
setLoading(true);
const result = await onFinish(values);
if (result !== false && (layoutType === 'ModalForm' || layoutType === 'DrawerForm')) {
setOpen(false);
}
} finally {
setLoading(false);
}
}, [onFinish, layoutType]);
// 打开弹窗/抽屉
const handleOpen =() => setOpen(true);
// 关闭弹窗/抽屉
const handleClose = () => setOpen(false);
// 渲染表单项
const renderFormItem = useCallback((column: FormColumn<T>, index: number): React.ReactNode => {
const {
dataIndex,
valueType,
title = '',
fieldProps = {},
dependency,
fieldRender,
colProps: columnColProps = { span: 12 }
} = column;
// Form.Item 允许的属性列表
const formItemPropKeys = [
'colon', 'extra', 'getValueFromEvent', 'help', 'hidden', 'htmlFor',
'initialValue', 'name', 'normalize',
'noStyle', 'preserve', 'tooltip', 'trigger',
// 验证相关
'required', 'rules', 'validateFirst', 'validateDebounce', 'validateStatus', 'hasFeedback',
'validateTrigger', 'valuePropName', 'messageVariables',
// 布局
'wrapperCol', 'layout', 'label', 'labelAlign', 'labelCol'
];
const formItemProps = pick(column, formItemPropKeys);
const key = String(dataIndex) || `form-item-${index}`;
if (dependency) {
// 有依赖时使用 Form.Item 的 shouldUpdate
return (
<Form.Item noStyle shouldUpdate>
{({getFieldsValue}) => {
const values = getFieldsValue() as T;
// 判断是否隐藏
const isHidden = dependency.visible ? !dependency.visible(values) : false;
if (isHidden) return null;
// 判断是否禁用
const isDisabled = dependency.disabled ? dependency.disabled(values) : false;
// 动态 fieldProps
const dynamicFieldProps = dependency.fieldProps ? dependency.fieldProps(values) : {};
const mergedFieldProps = {
...fieldProps,
...dynamicFieldProps,
disabled: isDisabled || fieldProps?.disabled,
}
const defaultFieldRender = (
<FieldRender
valueType={valueType}
placeholder={title}
{...mergedFieldProps}
/>
);
return grid ?
<Col
{...colProps}
{...columnColProps}
key={key}
>
<Form.Item
key={key}
name={dataIndex}
label={column.title || column.label}
{...formItemProps as FormItemProps}
>
{fieldRender ? fieldRender(xinForm) : defaultFieldRender}
</Form.Item>
</Col>
:
<Form.Item
key={key}
name={dataIndex}
label={column.title || column.label}
{...formItemProps as FormItemProps}
>
{fieldRender ? fieldRender(xinForm) : defaultFieldRender}
</Form.Item>
}}
</Form.Item>
);
} else {
const defaultFieldRender = (
<FieldRender
valueType={valueType}
placeholder={title}
{...fieldProps}
/>
);
return grid ?
<Col
{...colProps}
{...columnColProps}
key={key}
>
<Form.Item
key={key}
name={dataIndex}
label={column.title || column.label}
{...formItemProps as FormItemProps}
>
{fieldRender ? fieldRender(xinForm) : defaultFieldRender}
</Form.Item>
</Col>
:
<Form.Item
key={key}
name={dataIndex}
label={column.title || column.label}
{...formItemProps as FormItemProps}
>
{fieldRender ? fieldRender(xinForm) : defaultFieldRender}
</Form.Item>
}
}, [grid, form]);
// 渲染提交按钮
const renderSubmitter = useMemo(() => {
if (submitter?.render === false) return null;
const submitButton = (
<Button
type="primary"
loading={loading}
onClick={() => xinForm.submit()}
{...submitter?.submitButtonProps}
>
{submitter?.submitText || t('xin.form.submit')}
</Button>
);
const closeButton = (
<Button
loading={loading}
onClick={handleClose}
{...submitter?.closeButtonProps}
>
{submitter?.closeText || t('xin.form.cancel')}
</Button>
);
const resetButton = (
<Button
loading={loading}
onClick={() => xinForm.resetFields()}
{...submitter?.resetButtonProps}
>
{submitter?.resetText || t('xin.form.reset')}
</Button>
);
const buttons: SubmitterButton = {
submit: submitButton,
close: closeButton,
reset: resetButton,
};
if (typeof submitter?.render === 'function') {
return submitter.render(buttons, formRef);
}
if(layoutType === 'Form') {
return (
<Form.Item>
<Space>
{buttons.reset}
{buttons.submit}
</Space>
</Form.Item>
);
}else {
return (
<Space>
{buttons.reset}
{buttons.submit}
{buttons.close}
</Space>
);
}
}, [loading, form, submitter, t]);
// 表单内容
const formContent = useMemo(() => (
<Form
{...formProps}
form={xinForm}
onFinish={handleFinish}
>
{grid ? (
<Row {...rowProps}>
{columns.map((column, index) => renderFormItem(column, index))}
</Row>
) : (
columns.map((column, index) => renderFormItem(column, index))
)}
{(layoutType === 'Form') && renderSubmitter}
</Form>
), [ form, handleFinish, props, grid, rowProps, columns, renderFormItem, layoutType, renderSubmitter ]);
// 触发器
const triggerElement = useMemo(() => {
if (!trigger) return null;
return React.cloneElement(trigger as React.ReactElement<{ onClick?: () => void }>, {
onClick: handleOpen,
});
}, [trigger, handleOpen]);
// 根据 layoutType 渲染
if (layoutType === 'ModalForm') {
return (
<>
{triggerElement}
<Modal
open={open}
onCancel={handleClose}
footer={renderSubmitter}
{...modalProps}
>
{formContent}
</Modal>
</>
);
}
if (layoutType === 'DrawerForm') {
return (
<>
{triggerElement}
<Drawer
open={open}
onClose={handleClose}
footer={renderSubmitter}
{...drawerProps}
>
{formContent}
</Drawer>
</>
);
}
return formContent;
}
export default XinForm;
export type { XinFormProps, XinFormRef };