需求:

在新增&编辑表单中,共分三个表单模块,第二个模块设计为一个可编辑表格组件,其中可选下拉列表依赖外层第一个模块的某条数据值,提供新增、编辑、删除、按规定条件去重等功能,并在第三个模块中自动计算列表数值总和

实现:

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组件实现表单中的可编辑列表组件的更多相关文章

  1. 【antd】如何自定义antd组件form表单中Form.Item里的内容组件

    需求:现有一个form表单,但是其中一个元素比较复杂,并不是简单的输入框或者下拉框之类的.但是我又希望能通过form.validateFields().then()去获得它的值,就不需要在当前页面写大 ...

  2. React中ref的三种用法 可以用来获取表单中的值 这一种类似document.getXXId的方式

    import React, { Component } from "react" export default class MyInput extends Component { ...

  3. ABBYY FineReader 15 新增编辑表格单元格功能

    ABBYY FineReader 15(Windows系统)新增编辑表格单元格功能,在PDF文档存在表格的前提下,可将表中的每个单元格作为单独的文字块进行单独编辑,单元格内的编辑不会影响同一行中其他单 ...

  4. ReactNative: 创建自定义List列表组件

    一.介绍 在App中,很多数据消息显示都是一行行动态展示的,例如新闻标题,其实每一条新闻标题都可以独立成一个简单的列表组件,之前我们使用Text组件将数据都写死了,为了提高组件的灵活性,我们可以使用T ...

  5. 封装react antd的表格table组件

    封装组件是为了能在开发过程中高度复用功能和样式相似的组件,以便我们只关注于业务逻辑层的处理,提高开发效率,提高逼格,降低代码重复率,降低劳动时间,减少加班的可能. 本次组件的封装采用了函数式组件即无状 ...

  6. 封装react antd的form表单组件

    form表单在我们日常的开发过程中被使用到的概率还是很大的,比如包含了登录.注册.修改个人信息.新增修改业务数据等的公司内部管理系统.而在使用时这些表单的样式如高度.上下边距.边框.圆角.阴影.高亮等 ...

  7. 封装react antd的upload上传组件

    上传文件也是我们在实际开发中常遇到的功能,比如上传产品图片以供更好地宣传我们的产品,上传excel文档以便于更好地展示更多的产品信息,上传zip文件以便于更好地收集一些资料信息等等.至于为何要把上传组 ...

  8. LigerUI编辑表格组件单元格校验问题

    这几天在使用LigerUI(版本为1.2.2)编辑表格组件的时候,遇到几个小问题,从官方demo和api中没有找到解决的办法 问题1.从数据库查询出来的主键单元格不可编辑问题 主键单元格已经保存之前编 ...

  9. React antd如何实现<Upload>组件上传附件再次上传已清除附件缓存问题。

    最近在公司做React+antd的项目,遇到一个上传组件的问题,即上传附件成功后,文件展示处仍然还有之前上传附件的缓存信息,需要解决的问题是,要把上一次上传的附件缓存在上传成功或者取消后,可以进行清除 ...

  10. 封装React AntD的dialog弹窗组件

    前一段时间分享了基于vue和element所封装的弹窗组件(封装Vue Element的dialog弹窗组件),今天就来分享一个基于react和antD所封装的弹窗组件,反正所使用的技术还是那个技术, ...

随机推荐

  1. Delphi中Stringlist的自定义排序(将函数地址做为参数)

    近日,在编制一个程序过程,因为数据量较小,就使用了stringlist来暂存数据.在使用过程中,遇到了一个问题.Stringlist字符串列表的默认排序方法是按ASCII码的方式进行排序,如3,10, ...

  2. 使用layui+jQuery实现点击删除单行数据

    使用layui+jQuery实现点击删除单行数据 首先要用到layui的官网手册 地址:https://www.layui.com/ 注意1.  此功能是在使用layui展示数据的基础上实现 3.  ...

  3. nodejs配合jwt

    使用npm下载包: npm i jsonwebtoken --save 引入此包: const jsonwebtoken =require('jsonwebtoken'); JWT的组成: JWT由三 ...

  4. Salesforce 发送Email时遇到的问题(Case当中的Filed不出现Email选项:ケースのフィールドにメールタブが表示されない)。

    普段はケースの発生源はメールと選択する場合.ディフォルトで「フィールド」の中に「メール」というタブが出てきますが. (平时当我们选择[Case]的[来源]为[Email]时,默认就会在field中出现 ...

  5. MaaS模型即服务

    chatgpt的API开放让我看到了科研和产业结合的一种方式, 最新的科研成果也能飞入百姓家了. 也看到了MaaS的未来,估计要出现一批提供在线模型的新创公司了.

  6. Python语言基础学习报告

    这个学期我们开设了一门新课程---Python,早在很久之前,我就经常在各种有关职场的帖子下面看到这个词,且多为夸赞,因此,尽管大一深受C语言和数据结构两门课的折磨,我还是怀着十分的认真和敬意开始了这 ...

  7. fetchAllAssoc 小分析

    这个函数出现在了两个地方 includes\database\database.inc line 2245 includes\database\prefetch.inc line 481 foreac ...

  8. linux下项目自动化备份

    #! /bin/bash # 日期: 20220927 # 执行环境: 192.168.25.38 # 功能: 每周自动备份/data下"elasticsearch|project|soft ...

  9. 从FGUI中取一张图片并返回一个Sprite

    从Fgui中的图集中取一个图素,把图素用到场景等非UI的地方. 此操作会动态创建一个Sprite对象,效率不好,不适合大量使用. private static Dictionary<string ...

  10. 【Ubuntu】Ubuntu 技巧集锦

    『Ubuntu 22.04 国内镜像 阿里云/163源/清华大学/中科大』 『各种 Proxy 设置 GUI/Terminal/APT』 『设置 wget Proxy』 『设置右键菜单-新建文档』 『 ...