首先定义了数据以及函数类型的接口,使用createSlice函数创建了一个名为foods的slice,初始状态为一个包含空的foodsList、activeIndex为0和空的cartList的对象.在reducers字段中定义了一系列的reducer函数,包括:
setFoodsList:用于设置foodsList的值。
changeActiveIndex:用于改变activeIndex的值。
addCart:用于将食物添加到购物车中。
increment:用于增加购物车中某个食物的数量。
decrement:用于减少购物车中某个食物的数量。
clearCart:用于清空购物车。
然后通过异步的action creator函数fetchFoodList,用于从服务器获取食物列表数据,并通过setFoodsList将数据存入foodsList中。
import { Dispatch, createSlice, PayloadAction } from '@reduxjs/toolkit'
import axios from 'axios'
export interface Takeaway {
tag: string;
name: string;
foods: Food[];
}
export interface Food {
id: number;
name: string;
like_ratio_desc: string;
month_saled: number;
unit: string;
food_tag_list: string[];
price: number;
picture: string;
description: string;
tag: string;
count: number;
}
export interface FoodState {
foodsList:Takeaway[];
activeIndex: number;
cartList: Food[];
}
interface IncrementPayload {
id: number;
}
interface DecrementPayload {
id: number;
}
const foodStore = createSlice({
name: 'foods',
initialState: {
foodsList: [],
activeIndex: 0,
cartList: []
} as FoodState,
reducers: {
setFoodsList(state, action: PayloadAction<Takeaway[]>) {
state.foodsList = action.payload
},
changeActiveIndex(state, action: PayloadAction<number>) {
state.activeIndex = action.payload
},
addCart(state, action: PayloadAction<Food>) {
const item = state.cartList.find(item => item.id === action.payload.id)
if (item) {
item.count++
} else {
state.cartList.push(action.payload)
}
},
increment(state, action: PayloadAction<IncrementPayload>) {
const item = state.cartList.find(item => item.id === action.payload.id)
if (item) {
item.count++
}
},
decrement(state, action: PayloadAction<DecrementPayload>) {
const item = state.cartList.find(item => item.id === action.payload.id)
if (item) {
item.count--
if (item.count === 0) {
state.cartList = state.cartList.filter(item => item.id !== action.payload.id)
}
}
},
clearCart(state) {
state.cartList = []
}
}
})
const { setFoodsList, changeActiveIndex, addCart, increment, decrement, clearCart } = foodStore.actions
const fetchFoodList : () => (dispatch: Dispatch) => Promise<void> = () => {
return async (dispatch: Dispatch) => {
const res = await axios.get('http://localhost:3004/takeaway')
dispatch(setFoodsList(res.data))
}
}
export { fetchFoodList, changeActiveIndex, addCart, increment, decrement, clearCart }
const reducer = foodStore.reducer
export default reducer
对dispatch和selector钩子函数进行封装。在React组件中使用useAppDispatch和useAppSelector钩子函数时,就可以获得类型安全的dispatch函数和选择器函数,而不需要手动指定类型。
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from '../store'
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
通过调用configureStore函数创建了一个Redux store实例,并将foods的reducer与store进行关联,使得foods的状态可以通过store进行管理。同时,导出了根状态类型RootState和dispatch函数类型AppDispatch,以供其他模块使用。
import reducer from "./modules/takeaway";
import { configureStore } from "@reduxjs/toolkit";
const store=configureStore({
reducer:{
foods:reducer
}
})
export type RootState = ReturnType<typeof store.getState>
// 推断出类型: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch
export default store
将React应用挂载到DOM中,并使用Redux提供的Provider组件将Redux store传递给应用。
import ReactDOM from 'react-dom/client';
import App from './App';
import { Provider } from 'react-redux';
import store from './store';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<Provider store={store}>
<App />
</Provider>
);
调用useAppSelector钩子,将state.foods中的foodsList和activeIndex属性解构,再调用useAppDispatch钩子,将返回的dispatch函数赋值给常量dispatch。这个dispatch函数用于分发actions到Redux store。使用useEffect钩子来在组件挂载时执行dispatch中的fetchFoodList来获取数据。当dispatch发生变化时,回调函数将被调用。并且return回各个组件组合起来的页面,通过map方法遍历foodlist,并且在索引相等时才会渲染FoodsCategory组件,通过父组件向子组件传递key name foods
import NavBar from './components/NavBar'
import Menu from './components/Menu'
import Cart from './components/Cart'
import '../src/App.css'
import FoodsCategory from './components/FoodsCategory'
import { useEffect } from 'react'
import { fetchFoodList } from './store/modules/takeaway'
import { useAppDispatch, useAppSelector } from './store/hook'
const App = () => {
const {foodsList,activeIndex}=useAppSelector(state=>state.foods)
const dispatch = useAppDispatch()
useEffect(() => {
dispatch(fetchFoodList())
}, [dispatch])
return (
<div className="home">
{/* 导航 */}
<NavBar />
{/* 内容 */}
<div className="content-wrap">
<div className="content">
<Menu />
<div className="list-content">
<div className="goods-list">
{/* 外卖商品列表 */}
{foodsList.map((item,index) => {
return (
activeIndex===index&& <FoodsCategory
key={item.tag}
// 列表标题
name={item.name}
// 列表商品
foods={item.foods}
/>
)
})}
</div>
</div>
</div>
</div>
{/* 购物车 */}
<Cart />
</div>
)
}
export default App
定义了一个组件用于对应的界面
import './index.css'
const NavBar = () => {
return (
<nav className="nav">
<div className="menu">
<div className="menu-item active">
点菜<span className="menu-active-bar"></span>
</div>
<div className="menu-item">
评价<span className="menu-comment">1796</span>
</div>
<div className="menu-item">商家</div>
</div>
<div className="menu-search">
<div className="menu-form">
<div className="menu-search-icon"></div>
<div className="menu-search-text">请输入菜品名称</div>
</div>
</div>
</nav>
)
}
export default NavBar
通过useAppSelector和useAppDispatch钩子,从Redux store中获取了foodsList和activeIndex状态,并将其解构赋值给常量foodsList和activeIndex。代码使用map方法对foodsList数组进行遍历,生成一个新的数组menus。menus数组包含了每个食物类别的tag和name属性。在菜单列表的渲染部分,代码使用map方法对menus数组进行遍历。对于每个菜单项(item),代码渲染了一个div元素,并设置了onClick事件处理函数,用于在点击菜单项时分发changeActiveIndex action来更新activeIndex的值。在div元素的className属性中,使用了classnames库来动态添加类名。其中,'list-menu-item’是固定的类名,而active类名则根据activeIndex的值来动态添加。当activeIndex等于当前索引(index)时,会添加’active’类名,表示当前菜单项处于激活状态。
import classNames from 'classnames'
import './index.css'
import { changeActiveIndex } from '../../store/modules/takeaway'
import { useAppDispatch, useAppSelector } from '../../store/hook'
const Menu = () => {
const { foodsList, activeIndex } = useAppSelector(state => state.foods)
const dispatch = useAppDispatch()
const menus = foodsList.map(item => ({ tag: item.tag, name: item.name }))
return (
<nav className="list-menu">
{/* 添加active类名会变成激活状态 */}
{menus.map((item, index) => {
return (
<div
onClick={() => dispatch(changeActiveIndex(index))}
key={item.tag}
className={classNames(
'list-menu-item',
activeIndex === index && 'active'
)}
>
{item.name}
</div>
)
})}
</nav>
)
}
export default Menu
通过结构父组件传过来的name和foods,并且使用map遍历传递foods中的所有数据字段的值,…item为展开单个数组项里的值传递
import { Food, Takeaway } from '../../store/modules/takeaway'
import FoodItem from './FoodItem'
import './index.css'
const FoodsCategory = ({ name, foods }: { name: string; foods: Food[] }) => {
return (
<div className="category">
<dl className="cate-list">
<dt className="cate-title">{name}</dt>
{foods.map(item => {
return <FoodItem key={item.id} {...item} />
})}
</dl>
</div>
)
}
export default FoodsCategory
用于食物数据列表的渲染,接收父组件传递过来的数值依次渲染对应的位置,通过dispatch函数调用在点击加号时分发addCart action该对象包含了添加到购物车的商品的各个属性,如id、picture、name、unit等。通过传递这些属性,可以将商品添加到购物车。
import { useDispatch } from 'react-redux'
import './index.css'
import { Food, addCart } from '../../../store/modules/takeaway'
const Foods= ({
id,
name,
like_ratio_desc,
month_saled,
unit,
food_tag_list,
price,
picture,
description,
tag,
count,
}:Food) => {
const dispatch = useDispatch()
return (
<dd className="cate-goods">
<div className="goods-img-wrap">
<img src={picture} alt="" className="goods-img" />
</div>
<div className="goods-info">
<div className="goods-desc">
<div className="goods-title">{name}</div>
<div className="goods-detail">
<div className="goods-unit">{unit}</div>
<div className="goods-detail-text">{description}</div>
</div>
<div className="goods-tag">{food_tag_list.join(' ')}</div>
<div className="goods-sales-volume">
<span className="goods-num">月售{month_saled}</span>
<span className="goods-num">{like_ratio_desc}</span>
</div>
</div>
<div className="goods-price-count">
<div className="goods-price">
<span className="goods-price-unit">¥</span>
{price}
</div>
<div className="goods-count">
<span className="plus" onClick={() => dispatch(addCart({
id,
picture,
name,
unit,
description,
food_tag_list,
month_saled,
like_ratio_desc,
price,
tag,
count
}))}></span>
</div>
</div>
</div>
</dd>
)
}
export default Foods
首先通过useAppSelector获取状态中的carlist,再通过useAppDispatch获取分发函数,totalprice用于计算carlist的金额总和,使用reduce方法,再设置visible的状态用于控制购物车的显示和隐藏,onshow函数为点击显隐的函数,useEffect设置当购物车列表为0的时候隐藏,并且是根据carlist的长度控制变化的,在点击清空购物车时调用dispatch分发的clearCart方法,并且在子组件传入了食物的数量和加减的方法
import classNames from 'classnames'
import Count from '../Count'
import './index.css'
import { increment, decrement, clearCart } from '../../store/modules/takeaway'
import { useEffect, useState} from 'react'
import { useAppDispatch, useAppSelector } from '../../store/hook'
const Cart = () => {
const { cartList } = useAppSelector(state => state.foods)
const dispatch = useAppDispatch()
const totalprice = cartList.reduce((a, c) => a + c.price * c.count, 0)
const [visible, setvisible] = useState(false)
const onshow = () => {
if (cartList.length > 0) {
setvisible(!visible)
}
}
useEffect(() => {
if (cartList.length === 0) {
setvisible(false);
}
}, [cartList]);
return (
<div className="cartContainer">
{/* 遮罩层 添加visible类名可以显示出来 */}
<div onClick={() => setvisible(false)}
className={classNames('cartOverlay', visible && 'visible')}
/>
<div className="cart">
{/* fill 添加fill类名可以切换购物车状态*/}
{/* 购物车数量 */}
<div onClick={onshow} className={classNames('icon', cartList.length > 0 && 'fill')}>
{cartList.length > 0 && <div className="cartCornerMark">{cartList.length}</div>}
</div>
{/* 购物车价格 */}
<div className="main">
<div className="price">
<span className="payableAmount">
<span className="payableAmountUnit">¥</span>
{totalprice.toFixed(2)}
</span>
</div>
<span className="text">预估另需配送费 ¥5</span>
</div>
{/* 结算 or 起送 */}
{totalprice >= 20 ? (
<div className="goToPreview" onClick={onshow}>去结算</div>
) : (
<div className="minFee">¥20起送</div>
)}
</div>
{/* 添加visible类名 div会显示出来 */}
<div className={classNames('cartPanel', totalprice > 0 && visible && 'visible')}>
<div className="header">
<span className="text">购物车</span>
<span className="clearCart" onClick={() => dispatch(clearCart())}>
清空购物车
</span>
</div>
{/* 购物车列表 */}
<div className="scrollArea">
{cartList.map(item => {
if (item.count > 0) {
return (
<div className="cartItem" key={item.id}>
<img className="shopPic" src={item.picture} alt="" />
<div className="main">
<div className="skuInfo">
<div className="name">{item.name}</div>
</div>
<div className="payableAmount">
<span className="yuan">¥</span>
<span className="price">{item.price}</span>
</div>
</div>
<div className="skuBtnWrapper btnGroup">
<Count
onPlus={() => dispatch(increment({ id: item.id }))}
onMinus={() => dispatch(decrement({ id: item.id }))}
count={item.count}
/>
</div>
</div>
)
}
})}
</div>
</div>
</div>
)
}
export default Cart
通过接受父组件传来的方法和数据进行渲染和绑定
import './index.css'
interface count{
onPlus:()=>void
onMinus:()=>void
count:number
}
const Count = ({ onPlus, onMinus, count }:count) => {
return (
<div className="goods-count">
<span className="minus" onClick={onMinus}></span>
<span className="count">{count}</span>
<span className="plus" onClick={onPlus}></span>
</div>
)
}
export default Count
以上就是基于react+typescript+redux的美团外卖点餐购物车的项目的所有功能实现代码了,不清楚数据和目录结构的准备工作请参考我的另外一篇博客基于react+typescript+redux的美团外卖点餐购物车的项目(一),最后附上项目的源码,可供参考github地址