前言
最近前端大佬用@alifd/next
来写后台管理系统,给我分配了一些页面类似下面的页面
其中分公司、存储地址、存储区三级联动。table中每一项都可以进行删除。添加按钮的弹框的页面类似下面这个样子。
点击弹框的行,多选,全选都可以进行更新到第一张图的页面
遇到的问题
- 怎么控制这个table变化呢,怎么进行,输入框单独可以操作,下拉框单独。如果对一个数据元进行来回更改,那么页面的消耗就很大。等等问题,下面就来例举几个吧
实现
之前看了一下官方的field
关于类似方案的例子,发现写的比较简洁。也不好操作。所以我就尝试form
+table
组合式写法魔改了一下
1,利用form对table进行包裹,因为form内嵌了field
,这样应该就比较方便一些。
const fieldTable = Field.useField(); //初始化fieldTable
<Form field={fieldTable}>
<Table.StickyLock
size="medium"
primaryKey="id" // 识别的唯一值
fixedHeader
maxBodyHeight={400} //和fixedHeader结合,设置table盒子的高度
dataSource={dataSource} //数据源
columns={columns} // 列表
rowSelection={rowSelection} // 选择每一行的操作,默认多选
/>
</Form>
选择rowSelection
设置
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const [selectedRows, setSelectedRows] = useState([]);
const rowSelection = {
selectedRowKeys,
onChange: (_selectedRowKeys, _selectedRows) => { // 单选时的操作
setSelectedRowKeys(_selectedRowKeys);
setSelectedRows(_selectedRows);
},
onSelectAll: (selected, _selectedRows) => { //全选时的操作
setSelectedRowKeys(_selectedRows.map((item) => item.cid));
},
getProps: (record) => { // 禁用的☑️的操作
return {
disabled: mode === 'view',
};
},
};
2,对columns进行封装,(删除了几个重复类型的,大致能懂应该)
const columns = [
// 新增/编辑用到的id,itemId,不显示但是可以区分(用是否有itemId来区分是点击编辑时拿到的数据,还是编辑时新增还是新增时新增的数据)
{
width: '0px', // 不会显示在页面
dataIndex: 'id',
align: 'center',
lock: 'left',
cell: (value, index, record) => {
return <div style={{ display: 'none' }}>{FormChildItem(value, index, record, 'id')}</div>;
},
},
{
width: '0px',// 不会显示在页面
dataIndex: 'itemId',
align: 'center',
lock: 'left',
cell: (value, index, record) => {
return <div style={{ display: 'none' }}>{FormChildItem(value, index, record, 'itemId')}</div>;
},
},
{
title: '类型',
dataIndex: 'wirType',
align: 'center',
lock: 'left',
cell: (value, index, record) => {
return value ? (
<Tag size="small" type="normal" color={wirTypeColor[value]}>
{wirTypeName[value] || 'BTS备货'}
</Tag>
) : (
<Tag size="small" type="normal" color="turquoise">
BTS备货
</Tag>
);
},
},
// 下面是formItem进行嵌套的
mode === 'add' ? isNull() : isRequireNo(), // 根据是编辑还是新增,动态显示table的列
{
title: '物料名称',
dataIndex: 'itemName',
align: 'center',
lock: 'left',
cell: (value, index, record) => {
return FormChildItem(value, index, record, 'itemName');
},
},
{
title: '键号',
dataIndex: 'itemCode',
align: 'center',
cell: (value, index, record) => {
return FormChildItem(value, index, record, 'itemCode');
},
},
{
title: '申请数量',
dataIndex: 'applyQuantity',
align: 'center',
cell: (value, index, record) => {
return FormChildItemRequire(value, index, record, 'applyQuantity');
},
},
{
title: '存储区',
dataIndex: 'areaId',
align: 'center',
cell: (value, index, record) => {
return FormChildItemSelect(value, index, record, 'areaId');
},
},
];
isNull
用来对三元表达式进行操作,不然写table就会报错
const isNull = () => {
return {
align: 'center',
width: '0px',
};
};
const isRequireNo = () => {
return {
title: '订单编号',
dataIndex: 'requireNo',
align: 'center',
lock: 'left',
cell: (value, index, record) => {
return <div>{value ? `${value}⚠️` : ''}</div>;
},
};
};
3 封装的FormChildItem
普通的表单显示,FormChildItemRequire
可校验的Input
,FormChildItemSelect
可校验的select
。为什么要给普通FormChildItem
也用表单包裹呢,主要是后端同学要用到里面的数据。要做处理,下面因该就会明白点。
- 下面是用每一行的
id
来进行区分的,⚠️不要用索引index来代替,因为删除的时候会出问题
const FormChildItem = (value, index, record, _name) => {
return (
<FormItem
isPreview // 查看的时候不能修改的一个属性
name={`${_name}-${record.id}`}
labelAlign="left"
>
<Input defaultValue={value} />
</FormItem>
);
};
const FormChildItemSelect = (value, index, record, _name) => {
return (
<FormItem
isPreview={mode === 'view'}
asterisk
name={`${_name}-${record.id}`}
required
requiredMessage="请选择存储区"
labelAlign="left"
>
<Select style={{ width: '110px' }} defaultValue={value} dataSource={storageType} />
</FormItem>
);
};
const FormChildItemRequire = (value, index, record, _name) => {
return (
<FormItem
isPreview={mode === 'view'}.
asterisk
name={`${_name}-${record.id}`}
required
requiredMessage="请输入申请数量"
labelAlign="left"
>
<NumberPicker defaultValue={value} min={0} />
</FormItem>
);
};
4,既然讲到了删除,那就写一下删除的方法(新增就不写了,比较简单,弹框子组件用setDatasource来设置传值)
// 点击删除按钮删除
const clickDeleteDataObj = () => {
if (!selectedRowKeys?.length) {
Message.error('请选择物料明细进行删除');
} else {
const newArr = removeDuplicates(dataSource, selectedRows); //dataSource:新增时通过从弹框选中拿到的数据数组;selectedRows:第一个页面删除时拿到的行数数组
setDataSource(newArr); // 重新设置table的源数据
// 重置key
setSelectedRowKeys([]); //清空被第一个页面被选中的框
}
};
// removeDuplicates删除的方法, 删除table数据,通过id来删除,简单有效。
const removeDuplicates = (arr1, arr2) => {
const ids = new Set(arr2.map((item) => item.id));
return arr1.filter((item) => !ids.has(item.id));
};
5,保存的时候校验该怎么做呢?
这里做了两个表单校验,通过页面1可以观察到,顶部也是个form要进行校验,它的field
名字就叫field
// 保存草稿| 提交申请
const handleOk = (type: number) => {
// type 1为保存草稿,2为提交申请
field.validate(field.getNames(), async (error) => {
if (error === null) {
const paramsValue: Object = field.getValues(); // 获取顶部的form数据
if (!dataSource?.length) {
return Message.error('请添加物料信息');
}
// 对table进行校验
fieldTable.validate(fieldTable.getNames(), async (errorIn) => {
const paramsValueIn: Object = fieldTable.getValues(); // 获取table的数据,这里获取的数据就是被FormItem包裹的数据,
//但是拿到的是对象,而且是所有`name-id`的对象,所以下面的方法将对象变为数组
const detailVoList = objChangeArr(paramsValueIn); //
if (errorIn === null) {
let TCode = 0;
let TMsg = '';
if (mode === 'add') {
// 新增,submitImmediately:保存草稿(false)和提交申请(true)
const { code = 0, msg = '' } = await services.fetchAddReplenishmentDraftAndApply({
...paramsValue,
detailVoList,
submitImmediately: type === 2,
});
TCode = code;
TMsg = msg;
} else {
// 编辑
// 只需要editDataSource被标记删除的数据
const _editDetailVoList = editDataSource.filter((v: any) => v.delete);
console.log(_editDetailVoList, '_editDetaVoList');
const { code = 0, msg = '' } = await services.fetchEditReplenishmentDraftAndApply({
...paramsValue,
detailVoList: [...detailVoList, ...(_editDetailVoList || [])],
submitImmediately: type === 2,
});
TCode = code;
TMsg = msg;
}
if (TCode === 101) {
Message.success(TMsg);
onOk?.(); // 父组件传入的值,不用管
field.resetToDefault();
}
} else {
// console.log(error);
}
});
} else {
// console.log(error);
}
});
};
- 对象转数组
类似这样的对象
转为数组
// 将对象转换成数组
const objChangeArr = (obj) => {
const _detailVoList = Object.keys(obj)
.reduce((acc: any, key: string) => {
const [prefix, id] = key.split('-');
if (prefix === 'id') {
acc.push({
// 编辑时,在有名为itemId的前提下,拿到id,否则(编辑时新增的,新增时新增的)就不传id,
id: obj[`itemId-${id}`] ? obj[`id-${id}`] : undefined, // 这个属于业务逻辑,不注重
itemId: obj[`itemId-${id}`] || obj[`id-${id}`],
itemCode: obj[`itemCode-${id}`],
applyQuantity: obj[`applyQuantity-${id}`],
areaId: obj[`areaId-${id}`],
});
}
return acc;
}, [])
.filter(Boolean);
return _detailVoList;
};
还有其他问题
- 1,保存,编辑时回显的被删除的数据,并打上标记delete
可以页面初始化的时候设置另外一个datasource数据源进行保存,然后合并进去 - 2,三级联动问题,参考第一个页面,重新选择分公司或者存储地址,table里面的数据要清空,所以我是这样写的
// 清空所有存储区 // 这里利用dataSource来进行遍历。通过field的setValue,重新设置为空字符串来清空
const clearAreaId = () => {
dataSource?.map((itemClear: any) => {
const areaIdNum = `areaId-${itemClear.id}`;
fieldTable.setValue(areaIdNum, '');
return itemClear;
});
};
后记
这么写完后,我这边功能没什么问题了,但是大佬却说我写的过于复杂,不好维护,要改成field写法,但是对于field写法,我是感觉不想写。不知道其他大佬有什么建议没有。