一、需求场景
- 当表单内都是
下拉框组件时,提交时可以将所有选中的id放到数组中,回显时通过这个数组去回显。实际业务场景中,此表单内只有下拉框这种类型的组件,下拉框可以支持多选,可以是级联下拉 - 写成动态表单的好处:需求可能对字段进行拓展,并且基本都是下拉类型的组件,封装成动态表单,后续再增加或减少字段类型,只需要后端维护一下某个字段的增减和对这个字段的下拉的维护,前端不需要改动代码
二、分析:
- 提交时通过
Object.values()获取tagIdList,需要将二维数组转为一维数组 - 回显时,通过接口获取每个字段,每个字段对应的下拉选项,以及通过
tagIdList和每个下拉中的id比对回显在下拉框中 - id比对时,
多选有多个id,级联组件需要把当前id的上级id找到才能回显
三、实现(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