动态表单(都是下拉框组件)的一种设计思路【设置标签】

541 阅读1分钟

一、需求场景

  1. 当表单内都是下拉框组件时,提交时可以将所有选中的id放到数组中,回显时通过这个数组去回显。实际业务场景中,此表单内只有下拉框这种类型的组件,下拉框可以支持多选,可以是级联下拉
  2. 写成动态表单的好处:需求可能对字段进行拓展,并且基本都是下拉类型的组件,封装成动态表单,后续再增加或减少字段类型,只需要后端维护一下某个字段的增减和对这个字段的下拉的维护,前端不需要改动代码

二、分析:

  1. 提交时通过Object.values()获取tagIdList,需要将二维数组转为一维数组
  2. 回显时,通过接口获取每个字段,每个字段对应的下拉选项,以及通过tagIdList和每个下拉中的id比对回显在下拉框中
  3. id比对时,多选有多个id,级联组件需要把当前id的上级id找到才能回显 image.png

三、实现(react+antd)

1、绑定数据

    <Modal
      title="设置标签"
      visible={visible}
      onOk={onOk}
      onCancel={() => onCancel()}
    >
      <Form form={form}>
        {labelList.map(({ label, value, title, options }) => (
          <Form.Item
            label={label}
            key={value}
            name={`select${value}`} // 通过select1 select2...绑定每个字段的值
          >
            {value === 5 ? ( // 5=级联下拉
              <Cascader options={options} allowClear placeholder={`请选择${label}`} />
            ) : (
              <Select
                options={options}
                allowClear
                placeholder={`请选择${label}`}
                mode={value === 1 ? 'multiple' : undefined} // 1=多选
              />
            )}
          </Form.Item>
        ))}
      </Form>
    </Modal>

2、提交

  const onOk = async () => {
    const formData = await form.validateFields() // 只需要将select1 select2...对应的值提交,通过Object.values()获取
    const arr = Object.values(formData).reduce((acc, cur) => acc.concat(cur), []) // 二维数组扁平化
    const tagIdList: number[] = arr.filter((item: number | string) => typeof item === 'number') // 过滤掉字符串和undefined
    const params = { ids: info.ids, tagIdList }
    const { success } = await setTagApi(params)
    if (success) {
      onCancel('refresh')
      message.success('设置标签成功')
    }
  }

3、回显

需要得到这样一个对象,用于回显,其中1=多选5=级联为数组形式

    { select1: [19, 23, 122], select2: 54, select3: 55, select4: 56, select5: ["A", 68] }

封装getEchoObj方法,依赖getNodePathByValue方法和getIdsByTree方法

  // 根据value值获取当前节点的路径 ['A', 33]
  const getNodePathByValue = (tree: SelectListType[], value: number, path: number[] = []) => {
    if (!tree) return []
    for (const item of tree) {
      path.push(item.value as number)
      if (item.value === value) return path
      if (item.children) {
        const findChildren: number[] = getNodePathByValue(item.children, value, path) // 形如 ['A', 33] 的数组
        if (findChildren.length) return findChildren
      }
      path.pop()
    }
    return []
  }
  // 获取tree中所有的value集合
  const getIdsByTree = (tree: SelectListType[], result: number[] = []) => {
    for (const item of tree) {
      if (item.children) {
        result.push(item.value as number)
        getIdsByTree(item.children, result)
      }
    }
    return result
  }
  // 获取回显的对象
  const getEchoObj = (list: SelectListType[], tagIdList: number[]) => {
    const obj = {}
    const multipleArr: number[] = [] // 【操作角色】多选,需要用数组回显
    for (const item of list) {
      obj[`select${item.value}`] = undefined
      if (item.value === 5) {
        const values = getIdsByTree(item.options)
        const id = values.find((i) => tagIdList.includes(i))
        obj[`select${item.value}`] = getNodePathByValue(item.options, id) // 【竞对关系】是级联组件,需要将其父级找到用数组回显
      } else {
        item.options.forEach((i) => {
          tagIdList.forEach((j) => {
            if (i.value === j) {
              if (item.value === 1) {
                multipleArr.push(j)
                obj[`select${item.value}`] = multipleArr
              } else {
                obj[`select${item.value}`] = j
              }
            }
          })
        })
      }
    }
    return obj
  }

四、完整代码

