仿照拼多多后台的动态表格

845 阅读5分钟

拼多多版UI界面

image.png

我的UI实现

image.png

动态表格的核心功能是:用户在商品规格输入规格 价格及库存表格就会动态地生成对应的行和列

问题:点击新增第二个商品种类 输入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;
    });
  };

数据流向

image.png

首先是数据,因为编辑价格的表格是根据sku表格的数据来渲染的,所以我们要先处理数据源,在这里,表头的数据是有动态生成的部分,这部分就通过redux toolkit来实现。然后是body部分,body部分是由datasource驱动的,datasource是由tempGoodsType加上用户输入的price、stock和image来的,tempGoodsType是经过数据格式处理的skuTypeData.

image.png

首先看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 }
]