需求:

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

ant desgin pro使用教程 antd pro components_Source

 

 

实现:

1.表单初始化接口的返回约定为三个数组,按模块对应:

const [dataSource, setDataSource] = useState<{
    base_info?: API.FormListType[];
    detail_info?: API.FormListType[];
    total_info?: API.FormListType[];
  }>({});

  

ant desgin pro使用教程 antd pro components_ant desgin pro使用教程_02

 

 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

最终效果:

ant desgin pro使用教程 antd pro components_表单_03