React+antd 实现sku商品规格功能

使用业务场景

只要是做电商类相关的产品,比如购物 APP、购物网站等等,都会遇到这么一个场景,每个商品对应着多个规格,用户可以根据不同的规格组合,选择出自己想要的产品。我们自己在生活中也会经常用到这个功能。

需要完成功能点

1.添加规格名称

2.添加规格值

3.删除规格

4.删除规格值

HTML

const columns: any[] = [
    ...specContent.map(t => {
      return {
        title: t.label,
        render: (item: any) => {
          return item[t.label]
        }
      }
    }),
    {
      title: '价格',
      render: (item: SkuData, _: SkuData, index: number) => {
        return <InputNumber
          min={0}
          defaultValue={submitList[index].piece}
          onChange={(e) => {
            submitList[index].piece = e || 0
            setSubmitList(submitList);
          }}
        />
      }
    },
    {
      title: '库存',
      render: (item: SkuData, _: SkuData, index: number) => {
        return <InputNumber
          min={0}
          defaultValue={submitList[index].stock}
          onChange={(e) => {
            submitList[index].stock = e || 0
            setSubmitList(submitList);
          }}
        />
      }
    }
  ]
  const ElInputContent = () => (
    <Input
      ref={inputRef}
      value={specLabelStr}
      style={{ width: 350 }}
      placeholder="请输入规格名称 按下Enter键确认"
      onPressEnter={onAddSpecLabel}
      onChange={(value) => setSpecLabelStr(value.target.value)}
      addonAfter={<span style={{ cursor: 'pointer' }} onClick={onAddSpecLabel}>确认添加</span>}
    />
  );
  return <div >
    <div style={{ width: '65%', margin: '130px auto' }} >
      <Card title={
        <Popover placement="bottomLeft" trigger="click" content={ElInputContent}>
          <Button type="dashed" icon={<PlusOutlined />} onClick={() => setVisible(!visible)} >添加规格</Button>
        </Popover>
      } extra={
        <h3>商品规格</h3>
      }>
        <div>
          {specContent.map((item, index) => {
            return <div key={index}>
              <h3>
                <span style={{ marginRight: 12 }}>{item.label}</span>
                <DeleteOutlined onClick={() => onDeleteSpec(index)} style={{ color: 'red' }} />
              </h3>
              <div style={{ display: 'flex', alignItems: 'center' }} ref={tagInputRef}>
                <div> {item.tags.map((str, strKey) => (
                  <Tag
                    style={{ fontSize: 16 }}
                    color="processing" key={strKey}>
                    <span>{str}</span>
                    <CloseOutlined onClick={() => onDeleteSpecTag(index, strKey)} />
                  </Tag>
                ))}</div>
                {
                  inputVisible && index === tagIndex ?
                    <Input
                      placeholder="请输入规格值"
                      value={inputTagValue}
                      size="small"
                      style={{ width: 120 }}
                      onChange={(e) => setInputTagValue(e.target.value)}
                      onBlur={() => onAddSpecTag(index)}
                      onPressEnter={() => onAddSpecTag(index)}
                    /> :
                    <Tag
                      icon={<PlusOutlined />}
                      style={{ cursor: 'pointer' }}
                      onClick={() => {
                        setTagIndex(index);
                        setInputVisible(!inputVisible)
                      }}
                    >添加规格值</Tag>
                }
              </div>
            </div>
          })}
        </div>
        <Table rowKey={'sku'} dataSource={submitList} columns={columns} pagination={false} />
      </Card>
    </div>
  </div>
}

添加规格名称

 const [submitList, setSubmitList] = useState<SkuData[]>([]); // 提交数据
  const [specContent, setSpecContent] = useState<ISpecTagValue[]>([]); //规格内容
  const [specLabelStr, setSpecLabelStr] = useState<string>('');// 规格名称输入值
  const [visible, setVisible] = useState<boolean>(false); // 点击添加规格按钮控制获取input 元素,控制输入默认选择focus
  const inputRef = useRef<InputRef>(null);// 规格输入框
  const [inputVisible, setInputVisible] = useState<boolean>(false);
  const [inputTagValue, setInputTagValue] = useState<string>('');
  const [tagIndex, setTagIndex] = useState<number | null>(null)
  const tagInputRef = useRef(null);


  // 添加规格名称
  function onAddSpecLabel() {
    if (specLabelStr) {
      setSpecContent(specContent.concat({ label: specLabelStr, tags: [] }));
      setSpecLabelStr('');
      message.success('添加规格明成功');
      tableSku();
    } else {
      message.error('请填写规格名称');
    }
  }

添加规格值

 // 添加规格值
  function onAddSpecTag(index: number) {
    if (inputTagValue) {
      const specList = [...specContent];
      specList[index].tags.push(inputTagValue);
      setSpecContent(specList);
      setInputTagValue('');// 清空输入内容
      tableSku();
      message.success('添加规格值成功');
    };
    setInputVisible(false);
  }

删除规格

  // 删除规格
  function onDeleteSpec(index: number) {
    const specList = [...specContent];
    specList.splice(index, 1);
    setSpecContent(specList);
    message.success('删除规格成功');
    tableSku();
  }

删除规格值

  //删除规格值 
  function onDeleteSpecTag(labelIndex: number, tagIndex: number) {
    const specList = [...specContent];
    specList[labelIndex].tags.splice(tagIndex, 1);
    setSpecContent(specList);
    tableSku();
  }
  

最后附上我全部代码

如果还是没有解决你的问题可以拉取我的github源码yangjike123/React_goodsSpec (github.com)

