拼多多版UI界面
我的UI实现
动态表格的核心功能是:用户在商品规格输入规格 价格及库存表格就会动态地生成对应的行和列
问题:点击新增第二个商品种类 输入price和stock的值 再删除第二个商品种类 然后price的值被清理 但是stock变成price的值 ;再次点击新增二个商品种类 price的值就归位了
尝试看antd的代码 发现问题是tempGoodsType改变得太频繁 所以datasource就会在编辑price的时候出现问题
尝试用原生的html实现下方的价格及库存表格, 这样子,当dataSource有值的时候,tbody就会渲染
<div>
<form
onSubmit={onFinish}
style={{
border: "1px solid #f0f0f0",
borderRadius: "10px",
paddingTop: "10px",
}}
>
<table style={{ width: "100%" }}>
<thead>
<tr>
{columns.map((col, index) => (
<th key={index}>{col.title as string}</th>
))}
</tr>
</thead>
<tbody>
{dataSource.map(
(item, index) =>
item && (
<tr
key={index}
style={{
borderTop: "1px solid #f0f0f0",
marginTop: "3px",
padding: "3px",
}}
>
{columns.map((col, colIndex) => (
<td key={colIndex} style={{ textAlign: "center" }}>
{col && col.render
? col.render(null, item, index)
: //@ts-ignore
// 没有render,表示是不能编辑的 就直接渲染数据 item有可能是undefined
item[col.dataIndex]}
</td>
))}
</tr>
)
)}
</tbody>
</table>
</form>
</div>
columns的实现如下,包含了动态生成的列:
const dynamicColumns = () => {
return skuTypeData.specifications.map((item) => {
return {
title: item?.name,
dataIndex: item?.name,
editable: true,
};
});
};
// 实现了render方法的都是用户可以编辑修改的
const columns: (ColumnTypes[number] & {
editable?: boolean;
dataIndex: string;
})[] = [
...dynamicColumns(),
{
title: "price",
dataIndex: "price",
editable: false,
render: (_: any, record: any, index: any) => (
// 三个参数代表:_ 当前input的值,record当前行的值,index表示当前行的下标
<input
type="number"
style={inputStyle}
min={1}
value={dataSource[index]?.price || ""}
onChange={(e) => {
console.log("record", _, record, index);
const value = e.target.value;
const newDataSource = [...dataSource];
newDataSource[index] = { ...newDataSource[index], price: value };
setDataSource(newDataSource);
}}
/>
),
},
{
title: "stock",
dataIndex: "stock",
editable: true,
render: (_: any, record: any, index: any) => (
<input
type="number"
style={inputStyle}
min={1}
value={dataSource[index]?.stock || ""}
onChange={(e) => {
const value = e.target.value;
const newDataSource = [...dataSource];
newDataSource[index] = { ...newDataSource[index], stock: value };
setDataSource(newDataSource);
}}
/>
),
},
{
title: "image",
dataIndex: "image",
editable: true,
key: "image",
render: (_: any, record: any, index: any) => (
<>
<div>
<ImgCrop rotationSlider>
<Upload
name="avatar"
listType="picture-card"
showUploadList={true}
beforeUpload={beforeUpload}
showUploadList={{ showPreviewIcon: false }}
// @ts-ignore
fileList={fileListArray[index]}
onChange={(info) => handleImageChange(info, index)}
>
{/* @ts-ignore */}
{(!fileListArray[index] || fileListArray[index].length < 1) &&
uploadButton()}
</Upload>
</ImgCrop>
</div>
</>
),
},
];
最后一列是上传图片,使用了antd的upload组件,比较复杂的点是:如果我在这里用的是上传头像那种只有1张图片的,那么上传一行的upload图片,表格中所有的图片都会变成那张图片,另外因为后端不是通过图床(像antd那样)来存储图片,而要求前端传入base64,所以我将**原生的fileList外层又包裹了一个数组,通过索引这个数组的下标来得到每个upload组件对应的fileList。**另外将图片从file格式转为base64的格式。
图片的处理:
const handleImageChange = async (info: any, index: number) => {
let base64: string;
if (info.fileList.length !== 0) {
base64 = await getBase64(info.file.originFileObj);
}
if (info.fileList.length !== 0) {
info.fileList[0].status = "done";
}
setFileListArray((prev) => {
const newFileList = [...prev];
// @ts-ignore
newFileList[index] = info.fileList;
return newFileList;
});
setDataSource((prev) => {
const newDataSource = [...prev];
newDataSource[index] = {
...newDataSource[index],
images: [{ image: base64 }],
};
return newDataSource;
});
};
数据流向
首先是数据,因为编辑价格的表格是根据sku表格的数据来渲染的,所以我们要先处理数据源,在这里,表头的数据是有动态生成的部分,这部分就通过redux toolkit来实现。然后是body部分,body部分是由datasource驱动的,datasource是由tempGoodsType加上用户输入的price、stock和image来的,tempGoodsType是经过数据格式处理的skuTypeData.
首先看tempGoodsType的生成:
let tempGoodsType = useGoodsType ();
这个是怎么实现返回去改sku type不清空price的呢?那就需要给每个type添加唯一的id,改变sku type的时候,去改变对应的datasource的type,而不改变其他值: 生成唯一id:如果用户添加了2种sku大类,就用2个for循环去生成‘x-y’形式的uniqueId,如果用户只添加了一种sku大类,就用‘x- -1’来表示,用-1来表示第二种sku的下标,因为该sku不存在;
useGoodsType 的主要作用是(1)处理skuTypeData.specifications数据的格式,把它转化成tempGoodsType的格式,便于价格表格的渲染;(2)做sku的组合;specifications最多有2个对象(用户最多只能添加2个sku),通过2个for循环,我们就可以生成所有的sku组合。比如用户添加了如下2种重量规格,还添加了3种颜色规格,通过2个for循环就可以生成6种规格
"specifications": [
{
"id": 229,
"name": "重量",
"values": [
{
"id": 419,
"name": "重量",
"value": "约 1.15ggg"
},
{
"id": 420,
"name": "重量",
"value": "约 1.2g"
}
]
}
],
tempGoodsType:
[{重量: '约 1.15ggg', uniqueId: '0--1'}
{重量: '约 1.2g', uniqueId: '1--1'}]
const useGoodsType = () => {
let tempGoodsType: any[] = [];
const skuTypeData = useSelector((state: AppState) => state.SKUType);
if (
skuTypeData.specifications.length !== 0 &&
skuTypeData.specifications[0] &&
skuTypeData.specifications[0].values &&
skuTypeData.specifications[0].values.length !== 0
) {
const sku1ValuesSum = skuTypeData.specifications[0].values.filter(
(value) => value !== undefined
).length;
const sku1Name = skuTypeData.specifications[0].name;
for (let i = 0; i < sku1ValuesSum; i++) {
let forTableRender: { [x: string]: string; uniqueId: any; };
const sku1 = skuTypeData.specifications[0].values[i]?.value;
const id = skuTypeData.specifications[0].values[i]?.id;
//if the user set the second sku
if (
skuTypeData.specifications[1] &&
skuTypeData.specifications[1].values !== undefined
) {
const sku2Values = skuTypeData.specifications[1].values.length;
const sku2Name = skuTypeData.specifications[1].name;
for (let j = 0; j < sku2Values; j++) {
const sku2 = skuTypeData.specifications[1].values[j]?.value;
const id = skuTypeData.specifications[1].values[j]?.id;
forTableRender = {
[sku1Name]: sku1, //gram:20g
[sku2Name]: sku2, //color:red//在这轮循环一直在变
uniqueId: `${i}-${j}`,
};
// Check if uniqueId exists
// 这里还有1种情况需要替代原先的对象:
// forTableRender的uniqueId 等于
const existingIndex = tempGoodsType.findIndex(item => item.uniqueId === forTableRender.uniqueId);
if (existingIndex !== -1) {
// Replace the object
tempGoodsType[existingIndex] = forTableRender;
} else {
// Push the new object
tempGoodsType.push(forTableRender);
}
}
} else {
forTableRender = {
[sku1Name]: sku1,
uniqueId: `${i}--1`,
};
// Check if uniqueId exists
const existingIndex = tempGoodsType.findIndex(item => item.uniqueId === forTableRender.uniqueId);
if (existingIndex !== -1) {
// Replace the object
tempGoodsType[existingIndex] = forTableRender;
} else {
// Push the new object
tempGoodsType.push(forTableRender);
}
}
}
}
return tempGoodsType;
};
datasource的生成
const updatedDatasource = dataSource
.map((dataItem) => {
// 这里是在找对应的uniqueId
const tempItem = tempGoodsType.find(
(temp) => temp.uniqueId === dataItem?.uniqueId
);
// 如果找不到对应的id 就考虑0--1的情况
const doubleSKU = tempGoodsType.find(
(temp) => temp.uniqueId === "0-0"
);
// 处理一开始有2个sku 后面又删除一个sku 剩下一个sku 要保持原来datasource表格里面数据
const singleSKU = tempGoodsType.find(
(temp) => temp.uniqueId === "0--1"
);
if (
tempItem ||
(dataItem?.uniqueId == "0--1" && doubleSKU) ||
(dataItem?.uniqueId == "0-0" && singleSKU)
) {
return {
...tempItem, // spread all properties from tempItem
price: dataItem.price,
stock: dataItem.stock,
images: dataItem.images,
};
}
})
.filter((item) => item !== undefined);
假设用户设置了两个规格:
- 规格1:重量 - [50g, 60g]
- 规格2:成色 - [999K金, 990K金]
useGoodsType 会生成4个组合:
[
{ 重量: "50g", 成色: "999K金", uniqueId: "0-0", id0: 1, id1: 3 },
{ 重量: "50g", 成色: "990K金", uniqueId: "0-1", id0: 1, id1: 4 },
{ 重量: "60g", 成色: "999K金", uniqueId: "1-0", id0: 2, id1: 3 },
{ 重量: "60g", 成色: "990K金", uniqueId: "1-1", id0: 2, id1: 4 }
]