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
最终效果: