使用React中的hooks完成简易购物车功能—含代码分析

前言:

  1. React
    • React 是一个用于构建用户界面的JavaScript库 ,特别适合于:构建单页应用(SPA)。
    • 它通过组件化 的方式将UI 划分为可复用的独立部分,并且使用虚拟DOM 来优化渲染性能。
  2. Hooks
    • React 中的Hooks 是一些特殊的函数,允许我们在:函数组件 中使用状态和其他 React 特性。

1、购物车样例

1.1、示例图

在这里插入图片描述

1.2、弹窗示例图—满减

在这里插入图片描述

1.3、弹窗示例图—折扣

在这里插入图片描述

1.4、添加效果图

在这里插入图片描述

2、需求概括

做一个简易购物车 ,里面包含:

  1. 展示商品名商品价格商品数据
  2. 商品数量可以进行加/减,还有小计
  3. 点击全选时,单选需要全部勾上,反之亦然
  4. 单选全部勾上的时候,需要联动全选
  5. 做一个总价功能
  6. 商品数量为零的时候商品直接删掉
  7. 总价满50打9折满100打8折
  8. 新增添加按钮,点击添加按钮出现一个弹窗
    • 弹窗里面可以添加商品
    • 必须有商品名商品价格库存活动类型
    • 活动类型如果选择满减活动,则再出现两个输入框,输入:满多少减多少
    • 活动类型如果选择打折活动,则出现一个输入框,输入:多少折扣

3、代码实现

具体实现的步骤思路均放在代码中按照先后顺序标注

3.1、购物车功能主页面

// 2、从react中引入`useState`钩子函数 => 用于在函数组件中添加状态管理功能
      import { useState } from "react";
// 3、引入购物车弹窗组件
      import GoodsModal from "./02-购物车弹窗.jsx";
