zy-react-library/src/components/FormBuilder/FormItemsRenderer.js

693 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { InfoCircleOutlined } from "@ant-design/icons";
import {
Button,
Checkbox,
Col,
DatePicker,
Divider,
Form,
Input,
InputNumber,
Radio,
Row,
Select,
Tooltip,
} from "antd";
import dayjs from "dayjs";
import { FORM_ITEM_RENDER_ENUM } from "../../enum/formItemRender";
import { getDataType } from "../../utils";
const { TextArea } = Input;
const { RangePicker } = DatePicker;
/**
* 表单项渲染器组件
* @param {object} props - 组件属性
* @param {Array} props.options - 表单配置项数组
* @param {object} props.labelCol - label 栅格配置
* @param {number} props.gutter - 栅格间距
* @param {number} props.span - 默认栅格占据列数
* @param {boolean} props.collapse - 是否折叠仅显示前3项
* @param {boolean} props.useAutoGenerateRequired - 是否自动生成必填规则
* @param {object} props.initialValues - 初始值
*/
const FormItemsRenderer = ({
options,
labelCol,
gutter = 24,
span = 12,
collapse = false,
useAutoGenerateRequired = true,
initialValues,
}) => {
const form = Form.useFormInstance();
// 获取表单值,优先使用 initialValues
const getFormValues = () => {
const formValues = form.getFieldsValue();
// 如果表单值为空但有 initialValues则使用 initialValues
if (Object.keys(formValues).length === 0 && initialValues) {
return initialValues;
}
return formValues;
};
// 获取传给组件的属性
const getComponentProps = (option) => {
return typeof option.componentProps === "function"
? option.componentProps(getFormValues())
: (option.componentProps || {});
};
// 获取 Form.List 独有的属性
const getFormListUniqueProps = (option) => {
const defaultProps = {
showAddButton: true,
showRemoveButton: true,
addButtonText: "添加",
removeButtonText: "删除",
options: [],
addDefaultValue: {},
addInsertIndex: undefined,
};
if (typeof option.formListUniqueProps === "function") {
return {
...defaultProps,
...option.formListUniqueProps(getFormValues()),
};
}
return {
...defaultProps,
...(option.formListUniqueProps || {}),
};
};
// 设置日期组件的属性
const setDateComponentProps = (option, formItemProps) => {
// 为日期组件添加特殊处理
if ([
FORM_ITEM_RENDER_ENUM.DATE,
FORM_ITEM_RENDER_ENUM.DATE_MONTH,
FORM_ITEM_RENDER_ENUM.DATE_YEAR,
FORM_ITEM_RENDER_ENUM.DATETIME,
].includes(option.render)) {
formItemProps.getValueFromEvent = (_, dateString) => dateString;
formItemProps.getValueProps = value => ({ value: value ? dayjs(value) : undefined });
}
// 为日期周组件添加特殊处理
if ([
FORM_ITEM_RENDER_ENUM.DATE_WEEK,
].includes(option.render)) {
formItemProps.getValueFromEvent = (_, dateString) => dateString;
formItemProps.getValueProps = value => ({ value: value ? dayjs(value, "YYYY-wo") : undefined });
}
// 为日期范围组件添加特殊处理
if ([
FORM_ITEM_RENDER_ENUM.DATE_RANGE,
FORM_ITEM_RENDER_ENUM.DATETIME_RANGE,
].includes(option.render)) {
formItemProps.getValueFromEvent = (_, dateString) => dateString;
formItemProps.getValueProps = value => ({ value: Array.isArray(value) ? value.map(v => v ? dayjs(v) : undefined) : undefined });
}
};
// 获取传给 formItem 的属性
const getFormItemProps = (option) => {
const formItemProps = typeof option.formItemProps === "function"
? option.formItemProps(getFormValues())
: (option.formItemProps || {});
setDateComponentProps(option, formItemProps);
return formItemProps;
};
// 获取items里的value和label字段key
const getItemsFieldKey = (option) => {
return {
valueKey: option?.itemsField?.valueKey || "bianma",
labelKey: option?.itemsField?.labelKey || "name",
};
};
// 获取 required
const getRequired = (required) => {
// 支持动态计算 required
return typeof required === "function"
? required(getFormValues())
: (required ?? true);
};
// 获取验证规则
const getRules = (option) => {
if (option.render === FORM_ITEM_RENDER_ENUM.DIVIDER)
return [];
const rules = [];
/** @type {string | Function} */
const render = option.render || FORM_ITEM_RENDER_ENUM.INPUT;
switch (render) {
case FORM_ITEM_RENDER_ENUM.INPUT:
option.useConstraints !== false && rules.push({ max: 50, message: "最多输入50字符" });
break;
case FORM_ITEM_RENDER_ENUM.TEXTAREA:
option.useConstraints !== false && rules.push({ max: 500, message: "最多输入500字符" });
break;
case FORM_ITEM_RENDER_ENUM.INPUT_NUMBER:
case FORM_ITEM_RENDER_ENUM.NUMBER:
option.useConstraints !== false && rules.push({ pattern: /^(\d+)(\.\d{1,2})?$/, message: "请输入正确的数字,最多保留两位小数" });
option.useConstraints !== false && rules.push({
validator: (_, value) => {
if (value && Math.abs(Number.parseFloat(value)) > 999999999) {
return Promise.reject("输入数值超出安全范围");
}
return Promise.resolve();
},
});
break;
case FORM_ITEM_RENDER_ENUM.INTEGER:
option.useConstraints !== false && rules.push({ pattern: /^(\d+)$/, message: "请输入正确的整数" });
option.useConstraints !== false && rules.push({
validator: (_, value) => {
if (value && Math.abs(Number.parseFloat(value)) > 999999999) {
return Promise.reject("输入数值超出安全范围");
}
return Promise.resolve();
},
});
break;
}
if (!useAutoGenerateRequired)
return option.rules ? (Array.isArray(option.rules) ? [...option.rules, ...rules] : [option.rules, ...rules]) : rules;
if (getRequired(option.required)) {
const isBlurTrigger = !option.render || [
FORM_ITEM_RENDER_ENUM.INPUT,
FORM_ITEM_RENDER_ENUM.TEXTAREA,
FORM_ITEM_RENDER_ENUM.INPUT_NUMBER,
FORM_ITEM_RENDER_ENUM.NUMBER,
FORM_ITEM_RENDER_ENUM.INTEGER,
].includes(option.render);
rules.push({ required: true, message: `${isBlurTrigger ? "请输入" : "请选择"}${option.label}` });
}
return option.rules ? (Array.isArray(option.rules) ? [...option.rules, ...rules] : [option.rules, ...rules]) : rules;
};
// 获取key
const getKey = (option) => {
return option.key || (getDataType(option.name) === "Array" ? option.name.join(".") : option.name);
};
// 使用 style 控制显示/隐藏
const getStyle = (index) => {
return collapse && index >= 3 ? { display: "none" } : undefined;
};
// 列数
const getCol = (option) => {
const itemSpan = option.render === FORM_ITEM_RENDER_ENUM.DIVIDER ? 24 : (option.span ?? span);
// 这里除以2是因为一行两列的span为4一行一列的span要为2不除以2的话一行一列的span不对
const itemLabelCol = option.labelCol ?? (itemSpan === 24 ? { span: labelCol.span / 2 } : labelCol);
const itemWrapperCol = option.wrapperCol ?? { span: 24 - itemLabelCol.span };
return { span: itemSpan, labelCol: itemLabelCol, wrapperCol: itemWrapperCol };
};
// 获取 col 样式
const getColStyle = (option) => {
return typeof option.colStyle === "function"
? option.colStyle(getFormValues())
: option.colStyle;
};
// 获取 col 标题Col 内部、Form.Item 之后)
const getColTitle = (option) => {
return typeof option.colTitle === "function"
? option.colTitle(getFormValues())
: option.colTitle;
};
// 获取 row 样式FormList 的每一行)
const getRowStyle = (option, field, fieldIndex) => {
return typeof option.rowStyle === "function"
? option.rowStyle(field, fieldIndex)
: option.rowStyle;
};
// 获取 row 标题FormList 每行末尾)
const getRowTitle = (option, field, fieldIndex) => {
return typeof option.rowTitle === "function"
? option.rowTitle(field, fieldIndex)
: option.rowTitle;
};
// 获取是否动态表单项
const getIsDynamicFormItem = (option, formItemProps) => {
return (option.shouldUpdate ?? option.dependencies) || (formItemProps.shouldUpdate ?? formItemProps.dependencies);
};
// 获取 hidden
const getHidden = (hidden) => {
// 支持动态计算 hidden
return typeof hidden === "function"
? hidden(getFormValues())
: (hidden ?? false);
};
// 获取 listOptions
const getListOptions = (listOptions, field, fieldIndex, add, remove, move) => {
return typeof listOptions === "function"
? listOptions(field, fieldIndex, { field, fieldIndex, add, remove, move })
: (listOptions ?? []);
};
// 获取可选项的属性(适用于 SELECT、RADIO、CHECKBOX
const getSelectableItemAttributes = (item, itemsFieldKey) => {
const value = item[itemsFieldKey.valueKey];
const label = typeof itemsFieldKey.labelKey === "function" ? itemsFieldKey.labelKey(item) : item[itemsFieldKey.labelKey];
const disabled = item.disabled;
return { value, label, disabled };
};
// 渲染表单控件
const renderFormControl = (option) => {
const componentProps = getComponentProps(option);
const itemsFieldKey = getItemsFieldKey(option);
/** @type {string | Function} */
const render = option.render ?? FORM_ITEM_RENDER_ENUM.INPUT;
const placeholder = option.placeholder ?? `${[FORM_ITEM_RENDER_ENUM.INPUT, FORM_ITEM_RENDER_ENUM.TEXTAREA, FORM_ITEM_RENDER_ENUM.INPUT_NUMBER, FORM_ITEM_RENDER_ENUM.NUMBER, FORM_ITEM_RENDER_ENUM.INTEGER].includes(render) ? "输入" : "选择"}${option.label}`;
switch (render) {
case FORM_ITEM_RENDER_ENUM.INPUT:
return <Input placeholder={placeholder} maxLength={option.useConstraints !== false ? 50 : 9999} {...componentProps} />;
case FORM_ITEM_RENDER_ENUM.TEXTAREA:
return <TextArea placeholder={placeholder} maxLength={option.useConstraints !== false ? 500 : 9999} showCount={true} rows={3} {...componentProps} />;
case FORM_ITEM_RENDER_ENUM.INPUT_NUMBER:
case FORM_ITEM_RENDER_ENUM.NUMBER:
case FORM_ITEM_RENDER_ENUM.INTEGER:
return <InputNumber placeholder={placeholder} style={{ width: "100%" }} {...componentProps} />;
case FORM_ITEM_RENDER_ENUM.SELECT:
return (
<Select
placeholder={placeholder}
showSearch={{ optionFilterProp: "label" }}
allowClear
options={(option.items || []).map(item => getSelectableItemAttributes(item, itemsFieldKey))}
{...componentProps}
/>
);
case FORM_ITEM_RENDER_ENUM.RADIO:
return <Radio.Group options={(option.items || []).map(item => getSelectableItemAttributes(item, itemsFieldKey))} {...componentProps} />;
case FORM_ITEM_RENDER_ENUM.CHECKBOX: {
const selectableOptions = (option.items || []).map(item => getSelectableItemAttributes(item, itemsFieldKey));
// checkboxCol 时需要栅格布局Checkbox.Group 的 options 不支持列布局,此处使用 children + Row/Col 保持原有行为
if (option.checkboxCol) {
return (
<Checkbox.Group {...componentProps}>
<Row>
{selectableOptions.map(({ value, label, disabled }) => (
<Col span={option.checkboxCol} key={value}>
<Checkbox value={value} disabled={disabled}>
{label}
</Checkbox>
</Col>
))}
</Row>
</Checkbox.Group>
);
}
return <Checkbox.Group options={selectableOptions} {...componentProps} />;
}
case FORM_ITEM_RENDER_ENUM.DATE:
return <DatePicker placeholder={placeholder} format="YYYY-MM-DD" style={{ width: "100%" }} {...componentProps} />;
case FORM_ITEM_RENDER_ENUM.DATE_MONTH:
return (
<DatePicker
picker="month"
placeholder={placeholder}
format="YYYY-MM"
style={{ width: "100%" }}
{...componentProps}
/>
);
case FORM_ITEM_RENDER_ENUM.DATE_YEAR:
return (
<DatePicker
picker="year"
placeholder={placeholder}
format="YYYY"
style={{ width: "100%" }}
{...componentProps}
/>
);
case FORM_ITEM_RENDER_ENUM.DATE_WEEK:
return (
<DatePicker
picker="week"
placeholder={placeholder}
format="YYYY-wo"
style={{ width: "100%" }}
{...componentProps}
/>
);
case FORM_ITEM_RENDER_ENUM.DATE_RANGE:
return (
<RangePicker
placeholder={[`请选择开始${option.label}`, `请选择结束${option.label}`]}
format="YYYY-MM-DD"
style={{ width: "100%" }}
{...componentProps}
/>
);
case FORM_ITEM_RENDER_ENUM.DATETIME:
return (
<DatePicker
showTime
placeholder={placeholder}
format="YYYY-MM-DD HH:mm:ss"
style={{ width: "100%" }}
{...componentProps}
/>
);
case FORM_ITEM_RENDER_ENUM.DATETIME_RANGE:
return (
<RangePicker
showTime
placeholder={[`请选择开始${option.label}`, `请选择结束${option.label}`]}
format="YYYY-MM-DD HH:mm:ss"
style={{ width: "100%" }}
{...componentProps}
/>
);
case FORM_ITEM_RENDER_ENUM.DIVIDER:
return null;
default:
return render;
}
};
// 渲染 label带提示
const renderLabel = (option) => {
if (!option.tip)
return option.label;
return (
<>
{option.label}
<Tooltip title={option.tip}>
<InfoCircleOutlined style={{ marginLeft: 4, fontSize: 12 }} />
</Tooltip>
</>
);
};
// 渲染普通表单项
const renderFormItem = ({ option, style, col, index, preserve }) => {
const formItemProps = getFormItemProps(option);
delete formItemProps.dependencies;
delete formItemProps.shouldUpdate;
if (getHidden(option.hidden))
return null;
return (
<Col key={getKey(option) || index} span={col.span} style={{ ...style, ...getColStyle(option) }}>
{getColTitle(option)}
<Form.Item
name={option.name}
label={renderLabel(option)}
rules={getRules(option)}
labelCol={col.labelCol}
wrapperCol={col.wrapperCol}
preserve={preserve}
required={useAutoGenerateRequired ? (renderLabel(option) === " " ? false : getRequired(option.required)) : false}
colon={renderLabel(option) !== " "}
{...formItemProps}
>
{renderFormControl(option)}
</Form.Item>
</Col>
);
};
// 渲染特殊类型的表单项
const renderOtherTypeItem = ({ option, style, col, index, preserve }) => {
const componentProps = getComponentProps(option);
if (getHidden(option.hidden))
return null;
// 如果是 customizeRender 类型,完全交给外部控制渲染
if (option.customizeRender) {
return (
<Col key={getKey(option) || index} span={col.span} style={style}>
{option.render}
</Col>
);
}
// 如果是 onlyForLabel 类型不渲染任何UI只在表单中保存数据
if (option.onlyForLabel) {
return (
<Form.Item
key={getKey(option) || index}
name={option.name}
noStyle
preserve={preserve}
>
<input type="hidden" />
</Form.Item>
);
}
// 如果是分割线
if (option.render === FORM_ITEM_RENDER_ENUM.DIVIDER) {
return (
<Col key={getKey(option) || index} span={col.span} style={style}>
<Divider titlePlacement="start" {...componentProps}>{option.label}</Divider>
</Col>
);
}
return null;
};
// 渲染 Form.List
const renderFormList = ({ option, index, col, style }) => {
const formListUniqueProps = getFormListUniqueProps(option);
const componentProps = getComponentProps(option);
if (getHidden(option.hidden))
return null;
return (
<Col key={getKey(option) || index} span={col.span} style={{ ...style, ...getColStyle(option) }}>
{getColTitle(option)}
<Form.List name={option.name} {...componentProps}>
{(fields, { add, remove, move }) => (
<>
{fields.map((field, fieldIndex) => {
const listOptions = getListOptions(formListUniqueProps.options, field, fieldIndex, add, remove, move);
const rowStyle = getRowStyle(option, field, fieldIndex);
const rowTitle = getRowTitle(option, field, fieldIndex);
return (
<Row gutter={gutter} key={field.key} style={rowStyle}>
{rowTitle}
{listOptions.map((listOption, listIndex) => {
const col = getCol(listOption);
const formItemProps = getFormItemProps(listOption);
const params = {
option: listOption,
style,
col,
index: `${fieldIndex}_${listIndex}`,
preserve: true,
};
// 如果配置了 shouldUpdate 或 dependencies使用 Form.Item 的联动机制
// 注意:动态检测必须在 renderOtherTypeItem 之前否则特殊类型customizeRender / onlyForLabel / DIVIDER无法响应联动
if (getIsDynamicFormItem(listOption, formItemProps))
return renderDynamicFormItem(params);
const otherTypeItem = renderOtherTypeItem(params);
if (otherTypeItem)
return otherTypeItem;
if (listOption.render === FORM_ITEM_RENDER_ENUM.FORM_LIST)
return renderFormList(params);
// 从后往前查找最后一个普通可见表单项,特殊项不允许承载操作按钮。
const findLastButtonIndex = () => {
for (let i = listOptions.length - 1; i >= 0; i--) {
const opt = listOptions[i];
if (!getHidden(opt.hidden) && !opt.onlyForLabel && opt.render !== FORM_ITEM_RENDER_ENUM.FORM_LIST) {
return i;
}
}
return -1;
};
const lastButtonIndex = findLastButtonIndex();
if (listIndex === lastButtonIndex) {
delete formItemProps.dependencies;
delete formItemProps.shouldUpdate;
return (
<Col key={getKey(listOption) || listIndex} span={col.span} style={{ ...style, ...getColStyle(listOption) }}>
{getColTitle(listOption)}
<Form.Item
label={renderLabel(listOption)}
labelCol={col.labelCol}
wrapperCol={col.wrapperCol}
preserve={false}
required={useAutoGenerateRequired ? (renderLabel(listOption) === " " ? false : getRequired(listOption.required)) : false}
colon={renderLabel(listOption) !== " "}
{...formItemProps}
>
<div style={{ display: "flex", gap: 10, alignItems: "center", justifyContent: "space-between" }}>
<div style={{ flex: 1, minWidth: 0 }}>
<Form.Item
noStyle
rules={getRules(listOption)}
name={listOption.name}
>
{renderFormControl(listOption)}
</Form.Item>
</div>
{
// 只有当不是第一行时才显示删除按钮
fieldIndex >= 1
? (
formListUniqueProps.showRemoveButton
&& (
<Button
type="primary"
danger
onClick={() => remove(field.name)}
>
{formListUniqueProps.removeButtonText}
</Button>
)
)
: (
// 第一行显示添加按钮
formListUniqueProps.showAddButton
&& (
<Button
type="primary"
onClick={() => add(formListUniqueProps.addDefaultValue, formListUniqueProps.addInsertIndex)}
>
{formListUniqueProps.addButtonText}
</Button>
)
)
}
</div>
</Form.Item>
</Col>
);
}
return renderFormItem(params);
})}
</Row>
);
})}
</>
)}
</Form.List>
</Col>
);
};
// 渲染需要动态更新的表单项
const renderDynamicFormItem = ({ option, index, style, col, preserve }) => {
const formItemProps = getFormItemProps(option);
return (
<Form.Item
key={getKey(option) || index}
noStyle
preserve={preserve}
shouldUpdate={option.shouldUpdate ?? formItemProps.shouldUpdate}
dependencies={option.dependencies ?? formItemProps.dependencies}
>
{() => {
const otherTypeItem = renderOtherTypeItem({ option, style, col, index, preserve });
if (otherTypeItem)
return otherTypeItem;
if (option.render === FORM_ITEM_RENDER_ENUM.FORM_LIST)
return renderFormList({ option, index, col, style });
return renderFormItem({ option, style, col, index, preserve });
}}
</Form.Item>
);
};
return (
<>
{options.map((option, index) => {
const col = getCol(option);
const style = getStyle(index);
const formItemProps = getFormItemProps(option);
const params = {
option,
style,
col,
index,
preserve: false,
};
// 如果配置了 shouldUpdate 或 dependencies使用 Form.Item 的联动机制
if (getIsDynamicFormItem(option, formItemProps))
return renderDynamicFormItem(params);
// 处理特殊类型的表单项
const otherTypeItem = renderOtherTypeItem(params);
if (otherTypeItem)
return otherTypeItem;
// 如果是 Form.List
if (option.render === FORM_ITEM_RENDER_ENUM.FORM_LIST)
return renderFormList(params);
// 普通表单项(静态配置)
return renderFormItem(params);
})}
</>
);
};
FormItemsRenderer.displayName = "FormItemsRenderer";
export default FormItemsRenderer;