基于react+typescript+redux的美团外卖点餐购物车的项目(二)附源码

文章目录

  • 一、redux配置
      • 1.store/modules/takeaway.tsx:
      • 2.store/hook.tsx
      • 3.store/index.tsx
      • 4.src/index.tsx
  • 二、页面布局
      • 1.app.tsx
      • 2.NavBar/index.tsx
      • 3.Menu/index.tsx
      • 4.FoodsCategory/index.tsx
      • 5.FoodsCategory/FoodItem/index.tsx
      • 6.Cart/index.tsx
      • 7.Count/index.tsx
  • 总结


一、redux配置

1.store/modules/takeaway.tsx:

首先定义了数据以及函数类型的接口,使用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

2.store/hook.tsx

对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

3.store/index.tsx

通过调用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

4.src/index.tsx

将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>
);

二、页面布局

1.app.tsx

调用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

2.NavBar/index.tsx

定义了一个组件用于对应的界面

在这里插入图片描述

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

3.Menu/index.tsx

通过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’类名,表示当前菜单项处于激活状态。

基于react+typescript+redux的美团外卖点餐购物车的项目(二)附源码_第1张图片

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

4.FoodsCategory/index.tsx

通过结构父组件传过来的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

5.FoodsCategory/FoodItem/index.tsx

用于食物数据列表的渲染,接收父组件传递过来的数值依次渲染对应的位置,通过dispatch函数调用在点击加号时分发addCart action该对象包含了添加到购物车的商品的各个属性,如id、picture、name、unit等。通过传递这些属性,可以将商品添加到购物车。
基于react+typescript+redux的美团外卖点餐购物车的项目(二)附源码_第2张图片

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

6.Cart/index.tsx

首先通过useAppSelector获取状态中的carlist,再通过useAppDispatch获取分发函数,totalprice用于计算carlist的金额总和,使用reduce方法,再设置visible的状态用于控制购物车的显示和隐藏,onshow函数为点击显隐的函数,useEffect设置当购物车列表为0的时候隐藏,并且是根据carlist的长度控制变化的,在点击清空购物车时调用dispatch分发的clearCart方法,并且在子组件传入了食物的数量和加减的方法

基于react+typescript+redux的美团外卖点餐购物车的项目(二)附源码_第3张图片

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

7.Count/index.tsx

通过接受父组件传来的方法和数据进行渲染和绑定

在这里插入图片描述

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地址

你可能感兴趣的:(react.js,typescript,前端)