点击查看详细内容
// 设置标签Modal  使用场景:1、客户经营-客户详情-客户关系发展-联系人-设置标签 2、壳子信息管理-联系人库-设置标签
import React, { useEffect, useState } from 'react'
import { Form, Select, Cascader, message, Modal, Tooltip } from 'antd'
import { getTagListApi, getTagSubTypeApi } from '@/services/select'
import { setTagApi } from './service'
import { ServeOptionsType, OptionsType } from '@/utils/typescriptInterface'
import './index.less'

interface props {
  visible: boolean
  onCancel: (val?: string) => void // 如果取消时传递了字符串refresh,则刷新列表
  info: {
    ids: number[] // 联系人id
    tagIdList: number[] // 标签id 用户回显标签
    contactNames?: string // 批量设置时需回显联系人名称
  }
}

// 下拉框列表:value label title options children
interface SelectListType {
  label: string
  value: number | string // 竞对关系的value值为string,其余都是number
  title?: string | JSX.Element
  options?: OptionsType[]
  children?: SelectListType[]
  id?: number
  name?: string
}

/*
  labelList数据结构:
    [
      { label: '操作角色', value: 1, options: [...], title: '' },
      { label: '客户态度', value: 2, options: [...], title: '' },
      { label: '决策角色', value: 3, options: [...], title: '' },
      { label: '社交类型', value: 4, options: [...] },
      { label: '竞对关系', value: 5, options: [...], title: '' } // 级联组件,options为树结构
    ]

  提交(onOk):
    1、参数一:ids是一个数组,存一个或多个id
    2、参数二:tagIdList也是一个数组,存放所有的下拉框中选中的id

  回显(init):
    1、通过getTagSubTypeApi获取有几个字段
    2、通过getTagListApi获取每个字段对应的下拉options
      ①竞对关系的第一级id都是null,需要将name(此处的name是唯一的)赋值给value
    3、通过getEchoObj方法,获取对象,形如:{select1: [23], select2: 54, select3: 55, select4: 56, select5: ['A', 33]},用于回显
      ①操作角色比较特殊,它是多选的,所以用数组multipleArr去存储
      ②竞对关系也比较特殊,是一个级联组件,回显时也是用数组去存储,通过getNodePathByValue方法获取当前节点的路径,形如:['A', 33]

*/