import { Button, Card, Input, InputNumber, InputRef, message, Popover, Table, Tag } from "antd";
import { useState, useRef, useEffect } from "react";
import { PlusOutlined, DeleteOutlined, CloseOutlined } from '@ant-design/icons';
import "./App.css";
import { SkuData } from "./interface.js";
interface ISpecTagValue {
  label: string,
  tags: string[]
}
export default () => {
  const [submitList, setSubmitList] = useState<SkuData[]>([]); // 提交数据
  const [specContent, setSpecContent] = useState<ISpecTagValue[]>([]); //规格内容
  const [specLabelStr, setSpecLabelStr] = useState<string>('');// 规格名称输入值
  const [visible, setVisible] = useState<boolean>(false); // 点击添加规格按钮控制获取input 元素,控制输入默认选择focus
  const inputRef = useRef<InputRef>(null);// 规格输入框
  const [inputVisible, setInputVisible] = useState<boolean>(false);
  const [inputTagValue, setInputTagValue] = useState<string>('');
  const [tagIndex, setTagIndex] = useState<number | null>(null)
  const tagInputRef = useRef(null);


  // 添加规格名称
  function onAddSpecLabel() {
    if (specLabelStr) {
      setSpecContent(specContent.concat({ label: specLabelStr, tags: [] }));
      setSpecLabelStr('');
      message.success('添加规格明成功');
      tableSku();
    } else {
      message.error('请填写规格名称');
    }
  }

  // 删除规格
  function onDeleteSpec(index: number) {
    const specList = [...specContent];
    specList.splice(index, 1);
    setSpecContent(specList);
    message.success('删除规格成功');
    tableSku();
  }

  // 添加规格值
  function onAddSpecTag(index: number) {
    if (inputTagValue) {
      const specList = [...specContent];
      specList[index].tags.push(inputTagValue);
      setSpecContent(specList);
      setInputTagValue('');// 清空输入内容
      tableSku();
      message.success('添加规格值成功');
    };
    setInputVisible(false);
  }

  function onDeleteSpecTag(labelIndex: number, tagIndex: number) {
    const specList = [...specContent];
    specList[labelIndex].tags.splice(tagIndex, 1);
    setSpecContent(specList);
    tableSku();
  }
  function tableSku() {// 绘制商品规格sku
    let temp: any[] = [];
    specContent.forEach((item, index) => {
      if (!temp.length) {
        // specContent当只有一个数据时候只需要
        temp.push(...item.tags.map(str => {
          const oldItem = submitList.find(t => t.sku === str);
          if (oldItem) {
            return { ...oldItem };
          } else {
            return {
              [`skuName${index + 1}`]: item.label,
              [`skuValue${index + 1}`]: str,
              [item.label]: str,
              stock: 0,
              piece: 0,
              sku: str
            }
          }
        }))
      } else {
        const array: SkuData[] = [];
        temp.forEach(obj => {
          if (item.tags.length === 0) array.push(obj);
          array.push(
            ...item.tags.map(t => {
              obj.sku && (obj.sku = obj.sku + t);
              const oldItem = submitList.find(t => t.sku === obj.sku);
              if (oldItem) {
                return { ...oldItem };
              } else {
                return {
                  ...obj,
                  [`skuName${index + 1}`]: item.label,
                  [`skuValue${index + 1}`]: t,
                  [item.label]: t,
                  stock: 0,
                  piece: 0
                };
              }
            })
          )
        });
        temp = array;
      }
    });
    setSubmitList(temp);
  }
  useEffect(() => {
    console.log(submitList, 'SubmitList')
  }, [specContent])
  useEffect(() => {
    inputRef.current?.focus();
  }, [visible])
  useEffect(() => {
    (tagInputRef.current as any)?.childNodes[1].focus();
    (tagInputRef.current as any)?.childNodes[0].focus();
  }, [inputVisible, tagIndex])

  const columns: any[] = [
    ...specContent.map(t => {
      return {
        title: t.label,
        render: (item: any) => {
          return item[t.label]
        }
      }
    }),
    {
      title: '价格',
      render: (item: SkuData, _: SkuData, index: number) => {
        return <InputNumber
          min={0}
          defaultValue={submitList[index].piece}
          onChange={(e) => {
            submitList[index].piece = e || 0
            setSubmitList(submitList);
          }}
        />
      }
    },
    {
      title: '库存',
      render: (item: SkuData, _: SkuData, index: number) => {
        return <InputNumber
          min={0}
          defaultValue={submitList[index].stock}
          onChange={(e) => {
            submitList[index].stock = e || 0
            setSubmitList(submitList);
          }}
        />
      }
    }
  ]
  const ElInputContent = () => (
    <Input
      ref={inputRef}
      value={specLabelStr}
      style={{ width: 350 }}
      placeholder="请输入规格名称 按下Enter键确认"
      onPressEnter={onAddSpecLabel}
      onChange={(value) => setSpecLabelStr(value.target.value)}
      addonAfter={<span style={{ cursor: 'pointer' }} onClick={onAddSpecLabel}>确认添加</span>}
    />
  );
  return <div >
    <div style={{ width: '65%', margin: '130px auto' }} >
      <Card title={
        <Popover placement="bottomLeft" trigger="click" content={ElInputContent}>
          <Button type="dashed" icon={<PlusOutlined />} onClick={() => setVisible(!visible)} >添加规格</Button>
        </Popover>
      } extra={
        <h3>商品规格</h3>
      }>
        <div>
          {specContent.map((item, index) => {
            return <div key={index}>
              <h3>
                <span style={{ marginRight: 12 }}>{item.label}</span>
                <DeleteOutlined onClick={() => onDeleteSpec(index)} style={{ color: 'red' }} />
              </h3>
              <div style={{ display: 'flex', alignItems: 'center' }} ref={tagInputRef}>
                <div> {item.tags.map((str, strKey) => (
                  <Tag
                    style={{ fontSize: 16 }}
                    color="processing" key={strKey}>
                    <span>{str}</span>
                    <CloseOutlined onClick={() => onDeleteSpecTag(index, strKey)} />
                  </Tag>
                ))}</div>
                {
                  inputVisible && index === tagIndex ?
                    <Input
                      placeholder="请输入规格值"
                      value={inputTagValue}
                      size="small"
                      style={{ width: 120 }}
                      onChange={(e) => setInputTagValue(e.target.value)}
                      onBlur={() => onAddSpecTag(index)}
                      onPressEnter={() => onAddSpecTag(index)}
                    /> :
                    <Tag
                      icon={<PlusOutlined />}
                      style={{ cursor: 'pointer' }}
                      onClick={() => {
                        setTagIndex(index);
                        setInputVisible(!inputVisible)
                      }}
                    >添加规格值</Tag>
                }
              </div>
            </div>
          })}
        </div>
        <Table rowKey={'sku'} dataSource={submitList} columns={columns} pagination={false} />
      </Card>
    </div>
  </div>
}
最终实现效果

  • 9
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,下面是一个简单的示例代码: 首先安装所需依赖: ```bash npm install react antd axios --save ``` 然后创建一个 `Login` 组件和一个 `Register` 组件,分别用于登录和注册的页面展示。其中,我们使用了 `Form` 和 `Input` 组件来实现表单输入,以及 `Button` 组件来实现按钮的功能。 ```jsx // Login.jsx import React, { useState } from 'react'; import { Form, Input, Button, message } from 'antd'; import axios from 'axios'; const Login = () => { const [loading, setLoading] = useState(false); const onFinish = async (values) => { setLoading(true); try { const response = await axios.post('/api/login', values); message.success(response.data.message); setLoading(false); } catch (error) { message.error(error.response.data.message); setLoading(false); } }; return ( <Form name="login" onFinish={onFinish}> <Form.Item name="email" rules={[{ required: true, message: 'Please input your email!' }]} > <Input placeholder="Email" /> </Form.Item> <Form.Item name="password" rules={[{ required: true, message: 'Please input your password!' }]} > <Input.Password placeholder="Password" /> </Form.Item> <Form.Item> <Button type="primary" htmlType="submit" loading={loading}> Login </Button> </Form.Item> </Form> ); }; export default Login; ``` ```jsx // Register.jsx import React, { useState } from 'react'; import { Form, Input, Button, message } from 'antd'; import axios from 'axios'; const Register = () => { const [loading, setLoading] = useState(false); const onFinish = async (values) => { setLoading(true); try { const response = await axios.post('/api/register', values); message.success(response.data.message); setLoading(false); } catch (error) { message.error(error.response.data.message); setLoading(false); } }; return ( <Form name="register" onFinish={onFinish}> <Form.Item name="name" rules={[{ required: true, message: 'Please input your name!' }]} > <Input placeholder="Name" /> </Form.Item> <Form.Item name="email" rules={[{ required: true, message: 'Please input your email!' }]} > <Input placeholder="Email" /> </Form.Item> <Form.Item name="password" rules={[{ required: true, message: 'Please input your password!' }]} > <Input.Password placeholder="Password" /> </Form.Item> <Form.Item name="confirmPassword" rules={[ { required: true, message: 'Please confirm your password!' }, ({ getFieldValue }) => ({ validator(_, value) { if (!value || getFieldValue('password') === value) { return Promise.resolve(); } return Promise.reject(new Error('The two passwords do not match!')); }, }), ]} > <Input.Password placeholder="Confirm Password" /> </Form.Item> <Form.Item> <Button type="primary" htmlType="submit" loading={loading}> Register </Button> </Form.Item> </Form> ); }; export default Register; ``` 在这里我们使用了 `axios` 来发送登录和注册的请求,需要注意的是,这里的请求地址是 `/api/login` 和 `/api/register`,而不是实际的后端接口地址,根据实际情况进行修改。 最后在需要展示登录和注册功能的页面中引入 `Login` 和 `Register` 组件即可。 ```jsx import React from 'react'; import Login from './Login'; import Register from './Register'; const LoginPage = () => { return ( <div> <h1>Login</h1> <Login /> <hr /> <h1>Register</h1> <Register /> </div> ); }; export default LoginPage; ``` 以上就是一个简单的使用 `react` 和 `antd` 实现登录和注册功能的示例代码,希望对你有帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值