React++antd+ProComponents可编辑表格EditableProTable组件实现表单中的可编辑列表组件
需求:
在新增&编辑表单中,共分三个表单模块,第二个模块设计为一个可编辑表格组件,其中可选下拉列表依赖外层第一个模块的某条数据值,提供新增、编辑、删除、按规定条件去重等功能,并在第三个模块中自动计算列表数值总和
实现:
1.表单初始化接口的返回约定为三个数组,按模块对应:
const [dataSource, setDataSource] = useState<{
base_info?: API.FormListType[];
detail_info?: API.FormListType[];
total_info?: API.FormListType[];
}>({});
2.表单初始化接口返回后,配置表单dataSource【其中第三个模块配置为2位小数只读,第二个模块配置自定义组件】:
setCreateForm({
base_info: createList?.base_info?.map((el: any) => {
if (el.id === 'attachment') {
return {
...el,
renderFormItem: () => (
<File
optionData={{
type: 'default',
value: '选择文件',
props: { is_approval_file: 1 },
api: 'onUploadGeneralUpload',
}}
/>
),
};
}
return el;
}),
detail_info: createList?.detail_info?.map((el: any) => {
if (el.id === 'detail_info') {
return {
...el,
renderFormItem: () => <Condition id={undefined} />,
};
}
return el;
}),
total_info: createList?.total_info?.map((el: any) => {
return {
...el,
readonly: true,
value: Number(el.value).toFixed(2),
};
}),
});
表单组件:
// 去掉接口信息等的部分代码
import { useState, useRef } from 'react';
import { Button, Spin, Typography, Modal } from 'antd';
import { DrawerForm } from '@ant-design/pro-form';
import SchemaForm from '@/components/SchemaForm';
import ModuleTitle from '@/components/ModuleTitle';
import { ExclamationCircleOutlined } from '@ant-design/icons';
import type { FormInstance } from 'antd';
import type { ParamsType } from '@ant-design/pro-provider';
import Condition from './Condition'; type UpdateFormProps = {
onUpdate?: () => void;
record?: SettleApplicationParams;
createForm: {
base_info?: API.FormListType[];
detail_info?: API.FormListType[];
total_info?: API.FormListType[];
};
}; const { Text } = Typography; const UpdateForm: React.FC<UpdateFormProps> = ({ record, onUpdate, createForm }) => {
const formRef = useRef<FormInstance>();
const [dataSource, setDataSource] = useState<{
base_info?: API.FormListType[];
detail_info?: API.FormListType[];
total_info?: API.FormListType[];
}>({});
const [loading, setLoading] = useState<boolean>(false);
const [visible, setVisible] = useState<boolean>(false);
const [formValues, setFormValues] = useState<ParamsType>({}); const baseFormChange = (changedValues: ParamsType, allValues: ParamsType) => {
// 如果【detail_info】有值,修改【company_id 】需要弹窗提示,确认则清空第二个模块的数据,否则关闭弹窗,值不变
if (
allValues.detail_info.length > 0 &&
(changedValues.company_id || !allValues.company_id)
) {
Modal.confirm({
icon: <ExclamationCircleOutlined />,
content: <Text strong>切换xx将会清空以下xx信息,请确认是否切换</Text>,
okText: '确认',
cancelText: '取消',
onOk() {
setDataSource({
...dataSource,
detail_info: dataSource?.detail_info?.map((el: any) => {
if (el.id === 'detail_info') {
return {
...el,
value: [],
renderFormItem: () => (
<Condition
id={changedValues.company_id || allValues.company_id}
/>
),
};
}
return el;
}),
});
formRef.current?.setFieldsValue({
channel_company_id: changedValues.company_id || allValues.company_id,
detail_info: [],
});
setFormValues(formRef.current?.getFieldsValue(true));
},
onCancel() {
formRef.current?.setFieldsValue({
company_id: formValues.company_id,
});
setFormValues(formRef.current?.getFieldsValue(true));
},
});
}
// 如果【detail_info】无值,修改【company_id】,第二模块组件传参需要传最新的【company_id】
if (!allValues.detail_info.length && changedValues.company_id) {
setDataSource({
...dataSource,
detail_info: dataSource?.detail_info?.map((el: any) => {
if (el.id === 'detail_info') {
return {
...el,
value: [],
renderFormItem: () => <Condition id={changedValues.company_id} />,
};
}
return el;
}),
});
formRef.current?.setFieldsValue({ company_id: changedValues.company_id });
setFormValues(formRef.current?.getFieldsValue(true));
}
// 【总计】的数据根据第二模块列表的值计算
if (changedValues.detail_info) {
// 第二模块中的第一列数值关联
let bill_turnover_total = 0;
// 第二模块中的第二列数值关联
let bill_divide_turnover_total = 0;
changedValues.detail_info?.forEach(
(item: { bill_divide_turnover: number; bill_turnover: number }) => {
bill_turnover_total += Number(item.bill_turnover);
bill_divide_turnover_total += Number(item.bill_divide_turnover);
},
);
formRef.current?.setFieldsValue({
bill_turnover_total: Number(bill_turnover_total).toFixed(2),
bill_divide_turnover_total: Number(bill_divide_turnover_total).toFixed(2),
});
setFormValues(formRef.current?.getFieldsValue(true));
}
}; // 获取单条数据
const getInfo = async () => {
if (!record?.id) return;
try {
const { result } = await 接口(record?.id);
if (result) {
setDataSource({
base_info: createForm?.base_info?.map((el) => {
return { ...el, value: (el.id && result && result[el.id]) || el.value };
}),
detail_info: createForm?.detail_info?.map((el: any) => {
if (el.id === 'detail_info') {
return {
...el,
value: (el.id && result && result[el.id]) || el.value,
renderFormItem: () => <Condition id={result.company_id} />,
};
}
return { ...el, value: (el.id && result && result[el.id]) || el.value };
}),
total_info: createForm?.total_info?.map((el) => {
return { ...el, value: (el.id && result && result[el.id]) || el.value };
}),
});
setVisible(true);
}
} catch (error) {
//
} finally {
setLoading(false);
}
}; // 表单处理
async function showForm() {
setLoading(true);
if (record?.id) {
getInfo();
} else {
setDataSource(createForm);
setLoading(false);
setVisible(true);
}
} return (
<>
{record && record.id ? (
<Spin spinning={loading}>
<a key="edit" onClick={showForm}>
编辑
</a>
</Spin>
) : (
<Button type="primary" key="add" onClick={showForm}>
新增
</Button>
)}
<DrawerForm
formRef={formRef}
width={'70%'}
visible={visible}
title={`${record && record.id ? '编辑' : '新增'}xxx`}
drawerProps={{
bodyStyle: { paddingTop: 8 },
onClose: () => setVisible(false),
destroyOnClose: true,
}}
onValuesChange={baseFormChange}
onFinish={async (formData) => {
const { code } =
record && record.id
? await 编辑接口({
id: record && record.id,
...formData,
})
: await 新增接口({ ...formData });
if (code === 0 && onUpdate) {
formRef.current?.resetFields();
onUpdate();
setVisible(false);
return true;
}
return false;
}}
>
<ModuleTitle title="第一模块信息" />
<SchemaForm dataSource={dataSource?.base_info} layoutType="Embed" submitter={false} />
<ModuleTitle title="第二模块信息" />
<SchemaForm dataSource={dataSource?.detail_info} layoutType="Embed" submitter={false} />
<ModuleTitle title="第三模块总计" />
<SchemaForm dataSource={dataSource?.total_info} layoutType="Embed" submitter={false} />
</DrawerForm>
</>
);
}; export default UpdateForm;
3.表单组件定义完毕,表单项关联也进行了处理,下一步就是自定义组件的书写:
【组件内部需要判断前三列选项是否重复已有数据&进行接口请求进行后台数据重复判断】
【组件内部第一列下拉数据依赖于外层数据,第二列数据依赖于第一列数据的选项值】
/* 组件 */
import { useState, useEffect, useMemo } from 'react';
import { Form, message } from 'antd';
import moment from 'moment';
import { isEmpty } from 'lodash';
import { EditableProTable } from '@ant-design/pro-table';
import type { ProColumns } from '@ant-design/pro-table';
import type { FormInstance } from 'antd'; type ConditionProps = {
onChange?: (data: SettleApplicationLogsParams[]) => void;
value?: SettleApplicationLogsParams[];
id?: string;
}; const Condition: React.FC<ConditionProps> = (props) => {
const { value, id, onChange } = props;
const [form] = Form.useForm();
const [dataSource, setDataSource] = useState<SettleApplicationLogsParams[]>([]);
const [companyOpChannel, setCompanyOpChannel] = useState<Record<string, API.FormListType[]>>({});
const [companyGame, setCompanyGame] = useState<{ label: string; value: string }[]>([]);
const [editableKeys, setEditableRowKeys] = useState<React.Key[]>(() => []);
const [channelCompanyId, setChannelCompanyId] = useState<string | undefined>(undefined); // 获取第二列下拉数据【需要依赖第一列的选项值】
const getChannelCompanyOpChannel = async (game_id: string, company_id?: string) => {
if (!company_id || !game_id) return;
try {
const { result } = await 接口({ company_id, game_id });
if (result) {
const optionList = (result || []).map((itemO: any) => {
return {
label: itemO.value,
value: itemO.id,
};
});
setCompanyOpChannel({ ...companyOpChannel, [game_id]: optionList });
}
} catch (error) {
//
}
}; // 获取第一列下拉数据
const getFirstList = async (company_id: string) => {
if (!company_id) return;
try {
const { result } = await 接口({ company_id });
if (result) {
const optionList = (result || []).map((itemO: any) => {
return {
label: itemO.value,
value: itemO.id,
};
});
setCompanyGame(optionList);
}
} catch (error) {
//
}
}; const tableColumns: ProColumns[] = [
{
title: '第一列下拉',
dataIndex: 'game_id',
valueType: 'select',
render: (_, row) => row.game_name || '-',
fieldProps: (_form: FormInstance, { rowKey }: { rowKey: string }) => {
if (!channelCompanyId) {
return { disabled: true, options: [], placeholder: '请选择外层第一模块依赖值' };
}
if (companyGame.length === 0) {
return { allowClear: false, options: [] };
} return {
allowClear: false,
showSearch: true,
options: companyGame,
onChange: (val: string) => {
if (!rowKey) return;
// 重置运营渠道列表
getChannelCompanyOpChannel(val, channelCompanyId || undefined);
const fieldsValue = _form.getFieldsValue();
_form.setFieldsValue({
...fieldsValue,
[rowKey]: {
...fieldsValue[rowKey],
game_id: val,
game_name:
companyGame?.find((el: { value: string; label: string }) => el.value === val)
?.label || '-',
op_channel: null,
op_channel_name: null,
},
});
},
};
},
formItemProps: () => {
return {
rules: [{ required: true, message: '此项为必填项' }],
};
},
},
{
title: '第二列下拉',
dataIndex: 'op_channel',
valueType: 'select',
render: (_, row) => row.op_channel_name || '-',
fieldProps: (_form: FormInstance, { rowKey }: { rowKey: string }) => {
const rowValue = _form?.getFieldsValue(true) || {};
if (!rowKey || isEmpty(rowKey) || isEmpty(rowValue)) {
return { disabled: true, options: [], placeholder: '请选择第1列下拉' };
}
const key = rowKey[0];
const { game_id } = rowValue[key] || {};
if (!game_id || !companyOpChannel[game_id] || companyOpChannel[game_id].length === 0) {
return { allowClear: false, options: [] };
}
return {
allowClear: false,
showSearch: true,
options: companyOpChannel[game_id],
onChange: (val: string) => {
if (!rowKey) return;
const fieldsValue = _form.getFieldsValue();
_form.setFieldsValue({
...fieldsValue,
[rowKey]: {
...fieldsValue[rowKey],
op_channel: val,
op_channel_name:
companyOpChannel[game_id]?.find((el) => el.value === val)?.label || '-',
},
});
},
};
},
},
{
title: '选择月份',
dataIndex: 'settle_time',
valueType: 'dateMonth',
render: (_, row) => moment(row.settle_time).format('YYYY-MM'),
formItemProps: () => {
return {
rules: [{ required: true, message: '此项为必填项' }],
};
},
},
{
title: '数值1',
dataIndex: 'bill_turnover',
valueType: 'digit',
fieldProps: { precision: 2, min: 0 },
render: (_, row) => Number(row.bill_turnover).toFixed(2),
formItemProps: () => {
return {
rules: [{ required: true, message: '此项为必填项' }],
};
},
},
{
title: '数值2',
dataIndex: 'bill_divide_turnover',
valueType: 'digit',
fieldProps: { precision: 2, min: 0 },
render: (_, row) => Number(row.bill_divide_turnover).toFixed(2),
formItemProps: () => {
return {
rules: [{ required: true, message: '此项为必填项' }],
};
},
},
{
title: '操作',
valueType: 'option',
width: 200,
render: (text: any, record: any, _: any, action: any) => [
<a
key="editable"
onClick={() => {
action?.startEditable?.(record.id);
}}
>
编辑
</a>,
<a
key="delete"
onClick={() => {
setDataSource(dataSource.filter((item) => item.id !== record.id));
if (onChange) {
onChange(dataSource.filter((item) => item.id !== record.id));
}
}}
>
删除
</a>,
],
},
]; useEffect(() => {
if (value) {
if (value.length === 0 && dataSource.length === 0) {
return;
} else {
setDataSource(value);
if (onChange) {
onChange(value);
}
}
}
}, []); // 外层依赖值改变时,清空数据并请求新的第一列下拉列表
useEffect(() => {
setCompanyOpChannel({});
setCompanyGame([]);
setEditableRowKeys([]);
setChannelCompanyId(id);
if (dataSource.length > 0) {
setDataSource([]);
if (onChange) {
onChange([]);
}
}
if (id) getFirstList(id);
}, [id]); return useMemo(
() => (
<EditableProTable
recordCreatorProps={{
position: 'bottom',
disabled: dataSource.length >= 10 || !id,
creatorButtonText: '新增',
record: () => ({ id: (Math.random() * 1000000).toFixed(0) }),
}}
maxLength={10}
locale={{ emptyText: '暂无数据' }}
loading={false}
toolBarRender={false}
columns={tableColumns}
value={dataSource}
onChange={(values: SettleApplicationLogsParams[]) => {
setDataSource(values);
if (onChange) {
onChange(values);
}
}}
scroll={{ y: '235px' }}
editable={{
form,
type: 'multiple',
editableKeys,
onChange: setEditableRowKeys,
onSave: async (rowKey, data) => {
// 校验[当前前三列是否已存在记录]
const repeatData = dataSource?.filter(
(item) =>
item.game_id === data.game_id &&
item.op_channel === data.op_channel &&
item.settle_time === data.settle_time,
);
if (repeatData.length) {
message.error('已存在相同记录');
return Promise.reject();
}
try {
const { code, message: ResMessage } = await 接口({
...data,
company_id: id,
});
if (code === 0) {
return Promise.resolve();
} else {
message.error(ResMessage);
return Promise.reject();
}
} catch (error) {
return Promise.reject();
}
},
}}
rowKey="id"
/>
),
[tableColumns],
);
}; export default Condition;
基本代码官方文档都有,嘻嘻:https://procomponents.ant.design/components/editable-table/#editable-%E7%BC%96%E8%BE%91%E8%A1%8C%E9%85%8D%E7%BD%AE
最终效果:
React++antd+ProComponents可编辑表格EditableProTable组件实现表单中的可编辑列表组件的更多相关文章
- 【antd】如何自定义antd组件form表单中Form.Item里的内容组件
需求:现有一个form表单,但是其中一个元素比较复杂,并不是简单的输入框或者下拉框之类的.但是我又希望能通过form.validateFields().then()去获得它的值,就不需要在当前页面写大 ...
- React中ref的三种用法 可以用来获取表单中的值 这一种类似document.getXXId的方式
import React, { Component } from "react" export default class MyInput extends Component { ...
- ABBYY FineReader 15 新增编辑表格单元格功能
ABBYY FineReader 15(Windows系统)新增编辑表格单元格功能,在PDF文档存在表格的前提下,可将表中的每个单元格作为单独的文字块进行单独编辑,单元格内的编辑不会影响同一行中其他单 ...
- ReactNative: 创建自定义List列表组件
一.介绍 在App中,很多数据消息显示都是一行行动态展示的,例如新闻标题,其实每一条新闻标题都可以独立成一个简单的列表组件,之前我们使用Text组件将数据都写死了,为了提高组件的灵活性,我们可以使用T ...
- 封装react antd的表格table组件
封装组件是为了能在开发过程中高度复用功能和样式相似的组件,以便我们只关注于业务逻辑层的处理,提高开发效率,提高逼格,降低代码重复率,降低劳动时间,减少加班的可能. 本次组件的封装采用了函数式组件即无状 ...
- 封装react antd的form表单组件
form表单在我们日常的开发过程中被使用到的概率还是很大的,比如包含了登录.注册.修改个人信息.新增修改业务数据等的公司内部管理系统.而在使用时这些表单的样式如高度.上下边距.边框.圆角.阴影.高亮等 ...
- 封装react antd的upload上传组件
上传文件也是我们在实际开发中常遇到的功能,比如上传产品图片以供更好地宣传我们的产品,上传excel文档以便于更好地展示更多的产品信息,上传zip文件以便于更好地收集一些资料信息等等.至于为何要把上传组 ...
- LigerUI编辑表格组件单元格校验问题
这几天在使用LigerUI(版本为1.2.2)编辑表格组件的时候,遇到几个小问题,从官方demo和api中没有找到解决的办法 问题1.从数据库查询出来的主键单元格不可编辑问题 主键单元格已经保存之前编 ...
- React antd如何实现<Upload>组件上传附件再次上传已清除附件缓存问题。
最近在公司做React+antd的项目,遇到一个上传组件的问题,即上传附件成功后,文件展示处仍然还有之前上传附件的缓存信息,需要解决的问题是,要把上一次上传的附件缓存在上传成功或者取消后,可以进行清除 ...
- 封装React AntD的dialog弹窗组件
前一段时间分享了基于vue和element所封装的弹窗组件(封装Vue Element的dialog弹窗组件),今天就来分享一个基于react和antD所封装的弹窗组件,反正所使用的技术还是那个技术, ...
随机推荐
- 记一次Mybatis-Plus动态分表DynamicTableNameInnerInterceptor里无法动态替换表名的坑
首先上源码 protected String changeTable(String sql) { ExceptionUtils.throwMpe(null == tableNameHandler, & ...
- 记录web面经
1. npm版本号含义 例如: 2.3.1 (分别表示: 大版本,小版本, 补丁版本)大版本号: 大版本更新,功能添加,向下不兼容.小版本号:功能新增,向下兼容.补丁版本号: 修复bug.~符号含义: ...
- 音视频技术入门课- 05 使用FFmpeg与OBS进行直播推流
做直播推流的前提是要有直播服务器接收直播流,所以需要我们自己建设一个流媒体服务器. 流媒体服务器SRS SRS是一个简单高效的实时视频服务器,支持RTMP/WebRTC/HLS/HTTP-FLV/SR ...
- thinkphp6+composer+无集成工具 配置php项目环境
安装composer 下载地址:https://getcomposer.org/Composer-Setup.exe 安装步骤 点击finish完成即可. 打开cmd输入composer查看是否安装成 ...
- 本地部署一套k8s集群
我这里准备三台本地vmware虚拟机,版本号centos7.9,一台master节点,一台node1,一台node2 kubeadm方式部署.Kubeadm 是一个 K8s 部署工具,提供 kubea ...
- Python-celery介绍与快速上手
1.celery介绍: celery是一个基于Python开发的模块,可以帮助我们在开发过程中,对任务进行分发和处理. 详细介绍取自:Python之celery的简介与 ...
- Fortran 笔记之 继承和聚合
继承(类扩展)和聚合 参考自Introduction to Modern Fortran for the Earth System Sciences 我们在3.3部分的开头提到过,OOP范式通常会导致 ...
- https原理(七)其他
1 fiddler原理+fiddler为什么抓chrome而不能抓curl和httpclient?fiddler为什么能篡改报文? 中的ssl pinning 本质即是双向ssl https://zh ...
- hive安装准备 (mysql8.0安装)
1.先准备好安装的所需材料 2.开始准备安装 先安装mysql 解压命令:tar -xvJf (注意:这里' j '是大写) 解压后改名: mv mysql-8.0.24-linux-glibc2. ...
- ES 快速开始
ES语句构造麻烦,每次都拼,这次备份一下 1. 创建索引 PUT http://sae1002.qihoo.ai:9200/kosmos {"settings":{"in ...