const SetLabelModal: React.FC<props> = (props) => {
  const { visible, onCancel, info } = props
  const [form] = Form.useForm()
  const [loading, setLoading] = useState<boolean>(false) // 提交按钮loading
  const [labelList, setLabelList] = useState<SelectListType[]>([])

  const getTagList = async (subType: number) => {
    const { success, data } = await getTagListApi(2, subType) // 此处type恒为2
    if (success) {
      const options =
        subType === 5
          ? recursion(data)
          : data.map(({ id, name }: ServeOptionsType) => ({ label: name, value: id }))
      return options
    }
  }

  // 竞对关系(5)是级联下拉组件,需要通过递归去处理
  const recursion = (list: SelectListType[], result: SelectListType[] = []) => {
    result = list.map((item) => {
      if (item.children) item.children = recursion(item.children, [])
      return { label: item.name, value: item.id || item.name, children: item.children || [] }
    })
    return result
  }

  const titleMap = {
    1: '识别出该项目校长级、副校级和年级层级的最核心的三个人。决策人-指学校的最高决策对象,一般是校长或者书记等。执行人-指该项目的分管副校或者年级副校等副校这个层级的人。操作人-指该项目的年级负责人,一般是年级主任或者教务主任等。',
    2: '3-教练、2-支持且排他、1-支持、0-中立、-1-不认可。如未接触客户,空白。',
    3: '决策角色 D-决策者,S-支持者,I-影响者。',
    5: (
      <div style={{ textAlign: 'justify' }}>
        <div>1、竞对代码,竞争代码和对应的竞争全名如下:</div>
        <div style={{ color: '#E8AF5C' }}>
          A-皖新十分钟,C-优创教育,D-状元搭档,E-升学在线,F-52高考,G-赶考网,H-汇高考,K-阳光高考指导网,L-乐课,M-铭学锦程,N-91逃课,S-升学指导网,T-天府尚学,U-U课通,X-小杨科技,Y-优途,Z-智学网
        </div>
        <div>2、竞对态度,客户对竞争的态度只选2、3进行呈现。</div>
      </div>
    )
  }

  // 初始化:获取有几个下拉框,并且每个下拉框所拥有的下拉列表,并且将选中的值回显
  const init = async () => {
    const { success, data } = await getTagSubTypeApi(2) // 1=学校 2=联系人
    if (!success) return
    const list = await Promise.all(
      data.map(async ({ name, id }: ServeOptionsType) => ({
        label: name,
        value: id,
        options: await getTagList(id),
        title: titleMap[id]
      }))
    )
    setLabelList(list)
    // 回显值
    const obj = getEchoObj(list, info.tagIdList)
    form.setFieldsValue(obj)
  }

  useEffect(() => {
    info.ids.length && init()
  }, [info.ids])

  // 根据value值获取当前节点的路径 ['A', 33]
  const getNodePathByValue = (tree: SelectListType[], value: number, path: number[] = []) => {
    if (!tree) return []
    for (const item of tree) {
      path.push(item.value as number)
      if (item.value === value) return path
      if (item.children) {
        const findChildren: number[] = getNodePathByValue(item.children, value, path) // 形如 ['A', 33] 的数组
        if (findChildren.length) return findChildren
      }
      path.pop()
    }
    return []
  }
  // 获取tree中所有的value集合
  const getIdsByTree = (tree: SelectListType[], result: number[] = []) => {
    for (const item of tree) {
      if (item.children) {
        result.push(item.value as number)
        getIdsByTree(item.children, result)
      }
    }
    return result
  }
  // 获取回显的对象
  const getEchoObj = (list: SelectListType[], tagIdList: number[]) => {
    const obj = {}
    const multipleArr: number[] = [] // 【操作角色】多选,需要用数组回显
    for (const item of list) {
      obj[`select${item.value}`] = undefined
      if (item.value === 5) {
        const values = getIdsByTree(item.options)
        const id = values.find((i) => tagIdList.includes(i))
        obj[`select${item.value}`] = getNodePathByValue(item.options, id) // 【竞对关系】是级联组件,需要将其父级找到用数组回显
      } else {
        item.options.forEach((i) => {
          tagIdList.forEach((j) => {
            if (i.value === j) {
              if (item.value === 1) {
                multipleArr.push(j)
                obj[`select${item.value}`] = multipleArr
              } else {
                obj[`select${item.value}`] = j
              }
            }
          })
        })
      }
    }
    return obj
  }

  // 提交
  const onOk = async () => {
    const formData = await form.validateFields()
    const arr = Object.values(formData).reduce((acc, cur) => acc.concat(cur), []) // 二维数组扁平化
    const tagIdList: number[] = arr.filter((item: number | string) => typeof item === 'number') // 过滤掉字符串和undefined
    const params = { ids: info.ids, tagIdList }
    setLoading(true)
    const { success } = await setTagApi(params)
    if (success) {
      onCancel('refresh')
      message.success('设置标签成功')
    }
    setLoading(false)
  }

  return (
    <Modal
      className="SetLabelModal"
      title="设置标签"
      visible={visible}
      onOk={onOk}
      onCancel={() => onCancel()}
      okButtonProps={{ loading: loading }}
      centered
      forceRender
    >
      <Form form={form} labelCol={{ span: 5 }} wrapperCol={{ span: 19 }}>
        {info?.contactNames?.length ? (
          <Form.Item label="已选联系人" name="ids">
            <span style={{ color: '#416EFF', fontSize: '14px' }}>{info.contactNames}</span>
          </Form.Item>
        ) : null}

        {labelList.map(({ label, value, title, options }) => (
          <Form.Item
            label={label}
            key={value}
            name={`select${value}`}
            extra={<Tooltip placement="rightTop" title={title} />}
            className={value === 4 && 'socialType'}
          >
            {value === 5 ? (
              <Cascader options={options} allowClear placeholder={`请选择${label}`} />
            ) : (
              <Select
                options={options}
                allowClear
                placeholder={`请选择${label}`}
                mode={value === 1 ? 'multiple' : undefined}
                removeIcon={value === 1 && <i className={'iconfont icon-a-shanchu'}></i>}
              />
            )}
          </Form.Item>
        ))}
      </Form>
    </Modal>
  )
}

export default SetLabelModal