表单设计器
一、字段类型
基本字段类型
- 文本框、多行文本框、富文本框:通过
TextBox、TextArea、HtmlEditItem组件实现,分别对应简单的文本输入、多行文本输入和富文本编辑功能。在代码中,这些组件根据字段的widget属性进行选择和渲染,例如在FormItem组件中,通过getWidget函数判断字段类型并选择相应的组件进行展示。 - 数字框:使用
NumberBox组件,支持设置精度、范围等属性。在AttributeConfig组件中,针对数字框配置了精度、最大值、最小值等校验规则,确保输入数据符合要求。 - 日期选择框、时间选择框:通过
DateBox组件实现,支持日期和时间的选择,并可设置显示格式。在渲染时,根据字段的选项配置显示格式,并在值变化时进行格式化处理。
特殊字段类型
- 单选框、多选框:使用
SelectBox和MultiSelectBox组件,支持从预定义的选项中选择单个或多个值。在FormItem组件中,根据字段的lookups属性加载选项数据,并通过searchEnabled等属性增强用户体验。 - 引用选择框:通过
DataBox组件实现,用于从关联表单中选择数据。在DataBox组件中,通过EditModal.showFormSelect方法打开表单选择界面,选择数据后更新字段值。 - 多级选择框:使用
TreeSelectItem、TreeModal等组件,支持层级数据的选择。在TreeSelectItem组件中,通过TreeView组件渲染层级数据,并支持搜索、选择等功能。 - 成员选择框、内部机构选择框:通过
MemberBoxProps、DepartmentBox组件实现,用于选择组织架构中的成员或部门。这些组件通过组织架构的数据接口加载成员或部门列表,并支持搜索、选择等操作。
自定义字段类型
- 地图选择框:通过
MapEditItem组件实现,用于地图位置的选择。在组件中,通过调用地图选择模态框,获取用户选择的经纬度信息,并将其展示在文本框中。 - 文件选择框:使用
SelectFilesItem组件,支持文件的上传和选择。组件中集成了文件上传功能,并展示了已上传文件的列表,支持预览和下载操作。
文本框
case '文本框':
return <TextBox {...mixOptions} />;在FormItem组件中,通过TextBox组件实现简单的文本输入功能。mixOptions包含了字段的配置信息,如标签、值、校验规则等。TextBox组件会根据这些配置渲染相应的输入框,并支持基本的文本输入操作。
数字框
case '数字框':
return (
<NumberBox
{...mixOptions}
format={
isNumber(value) &&
value !== 0 &&
`#.${'0'.repeat(props.field.options?.accuracy ?? 2)}`
}
/>
);数字框通过NumberBox组件实现,支持设置精度和格式。format属性根据字段的精度配置动态生成显示格式,确保输入的数字符合要求。同时,mixOptions传递了字段的值、校验规则等信息,使组件能够进行数值范围校验等操作。
日期选择框
case '日期选择框':
return (
<DateBox
{...mixOptions}
type={'date'}
displayFormat={props.field?.options?.displayFormat || 'yyyy年MM月dd日'}
onValueChanged={(e) => {
mixOptions.onValueChanged.apply(this, [
{
...e,
value: e.value
? formatDate(
e.value,
props.field?.options?.displayFormat
? props.field?.options?.displayFormat
.replace(/年|月/g, '-')
.replace(/日|号/g, '')
: 'yyyy-MM-dd',
)
: undefined,
},
]);
}}
/>
);日期选择框使用DateBox组件实现,支持日期的选择和格式化显示。displayFormat属性根据字段配置设置日期的显示格式,onValueChanged事件处理函数对选择的日期进行格式化处理,确保返回的值符合预期格式。
单选框
case '单选框':
case '选择框':
if (!isRelevanceId) {
return (
<SelectBox
{...mixOptions}
searchEnabled
searchMode="contains"
searchExpr={'text'}
dataSource={props.field.lookups}
displayExpr={'text'}
valueExpr={'value'}
/>
);
} else {
return <TreeSelect {...mixOptions} lookups={props.field.lookups} />;
}单选框通过SelectBox组件实现,支持从预定义的选项中选择单个值。dataSource属性绑定字段的选项数据,searchEnabled等属性增强用户体验,使其能够快速查找选项。如果选项涉及关联数据,则使用TreeSelect组件来处理更复杂的关联选择场景。
引用选择框
case '引用选择框':
return (
<DataBox
{...mixOptions}
field={props.field}
attributes={props.form?.attributes}
target={props.belong}
metadata={props.form}
/>
);引用选择框通过DataBox组件实现,用于从关联表单中选择数据。组件接收字段、表单、所属目标等信息作为参数,通过内部的逻辑打开表单选择界面,允许用户选择数据,并将选择结果更新到字段值中。
多级选择框
case '多级选择框':
if (options?.displayType === DisplayType.POPUP) {
return (
<TreeModal
{...mixOptions}
metadata={props.form}
directory={props.belong.directory}
attribute={props.form?.attributes.find((it) => it.id === props.field.id)}
onValuesChange={props.onValuesChange}
/>
);
} else {
return <TreeSelectItem {...mixOptions} speciesItems={props.field.lookups} />;
}多级选择框根据显示类型的不同,选择使用TreeModal或TreeSelectItem组件。TreeModal组件通过模态框的方式提供更丰富的选择界面,支持层级数据的展示和选择;TreeSelectItem组件则直接在下拉框中渲染层级数据,适用于简单的层级选择场景。
文件选择框
const SelectFilesItem: React.FC<TreeSelectItemProps> = (props) => {
const initFiles: FileItemShare[] = [];
if (props.values && props.values.length > 0) {
try {
var temps = JSON.parse(props.values);
if (temps && Array.isArray(temps) && temps.length > 0) {
initFiles.push(...temps);
}
} catch {
/* empty */
}
}
const [open, setOpen] = useState(false);
const [fileList, setFileList] = useState<FileItemShare[]>(initFiles);
// 点击选择数据
const onClick = () => {
if (!form) {
return message.warning('未查询到关联表单,无法选择数据!');
}
EditModal.showFormSelect({
form: form!,
fields: formInst?.fields!,
belong: (target as IBelong)!,
multiple,
onSave: (values) => {
const dataSource: any = values.map((item: any) => ({
...item,
formId: targetFormId,
id: item.id,
value: item.id,
text: item[nameAttribute],
}));
// 需要设置表单值
if (allowSetFieldsValue) {
const toSetData = Object.keys(dataSource[0])
.filter((id: any) => !isNaN(id) && id !== field.id)
.reduce((pre: any, cur) => {
pre[cur] = values[0][cur];
return pre;
}, {});
toSetData[field.id] = JSON.stringify(dataSource);
props.setFieldsValue && props.setFieldsValue(toSetData);
} else {
props.setFieldsValue &&
props.setFieldsValue({
[field.id]: JSON.stringify(dataSource),
});
}
setDataSource(dataSource);
},
});
};
// 初始化
useAsyncLoad(async () => {
if (targetFormId) {
let formList: XForm[] = [];
if (target) {
formList = (await target?.resource.formColl.find([targetFormId])) || [];
if (formList.length) {
// 设置表单
setForm(formList[0]);
const formInst = new Form(
{ ...formList[0], id: formList[0].id + '_' },
target.directory,
);
await formInst.loadFields();
// 设置表单实例
setFormInst(formInst);
}
}
return formInst;
}
});
return (
<TextArea {...props} minHeight={130}>
<div className={cls.imageUploader}>
{fileList.map((i, x) => {
return (
<div
className={cls.fileItem}
key={i.name + x}
title={i.name}
onClick={() => {
command.emitter('executor', 'open', i, 'preview');
}}>
<TypeIcon iconType={i.contentType ?? '文件'} size={50} />
<span>{ellipsisText(i.name, 10)}</span>
</div>
);
})}
<Button
style={{ marginLeft: 10, height: height as any }}
type="default"
onClick={onClick}>
选择数据
</Button>
</div>
</TextArea>
);
};文件选择框通过SelectFilesItem组件实现,支持文件的上传和选择。组件中集成了文件上传功能,并展示了已上传文件的列表,支持预览和下载操作。在代码中,通过EditModal.showFormSelect方法打开文件选择界面,用户可以选择文件并将其信息更新到字段值中。setDataSource方法用于更新数据源,确保选择的文件信息能够正确地传递到表单数据中。
二、校验规则
必填校验
- 在
FormItem组件中,通过判断options?.isRequired来决定是否在标签后添加*标识必填字段,并在渲染组件时传递isValid属性进行校验。当字段值为空且为必填时,通过添加formItemRequired类名来高亮显示必填字段。
数字校验
- 在
AttributeConfig组件中,针对数字框配置了精度、最大值、最小值等校验规则。在FormConfig组件中,通过CustomBuilder组件实现了对数字框值范围的校验规则配置。
文本校验
- 对于文本框,配置了最大长度校验。在
AttributeConfig组件中,通过maxlength属性限制输入文本的长度。
引用数据校验
- 在
DataBox组件中,确保选择的数据符合关联表单的字段要求。通过EditModal.showFormSelect方法,在选择数据时进行字段匹配校验,确保选择的数据与表单字段对应。
表达式校验
- 在
ConditionsModal组件中,通过CustomBuilder组件实现了自定义校验规则的配置。用户可以通过可视化界面构建复杂的校验表达式,确保字段值满足特定的业务规则。
必填校验
const FormItem: React.FC<IFormItemProps> = (props) => {
// ...
const [isValid, setIsValid] = useState(true);
// ...
useEffectOnce(() => {
for (const rule of props.rules) {
switch (rule.typeName) {
case 'isRequired':
setLabel(props.field.name + (rule.value ? '*' : ''));
break;
}
}
});
// ...
const mixOptions: any = {
// ...
isValid,
// ...
};
};在FormItem组件中,通过判断options?.isRequired来决定是否在标签后添加*标识必填字段。isValid状态用于控制字段的校验状态,当字段值为空且为必填时,通过添加formItemRequired类名来高亮显示必填字段,提示用户进行输入。
数字校验
const AttributeConfig: React.FC<IAttributeProps> = ({
current,
notifyEmitter,
index,
}) => {
// ...
const loadItemConfig = () => {
// ...
switch (attribute.widget || '') {
case '数字框':
return (
<SimpleItem
dataField="options.accuracy"
editorType="dxNumberBox"
label={{ text: '精度' }}
editorOptions={{
min: 0,
step: 1,
format: '#',
showClearButton: true,
showSpinButtons: true,
}}
/>
);
// ...
}
};
};在AttributeConfig组件中,针对数字框配置了精度、最大值、最小值等校验规则。通过dxNumberBox编辑器,用户可以设置数字框的精度,确保输入的数字符合要求。这些配置信息会传递到渲染组件中,用于控制数字框的输入行为和校验规则。
文本校验
const AttributeConfig: React.FC<IAttributeProps> = ({
current,
notifyEmitter,
index,
}) => {
// ...
const loadItemConfig = () => {
// ...
switch (attribute.widget || '') {
case '文本框':
return (
<SimpleItem
dataField="options.maxLength"
editorType="dxNumberBox"
label={{ text: '最大长度' }}
/>
);
// ...
}
};
};对于文本框,配置了最大长度校验。在AttributeConfig组件中,通过maxlength属性限制输入文本的长度。用户可以在配置界面设置最大长度值,该值会传递到渲染组件中,用于控制文本框的输入行为,确保输入的文本长度不超过限制。
表达式校验
const ConditionsModal: React.FC<IProps> = (props) => {
// ...
return (
<Modal
// ...
onOk={() => {
props.onOk.apply(this, [
{
id: props.current?.id ?? getUuid(),
name: name!,
remark: remark ?? '',
condition: condition,
conditionText: conditionText,
type: 'attribute',
trigger: mappingData.map((a) => a.trigger),
},
]);
}}
>
<div style={{ padding: 5 }}>
<span>条件*:</span>
<CustomBuilder
fields={props.fields}
displayText={conditionText}
onValueChanged={(value, text) => {
setCondition(value);
setConditionText(text);
}}
/>
</div>
</Modal>
);
};在ConditionsModal组件中,通过CustomBuilder组件实现了自定义校验规则的配置。用户可以通过可视化界面构建复杂的校验表达式,CustomBuilder组件会将构建的表达式转换为可执行的校验规则。当用户确认配置后,这些校验规则会应用到相应的字段上,确保字段值满足特定的业务规则。
三、布局
标签与字段排列
- 在
FormItem组件中,通过labelMode、labelLocation等属性控制标签与字段的相对位置,并通过getItemWidth函数根据字段编号计算标签宽度。标签模式支持floating、static等类型,位置可设置为left、top等,以适应不同的布局需求。
表单整体布局
- 在
FormConfig组件中,提供了对表单整体布局的配置,如特性宽度、导入匹配设置等。通过options.itemWidth属性设置表单中每个字段的宽度,确保表单在不同设备上都能有良好的显示效果。
响应式布局
- 使用
Flex布局和Grid布局来实现字段的灵活排列,确保在不同设备上都能有良好的显示效果。在FormItem组件中,通过flexWrap属性控制字段容器的换行行为,适应不同屏幕宽度。
高级布局配置
- 在
ViewConfig组件中,提供了对视图类型的配置,以及单位设置、集群设置等高级布局选项。通过options.viewType属性选择视图类型,如默认视图、系统办事视图等,并根据视图类型配置相应的布局参数。
自定义布局
- 在
FormPrint组件中,通过PrintConfigModal实现了打印模板的自定义布局配置。用户可以通过拖拽、调整大小等方式自定义打印模板的布局,并保存配置以供后续使用。
标签与字段排列
const FormItem: React.FC<IFormItemProps> = (props) => {
// ...
const mixOptions: any = {
// ...
width: getItemWidth(props.numStr),
labelMode: 'floating',
labelLocation: 'left',
// ...
};
};在FormItem组件中,通过labelMode、labelLocation等属性控制标签与字段的相对位置。labelMode支持floating、static等类型,labelLocation可设置为left、top等,以适应不同的布局需求。width属性根据字段编号计算标签宽度,确保标签和字段在不同屏幕尺寸下都能有良好的排列。
表单整体布局
const FormConfig: React.FC<IAttributeProps> = ({ notifyEmitter, current }) => {
return (
<Form
// ...
formData={current.metadata}
onFieldDataChanged={notityAttrChanged}>
<GroupItem>
<SimpleItem dataField="options.itemWidth" editorType="dxNumberBox" label={{ text: '特性宽度' }} />
// ...
</GroupItem>
</Form>
);
};在FormConfig组件中,提供了对表单整体布局的配置,如特性宽度、导入匹配设置等。通过options.itemWidth属性设置表单中每个字段的宽度,确保表单在不同设备上都能有良好的显示效果。用户可以在配置界面调整这些布局参数,以满足不同的显示需求。
响应式布局
const FormItem: React.FC<IFormItemProps> = (props) => {
// ...
const mixOptions: any = {
// ...
width: getItemWidth(props.numStr),
// ...
};
};使用Flex布局和Grid布局来实现字段的灵活排列,确保在不同设备上都能有良好的显示效果。在FormItem组件中,通过flexWrap属性控制字段容器的换行行为,适应不同屏幕宽度。getItemWidth函数根据字段编号计算标签宽度,进一步优化布局的响应式表现。
自定义布局
const FormPrint: React.FC<Iprops> = (props) => {
// ...
return (
<>
<div className={cls[`app-roval-node`]}>
<div className={cls[`roval-node`]}>
<Card
type="inner"
title={
<div>
<Divider type="vertical" className={cls['divider']} />
<span>打印模板设置</span>
</div>
}
className={cls['card-info']}
extra={
<>
<a
onClick={() => {
setPrintModalCreate(true);
}}>
添加
</a>
</>
}>
<SelectBox
showClearButton
value={printType}
placeholder="请选择打印模板"
dataSource={primaryPrints}
displayExpr={'name'}
valueExpr={'id'}
onFocusIn={() => {
setPrintType('');
}}
onValueChange={(e) => {
if (props.current.metadata.printData) {
props.current.metadata.printData.type = e;
} else {
props.current.metadata.printData = { type: e, attributes: [] };
}
setPrintType(e);
if (e == null) {
setPrintModal(false);
} else {
setPrintModal(true);
}
}}
itemRender={(data) => (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ whiteSpace: 'nowrap' }}>{data.name}</span>
<CloseOutlined
onClick={(e) => {
e.stopPropagation();
const newPrintData = props.current.metadata.primaryPrints.filter(
(option: any) => option.id !== data.id,
);
const newPrintData2 =
props.current.metadata.printData.attributes.filter(
(option: any) => option.title !== data.id,
);
props.current.metadata.primaryPrints = newPrintData;
props.current.metadata.printData.attributes = newPrintData2;
}}
/>
</div>
)}
/>
</Card>
{printModal && (
<PrintConfigModal
refresh={() => {
setPrintModal(false);
}}
print={primaryPrints}
printType={props.current.metadata.printData.type}
current={props.current}
primaryForms={props.current.metadata}
/>
)}
{printModalCreate && (
<OpenFileDialog
multiple
title={`选择打印模板`}
rootKey={''}
accepts={['打印模板']}
excludeIds={primaryPrints.map((i: any) => i.id)}
onCancel={() => setPrintModalCreate(false)}
onOk={(files) => {
if (files.length > 0) {
const prints = (files as unknown[] as IPrint[]).map((i) => i.metadata);
props.current.metadata.primaryPrints = [
...(props.current.metadata.primaryPrints ?? []),
...prints,
];
setPrimaryPrints([...props.current.metadata.primaryPrints]);
}
setPrintModalCreate(false);
}}
/>
)}
</div>
</div>
</>
);
};在FormPrint组件中,通过PrintConfigModal实现了打印模板的自定义布局配置。用户可以通过界面选择、添加、删除打印模板,并对模板的布局进行调整。选中的模板会应用到表单的打印功能中,确保打印输出符合用户的需求。