// 1、定义并默认导出hook组件
      export default function ShopCart() {
  // 2.1、定义商品的详细数据信息
    // 1)goods:整体的数据名称
    // 2)setGoods:修改goods中数据的方法
        const [goods, setGoods] = useState([
          { name: "小龙虾",
            price: 10,
          // 库存
            inventory: 10},
          { name: "羊肉串",
            price: 15,
            inventory: 12,
            // 满30元减10块,不累计
            activityInfo: {
              // 满
              full: 30,
              // 减
              reduce: 10}},
          { name: "牛肉串",
            price: 20,
            inventory: 30,
            // 打8折
            activityInfo: {
              // 折扣
              discount: 8,
            },
          },
        ]);
// 1.2.6、创建数据`isAllSelect` => 用于设置复选框是否勾选/全选
        const [isAllSelect, setIsAllSelect] = useState(false);
// 3.1.1、创建数据isShow => 用于决定是否显示子组件弹窗
        const [isShow, setIsShow] = useState(false)
// 1.2.1、获取活动内容的处理函数
        const getActivityTypeName = (data) => {
    // 1)如果接收到的每一项数据里的activityInfo(这个值是用来区别哪个商品有活动信息的)有值,则继续执行下面的判断
          if (data.activityInfo) {
    // 2)如果`data.activityInfo`下面的`full`有值 => 则为:`满减活动`
            if (data.activityInfo.full) {
        // 2.1、返回:字符串拼接具体的满/减活动
              return `活动—满${data.activityInfo.full}${data.activityInfo.reduce}`;
            }
    // 3)如果`data.activityInfo`下面的`discount`有值 => 则为:`打折活动`
        // 3.1、返回:字符串拼接具体的打折信息
            if (data.activityInfo.discount) {
              return `活动—打${data.activityInfo.discount}`;
            }
          }
    // 4)其它返回空就行
          return "";
        };
// 1.2.4、加/减按钮的事件处理函数 => 用于更新商品数量,当商品数量为0时,删掉这个商品
    // 1)参数1:商品的下标信息;参数2:加/减(1/-1)
        const updateNum = (index, actionType) => {
    // 2)给当前商品的数量(num)进行初始化
          goods[index].num = goods[index].num || 1;
    // 5)如果当前商品的数量为1,且`actionType`为-1(减),则代表当前商品数量只有一个,但用户还在点减去商品,此时就要删掉这条商品
          if (goods[index].num === 1 && actionType === -1) {
        // 5.1、执行删除商品
            goods.splice(index, 1);
        // 5.2、重新渲染视图
            setGoods([...goods]);
        // 5.3、最后return,后续的代码就没必要继续操作了
            return;
          }
    // 3)对数量进行操作
          goods[index].num += actionType;
    // 4)进行渲染
        // 4.1、如果是这种写法的话,这是在用`可变数据`来实现功能,没有改变goods的引用,所以会造成更新但不渲染
          // setGoods(goods)
        // 4.2、使用这种解构的方式,就会重新创建一个引用地址,此时react认为数据是更新了的,就会渲染组件了
          setGoods([...goods]);
        };
// 1.2.5、计算小计 => 注意:需要考虑到相关活动的逻辑,在进行计算!
        const countPrice = (data) => {
    // 1)先计算出商品没有参加活动的原始小计(价格) => 价格*数量
          let total = data.price * (data.num || 1);
    // 2)判断该商品是否参加活动 => 判断data.activityInfo是否有值
          if (data.activityInfo) {
        // 2.1、如果参加了`满减活动` => 则判断当前活动的价格是否满足`满减活动`的要求
            if (data.activityInfo.full && data.activityInfo.full <= total) {
          // 返回原始小计的总价 - 折扣部分(10) 
              return total - data.activityInfo.reduce;
            }
        // 2.2、如果参加了`折扣活动`
            if (data.activityInfo.discount) {
          // 返回原始小计的总价 * 折扣金额 * 0.1(因为要保证折扣返回的是10以内的数字,所以需要手动*0.1)
              return total * data.activityInfo.discount * 0.1;
            }
          }
    // 3)如果没有参加活动,则直接计算:价格*数量的结果(total)
          return total;
        };
// 1.2.6、全选框事件处理函数
        const allSelectChange = () => {
    // 1)初始化全选状态 => 取反isAllSelect,默认为false,此时就为ture了
          const currentAllSelect = !isAllSelect;
    // 2)同步所有单选状态 => 循环遍历每一项goods中的数据,给每一项的select赋值为`currentAllSelect`:把每一个单选状态设置为true
          goods.forEach((item) => {
            item.select = currentAllSelect;
          });
    // 3)渲染视图
          setIsAllSelect(currentAllSelect);
      // 3.1、goods在被渲染时,需要重新解构 => 更改一下引用地址
          setGoods([...goods]);
        };
// 1.2.6、单选框事件处理函数
        const singleSelect = (index) => {
    // 1)初始化单选状态 => 让对应下标的复选框取反
          goods[index].select = !goods[index].select;
    // 2)判断当前是否全选 => 判断goods中的每一项里的select是否都为ture,如果返回ture,则setIsAllSelect执行修改全选框状态(全选)
          setIsAllSelect(goods.every((item) => item.select));
    // 3)goods在被渲染时,需要重新解构 => 更改一下引用地址
          setGoods([...goods]);
        };
// 1.3.1、计算总价格的处理函数
        const countTotalPrice = () => {
    // 1)计算所有商品的总价
        // 1.1、先过滤每一项商品是否被选中
        // 1.2、在累加选中商品金额(这里调用了计算小计的方法,因为要判断选中的商品是否参加了活动)
          const total = goods
            .filter((item) => item.select)
            .reduce((total, item) => (total += countPrice(item)), 0);
    // 2)再次计算商品总价的活动 => 总金额≥50,并且<100,则:总金额打9折
          if (total >= 50 && total < 100) {
            return total * 0.9;
          }
    // 3)再次计算商品总价的活动 => 总金额≥100,则:总金额打8折
          if (total >= 100) {
            return total * 0.8;
          }
    // 4)不满足的活动的情况下,直接返回总金额
          return total
        };
// 3.3.1、给子组件传递方法 => 接收子组件返回的值
        const ok = (data) => {
    // 1)接收到的数据追加在goods数据后面
          setGoods([...goods, data])
    // 2)改变isShow的值为false => 关闭弹窗
          setIsShow(false)
        }
// 3.3.2、给子组件传递方法 => 用于直接关闭弹窗
        const cancel = () => {
    // 1)改变isShow的值为false => 关闭弹窗
          setIsShow(false)
        }
        // 静态页面设置
        return (
          <div>
            <h1>总价:满509折,满1008</h1>
    /* 1.1、全选复选框以及增加商品信息的按钮设置
          1)全选复选框的逻辑在1.2的第6条里已详细说明
          2)添加商品信息按钮 => 点击后触发`setIsShow(修改isShow的处理函数)将isShow的值设置为ture` */
            <div>
              <input type="checkbox" checked={isAllSelect} onChange={allSelectChange}/> 全选
              <button onClick={() => setIsShow(true)}>添加商品信息</button>
            </div>
    /* 1.2、渲染商品信息数据
          1)使用map渲染商品信息
          2)key值设置为当前的下标值 => index
          3) getActivityTypeName(item) => 显示的是当前的活动信息,把商品数据中的每一项给传递进去,调用后会返回具体的活动信息
          4)按钮的+/-事件(updateNum()传入1/-1) => 按钮中的num同样还是空值,做了逻辑处理:item.num||1,这里不多解释
          5)小计这部分逻辑因为涉及到活动的计算,所以单独提取出函数编写:countPrice => 同样需要传入每一项数据,用来判断活动
          6)单选和全选联动这部分逻辑其实和之前的差不多
            6.1、设置isAllSelect属性给全选框,当它发生改变时触发事件处理函数 => 循环遍历所有的goods数据,修改它们的item.select
            6.2、设置单选框的checked为item.select(虚拟值,用于修改选中状态),当它被改变时触发事件处理函数 */
            <div>
              {goods.map((item, index) => (
                <div key={index}>
                   <input onChange={() => singleSelect(index)} type="checkbox" checked={item.select || false}/>
                      名称:{item.name},价格:{item.price}{getActivityTypeName(item)}
                  <button onClick={() => updateNum(index, 1)}>+</button>
                       {item.num || 1}
                  <button onClick={() => updateNum(index, -1)}>-</button>
                       小计:{countPrice(item)}
                </div>
              ))}
            </div>
    /* 1.3、总计部分 */
            <h2>总计:{countTotalPrice()}</h2>
    /* 3.1、显示弹窗(子)组件
          1)给子组件传递`isShow`属性:用于决定是否显示子组件的弹窗
          2)传递onOk方法 => 用于子组件给父组件传值
          3)传递cancel方法 => 用于直接关闭购物车弹窗 */
            <GoodsModal onOk={ok} onCancel={cancel} isShow={isShow}></GoodsModal>
          </div>
        );
      }

