需求:
在新增&编辑表单中,共分三个表单模块,第二个模块设计为一个可编辑表格组件,其中可选下拉列表依赖外层第一个模块的某条数据值,提供新增、编辑、删除、按规定条件去重等功能,并在第三个模块中自动计算列表数值总和
实现:
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
最终效果: