技术栈 React + antd;接到需求,做一个form嵌套表单,刚开始想自己做一个,后来想着参数处理可能比较麻烦就直接用antd的Form吧。
一、目标效果
先看一下效果图
其中点击新增标签及配置时可以加一个红色框里的内容,点击蓝框里的加号可以实现红框里的效果
二、实现
直接上代码,详细解释在里面
import React, { useEffect, useState } from 'react'
// 需要用到的antd组件
import { message, Form, Input, Button, Row, Col, Select, DatePicker, Checkbox, Modal } from "antd";
// 加减删除图标
import { MinusCircleOutlined, PlusCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import ajax from "@/services";
import './index.scss'
const { RangePicker } = DatePicker;
// 复合嵌套增减表单弹框
const EditLabelModal = (props) => {
// 接收父组件传来的参数
const {
open = false, // 弹框是否展示
id = "", // id用于新增和编辑
labelList = [], // 标签列表
onClose = () => { }, // 关闭弹框回调
onSubmit = () => { } // 提交表单事件
} = props
// 定义form实例
const [form] = Form.useForm();
// loading效果,提交时禁用按钮防止重复提交
const [loading, setLoading] = useState(false)
// 模拟关联表下拉数据
const [tableAll, setTableAll] = useState([
{ label: '表1', value: 'b1' },
{ label: '表2', value: 'b2' },
{ label: '表3', value: 'b3' },
{ label: '表4', value: 'b4' },
{ label: '表5', value: 'b5' },
])
// 运算符列表
const symbolList = [
{
label: "大于",
value: ">"
},
{
label: "大于等于",
value: ">="
},
{
label: "小于",
value: "<"
},
{
label: "小于等于",
value: "<="
},
{
label: "等于",
value: "="
},
{
label: "不等于",
value: "!="
},
{
label: "包含",
value: "like"
},
{
label: "不包含",
value: "not like"
},
{
label: "不为空",
value: "is not null"
},
{
label: "为空",
value: "isnull"
},
{
label: "介于",
value: ">=<="
}
]
// 详情数据(用于编辑)
const [detail, setDetail] = useState({}) // 存一下详情数据
// 字段列表
const [fieldList, setFieldList] = useState([
{ label: 'name', value: 'name' },
{ label: 'age', value: 'age' },
{ label: 'time', value: 'time' },
{ label: 'sex', value: 'sex' },
])
// 这个是因为运算符后面的内容可能是输入框也可能是日期,这里有date或者time的判断为日期选择器
const columnRules = /^(?=.*(date|time)).*$/
// 要改判断的字段在这里改-----↑----↑
// Form.List默认的小类
const [baseRules] = useState({ // 添加小类默认值
column: null,
symbol: "=",
value: "",
value2: "",
fieldType: "",
check: false,
date: "",
})
// Form.List默认的大类
const [baseLabelRules] = useState({ // 添加大类默认值
labelName: null,
description: "",
rules: [baseRules],
})
// 根据运算符和前面的字段展示后面的输入框或者日期选择器
const handleItemInput = (column, symbol) => {
if (symbol === "is not null" || symbol === "isnull") return <></>
if (column && columnRules.test(column)) {
if (symbol === ">=<=") return <RangePicker showTime />
return <DatePicker showTime />
}
return <Input />
}
// 处理提交时间(时间还是字符串,一个还是两个)
const handleSubTime = (item) => {
const { column, value, value2, symbol, date } = item
if (symbol == "is not null" || symbol == "isnull") return ""
if (column && columnRules.test(column)) {
if (Array.isArray(date)) {
let times = date.map(i => i.format('YYYY-MM-DD HH:mm:ss')).join(",")
return times
}
return date.format('YYYY-MM-DD HH:mm:ss')
}
return value2 ? (value + ',' + value2) : value
}
// 处理字段的类型(选择的字段是什么类型的)
const handleFieldType = (item) => {
const { fieldType, column } = item
if (fieldType) return fieldType
else {
if (column && Array.isArray(fieldList)) {
let _fieldType = fieldList.find(i => column == i.label)?.type || ""
return _fieldType
}
else return ""
}
}
// 处理提交规则
const handleRules = (rules) => {
let _rules = []
const baseObj = (obj) => {
return {
column: obj?.column || "",
symbol: obj?.symbol || "",
value: handleSubTime(obj) || "",
fieldType: handleFieldType(obj) || ""
}
}
if (Array.isArray(rules) && rules.length) {
if (rules.length === 1) {
_rules.push(baseObj(rules[0]))
} else {
rules.map((i, index) => {
if (index === 0) {
_rules.push(baseObj(i))
} else if (index === 1) {
_rules.push(
{
column: "",
symbol: i?.check ? "or" : "and",
value: "",
fieldType: ""
},
baseObj(i)
)
}
})
}
}
return _rules
}
// 整理提交数据结构
const handleSub = (data) => {
let _data = { ...data }
if (Array.isArray(_data.labelRules) && _data.labelRules.length) {
let _labelRules = []
_data.labelRules.map(i => {
_labelRules.push({
labelName: i.labelName,
description: i.description,
rules: handleRules(i.rules)
})
})
_data.labelRules = _labelRules
return _data
} else {
return false
}
}
// 处理编辑时间(回显)
const handleEditTime = (item) => {
const { column, value } = item
let _arr = value ? value.split(",") : []
if (column && columnRules.test(column)) {
if (_arr.length == 2) {
return [dayjs(_arr[0]), dayjs(_arr[1])]
} else return dayjs(_arr[0])
}
}
// 处理返回编辑的规则(回显)
const handleEditRules = (rules) => {
let _rules = []
if (Array.isArray(rules) && rules.length) {
if (rules.length === 1) {
_rules.push(Object.assign({}, baseRules, rules[0]))
} else {
rules.map((i, index) => {
if (index === 0) {
_rules.push(Object.assign({}, baseRules, i))
} else if (index === 2) {
_rules.push(Object.assign({}, baseRules, {
...i,
value: (!columnRules.test(i.column)) ? i.value.split(",")[0] : "",
value2: (!columnRules.test(i.column)) ? i.value.split(",")[1] : "",
check: rules[1].symbol === "or" ? true : false,
date: columnRules.test(i.column) ? handleEditTime(i) : "",
}))
}
})
}
}
return _rules
}
// 处理返回来的详情(回显)
const handleDetailData = data => {
let _data = { ...data }
if (Array.isArray(_data.labelRules) && _data.labelRules.length) {
let _labelRules = []
_data.labelRules.map(i => {
_labelRules.push({
...i,
labelName: i.labelName,
description: i.description,
rules: handleEditRules(i.rules)
})
})
_data.labelRules = _labelRules
return _data
} else {
return false
}
}
// 保存提交
const beforeSub = async () => {
try {
let data = await form.validateFields()
let _form = handleSub(data)
if (!_form) return message.error("至少保留一条标签规则")
let _sub = Object.assign({}, detail, _form)
delete _sub.labelModelRuleEntities
setLoading(true)
onSubmit(_sub, () => setLoading(false))
} catch { }
}
// 获取所有表(下拉)
const getAllTable = () => {
ajax.getLabelModelTable().then(res => {
if (res.status === 20000) {
const { data } = res
if (Array.isArray(data.tableList) && data.tableList.length) {
setTableAll(data.tableList.map(i => {
return {
label: i.tableName,
value: i.tableName
}
}))
} else {
setTableAll([])
}
} else {
message.error(res.message)
setTableAll([])
}
}).catch(err => {
console.log(err)
setTableAll([])
})
}
// 获取字段列表(下拉)
const getFieldList = (tableName) => {
ajax.getLabelModelTableColumn({
tableName
}).then(res => {
if (res.status === 20000) {
const { data } = res
if (Array.isArray(data) && data.length) {
setFieldList(data.map(i => {
return {
label: i.name,
value: i.name,
type: i.type
}
}))
} else {
setFieldList([])
}
} else {
message.error(res.message)
setFieldList([])
}
}).catch(err => {
console.log(err)
setFieldList([])
})
}
// 获取编辑内容详情
const getEditInfo = (id) => {
ajax.getLabelModelEditInfo({
labelModelId: id
}).then(res => {
if (res.status === 20000) {
let _data = Object.assign({}, res.data)
setDetail(_data)
// getFieldList(_data.relevanceTable)
let formData = handleDetailData(_data)
form.setFieldsValue(formData)
}
}).catch(err => {
console.log(err)
})
}
// 关联表修改(修改后清空输入值、字段)
const changeTable = (v) => {
// getFieldList(v)
let _f = form.getFieldsValue(true)
let { labelRules } = _f
let _labelRules = []
const handleRules = (rules) => {
if (Array.isArray(rules) && rules.length) {
rules.forEach(i => {
i.column = null
i.value = ""
i.value2 = ""
i.fieldType = ""
i.date = ""
})
return rules
}
return []
}
if (Array.isArray(labelRules) && labelRules.length) {
labelRules.map(item => {
_labelRules.push({
...item,
rules: handleRules(item.rules)
})
})
form.setFieldsValue(Object.assign({}, _f, { labelRules: _labelRules }))
}
}
// 下拉配置
const selectOption = {
filterOption: (input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
// 日期、输入框切换报错处理 // antd4.17版本没有setFieldValue方法,就用这个方法处理
const clearData = (indexs, index, v) => {
let _f = form.getFieldsValue(true)
let _value = _f.labelRules[indexs].rules[index].value
if (v && columnRules.test(v)) {
if (typeof _value == 'string') _f.labelRules[indexs].rules[index].value = ""
} else {
if (typeof _value == 'object') _f.labelRules[indexs].rules[index].value = ""
}
form.setFieldsValue(Object.assign({}, _f))
}
// 模拟编辑返回的数据
let f = {
"labelModelName": "驱蚊的",
"relevanceTable": "b2",
"labelRules": [
{
"labelName": "l1",
"description": "",
"rules": [
{
"column": "name",
"symbol": "=",
"value": "我的",
"fieldType": ""
},
{
"column": "",
"symbol": "or",
"value": "",
"fieldType": ""
},
{
"column": "time",
"symbol": ">=<=",
"value": "2023-03-01 14:53:50,2023-03-09 14:53:52",
"fieldType": ""
}
]
},
{
"labelName": "l3",
"description": "",
"rules": [
{
"column": "age",
"symbol": ">",
"value": "23",
"fieldType": ""
},
{
"column": "",
"symbol": "and",
"value": "",
"fieldType": ""
},
{
"column": "time",
"symbol": ">",
"value": "2023-03-27 14:54:17",
"fieldType": ""
}
]
}
]
}
useEffect(() => {
// 发送请求获取表数据(这里注掉用模拟的)
// getAllTable()
}, [])
useEffect(() => {
if (!open) {
// 关闭弹框重置表单
form.resetFields()
setDetail({})
setLoading(false)
// setFieldList([])
} else {
// 打开弹框如果有id就请求详情
if (id) getEditInfo(id)
// 这里模拟回显详情数据
form && form.setFieldsValue(handleDetailData(f))
}
}, [open])
return (
<Modal
className='edit-label-modal'
width={778}
confirmLoading={loading}
destroyOnClose={true}
okText='保存'
title={`${id ? '编辑' : '新增'}标签模型`}
open={open}
onOk={beforeSub}
onCancel={onClose}
>
<Form
form={form}
autoComplete="off"
labelCol={{ span: 6 }}
wrapperCol={{ span: 16 }}
disabled={loading} // 整个form表单禁用 antd4.21才有效。。。。
preserve={false} // 设置false可以实现重置表单
>
{/* 顶部的公共信息,没啥好说的 */}
<Row justify="space-between">
<Col span={14}>
<Form.Item
label="标签模型名称"
name="labelModelName"
rules={[{ required: true, message: '请输入标签模型名称' }]}
>
<Input placeholder="请输入标签模型名称" />
</Form.Item>
</Col>
<Col span={10}>
<Form.Item
name="relevanceTable"
label="关联表"
rules={[{ required: true, message: '请选择关联表' }]}
>
<Select
{...selectOption}
options={tableAll}
showSearch placeholder="请选择关联表"
// 选择关联表时做的处理
onChange={changeTable}
/>
</Form.Item>
</Col>
</Row>
{/* 关键来了,嵌套表单 */}
<Form.List
// 绑定后端要的值
name="labelRules"
// 这里初始化默认值为Form.List大类
initialValue={[baseLabelRules]}
>
{/* 使用Form.List内置方法 */}
{(fields, { add, remove }) => (
// fields Form.List列表
// add 增加一个
// remove 删除一个
<>
{/* 这里就是一个新增按钮执行add方法 */}
<Col span={14}>
<div className="label-rule-box">
<span>标签规则</span>
<Button type="dashed" ghost onClick={() => {
// 获取绑定的labelRules值来限制最多添加多少个
const _labelRules = form.getFieldValue('labelRules');
if (Array.isArray(_labelRules) && _labelRules.length >= 5) {
return message.warning("最多添加五个")
}
// add方法里加上Form.List大类
add(baseLabelRules)
}}>
新增标签及配置
</Button>
</div>
</Col>
{/* 这里注掉了的部分是antd 4.17的写法 */}
{/* {fields.map((field, indexs) => ( */}
{/* 下面用的antd 5.x版本 */}
{fields.map((field) => (
<div className="rules-main-group" key={field.key}>
<Col span={14}>
{/* 每一项里面必须这么写才能绑定对提交表单要用的值 */}
<Form.Item
{...field}
// 这个字段用name和key,之前看网上的一些写法用的fieldName,fieldKey,不好使
name={[field.name, 'labelName']}
key={[field.key, 'labelName']}
label=""
rules={[{ required: true, message: '请选择标签' }]}
>
<Select
{...selectOption}
options={labelList}
showSearch
placeholder="请选择标签"
fieldNames="label"
/>
</Form.Item>
</Col>
{/* 大的删除按钮,删除Form.List的一个大类 */}
<span className="del-btn" onClick={() => !loading && remove(field.name)}>
<CloseCircleOutlined />
</span>
{/* 嵌套来了,list里面套list */}
<Form.List
{...field}
// 这里面绑定的值就是大类下需要增减的字段名,注意name和key都要加
name={[field.name, 'rules']}
key={[field.key, 'rules']}
// 这里初始化默认值为Form.List小类
initialValue={[baseRules]}
>
{/* 给add和remove重命名使用,避免冲突 */}
{(fieldItems, { add: addItem, remove: removeItem }) => (
<>
{/* 和上面一样进行遍历渲染子form */}
{fieldItems.map((fieldItem, index) => {
return (
<div className="rules-item" key={fieldItem.key}>
{/* 这里是有两个表单时展示或、且的选择 */}
{fieldItems && fieldItems.length > 1 && (index === fieldItems.length - 1) &&
// 这里我用了一个check来做这个点击切换且、或的效果
// 使用CheckBox时需要添加一个noStyle再进行勾选
<Form.Item
noStyle
shouldUpdate
>
{({ getFieldValue }) => {
// 获取当前是否勾选
let check = getFieldValue(['labelRules', field.name, 'rules', fieldItem.name, 'check'])
return <Form.Item
{...fieldItem}
label=""
name={[fieldItem.name, 'check']}
key={[fieldItem.key, 'check']}
// 这里一定要加上,不然不生效
valuePropName="checked"
>
<div className="and-or-btn">
<Checkbox checked={check} />
<span>{check ? '或' : '且'}</span>
</div>
</Form.Item>
}}
</Form.Item>
}
{/* 剩下的就是数据处理了 */}
<div className="item-left">
<Form.Item
{...fieldItem}
name={[fieldItem.name, 'column']}
key={[fieldItem.key, 'column']}
label=""
rules={[{ required: true, message: '请选择字段' }]}
>
<Select
{...selectOption}
options={fieldList}
showSearch
placeholder="请选择字段"
// 这里注掉是之前4.17版本的写法
// onChange={(v) => clearData(indexs, index, v}
/>
</Form.Item>
<Form.Item
{...fieldItem}
name={[fieldItem.name, 'symbol']}
key={[fieldItem.key, 'symbol']}
label=""
rules={[{ required: true }]}
>
<Select
{...selectOption}
options={symbolList}
fieldNames="label"
// 这里注掉是之前4.17版本的写法
// onChange={() => clearData(indexs, index)}
onChange={() => form.setFieldValue(['labelRules', field.name, 'rules', fieldItem.name, 'date'], "")}
/>
</Form.Item>
</div>
<div className="item-right">
<Form.Item
noStyle
shouldUpdate
>
{({ getFieldValue }) => {
let _column = getFieldValue(['labelRules', field.name, 'rules', fieldItem.name, 'column'])
let _symbol = getFieldValue(['labelRules', field.name, 'rules', fieldItem.name, 'symbol'])
let isDate = _column && columnRules.test(_column)
if (_symbol === ">=<=" && !(isDate)) {
return <Row gutter={24}>
<Col span={12}>
<Form.Item
{...fieldItem}
name={[fieldItem.name, 'value']}
key={[fieldItem.key, 'value']}
wrapperCol={24}
label=""
rules={[{ required: true, message: '值不能为空' }]}
>
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
{...fieldItem}
name={[fieldItem.name, 'value2']}
key={[fieldItem.key, 'value2']}
wrapperCol={24}
label=""
rules={[{ required: true, message: '值不能为空' }]}
>
<Input />
</Form.Item>
</Col>
</Row>
}
return <Form.Item
{...fieldItem}
name={[fieldItem.name, isDate ? 'date' : 'value']}
key={[fieldItem.key, isDate ? 'date' : 'value']}
wrapperCol={24}
label=""
rules={[{ required: true, message: '值不能为空' }]}
>
{handleItemInput(_column, _symbol)}
</Form.Item>
}}
</Form.Item>
</div>
<div className="item-option">
{fieldItems && fieldItems.length === 1 &&
<PlusCircleOutlined onClick={() => !loading && addItem(baseRules)} /> ||
<MinusCircleOutlined onClick={() => !loading && removeItem(fieldItem.name)} />
}
</div>
</div>
);
})}
</>
)}
</Form.List>
</div>
))}
</>
)}
</Form.List>
</Form>
</Modal >
)
}
export default EditLabelModal
总结
以上就是一个完整的demo例子,实现的最主要关键就在于要使用 name 和 key 来绑定值,其他的都是按照项目具体需求去改,有问题的话希望不吝赐教,感谢