基于antd封装创建嵌套表单的方法

650 阅读1分钟

在做一些稍复杂一点的后端管理系统时,Form.List组件的使用就会很频繁:

像下图中这种,可能只是对应一个Array<string>或者稍微复杂一点的:

Array<{name: string; label: string;}>

image.png

再有复杂一点的,可能会有多层级表单嵌套

image.png

对于这种情况,我们封装了一个方法,可以避免去写复杂的Form.List

import { Button, Card, Form, Input, Row, Col } from 'antd'
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'
import { formItemLayout, formItemLayoutWithOutLabel } from './commonConfig'
import ImgFormItem from './ImgFormItem'

const createFormList = ({
  listName,
  listLabel,
  fields
}: {
  listName: string | Array<number | string>;
  listLabel: string;
  fields?: Array<{
    name: string; label: string; type?: string; span?: number; childFields?: Array<any>
  }>
}, initData?: any) => {
  const columnCount = fields?.length
  const columnSpan = !columnCount ? 23 : Math.floor(23 / columnCount)
  return <Form.List
    name={listName}
    rules={[
      {
        validator: async (_, names) => {
          if (!names || names.length < 1) {
            return Promise.reject(new Error('At least 1 item'));
          }
        },
      },
    ]}
  >
    {(innerFields, { add, remove }, { errors }) => (
      <>
        {innerFields.map((innerField, index) => {
          return <Form.Item
            {...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
            label={index === 0 ? listLabel : ''}
            required={false}
            key={innerField.key}
          >
            <Row style={{ background: '#eef', marginBottom: 10, padding: 10 }}>
              {(fields && fields.length > 0) ? 
              fields?.map((field, i) =>
                // 最终数据形式为
                // [ { field1: xxx, field2: xxx }, { field1: xxx, field2: xxx } ]
                <Col span={field.span || columnSpan} key={field.name}>
                  {
                    field.type === 'array' ?
                    createFormList({
                      listName: [innerField.name, field.name],
                      listLabel: field.label,
                      fields: field.childFields
                    }, initData ? initData[innerField.name] : undefined) :
                    field.type === 'img' ?
                    <ImgFormItem formVal={initData[innerField.name]} name={[innerField.name, field.name]} childName={field.name} label={field.label}></ImgFormItem> :
                    <Form.Item
                      name={[innerField.name, field.name]}
                      label={field.label}
                      validateTrigger={['onChange', 'onBlur']}
                      rules={[
                        {
                          required: true,
                        },
                      ]}
                    >
                      <Input placeholder="请输入" style={{ width: '96%' }} />
                    </Form.Item>
                  }
                </Col>
              ) : (
                // 最终数据形式为
                // [ 'xxx', 'xxx' ]
                <Col span={columnSpan}>
                  <Form.Item
                    name={[innerField.name]}
                    validateTrigger={['onChange', 'onBlur']}
                    rules={[
                      {
                        required: true,
                      },
                    ]}
                    noStyle
                  >
                    <Input placeholder="请输入" style={{ width: '96%' }} />
                  </Form.Item>
                </Col>
              )}
              <Col span={1}>
                {innerFields.length > 0 ? (
                  <MinusCircleOutlined
                    className="dynamic-delete-button"
                    onClick={() => remove(innerField.name)}
                  />
                ) : null}
              </Col>
            </Row>
          </Form.Item>
        })}
        <Form.Item
          label={innerFields.length === 0 ? listLabel: ''}
          {...(innerFields.length === 0 ? formItemLayout : formItemLayoutWithOutLabel)}>
          <Button
            type="dashed"
            onClick={() => add()}
            style={{ width: '100%' }}
            icon={<PlusOutlined />}
          >
            Add field
          </Button>
          <Form.ErrorList errors={errors} />
        </Form.Item>
      </>
    )}
  </Form.List>
}

export default createFormList

ImgFormItem:

import { Button, Card, Form, Input } from 'antd'
import { useEffect, useState } from 'react'

export default (props: {
  formVal: any;
  name: string | Array<string | number>;
  childName?: string;
  label?: string;
}) => {
  const { formVal, name, childName, label } = props
  const [url, setUrl] = useState('')

  useEffect(() => {
    setUrl(formVal?.[childName || name as string] || '')
  }, [formVal])

  return <>
    <Form.Item label={label || '图片链接'} name={name} rules={[{ required: true }]}>
      <Input onChange={e => setUrl(e.target.value)}></Input>
    </Form.Item>
    <Form.Item label='预览'><div style={{ maxHeight: 300, overflow: 'auto' }}><img src={url} width={'100%'}></img></div></Form.Item>
  </>
}

使用场景:

        createFormList({
          listName: 'menus',
          listLabel: '菜单配置项',
          fields: [
            { name: 'title', label: '名称' },
            { name: 'link', label: '跳转链接' },
            { name: 'submenu', label: '子菜单', type: 'array', childFields: [
              { name: 'title', label: '名称' },
              { name: 'link', label: '跳转链接' },
            ] },
          ]
        })
        createFormList({
          listName: 'titles',
          listLabel: '标题',
        })
        createFormList({
          listName: 'ourMainCustomers',
          listLabel: '下方轮播配置',
          fields: [
            // { name: 'imgUrl', label: '图片地址' },
            { name: 'imgUrl', label: '图片地址', type: 'img', span: 22 },
            { name: 'note', label: '说明', span: 22 },
            { name: 'previewUrl', label: '预览地址' },
            { name: 'previewImgUrl', label: '预览地址旁边小图片地址' },
          ]
        }, formVal?.ourMainCustomers || {})