前言:
React
:
React
是一个用于构建用户界面的JavaScript库
,特别适合于:构建单页应用(SPA)。- 它通过
组件化
的方式将UI
划分为可复用的独立部分,并且使用虚拟DOM
来优化渲染性能。Hooks
:
React
中的Hooks
是一些特殊的函数,允许我们在:函数组件
中使用状态和其他 React 特性。
1、购物车样例
1.1、示例图
1.2、弹窗示例图—满减
1.3、弹窗示例图—折扣
1.4、添加效果图
2、需求概括
做一个
简易购物车
,里面包含:
- 展示
商品名
、商品价格
、商品数据
- 商品数量可以进行
加/减
,还有小计
- 点击
全选
时,单选需要全部勾上,反之亦然 单选
全部勾上的时候,需要联动全选- 做一个
总价功能
- 当
商品数量为零
的时候商品直接删掉 - 总价
满50打9折
,满100打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>总价:满50打9折,满100打8折</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;
}