3.2、购物车弹窗组件

// 2、从react中引入`useState`钩子函数 => 用于在函数组件中添加状态管理功能      
      import { useState } from "react";
// 3、从react-dom模块中导入ReactDOM对象,以便在React应用中使用ReactDOM提供的功能,比如:将 React 组件渲染到 DOM 中
  // 3.1、因为当前设置的的是弹窗组件,它比较特殊,需要放在特殊的dom结构上,因为它会显示在需要的任意位置上的
      import ReactDOM from "react-dom";
  // 3.3、引入外部写好的css样式
      import Styles from "./02-弹窗样式.module.css";
  // 3.2、创建挂载的元素,并插在body下面 => 一个div
      const rootModal = document.createElement("div");
      document.body.appendChild(rootModal);
// 1、定义并默认导出hook组件 
  // 3.5、接收父组件传递过来的`props`参数(接收时,不需要再前面添加this,因为没有指向问题) => 因为弹窗是否打开,取决于父组件
      export default function GoodsModal(props) {
  // 3.4.4、使用`useState`钩子函数创建数据 
      // 1)`goodsForm`是一个对象,用来存储表单里的数据,最终我们希望得到一个和父组件中`goods数组`里的对象一模一样的对象
        const [goodsForm, setGoodsForm] = useState({});
  // 3.4.4、获取input输入框的内容并创建数据事件处理函数
        const formChange = (event, type) => {
      // 4)判断自定义的数据类型是否是"activity(选择了活动选项)"且事件对象中的target.value值为none(代表没有选择活动)
          if (type === "activity" && event.target.value === "none") {
      // 5)如果选择`不参加活动`,则要删除当前的活动
            delete goodsForm.activityInfo;
            setGoodsForm({
              ...goodsForm,
            });
          }
      // 1)创建一个空对象
          const obj = {};
      // 2)obj(空对象)的值为:事件对象.target.value => 输入框获取到的值
        // 注意:在Js中,用下面这种语法可以动态地为对象`obj`添加/更新一个属性,其属性名由变量type决定,属性值由event...提供
        // 示例:
        //     obj = {}
        //     type = "name"
        //     obj[type] = "张三"
        //     console.log(obj); => {name:"张三"}
          obj[type] = event.target.value;
      // 3)把`goodsForm(当前组件创建的空对象)`用剩余运算放进来,在把新添加的数据类型追加进去
        // 这样的话,`goodsForm`里就有了最新添加的数据,且结构和父组件里的数据是一个类型
          setGoodsForm({
            ...goodsForm,
            ...obj,
          });
        };
  // 3.6.6、活动折扣的事件处理函数
        const activityChange = (event, type) => {
      // 1)初始化满减活动
          goodsForm.activityInfo = goodsForm.activityInfo || {};
      // 2)判断自定义的数据类型是否为 "discount"折扣活动 => 如果是,则:
          if (type === "discount") {
            setGoodsForm({
        // 更新原始数据
              ...goodsForm,
        // 创建新的活动信息数据 => 折扣由用户输入的值定义
              activityInfo: {
                discount: event.target.value,
              },
            });
      // 3)其它情况则为"满减活动"
          } else {
        // 创建空对象
            const obj = {};
        // 动态添加/更新属性
            obj[type] = event.target.value;
        // 更新`goodsForm`里的数据
            setGoodsForm({
              ...goodsForm,
        // 创建新的活动信息数据 => 前面初始化了:goodsForm.activityInfo || {};所以这里至少会为一个空对象
        // 这里的显示信息为:具体的满多少减了多少
              activityInfo: {
                ...goodsForm.activityInfo,
        // 注意:满减活动有两个字段,不要丢了另一个
                ...obj,
              },
            });
          }
        };
  // 3.7.1、"确定"按钮的事件处理函数
        const save = () => {
      // 1)调用接收接收到的父组件传递的函数,并传入goodsForm(添加商品信息后的对象数据)
          props.onOk(goodsForm);
      // 2)让goodsForm数据为空 => 添加过的数据,不需要保留
          setGoodsForm({})
        };
  // 3.7.2、"取消"按钮的事件处理函数
      // 1)调用父组件传递过来的函数 => 不需要传值
        const cancel = () => {
          props.onCancel()
      // 2)让goodsForm数据为空 => 初始化数据
          setGoodsForm({})
        }
  // 3.6、props.isShow && => 判断父组件传递过来的isShow属性是否为ture,为ture则再又是右侧代码(弹窗),同样props.isShow前不用this
  // 3.4、返回使用ReactDOM.createPortal创建的结构 => 它返回的是:在组件的常规层次结构之外渲染子组件
      // 1)参数1:是相关的jsx代码
      // 2)参数2:rootModal是被挂载的dom元素 => 等于以下代码都是挂载到它身上的
      // 3)className={Styles["modal-box"]}和className={Styles["modal-content"]} => class类名,用于设置css样式
      // 4)名称、价格、库存:设置onChange事件,当输入框的值发生改变时,传入事件对象,并把当前input框的类型也传进去,用于区别类型
      // 5)活动:使用下拉菜单可选择是否添加活动,同样当下来菜单的值发生改变时,传入事件对象,并把自定义的类型数据传进去,用于区别
        return (
          props.isShow &&
          ReactDOM.createPortal(
            <div className={Styles["modal-box"]}>
              <div className={Styles["modal-content"]}>
                <h1>商品信息:</h1>
                <div>名称:<input type="text" onChange={(event) => formChange(event, "name")}/></div>
                <div>价格:<input type="number"onChange={(event) => formChange(event, "price")}/></div>
                <div>库存:<input type="number"onChange={(event) => formChange(event, "inventory")}/></div>
                <div>活动:
                   <select onChange={(event) => formChange(event, "activity")}>
                      <option value="none">不参加活动</option>
                      <option value="full">满减活动</option>
                      <option value="discount">打折活动</option>
                   </select>
      /* 6)如果选择了活动,则需要根据活动类型显示以下其中的一个div => 用于设置活动折扣
            6.1、如果goodsForm.activity为full => 显示满/减活动
            6.2、如果goodsForm.activity为discount => 显示折扣活动
            6.3、同样,如果它们两个中的任意输入框里的值发生了变化,就触发activityChange处理函数 => 传入事件对象及自定义类型名 */
                </div>
                   {goodsForm.activity === "full" && (
                      <div>
                        满:<input type="number"onChange={(event) => activityChange(event, "full")}/><br/>
                        减:<input type="number"onChange={(event) => activityChange(event, "reduce")}/>
                      </div>
                   )}
                   {goodsForm.activity === "discount" && (
                      <div>
                        几折:<input type="number"onChange={(event) => activityChange(event, "discount")}/>
                      </div>
                   )}
      // 7)两个按钮:确定按钮 => 获取
                <div>
                   <button onClick={save}>确定</button>
                   <button onClick={cancel}>取消</button>
                </div>
              </div>
            </div>,
            rootModal
          )
        );
      }

3.3、购物车弹窗组件css样式

.modal-box {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
}
.modal-content {
  width: 300px;
  height: 300px;
  background: white;
  border-radius: 10px;
  padding: 20px;
}
  • 9
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值