Skip to content

表单设计器

一、字段类型

基本字段类型

  • 文本框、多行文本框、富文本框:通过TextBoxTextAreaHtmlEditItem组件实现,分别对应简单的文本输入、多行文本输入和富文本编辑功能。在代码中,这些组件根据字段的widget属性进行选择和渲染,例如在FormItem组件中,通过getWidget函数判断字段类型并选择相应的组件进行展示。
  • 数字框:使用NumberBox组件,支持设置精度、范围等属性。在AttributeConfig组件中,针对数字框配置了精度、最大值、最小值等校验规则,确保输入数据符合要求。
  • 日期选择框、时间选择框:通过DateBox组件实现,支持日期和时间的选择,并可设置显示格式。在渲染时,根据字段的选项配置显示格式,并在值变化时进行格式化处理。

特殊字段类型

  • 单选框、多选框:使用SelectBoxMultiSelectBox组件,支持从预定义的选项中选择单个或多个值。在FormItem组件中,根据字段的lookups属性加载选项数据,并通过searchEnabled等属性增强用户体验。
  • 引用选择框:通过DataBox组件实现,用于从关联表单中选择数据。在DataBox组件中,通过EditModal.showFormSelect方法打开表单选择界面,选择数据后更新字段值。
  • 多级选择框:使用TreeSelectItemTreeModal等组件,支持层级数据的选择。在TreeSelectItem组件中,通过TreeView组件渲染层级数据,并支持搜索、选择等功能。
  • 成员选择框、内部机构选择框:通过MemberBoxPropsDepartmentBox组件实现,用于选择组织架构中的成员或部门。这些组件通过组织架构的数据接口加载成员或部门列表,并支持搜索、选择等操作。

自定义字段类型

  • 地图选择框:通过MapEditItem组件实现,用于地图位置的选择。在组件中,通过调用地图选择模态框,获取用户选择的经纬度信息,并将其展示在文本框中。
  • 文件选择框:使用SelectFilesItem组件,支持文件的上传和选择。组件中集成了文件上传功能,并展示了已上传文件的列表,支持预览和下载操作。

文本框

tsx
case '文本框':
  return <TextBox {...mixOptions} />;

FormItem组件中,通过TextBox组件实现简单的文本输入功能。mixOptions包含了字段的配置信息,如标签、值、校验规则等。TextBox组件会根据这些配置渲染相应的输入框,并支持基本的文本输入操作。

数字框

tsx
case '数字框':
  return (
    <NumberBox
      {...mixOptions}
      format={
        isNumber(value) &&
        value !== 0 &&
        `#.${'0'.repeat(props.field.options?.accuracy ?? 2)}`
      }
    />
  );

数字框通过NumberBox组件实现,支持设置精度和格式。format属性根据字段的精度配置动态生成显示格式,确保输入的数字符合要求。同时,mixOptions传递了字段的值、校验规则等信息,使组件能够进行数值范围校验等操作。

日期选择框

tsx
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事件处理函数对选择的日期进行格式化处理,确保返回的值符合预期格式。

单选框

tsx
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组件来处理更复杂的关联选择场景。

引用选择框

tsx
case '引用选择框':
  return (
    <DataBox
      {...mixOptions}
      field={props.field}
      attributes={props.form?.attributes}
      target={props.belong}
      metadata={props.form}
    />
  );

引用选择框通过DataBox组件实现,用于从关联表单中选择数据。组件接收字段、表单、所属目标等信息作为参数,通过内部的逻辑打开表单选择界面,允许用户选择数据,并将选择结果更新到字段值中。

多级选择框

tsx
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} />;
  }

多级选择框根据显示类型的不同,选择使用TreeModalTreeSelectItem组件。TreeModal组件通过模态框的方式提供更丰富的选择界面,支持层级数据的展示和选择;TreeSelectItem组件则直接在下拉框中渲染层级数据,适用于简单的层级选择场景。

文件选择框

tsx
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组件实现了自定义校验规则的配置。用户可以通过可视化界面构建复杂的校验表达式,确保字段值满足特定的业务规则。

必填校验

tsx
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类名来高亮显示必填字段,提示用户进行输入。

数字校验

tsx
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编辑器,用户可以设置数字框的精度,确保输入的数字符合要求。这些配置信息会传递到渲染组件中,用于控制数字框的输入行为和校验规则。

文本校验

tsx
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属性限制输入文本的长度。用户可以在配置界面设置最大长度值,该值会传递到渲染组件中,用于控制文本框的输入行为,确保输入的文本长度不超过限制。

表达式校验

tsx
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组件中,通过labelModelabelLocation等属性控制标签与字段的相对位置,并通过getItemWidth函数根据字段编号计算标签宽度。标签模式支持floatingstatic等类型,位置可设置为lefttop等,以适应不同的布局需求。

表单整体布局

  • FormConfig组件中,提供了对表单整体布局的配置,如特性宽度、导入匹配设置等。通过options.itemWidth属性设置表单中每个字段的宽度,确保表单在不同设备上都能有良好的显示效果。

响应式布局

  • 使用Flex布局和Grid布局来实现字段的灵活排列,确保在不同设备上都能有良好的显示效果。在FormItem组件中,通过flexWrap属性控制字段容器的换行行为,适应不同屏幕宽度。

高级布局配置

  • ViewConfig组件中,提供了对视图类型的配置,以及单位设置、集群设置等高级布局选项。通过options.viewType属性选择视图类型,如默认视图、系统办事视图等,并根据视图类型配置相应的布局参数。

自定义布局

  • FormPrint组件中,通过PrintConfigModal实现了打印模板的自定义布局配置。用户可以通过拖拽、调整大小等方式自定义打印模板的布局,并保存配置以供后续使用。

标签与字段排列

tsx
const FormItem: React.FC<IFormItemProps> = (props) => {
  // ...
  const mixOptions: any = {
    // ...
    width: getItemWidth(props.numStr),
    labelMode: 'floating',
    labelLocation: 'left',
    // ...
  };
};

FormItem组件中,通过labelModelabelLocation等属性控制标签与字段的相对位置。labelMode支持floatingstatic等类型,labelLocation可设置为lefttop等,以适应不同的布局需求。width属性根据字段编号计算标签宽度,确保标签和字段在不同屏幕尺寸下都能有良好的排列。

表单整体布局

tsx
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属性设置表单中每个字段的宽度,确保表单在不同设备上都能有良好的显示效果。用户可以在配置界面调整这些布局参数,以满足不同的显示需求。

响应式布局

tsx
const FormItem: React.FC<IFormItemProps> = (props) => {
  // ...
  const mixOptions: any = {
    // ...
    width: getItemWidth(props.numStr),
    // ...
  };
};

使用Flex布局和Grid布局来实现字段的灵活排列,确保在不同设备上都能有良好的显示效果。在FormItem组件中,通过flexWrap属性控制字段容器的换行行为,适应不同屏幕宽度。getItemWidth函数根据字段编号计算标签宽度,进一步优化布局的响应式表现。

自定义布局

tsx
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实现了打印模板的自定义布局配置。用户可以通过界面选择、添加、删除打印模板,并对模板的布局进行调整。选中的模板会应用到表单的打印功能中,确保打印输出符合用户的需求。

云原生应用研究院版权所有