九、案例:汉堡到家
大约 21 分钟约 6192 字
(一)项目准备
src/index.js
1. import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
// 设置移动端适配
// 100/x 表示 视口宽度为x rem
document.documentElement.style.fontSize = 100 / 750 + "vw";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
src/index.css
2. * {
box-sizing: border-box;
}
body {
margin: 0;
}
src/App.js
3. import React from "react";
export default function App() {
return (
<>
<div style={{ width: "750rem", height: 200, backgroundColor: "#bfa" }}></div>
</>
);
}
(二)完成 Meals 组件
src/index.css
1. * {
box-sizing: border-box;
}
body {
margin: 0;
}
img {
vertical-align: middle;
}
src/App.js
2. import React from "react";
import { Meals } from "./components/Meals/Meals";
export default function App() {
return (
<>
{/* 测试移动端适配视口宽度 */}
{/* <div
style={{ width: "750rem", height: 200, backgroundColor: "#bfa" }}
></div> */}
<Meals />
</>
);
}
src/components/Meals/Meals.jsx
3. import React from "react";
import { Meal } from "./Meal/Meal";
import classes from "./Meals.module.css";
export const Meals = () => {
return (
// 设置滚动条出现在Meals而不是body
<div className={classes.Meals}>
<Meal />
<Meal />
<Meal />
<Meal />
<Meal />
<Meal />
<Meal />
<Meal />
<Meal />
<Meal />
<Meal />
<Meal />
</div>
);
};
src/components/Meals/Meals.module.css
4. .Meals {
position: absolute;
top: 0;
bottom: 0;
overflow: auto;
}
src/components/Meals/Meal/Meal.jsx
5. import React from "react";
import classes from "./Meal.module.css";
export const Meal = () => {
return (
<div className={classes.Meal}>
<div className={classes.ImgBox}>
<img src="/img/meals/1.png" />
</div>
<div>
<h2 className={classes.Title}>汉堡包</h2>
<p className={classes.Desc}>百分百纯牛肉搭配爽脆酸瓜洋葱粒与美味番茄酱经典滋味让你无法抵挡!</p>
<div className={classes.PriceWrap}>
<span className={classes.Price}>12</span>
<div>数量</div>
</div>
</div>
</div>
);
};
src/components/Meals/Meal/Meal.module.css
5. .Meal {
border-bottom: 1px #f2f2f2 solid;
padding: 30rem 20rem;
display: flex;
align-items: center;
}
.ImgBox {
width: 280rem;
}
img {
width: 100%;
}
.Title {
font-size: 18px;
font-weight: normal;
margin: 0;
}
.Desc {
margin: 0;
font-size: 12px;
color: #bbb;
padding-right: 40rem;
}
.PriceWrap {
margin-top: 40rem;
display: flex;
justify-content: space-between;
}
.Price {
font-weight: bold;
font-size: 20px;
}
.Price::before {
content: "¥";
font-size: 12px;
}
(三)引入 FontAwesome
- https://fontawesome.com/v6/docs/web/use-with/react/add-icons#alternative-ways-to-add-icons
- https://fontawesome.com/docs/web/use-with/react/
1.安装依赖
yarn add @fortawesome/fontawesome-svg-core
yarn add @fortawesome/free-solid-svg-icons
yarn add @fortawesome/free-regular-svg-icons
yarn add @fortawesome/react-fontawesome@latest
// 也可以一起安装
yarn add @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/free-regular-svg-icons @fortawesome/react-fontawesome@latest
2.引入组件
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
3.引入图标
import { faPlus } from "@fortawesome/free-solid-svg-icons";
4.使用组件
<FontAwesomeIcon icon="{faPlus}" />
src/components/UI/Counter/Counter.jsx
5. import React from "react";
import classes from "./Counter.module.css";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPlus, faMinus } from "@fortawesome/free-solid-svg-icons";
export const Counter = (props) => {
return (
<div className={classes.Counter}>
{props.amount && props.amount !== 0 ? (
<>
<button className={classes.Sub}>
<FontAwesomeIcon icon={faMinus} />
</button>
<span className={classes.count}>{props.amount}</span>
</>
) : null}
<button className={classes.Add}>
<FontAwesomeIcon icon={faPlus} />
</button>
</div>
);
};
src/components/UI/Counter/Counter.module.css
6. .Counter {
display: flex;
align-items: center;
}
.Sub,
.Add {
border: none;
background-color: #fcbf49;
width: 36rem;
height: 36rem;
line-height: 36rem;
border-radius: 50%;
font-size: 14px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
}
.Sub {
background-color: #fff;
border: 1px solid #000;
}
.count {
font-size: 16px;
margin: 0 5px;
}
src/components/Meals/Meal/Meal.jsx
7. import React from "react";
import { Counter } from "../../UI/Counter/Counter";
import classes from "./Meal.module.css";
export const Meal = () => {
return (
<div className={classes.Meal}>
<div className={classes.ImgBox}>
<img src="/img/meals/1.png" alt="汉堡包" />
</div>
<div>
<h2 className={classes.Title}>汉堡包</h2>
<p className={classes.Desc}>百分百纯牛肉搭配爽脆酸瓜洋葱粒与美味番茄酱经典滋味让你无法抵挡!</p>
<div className={classes.PriceWrap}>
<span className={classes.Price}>12</span>
<div>
<Counter amount={2} />
</div>
</div>
</div>
</div>
);
};
src/components/Meals/Meal/Meal.module.css
8. .Meal {
border-bottom: 1px #f2f2f2 solid;
padding: 30rem 20rem;
display: flex;
align-items: center;
}
.ImgBox {
width: 280rem;
}
img {
width: 100%;
}
.Title {
font-size: 18px;
font-weight: normal;
margin: 0;
}
.Desc {
margin: 0;
font-size: 12px;
color: #bbb;
padding-right: 40rem;
}
.PriceWrap {
margin-top: 40rem;
padding-right: 40rem;
display: flex;
justify-content: space-between;
}
.Price {
font-weight: bold;
font-size: 20px;
}
.Price::before {
content: "¥";
font-size: 12px;
}
(四)加载 Meals 数据
src/components/Meals/Meal/Meal.jsx
1. import React from "react";
import { Counter } from "../../UI/Counter/Counter";
import classes from "./Meal.module.css";
export const Meal = (props) => {
return (
<div className={classes.Meal}>
<div className={classes.ImgBox}>
<img src={props.img} alt={props.title} />
</div>
<div>
<h2 className={classes.Title}>{props.title}</h2>
<p className={classes.Desc}>{props.desc}</p>
<div className={classes.PriceWrap}>
<span className={classes.Price}>{props.price}</span>
<div>
<Counter amount={props.amount} />
</div>
</div>
</div>
</div>
);
};
src/components/Meals/Meals.jsx
2. import React from "react";
import { Meal } from "./Meal/Meal";
import classes from "./Meals.module.css";
export const Meals = (props) => {
return (
// 设置滚动条出现在Meals而不是body
<div className={classes.Meals}>
{props.mealsData.map((meal) => (
<Meal key={meal.id} {...meal} />
))}
</div>
);
};
(五)添加删除购物车中的商品
src/App.js
1. import React, { useState } from "react";
import { Meals } from "./components/Meals/Meals";
// 模拟一组食物数据
const MEALS_DATA = [
{
id: "1",
title: "汉堡包",
desc: "百分百纯牛肉配搭爽脆酸瓜洋葱粒与美味番茄酱经典滋味让你无法抵挡!",
price: 12,
img: "/img/meals/1.png",
},
{
id: "2",
title: "双层吉士汉堡",
desc: "百分百纯牛肉与双层香软芝,加上松软面包及美味酱料,诱惑无人能挡!",
price: 20,
img: "/img/meals/2.png",
},
{
id: "3",
title: "巨无霸",
desc: "两块百分百纯牛肉,搭配生菜、洋葱等新鲜食材,口感丰富,极致美味!",
price: 24,
img: "/img/meals/3.png",
},
{
id: "4",
title: "麦辣鸡腿汉堡",
desc: "金黄脆辣的外皮,鲜嫩幼滑的鸡腿肉,多重滋味,一次打动您挑剔的味蕾!",
price: 21,
img: "/img/meals/4.png",
},
{
id: "5",
title: "板烧鸡腿堡",
desc: "原块去骨鸡排嫩滑多汁,与翠绿新鲜的生菜和香浓烧鸡酱搭配,口感丰富!",
price: 22,
img: "/img/meals/5.png",
},
{
id: "6",
title: "麦香鸡",
desc: "清脆爽口的生菜,金黄酥脆的鸡肉。营养配搭,好滋味的健康选择!",
price: 14,
img: "/img/meals/6.png",
},
{
id: "7",
title: "吉士汉堡包",
desc: "百分百纯牛肉与香软芝士融为一体配合美味番茄醬丰富口感一咬即刻涌现!",
price: 12,
img: "/img/meals/7.png",
},
];
export default function App() {
const [mealsData, setMealsData] = useState(MEALS_DATA);
const [cartData, setCartData] = useState({
items: [],
totalAmount: 0,
totalPrice: 0,
});
// 加入购物车
const addMealHandler = (meal) => {
const newCart = { ...cartData };
if (newCart.items.indexOf(meal) === -1) {
newCart.items.push(meal);
meal.amount = 1;
} else {
meal.amount += 1;
}
newCart.totalAmount += 1;
newCart.totalPrice += meal.price;
setCartData(newCart);
};
// 移出购物车
const removeMealHandler = (meal) => {
const newCart = { ...cartData };
meal.amount -= 1;
if (meal.amount === 0) newCart.items.filter((item) => item.id !== meal.id);
newCart.totalAmount -= 1;
newCart.totalPrice -= meal.price;
setCartData(newCart);
};
return (
<>
<Meals mealsData={mealsData} onAddMeal={addMealHandler} onRemoveMeal={removeMealHandler} />
</>
);
}
src/components/Meals/Meals.jsx
2. import React from "react";
import { Meal } from "./Meal/Meal";
import classes from "./Meals.module.css";
export const Meals = (props) => {
return (
// 设置滚动条出现在Meals而不是body
<div className={classes.Meals}>
{props.mealsData.map((meal) => (
<Meal key={meal.id} {...meal} meal={meal} onAddMeal={props.onAddMeal} onRemoveMeal={props.onRemoveMeal} />
))}
</div>
);
};
src/components/Meals/Meal/Meal.jsx
3. import React from "react";
import { Counter } from "../../UI/Counter/Counter";
import classes from "./Meal.module.css";
export const Meal = (props) => {
return (
<div className={classes.Meal}>
<div className={classes.ImgBox}>
<img src={props.img} alt={props.title} />
</div>
<div>
<h2 className={classes.Title}>{props.title}</h2>
<p className={classes.Desc}>{props.desc}</p>
<div className={classes.PriceWrap}>
<span className={classes.Price}>{props.price}</span>
<div>
<Counter {...props.meal} meal={props.meal} onAddMeal={props.onAddMeal} onRemoveMeal={props.onRemoveMeal} />
</div>
</div>
</div>
</div>
);
};
(六)Context 的使用
XxxContext.Provider()
1.- 表示数据的生产者,可以使用它来指定
Context
中的数据 - 通过
value
指定,在该组件内的所有子组件都可以访问到该数据 - 当通过
Context
访问数据时,会读取离引用该组件最近的Provider
的value
值 - 如果没有
Provider
,则使用Context
中定义的默认数据(一般不用,脱离state
)
Context
使用方式一
2.- 引入
Context
- 使用
XxxContext.Consumer
组件来创建元素- 标签体需要一个回调函数,将
context
设置为回调函数的参数,即context
中存储的数据
- 标签体需要一个回调函数,将
Context
使用方式二
3.- 引入
Context
- 使用钩子函数
useContext()
获取到context
- 需要一个
Context
作为参数
- 需要一个
Context
相当于一个公共的存储空间
4.- 将多个组件中都需要访问的数据统一存储到一个
Context
中,则无需props
逐层传递 - 通过
React.createContext()
创建Context
src/App.js
5. import React from "react";
import { A } from "./components/A";
import { B } from "./components/B";
import TestContext from "./store/testContext";
export default function App() {
return (
<>
<TestContext.Provider value={{ name: "bbb", age: 20 }}>
<A />
<TestContext.Provider value={{ name: "ccc", age: 22 }}>
<B />
</TestContext.Provider>
</TestContext.Provider>
</>
);
}
src/components/A.jsx
6. import React from "react";
import TestContext from "../store/testContext";
export const A = () => {
return (
<TestContext.Consumer>
{(ctx) => {
return (
<div>
{ctx.name}--{ctx.age}
</div>
);
}}
</TestContext.Consumer>
);
};
src/components/B.jsx
7. import React, { useContext } from "react";
import TestContext from "../store/testContext";
export const B = () => {
const ctx = useContext(TestContext);
return (
<div>
{ctx.name}--{ctx.age}
</div>
);
};
src/store/testContext.js
8. import React from "react";
const TestContext = React.createContext({
name: "aaa",
age: 19,
});
export default TestContext;
(七)完成搜索框
src/App.js
1. import React, { useState } from "react";
import { FilterMeals } from "./components/FilterMeals/FilterMeals";
import { Meals } from "./components/Meals/Meals";
import CartContext from "./store/CartContext";
// 模拟一组食物数据
const MEALS_DATA = [
{
id: "1",
title: "汉堡包",
desc: "百分百纯牛肉配搭爽脆酸瓜洋葱粒与美味番茄酱经典滋味让你无法抵挡!",
price: 12,
img: "/img/meals/1.png",
},
{
id: "2",
title: "双层吉士汉堡",
desc: "百分百纯牛肉与双层香软芝,加上松软面包及美味酱料,诱惑无人能挡!",
price: 20,
img: "/img/meals/2.png",
},
{
id: "3",
title: "巨无霸",
desc: "两块百分百纯牛肉,搭配生菜、洋葱等新鲜食材,口感丰富,极致美味!",
price: 24,
img: "/img/meals/3.png",
},
{
id: "4",
title: "麦辣鸡腿汉堡",
desc: "金黄脆辣的外皮,鲜嫩幼滑的鸡腿肉,多重滋味,一次打动您挑剔的味蕾!",
price: 21,
img: "/img/meals/4.png",
},
{
id: "5",
title: "板烧鸡腿堡",
desc: "原块去骨鸡排嫩滑多汁,与翠绿新鲜的生菜和香浓烧鸡酱搭配,口感丰富!",
price: 22,
img: "/img/meals/5.png",
},
{
id: "6",
title: "麦香鸡",
desc: "清脆爽口的生菜,金黄酥脆的鸡肉。营养配搭,好滋味的健康选择!",
price: 14,
img: "/img/meals/6.png",
},
{
id: "7",
title: "吉士汉堡包",
desc: "百分百纯牛肉与香软芝士融为一体配合美味番茄醬丰富口感一咬即刻涌现!",
price: 12,
img: "/img/meals/7.png",
},
];
export default function App() {
const [mealsData, setMealsData] = useState(MEALS_DATA);
const [cartData, setCartData] = useState({
items: [],
totalAmount: 0,
totalPrice: 0,
});
// 加入购物车
const addItem = (meal) => {
const newCart = { ...cartData };
if (newCart.items.indexOf(meal) === -1) {
newCart.items.push(meal);
meal.amount = 1;
} else {
meal.amount += 1;
}
newCart.totalAmount += 1;
newCart.totalPrice += meal.price;
setCartData(newCart);
};
// 移出购物车
const removeItem = (meal) => {
const newCart = { ...cartData };
meal.amount -= 1;
if (meal.amount === 0) newCart.items.filter((item) => item.id !== meal.id);
newCart.totalAmount -= 1;
newCart.totalPrice -= meal.price;
setCartData(newCart);
};
// 搜索meals
const filterMeals = (keyWord) => {
const newMealsData = MEALS_DATA.filter((item) => item.title.indexOf(keyWord) !== -1);
setMealsData(newMealsData);
};
return (
<>
<CartContext.Provider value={{ ...cartData, addItem, removeItem }}>
<FilterMeals onFilter={filterMeals} />
<Meals mealsData={mealsData} />
</CartContext.Provider>
</>
);
}
src/components/FilterMeals/FilterMeals.jsx
2. import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSearch } from "@fortawesome/free-solid-svg-icons";
import classes from "./FilterMeals.module.css";
export const FilterMeals = (props) => {
const inputChangeHandler = (e) => {
props.onFilter(e.target.value);
};
return (
<div className={classes.FilterMeals}>
<div className={classes.InputOrder}>
<input type="text" placeholder="请输入关键字" className={classes.SearchInput} onChange={inputChangeHandler} />
<FontAwesomeIcon icon={faSearch} className={classes.SearchIcon} />
</div>
</div>
);
};
src/components/FilterMeals/FilterMeals.module.css
3. .FilterMeals {
height: 120rem;
position: fixed;
left: 0;
right: 0;
z-index: 999;
border-bottom: 1px solid #f2f2f2;
display: flex;
align-items: center;
justify-content: center;
}
.InputOrder {
position: relative;
display: flex;
align-items: center;
}
.SearchInput {
width: 650rem;
height: 70rem;
border: none;
font-size: 34rem;
border-radius: 16px;
padding-left: 60rem;
outline: none;
background-color: #f2f2f2;
}
.SearchIcon {
color: #aaa;
font-size: 40rem;
position: absolute;
left: 10rem;
}
src/store/CartContext.js
4. import React from "react";
const CartContext = React.createContext({
items: [],
totalAmount: 0,
totalPrice: 0,
addItem: () => {},
removeItem: () => {},
});
export default CartContext;
(八)完成购物车条
src/App.js
1. import React, { useState } from "react";
import { Cart } from "./components/Cart/Cart";
import { FilterMeals } from "./components/FilterMeals/FilterMeals";
import { Meals } from "./components/Meals/Meals";
import CartContext from "./store/CartContext";
// 模拟一组食物数据
const MEALS_DATA = [
{
id: "1",
title: "汉堡包",
desc: "百分百纯牛肉配搭爽脆酸瓜洋葱粒与美味番茄酱经典滋味让你无法抵挡!",
price: 12,
img: "/img/meals/1.png",
},
{
id: "2",
title: "双层吉士汉堡",
desc: "百分百纯牛肉与双层香软芝,加上松软面包及美味酱料,诱惑无人能挡!",
price: 20,
img: "/img/meals/2.png",
},
{
id: "3",
title: "巨无霸",
desc: "两块百分百纯牛肉,搭配生菜、洋葱等新鲜食材,口感丰富,极致美味!",
price: 24,
img: "/img/meals/3.png",
},
{
id: "4",
title: "麦辣鸡腿汉堡",
desc: "金黄脆辣的外皮,鲜嫩幼滑的鸡腿肉,多重滋味,一次打动您挑剔的味蕾!",
price: 21,
img: "/img/meals/4.png",
},
{
id: "5",
title: "板烧鸡腿堡",
desc: "原块去骨鸡排嫩滑多汁,与翠绿新鲜的生菜和香浓烧鸡酱搭配,口感丰富!",
price: 22,
img: "/img/meals/5.png",
},
{
id: "6",
title: "麦香鸡",
desc: "清脆爽口的生菜,金黄酥脆的鸡肉。营养配搭,好滋味的健康选择!",
price: 14,
img: "/img/meals/6.png",
},
{
id: "7",
title: "吉士汉堡包",
desc: "百分百纯牛肉与香软芝士融为一体配合美味番茄醬丰富口感一咬即刻涌现!",
price: 12,
img: "/img/meals/7.png",
},
];
export default function App() {
const [mealsData, setMealsData] = useState(MEALS_DATA);
const [cartData, setCartData] = useState({
items: [],
totalAmount: 0,
totalPrice: 0,
});
// 加入购物车
const addItem = (meal) => {
const newCart = { ...cartData };
if (newCart.items.indexOf(meal) === -1) {
newCart.items.push(meal);
meal.amount = 1;
} else {
meal.amount += 1;
}
newCart.totalAmount += 1;
newCart.totalPrice += meal.price;
setCartData(newCart);
};
// 移出购物车
const removeItem = (meal) => {
const newCart = { ...cartData };
meal.amount -= 1;
if (meal.amount === 0) newCart.items.filter((item) => item.id !== meal.id);
newCart.totalAmount -= 1;
newCart.totalPrice -= meal.price;
setCartData(newCart);
};
// 搜索meals
const filterMeals = (keyWord) => {
const newMealsData = MEALS_DATA.filter((item) => item.title.indexOf(keyWord) !== -1);
setMealsData(newMealsData);
};
return (
<>
<CartContext.Provider value={{ ...cartData, addItem, removeItem }}>
<FilterMeals onFilter={filterMeals} />
<Meals mealsData={mealsData} />
<Cart />
</CartContext.Provider>
</>
);
}
src/components/Cart/Cart.jsx
2. import React, { useContext } from "react";
import classes from "./Cart.module.css";
import iconImg from "../../asset/bag.png";
import CartContext from "../../store/CartContext";
export const Cart = () => {
const ctx = useContext(CartContext);
return (
<div className={classes.Cart}>
<div className={classes.Icon}>
<img src={iconImg} alt="包" />
{ctx.totalAmount === 0 ? null : <span className={classes.TotalAmount}>{ctx.totalAmount}</span>}
</div>
{ctx.totalAmount === 0 ? <p className={classes.NoMeal}>未选购商品</p> : <p className={classes.Price}>{ctx.totalPrice}</p>}
<button className={`${classes.Button} ${ctx.totalAmount === 0 ? classes.Disabled : ""}`}>去结算</button>
</div>
);
};
src/components/Cart/Cart.module.css
3. .Cart {
position: fixed;
bottom: 30rem;
left: 0;
right: 0;
margin: auto;
width: 700rem;
height: 80rem;
background-color: #333;
border-radius: 20px;
display: flex;
justify-content: space-between;
z-index: 9999;
}
.Icon {
width: 80rem;
position: absolute;
bottom: -6rem;
}
.Icon img {
width: 100%;
}
.TotalAmount {
position: absolute;
right: -10rem;
width: 36rem;
height: 36rem;
line-height: 36rem;
text-align: center;
background-color: #f00;
border-radius: 50%;
color: #fff;
font-weight: bold;
font-size: 22rem;
}
.Price,
.NoMeal {
color: #fff;
margin-left: 120rem;
display: flex;
align-items: center;
font-weight: bold;
font-size: 36rem;
}
.Price::before {
content: "¥";
font-size: 24rem;
}
.NoMeal {
color: #aaa;
}
.Button {
border: none;
background-color: #fcbf49;
width: 200rem;
border-radius: 20px;
font-size: 36rem;
}
.Disabled {
background-color: #777;
color: #ccc;
}
(九)完成购物车详情
src/components/Cart/Cart.jsx
1. import React, { useContext, useState } from "react";
import classes from "./Cart.module.css";
import iconImg from "../../asset/bag.png";
import CartContext from "../../store/CartContext";
import { CartDetails } from "./CartDetails/CartDetails";
export const Cart = () => {
const ctx = useContext(CartContext);
const [showDetails, setShowDetails] = useState(false);
const toggleDetailsHandler = () => {
if (ctx.totalAmount === 0) return;
setShowDetails((prevState) => !prevState);
};
return (
<div className={classes.Cart} onClick={toggleDetailsHandler}>
{showDetails && <CartDetails />}
<div className={classes.Icon}>
<img src={iconImg} alt="包" />
{ctx.totalAmount === 0 ? null : <span className={classes.TotalAmount}>{ctx.totalAmount}</span>}
</div>
{ctx.totalAmount === 0 ? <p className={classes.NoMeal}>未选购商品</p> : <p className={classes.Price}>{ctx.totalPrice}</p>}
<button className={`${classes.Button} ${ctx.totalAmount === 0 ? classes.Disabled : ""}`}>去结算</button>
</div>
);
};
src/components/Cart/CartDetails/CartDetails.jsx
2. import React, { useContext } from "react";
import { BackDrop } from "../../UI/BackDrop/BackDrop";
import { Meal } from "../../Meals/Meal/Meal";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTrash } from "@fortawesome/free-solid-svg-icons";
import CartContext from "../../../store/CartContext";
import classes from "./CartDetails.module.css";
export const CartDetails = () => {
const ctx = useContext(CartContext);
return (
<div>
<BackDrop>
<div className={classes.CartDetails} onClick={(e) => e.stopPropagation()}>
<header className={classes.Header}>
<h2 className={classes.Title}>餐品详情</h2>
<div className={classes.Clear}>
<FontAwesomeIcon icon={faTrash} />
<span>清空购物车</span>
</div>
</header>
<div className={classes.MealList}>
{ctx.items.map((meal) => {
return <Meal key={meal.id} {...meal} meal={meal} noDesc />;
})}
</div>
</div>
</BackDrop>
</div>
);
};
src/components/Cart/CartDetails/CartDetails.module.css
3. .CartDetails {
background-color: #fff;
width: 750rem;
max-height: 1200rem;
position: absolute;
bottom: 0;
padding-bottom: 120rem;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
display: flex;
flex-direction: column;
}
.Header {
display: flex;
justify-content: space-between;
padding: 30rem;
}
.Title {
font-weight: bold;
font-size: 28rem;
margin: 0;
}
.Clear {
color: #aaa;
font-size: 24rem;
}
.Clear span {
margin-left: 12rem;
}
.MealList {
overflow: auto;
}
src/components/UI/BackDrop/BackDrop.jsx
4. import React from "react";
import ReactDOM from "react-dom";
import classes from "./BackDrop.module.css";
const backDropRoot = document.getElementById("backdrop");
export const BackDrop = (props) => {
return ReactDOM.createPortal(<div className={`${classes.BackDrop} ${props.className}`}>{props.children}</div>, backDropRoot);
};
src/components/UI/BackDrop/BackDrop.module.css
5. .BackDrop {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
background-color: rgba(0, 0, 0, 0.3);
z-index: 999;
}
(十)完成购物车列表和支付
src/components/Cart/Cart.jsx
1. import React, { useContext, useState } from "react";
import classes from "./Cart.module.css";
import iconImg from "../../asset/bag.png";
import CartContext from "../../store/CartContext";
import { CartDetails } from "./CartDetails/CartDetails";
import { CheckOut } from "./CheckOut/CheckOut";
export const Cart = () => {
const ctx = useContext(CartContext);
const [showDetails, setShowDetails] = useState(false);
const [showCheckout, setShowCheckout] = useState(false);
const toggleDetailsHandler = () => {
if (ctx.totalAmount === 0) {
setShowDetails(false);
return;
}
setShowDetails((prevState) => !prevState);
};
const showCheckoutHandler = () => {
if (ctx.totalAmount === 0) return;
setShowCheckout(true);
};
const hideCheckoutHandler = () => {
setShowCheckout(false);
};
return (
<div className={classes.Cart} onClick={toggleDetailsHandler}>
{showCheckout && <CheckOut onHide={hideCheckoutHandler} />}
{showDetails && <CartDetails />}
<div className={classes.Icon}>
<img src={iconImg} alt="包" />
{ctx.totalAmount === 0 ? null : <span className={classes.TotalAmount}>{ctx.totalAmount}</span>}
</div>
{ctx.totalAmount === 0 ? <p className={classes.NoMeal}>未选购商品</p> : <p className={classes.Price}>{ctx.totalPrice}</p>}
<button className={`${classes.Button} ${ctx.totalAmount === 0 ? classes.Disabled : ""}`} onClick={showCheckoutHandler}>
去结算
</button>
</div>
);
};
src/components/Cart/CartDetails/CartDetails.jsx
2. import React, { useContext, useState } from "react";
import { BackDrop } from "../../UI/BackDrop/BackDrop";
import { Meal } from "../../Meals/Meal/Meal";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTrash } from "@fortawesome/free-solid-svg-icons";
import CartContext from "../../../store/CartContext";
import classes from "./CartDetails.module.css";
import { Confirm } from "../../UI/Confirm/Confirm";
export const CartDetails = () => {
const ctx = useContext(CartContext);
const [showConfirm, setShowConfirm] = useState(false);
const showConfirmHandler = () => {
setShowConfirm(true);
};
const cancelHandler = (e) => {
e.stopPropagation();
setShowConfirm(false);
};
const okHandler = () => {
ctx.clearCart();
setShowConfirm(false);
};
return (
<div>
<BackDrop>
{showConfirm && <Confirm confirmText="确定清空购物车吗?" onCancel={cancelHandler} onOk={okHandler} />}
<div className={classes.CartDetails} onClick={(e) => e.stopPropagation()}>
<header className={classes.Header}>
<h2 className={classes.Title}>餐品详情</h2>
<div className={classes.Clear} onClick={showConfirmHandler}>
<FontAwesomeIcon icon={faTrash} />
<span>清空购物车</span>
</div>
</header>
<div className={classes.MealList}>
{ctx.items.map((meal) => {
return <Meal key={meal.id} {...meal} meal={meal} noDesc />;
})}
</div>
</div>
</BackDrop>
</div>
);
};
src/components/Cart/CheckOut/CheckOut.jsx
3. import React, { useContext } from "react";
import ReactDOM from "react-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import classes from "./CheckOut.module.css";
import CartContext from "../../../store/CartContext";
import { CheckOutItem } from "./CheckOutItem/CheckOutItem";
import { Bar } from "./Bar/Bar";
const checkOutRoot = document.getElementById("checkout");
export const CheckOut = (props) => {
const ctx = useContext(CartContext);
return ReactDOM.createPortal(
<div className={classes.CheckOut}>
<div className={classes.Close}>
<FontAwesomeIcon icon={faXmark} onClick={() => props.onHide()} />
</div>
<div className={classes.MealsDesc}>
<header className={classes.Header}>
<h2 className={classes.Title}>餐品详情</h2>
</header>
<div className={classes.Meals}>
{ctx.items.map((meal) => {
return <CheckOutItem key={meal.id} {...meal} meal={meal} />;
})}
</div>
<footer className={classes.Footer}>
<p className={classes.TotalPrice}>{ctx.totalPrice}</p>
</footer>
</div>
<Bar totalPrice={ctx.totalPrice} />
</div>,
checkOutRoot,
);
};
src/components/Cart/CheckOut/CheckOut.module.css
4. .CheckOut {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 9999;
background-color: #eee;
padding: 20rem;
}
.Close {
color: #000;
font-size: 36rem;
font-weight: bold;
}
.MealsDesc {
background-color: #fff;
border-radius: 20px;
padding: 0 20rem;
position: relative;
}
.Header {
border-bottom: 1px solid #f2f2f2;
display: flex;
align-items: center;
height: 80rem;
font-size: 20rem;
}
.Meals {
max-height: 900rem;
overflow: auto;
}
.Footer {
height: 120rem;
border-top: 1px dashed #f2f2f2;
display: flex;
align-items: center;
justify-content: flex-end;
font-weight: bold;
font-size: 30rem;
}
.Footer::before,
.Footer::after {
content: "";
position: absolute;
width: 20rem;
height: 20rem;
background-color: #eee;
border-radius: 50%;
bottom: 110rem;
left: -10rem;
}
.Footer::after {
left: auto;
right: -10rem;
}
.TotalPrice::before {
content: "合计:¥";
font-weight: normal;
font-size: 18rem;
margin-right: 10rem;
}
src/components/Cart/CheckOut/CheckOutItem/CheckOutItem.jsx
5. import React from "react";
import { Counter } from "../../../UI/Counter/Counter";
import classes from "./CheckOutItem.module.css";
export const CheckOutItem = (props) => {
return (
<div className={classes.CheckOutItem}>
<div className={classes.MealImg}>
<img src={props.img} alt={props.title} />
</div>
<div className={classes.Desc}>
<h2 className={classes.Title}>{props.title}</h2>
<div className={classes.PriceOuter}>
<Counter {...props.meal} meal={props.meal} />
<div className={classes.Price}>{props.price * props.amount}</div>
</div>
</div>
</div>
);
};
src/components/Cart/CheckOut/CheckOutItem/CheckOutItem.module.css
6. .CheckOutItem {
display: flex;
}
.MealImg {
width: 140rem;
}
.MealImg img {
width: 100%;
}
.Desc {
flex: auto;
margin-left: 20rem;
display: flex;
flex-direction: column;
justify-content: space-evenly;
}
.Title {
font-size: 24rem;
}
.PriceOuter {
display: flex;
justify-content: space-between;
}
.Price {
font-size: 26rem;
font-weight: bold;
}
.Price::before {
content: "¥";
font-weight: normal;
font-size: 24rem;
}
src/components/Cart/CheckOut/Bar/Bar.jsx
7. import React from "react";
import classes from "./Bar.module.css";
export const Bar = (props) => {
return (
<div className={classes.Bar}>
<div className={classes.TotalPrice}>{props.totalPrice}</div>
<button className={classes.Button}>去支付</button>
</div>
);
};
src/components/Cart/CheckOut/Bar/Bar.module.css
8. .Bar {
position: fixed;
bottom: 30rem;
left: 0;
right: 0;
margin: auto;
width: 700rem;
height: 80rem;
background-color: #333;
border-radius: 20px;
display: flex;
justify-content: space-between;
z-index: 9999;
}
.Button {
border: none;
background-color: #fcbf49;
width: 200rem;
border-radius: 20px;
font-size: 36rem;
}
.TotalPrice,
.NoMeal {
color: #fff;
flex: 1;
margin-left: 50rem;
display: flex;
align-items: center;
font-weight: bold;
font-size: 36rem;
}
.TotalPrice::before {
content: "¥";
font-size: 24rem;
}
src/components/UI/Confirm/Confirm.jsx
9. import React from "react";
import { BackDrop } from "../BackDrop/BackDrop";
import classes from "./Confirm.module.css";
export const Confirm = (props) => {
return (
<BackDrop className={classes.ConfirmOuter} onClick={(e) => props.onCancel(e)}>
<div className={classes.Confirm}>
<p className={classes.ConfirmText}>{props.confirmText}</p>
<div>
<button className={classes.Cancel} onClick={(e) => props.onCancel(e)}>
取消
</button>
<button className={classes.Ok} onClick={() => props.onOk()}>
确认
</button>
</div>
</div>
</BackDrop>
);
};
src/components/UI/Confirm/Confirm.module.css
10. .ConfirmOuter {
z-index: 9999;
}
.Confirm {
width: 700rem;
height: 300rem;
background-color: #fff;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
border-radius: 20px;
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
}
.ConfirmText {
font-size: 38rem;
}
.Cancel,
.Ok {
border: 1px solid #ccc;
background-color: #fff;
width: 180rem;
height: 60rem;
border-radius: 20px;
font-size: 28rem;
}
.Ok {
background-color: #fcbf49;
border: none;
margin-left: 50rem;
}
src/store/CartContext.js
11. import React from "react";
const CartContext = React.createContext({
items: [],
totalAmount: 0,
totalPrice: 0,
addItem: () => {},
removeItem: () => {},
clearCart: () => {},
});
export default CartContext;
(十一)setState 的执行流程
- 直接在函数体中调用
setState
时,会触发Too many re-renders.
错误 - 一般情况下,当
state
新旧值相同时,setState
不触发组件重新渲染
setState()
执行流程(函数组件)
1.setCount()
=>dispatchSetDate()
=> 先判断组件当前处于什么阶段 =>
1)渲染阶段
- 不会检查
state
值是否相同
2)非渲染阶段
- 检查
state
值是否相同- 值不同:对组件重新渲染
- 值相同:不对组件重新渲染,
React
会在某些情况下继续执行当前组件渲染,不会触发子组件渲染,也不会产生实际的效果【通常发生在值第一次相同时】
src/App.jsx
2. import React, { useState } from "react";
import { B } from "./B";
export const App = () => {
console.log("App重新渲染了");
const [count, setCount] = useState(0);
// setCount(0);
/*
count 0
点击1 count 1 App重新渲染
点击2 count 1 App重新渲染
点击3 count 1
*/
const clickHandler = () => {
console.log("点击按钮");
setCount(1);
};
return (
<div>
{count}
<B />
<button onClick={clickHandler}>点我一下</button>
</div>
);
};
src/index.js
3. import ReactDOM from "react-dom/client";
import { App } from "./App";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
src/B.jsx
4. import React from "react";
export const B = () => {
console.log("B组件重新渲染了");
return <div>B</div>;
};
(十二)useEffect
useEffect()
是一个钩子函数- 需要一个函数作为参数,该函数将在组件渲染完毕后执行
- 可以将会产生副作用的代码编写到该回调函数中
src/App.jsx
1. import React, { useEffect, useState } from "react";
import { B } from "./B";
export const App = () => {
console.log("App重新渲染了");
const [count, setCount] = useState(0);
useEffect(() => {
setCount(1);
});
const clickHandler = () => {
console.log("点击按钮");
setCount(1);
};
return (
<div>
{count}
<B />
<button onClick={clickHandler}>点我一下</button>
</div>
);
};
src/index.js
2. import ReactDOM from "react-dom/client";
import { App } from "./App";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
src/B.jsx
3. import React from "react";
export const B = () => {
console.log("B组件重新渲染了");
return <div>B</div>;
};
(十三)Effect 的依赖项和清理函数
- 在
Effect
的回调函数中,可以指定一个函数作为返回值 - 该函数称为清理函数,会在下次
Effect
执行前调用
useEffect()
是一个钩子函数
1.- 需要一个函数作为参数,该函数将在组件渲染完毕后执行(每次渲染完成都会调用)
- 可以将会产生副作用的代码编写到该回调函数中
- 可以传递第二个参数,是一个数组
- 在数组中指定
Effect
的依赖项,只有依赖项发生变化时才会执行第一个参数的回调函数
- 在数组中指定
- 通常会将
Effect
中使用的所有变量都设置为依赖项 - 像
setState()
是由钩子函数useState()
生成的useState()
会确保组件的每次渲染都会获取到相同的setState()
对象,所以setState()
可以不设置到依赖中
- 如果依赖项设置为空数组,则
Effect
只在组件初始化时执行一次
src/components/FilterMeals/FilterMeals.jsx
2. import React, { useEffect, useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSearch } from "@fortawesome/free-solid-svg-icons";
import classes from "./FilterMeals.module.css";
export const FilterMeals = (props) => {
const [keyWord, setKeyWord] = useState("");
const inputChangeHandler = (e) => {
setKeyWord(e.target.value.trim());
// props.onFilter(e.target.value);
};
useEffect(() => {
// 降低数据过滤的次数,输完了再过滤
// 当用户停止输入动作1s后,才做查询
// 开启定时器的同时应该关闭上一个定时器
const timer = setTimeout(() => {
props.onFilter(keyWord);
}, 1000);
return () => {
clearTimeout(timer);
};
}, [props, keyWord]);
return (
<div className={classes.FilterMeals}>
<div className={classes.InputOrder}>
<input type="text" placeholder="请输入关键字" value={keyWord} className={classes.SearchInput} onChange={inputChangeHandler} />
<FontAwesomeIcon icon={faSearch} className={classes.SearchIcon} />
</div>
</div>
);
};
src/components/Cart/Cart.jsx
3. import React, { useContext, useEffect, useState } from "react";
import classes from "./Cart.module.css";
import iconImg from "../../asset/bag.png";
import CartContext from "../../store/CartContext";
import { CartDetails } from "./CartDetails/CartDetails";
import { CheckOut } from "./CheckOut/CheckOut";
export const Cart = () => {
const ctx = useContext(CartContext);
const [showDetails, setShowDetails] = useState(false);
const [showCheckout, setShowCheckout] = useState(false);
// 在组件每次重新渲染时,检查一下商品的总数量,如果为0则隐藏弹出层
// 组件每次重新渲染时,组件的函数体就会执行
// 以下代码会报错(Too many re-renders.)
// if (ctx.totalAmount === 0) setShowDetails(false);
useEffect(() => {
console.log("useEffect执行了");
if (ctx.totalAmount === 0) {
setShowDetails(false);
setShowCheckout(false);
}
}, [ctx, setShowDetails, setShowCheckout]);
// useEffect(() => {
// console.log("useEffect执行了");
// if (ctx.totalAmount === 0) {
// setShowDetails(false);
// setShowCheckout(false);
// }
// }, []);
const toggleDetailsHandler = () => {
if (ctx.totalAmount === 0) {
setShowDetails(false);
return;
}
setShowDetails((prevState) => !prevState);
};
const showCheckoutHandler = () => {
if (ctx.totalAmount === 0) return;
setShowCheckout(true);
};
const hideCheckoutHandler = () => {
setShowCheckout(false);
};
return (
<div className={classes.Cart} onClick={toggleDetailsHandler}>
{showCheckout && <CheckOut onHide={hideCheckoutHandler} />}
{showDetails && <CartDetails />}
<div className={classes.Icon}>
<img src={iconImg} alt="包" />
{ctx.totalAmount === 0 ? null : <span className={classes.TotalAmount}>{ctx.totalAmount}</span>}
</div>
{ctx.totalAmount === 0 ? <p className={classes.NoMeal}>未选购商品</p> : <p className={classes.Price}>{ctx.totalPrice}</p>}
<button className={`${classes.Button} ${ctx.totalAmount === 0 ? classes.Disabled : ""}`} onClick={showCheckoutHandler}>
去结算
</button>
</div>
);
};
(十四)useReducer
- 整合器(功能聚合):
useReducer(reducer, initialArg, init)
1.参数
reducer
:整合函数- 对于当前
state
的所有操作都应该在该函数中定义 - 该函数的返回值会成为
state
的新值 reducer
在执行时会收到两个参数state
:当前最新的 state 值action
:需要一个对象,存储dispatch
所发送的指令,可以根据action
中不用的type
返回不同的值 => 执行不同的操作
- 对于当前
initialArg
:state
的初始值,作用和useState()
中的值是一样的
2.返回值
- 数组
- 参数 1:
state
- 用来获取
state
的值
- 用来获取
- 参数 2:
stateDispatch
- 用来修改
state
的派发器 - 通过派发器可以发送操作
state
的命令,具体的修改行为将由reducer
函数执行
- 用来修改
注意
为了避免 reducer
在每次组件渲染时会重复创建
通常 reducer
会定义到组件的外部再引入
src/App.jsx
3. import React, { useReducer, useState } from "react";
import countReducer from "./reducers/countReducer";
export const App = () => {
// const [count, setCount] = useState(1);
// const addHandler = () => {
// setCount((pre) => pre + 1);
// };
// const subHandler = () => {
// setCount((pre) => pre - 1);
// };
// const [count, countDispatch] = useReducer((state, action) => {
// // console.log("reducer执行了", state, action);
// // return state + 1;
// switch (action.type) {
// case "ADD":
// return state + 1;
// case "SUB":
// return state - 1;
// default:
// return state;
// }
// }, 1);
const [count, countDispatch] = useReducer(countReducer, 1);
const addHandler = () => {
countDispatch({ type: "ADD" });
};
const subHandler = () => {
countDispatch({ type: "SUB" });
};
return (
<div
style={{
fontSize: 30,
width: 200,
height: 200,
backgroundColor: "lightblue",
margin: "100px auto",
textAlign: "center",
}}
>
{/* <button onClick={subHandler}>减少</button>
{count}
<button onClick={addHandler}>增加</button> */}
<button onClick={subHandler}>减少</button>
{count}
<button onClick={addHandler}>增加</button>
</div>
);
};
src/reducers/countReducer.js
4. export default (state, action) => {
// console.log("reducer执行了", state, action);
// return state + 1;
switch (action.type) {
case "ADD":
return state + 1;
case "SUB":
return state - 1;
default:
return state;
}
};
(十五)使用 useReducer 修改案例
src/App.js
1. import React, { useState, useReducer } from "react";
import { Cart } from "./components/Cart/Cart";
import { FilterMeals } from "./components/FilterMeals/FilterMeals";
import { Meals } from "./components/Meals/Meals";
import CartContext from "./store/CartContext";
import cartReducer from "./reducers/cartReducer";
// 模拟一组食物数据
const MEALS_DATA = [
{
id: "1",
title: "汉堡包",
desc: "百分百纯牛肉配搭爽脆酸瓜洋葱粒与美味番茄酱经典滋味让你无法抵挡!",
price: 12,
img: "/img/meals/1.png",
},
{
id: "2",
title: "双层吉士汉堡",
desc: "百分百纯牛肉与双层香软芝,加上松软面包及美味酱料,诱惑无人能挡!",
price: 20,
img: "/img/meals/2.png",
},
{
id: "3",
title: "巨无霸",
desc: "两块百分百纯牛肉,搭配生菜、洋葱等新鲜食材,口感丰富,极致美味!",
price: 24,
img: "/img/meals/3.png",
},
{
id: "4",
title: "麦辣鸡腿汉堡",
desc: "金黄脆辣的外皮,鲜嫩幼滑的鸡腿肉,多重滋味,一次打动您挑剔的味蕾!",
price: 21,
img: "/img/meals/4.png",
},
{
id: "5",
title: "板烧鸡腿堡",
desc: "原块去骨鸡排嫩滑多汁,与翠绿新鲜的生菜和香浓烧鸡酱搭配,口感丰富!",
price: 22,
img: "/img/meals/5.png",
},
{
id: "6",
title: "麦香鸡",
desc: "清脆爽口的生菜,金黄酥脆的鸡肉。营养配搭,好滋味的健康选择!",
price: 14,
img: "/img/meals/6.png",
},
{
id: "7",
title: "吉士汉堡包",
desc: "百分百纯牛肉与香软芝士融为一体配合美味番茄醬丰富口感一咬即刻涌现!",
price: 12,
img: "/img/meals/7.png",
},
];
export default function App() {
const [mealsData, setMealsData] = useState(MEALS_DATA);
const [cartData, cartDispatch] = useReducer(cartReducer, {
items: [],
totalAmount: 0,
totalPrice: 0,
});
// 搜索meals
const filterMeals = (keyWord) => {
const newMealsData = MEALS_DATA.filter((item) => item.title.indexOf(keyWord) !== -1);
setMealsData(newMealsData);
};
return (
<>
<CartContext.Provider value={{ ...cartData, cartDispatch }}>
<FilterMeals onFilter={filterMeals} />
<Meals mealsData={mealsData} />
<Cart />
</CartContext.Provider>
</>
);
}
src/store/CartContext.js
2. import React from "react";
const CartContext = React.createContext({
items: [],
totalAmount: 0,
totalPrice: 0,
cartDispatch: () => {},
});
export default CartContext;
src/reducers/cartReducer.js
3. const cartReducer = (state, action) => {
// 对原状态进行浅复制
const newCart = { ...state };
// 获取action参数
const { type, meal } = action;
switch (type) {
case "addCart":
if (newCart.items.indexOf(meal) === -1) {
newCart.items.push(meal);
meal.amount = 1;
} else {
meal.amount += 1;
}
newCart.totalAmount += 1;
newCart.totalPrice += meal.price;
return newCart;
case "removeCart":
meal.amount -= 1;
if (meal.amount === 0) newCart.items.splice(newCart.items.indexOf(meal));
newCart.totalAmount -= 1;
newCart.totalPrice -= meal.price;
return newCart;
case "clearCart":
newCart.items.forEach((item) => delete item.amount);
newCart.items = [];
newCart.totalAmount = 0;
newCart.totalPrice = 0;
return newCart;
default:
return state;
}
};
export default cartReducer;