好久不见,小编携文来扰!🔨
在最近研发的项目中,小编遇到一个比(you)较(dian)复(bian)杂(tai)的表单需求。讲真不难,就是构造数据和数据处理有点麻烦。起初画好页面后,以为万事大吉,直接掉接口塞数据就成。直到看到后端模型定义好的传参格式,一时间竟有点懵,最后只能推翻重画...
整理这篇文章的初衷,是感觉其实项目中会经常遇到这类复杂的表单,有必要记录一下,便于以后遇到类似的需求写起来有个参考。
使用组件库: ant design
需求描述: 批量选取Table
中的商家,并为各个商家配置一个或多个车型和车辆营运类型。
图示:
大致流程: 选取商家确定后,将选好的商家遍历注入到表单中,并按相关需求整理好数据提交。
正文
1.首先看下接口模型定义的格式(如下):
carWithCompany: [{ // object []
travelCompanyId: '', // string
carModelList: [{ // object []
carModel: '', // string
operationType: [] // string[]
}]
}]
复制代码
思考:
最初,小编刚开始没有拿接口定义的格式时,直接用map
遍历选取的商家,再内嵌一个Form.List来实现内部选取的车型&车辆营运类型。但是看后端接口模型定义好的格式后,发现这么处理数据会很麻烦。后来索性直接用Form.List内嵌Form.List来实现。但是遇到一个问题:最外层的Form.List是不需要添加按钮去添加条数的,它的数量是通过已选取的商家数量去控制。
机智的小编去请教了前端大佬,大佬反手就是一个demo丢过来(👍)。看了后发现想起来useRef
是个好东西(平常比较少用)。
2.给Table
造点数据
const [data, setData] = useState([
{
id: "1",
name: "商家1",
alias: "简称1",
type: "自营"
},
{
id: "2",
name: "商家2",
alias: "简称2",
type: "自营"
},
{
id: "3",
name: "商家3",
alias: "简称3",
type: "自营"
}
]);
复制代码
3.接下来是处理Modal
弹窗中多选的数据,并遍历到Form.List
中,这里需要重点关注下useRef
的使用(多选选取的条数需要替换原本通过使用Form.List
的添加方法手动添加的条数)。具体核心代码如下:
// 每次选中条数变通过useRef出发Form.List方法更新
useEffect(() => {
const arr = company.selectedRows;
arr?.map(() => {
return addRef.current();
});
}, [company.selectedRows]);
// 处理firstFields,传入相应选中数据
addRef.current = add;
firstFields.map((field, index) => {
return (field.record = company.selectedRows[index]);
});
let result = firstFields?.filter((item) => item.record);
复制代码
页面代码如下:
import React, { useState, useRef, useEffect } from "react";
import ReactDOM from "react-dom";
import "antd/dist/antd.css";
import {Form, Input, Button, Space, Modal, Table, message, Select} from "antd";
import { DeleteOutlined, CloseOutlined } from "@ant-design/icons";
const Demo = () => {
const [form] = Form.useForm();
const addRef = useRef(null);
const [visible, setVisible] = useState(false);
const [company, setCompany] = useState(
{
selectedRows: [],
tmpRows: []
},
[]
);
const [data, setData] = useState([
{
id: "1",
name: "商家1",
alias: "简称1",
type: "自营"
},
{
id: "2",
name: "商家2",
alias: "简称2",
type: "自营"
},
{
id: "3",
name: "商家3",
alias: "简称3",
type: "自营"
}
]);
const columns = [
{
title: "商家名称",
dataIndex: "name"
},
{
title: "商家简称",
dataIndex: "alias"
},
{
title: "商家运营类型",
dataIndex: "type"
}
];
const onFinish = (values) => {
console.log("Received values of form:", values);
};
const handleClick = () => {
setVisible(true);
};
const handleOk = () => {
if (company.tmpRows?.length === 0) {
message.error("请选择商家");
return false;
} else {
setCompany({ //处理确认选中的商家
selectedRows: company.tmpRows.slice(),
tmpRows: company.tmpRows
});
setVisible(false);
}
};
const handleCancel = () => {
setVisible(false);
};
useEffect(() => { // 解释如上
company.selectedRows?.map(() => {
return addRef.current();
});
}, [company.selectedRows]);
return (
<section>
<Form form={form} onFinish={onFinish} autoComplete="off">
<Form.Item>
<Button type="primary" onClick={handleClick}>
选择商家
</Button>
</Form.Item>
{company.selectedRows?.length > 0 && (
<Form.List name="carWithCompany">
{(firstFields, { add, remove }) => {
addRef.current = add;
firstFields.map((field, index) => {
return (field.record = company.selectedRows[index]);
});
let result = firstFields?.filter((item) => item.record); // 过滤record是undefiend的情况
return (
<>
{result.map((firstField, i) => {
return (
<div key={firstField.key}>
<div style={{ display: 'inline-block' }}>
<h4>商家名称</h4>
<p>{firstField.record?.name}</p>
<Form.Item
name={[firstField.name, "id"]}
initialValue={firstField.record?.id}
style={{ height: "60%" }}>
<Input style={{ display: "none" }} />
</Form.Item>
</div>
<div style={{ display: 'inline-block' }}>
<div>
<span style={{display: 'inline-block', marginRight: 250}}>车型</span>
<span>车辆营运类型</span>
</div>
<Form.List name={[firstField.name, "carModelList"]}>
{(secondfields, { add, remove }) => (
<>
{secondfields.map(
({ key, name, fieldKey, ...restField }) => (
<Space
key={key}
align="baseline">
<Form.Item
{...restField}
name={[name, "carModel"]}
rules={[
{
required: true,
message: "请选择车型"
}
]}>
<Select style={{ width: 150 }}>
<Select.Option value="0001"> 1车型 </Select.Option>
<Select.Option value="0002">2车型</Select.Option>
<Select.Option value="0003">3车型</Select.Option>
</Select>
</Form.Item>
<Form.Item
style={{ marginLeft: 26 }}
{...restField}
name={[name, "operationType"]}
rules={[
{
required: true,
message: "请选择营运类型"
}
]}>
<Select
mode="multiple"
style={{ width: 250 }}>
<Select.Option value="0001">01</Select.Option>
<Select.Option value="0002">02</Select.Option>
<Select.Option value="0003">03</Select.Option>
</Select>
</Form.Item>
<DeleteOutlined onClick={() => remove(name)} />
</Space>
)
)}
<Form.Item>
<Button
style={{ marginLeft: 24 }}
type="primary"
onClick={() => add()} // 添加车型&营运类型
>
添加
</Button>
</Form.Item>
</>
)}
</Form.List>
</div>
<CloseOutlined // 整条数据删除
style={{ position: 'absolute',display: 'inline-block', margin: '16px 24px 0 0'}}
onClick={() => {
remove(firstField.name);
company.selectedRows?.splice(i, 1);
company.tmpRows?.splice(i, 1);
result.splice(i, 1);
setCompany({
selectedRows: company.selectedRows,
tmpRows: company.tmpRows
});
}}
/>
</div>
);
})}
</>
);
}}
</Form.List>
)}
<Form.Item>
<Button type="primary" htmlType="submit">提交</Button>
</Form.Item>
</Form>
<Modal
title="选择商家组织"
visible={visible}
onCancel={handleCancel}
onOk={handleOk}
>
<Table
columns={columns}
dataSource={data}
rowKey={(row) => row.id}
rowSelection={{
selectedRowKeys: company.tmpRows.map((item) => item.id),
onSelect: (row, selected) => {
let old = company.tmpRows.slice();
if (selected) old.push(row);
else old = old.filter((item) => item.id !== row.id);
setCompany({ tmpRows: old });
},
onSelectAll: (selected, rows, changeRows) => {
let old = company.tmpRows.slice();
if (selected) old.push(...changeRows);
else {
old = old.filter((item) => {
for (const row of changeRows)
if (row.id === item.id) return false;
return true;
});
}
setCompany({ tmpRows: old });
}
}}
pagination={{
total: data.length,
showTotal: (total) => `共${total}条数据`
}}
/>
</Modal>
</section>
);
};
ReactDOM.render(<Demo />, document.getElementById("container"));
复制代码
代码很长,但是为了能更快的拿代码去编译,更直观的看,小编就没采取附上gitlab链接,而是选择直接粘上,有强迫症的童鞋可忽略。
最终demo表单的效果图如下:
这么看图片有点朴实无华,样式什么的需要的话自己去画吧,时间紧任务重。
最终处理的数据结构如下:
这样,就构造好了后端小哥需要的数据结构啦(嘿嘿),可直接使用antd
的codesandbox
打开。
注意
最后,需要注意的几点问题:
- 删除重新添加
选好Table中的数据map到Form.List,进行删除操作,再另行选取数据的时候,可能会出现以下情况:
这时候就需要过滤一下。代码中 let result = firstFields?.filter((item) => item.record);
的作用就是处理过滤。
2.数据回显问题
当编辑的时候表单需要的数据需要回显,这时候处理嵌套的Form.List数据,使其一一对应。小编处理的方式是这部分数据map
时候return
两次,可能说的抽象了点,大致代码如下:
result.map(item => {
return {
b: item.id,
c: item.content?.map(items => {
return {
d: items.car,
e: items.type
}
})
}
})
复制代码
到这里其实也就差不多了,文章写的比较匆忙,有些东西估计没注意到,但功能大体的代码逻辑是没有啥大问题的,如需自取吧哈哈。
文末:附上一小段前段时间跟前端大佬的对话哈哈哈
前端大佬:你要举一反三
小编: 我反了, 反不动。。
很幸运,工作中遇到了有趣的人,亦师亦友,给他比个❤️吧嘿嘿