React+Redux+Ant Design+TypeScript 电子商务实战-客户端应用 05 购物车和订单

将产品添加到购物车

定义方法

// src\helpers\cart.ts
import { Product } from '../store/models/product'

export interface CartItem extends Product {
  count: number
}

// 将产品添加到购物车
export const addItem = (item: Product, next: () => void) => {
  let cart: CartItem[] = []

  if (typeof window !== 'undefined') {
    if (localStorage.getItem('cart')) {
      // `!` 非空断言
      cart = JSON.parse(localStorage.getItem('cart')!)
    }
  }

  const cartItem = cart.find(product => item._id === product._id)
  if (cartItem) {
    cartItem.count++
  } else {
    cart.push({
      ...item,
      count: 1
    })
  }

  localStorage.setItem('cart', JSON.stringify(cart))

  // 执行回调
  next()
}

执行操作

// src\components\core\ProductItem.tsx
import { Button, Card, Col, Image, Row, Typography } from 'antd'
import { push } from 'connected-react-router'
import moment from 'moment'
import { useDispatch } from 'react-redux'
import { Link } from 'react-router-dom'
import { FC } from 'react'
import { API } from '../../config'
import { addItem } from '../../helpers/cart'
import { Product } from '../../store/models/product'

const { Title, Paragraph } = Typography

interface Props {
  product: Product
  showViewBtn?: boolean
  showCartBtn?: boolean
}

const ProductItem: FC<Props> = ({ product, showViewBtn = true, showCartBtn = true }) => {
  const dispatch = useDispatch()

  // 加入购物车
  const addToCart = () => {
    addItem(product, () => {
      // 添加完成后跳转到购物车页面
      // connected-react-router 通过向 store 派发动作的方式实现路由跳转
      // 可以在 store 中存储路由历史
      dispatch(push('/cart'))
    })
  }

  const showButtons = () => {
    const buttonArray = []
    if (showViewBtn) {
      buttonArray.push(
        <Button type="link">
          <Link to={`/product/${product._id}`}>查看详情</Link>
        </Button>
      )
    }
    if (showCartBtn) {
      buttonArray.push(
        <Button type="link" onClick={addToCart}>
          加入购物车
        </Button>
      )
    }

    return buttonArray
  }

  return (
    <Card
      cover={<Image src={`${API}/product/photo/${product._id}`} alt={product.name} preview={false} />}
      actions={showButtons()}
    >
      <Title level={5}>{product.name}</Title>
      <Paragraph ellipsis={{ rows: 2 }}>{product.description}</Paragraph>
      <Row>
        <Col span="12">销量:{product.sold}</Col>
        <Col span="12" style={{ textAlign: 'right' }}>
          价格:¥{product.price}
        </Col>
      </Row>
      <Row>
        <Col span="12">上架时间:{moment(product.createdAt).format('YYYY-MM-DD')}</Col>
        <Col span="12" style={{ textAlign: 'right' }}>
          所属分类:{product.category.name}
        </Col>
      </Row>
    </Card>
  )
}

export default ProductItem

构建购物车组件布局

添加获取购物车方法

// src\helpers\cart.ts
import { Product } from '../store/models/product'

export interface CartItem extends Product {
  count: number
}

// 将产品添加到购物车
export const addItem = (item: Product, next: () => void) => {
  let cart: CartItem[] = []

  if (typeof window !== 'undefined') {
    if (localStorage.getItem('cart')) {
      // `!` 非空断言
      cart = JSON.parse(localStorage.getItem('cart')!)
    }
  }

  const cartItem = cart.find(product => item._id === product._id)
  if (cartItem) {
    cartItem.count++
  } else {
    cart.push({
      ...item,
      count: 1
    })
  }

  localStorage.setItem('cart', JSON.stringify(cart))

  // 执行回调
  next()
}

// 获取本地购物车数据
export const getCart = () => {
  if (typeof window !== 'undefined') {
    if (localStorage.getItem('cart')) {
      return JSON.parse(localStorage.getItem('cart')!)
    }
  }
  return []
}

构建页面布局

// src\components\core\Cart.tsx
import { Row, Col } from 'antd'
import { useEffect, useState } from 'react'
import { CartItem as CartItemModel, getCart } from '../../helpers/cart'
import CartItem from './CartItem'
import Layout from './Layout'

const Cart = () => {
  const [cart, setCart] = useState<CartItemModel[]>([])

  useEffect(() => {
    setCart(getCart())
  }, [])

  const showCart = () => (
    // 为了拆分组件,使用 Antd 的 Table 组件的结构,但不使用这个组件
    <table style={{ width: '100%' }}>
      <thead className="ant-table-thead">
        <tr>
          <th className="ant-table-cell">产品封面</th>
          <th className="ant-table-cell">产品名称</th>
          <th className="ant-table-cell">产品价格</th>
          <th className="ant-table-cell">产品分类</th>
          <th className="ant-table-cell">产品数量</th>
          <th className="ant-table-cell">操作</th>
        </tr>
      </thead>
      <tbody className="ant-table-tbody">
        {cart.map(item => (
          <CartItem product={item} />
        ))}
      </tbody>
    </table>
  )

  return (
    <Layout title="购物车" subTitle="">
      <Row gutter={16}>
        <Col span="16">{showCart()}</Col>
        <Col span="8"></Col>
      </Row>
    </Layout>
  )
}

export default Cart

// src\components\core\CartItem.tsx
import { Button, Image, Input } from 'antd'
import { FC } from 'react'
import { API } from '../../config'
import { CartItem as CartItemModel } from '../../helpers/cart'

interface Props {
  product: CartItemModel
}

const CartItem: FC<Props> = ({ product }) => {
  return (
    <tr className="ant-table-row">
      <td className="ant-table-cell">
        <Image src={`${API}/product/photo/${product._id}`} width={120} alt={product.name} preview={false} />
      </td>
      <td className="ant-table-cell">{product.name}</td>
      <td className="ant-table-cell">{product.price}</td>
      <td className="ant-table-cell">{product.category.name}</td>
      <td className="ant-table-cell">
        <Input type="number" value={product.count} />
      </td>
      <td className="ant-table-cell">
        <Button danger type="primary">
          删除
        </Button>
      </td>
    </tr>
  )
}

export default CartItem

更改购物车产品数量

添加方法

// src\helpers\cart.ts
import { Product } from '../store/models/product'

export interface CartItem extends Product {
  count: number
}

...

// 更改购物车中产品数量
export const updateItem = (productId: string, count: number) => {
  const cart: CartItem[] = getCart()

  const cartItem = cart.find(product => productId === product._id)
  if (cartItem) {
    cartItem.count = count
    localStorage.setItem('cart', JSON.stringify(cart))
  }

  return cart
}

修改数量并更新购物车信息

// src\components\core\CartItem.tsx
import { Button, Image, Input } from 'antd'
import { API } from '../../config'
import { CartItem as CartItemModel, updateItem } from '../../helpers/cart'
import { ChangeEvent, FC } from 'react'

interface Props {
  product: CartItemModel
  setCart: (arg: CartItemModel[]) => void
}

const CartItem: FC<Props> = ({ product, setCart }) => {
  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
    const count = Math.max(parseInt(event.target.value), 1)
    setCart(updateItem(product._id, count))
  }

  return (
    <tr className="ant-table-row">
      <td className="ant-table-cell">
        <Image src={`${API}/product/photo/${product._id}`} width={120} alt={product.name} preview={false} />
      </td>
      <td className="ant-table-cell">{product.name}</td>
      <td className="ant-table-cell">{product.price}</td>
      <td className="ant-table-cell">{product.category.name}</td>
      <td className="ant-table-cell">
        <Input type="number" value={product.count} onChange={handleChange} />
      </td>
      <td className="ant-table-cell">
        <Button danger type="primary">
          删除
        </Button>
      </td>
    </tr>
  )
}

export default CartItem

// src\components\core\Cart.tsx
import { Row, Col } from 'antd'
import { useEffect, useState } from 'react'
import { CartItem as CartItemModel, getCart } from '../../helpers/cart'
import CartItem from './CartItem'
import Layout from './Layout'

const Cart = () => {
  const [cart, setCart] = useState<CartItemModel[]>([])

  useEffect(() => {
    setCart(getCart())
  }, [])

  const showCart = () => (
    // 为了拆分组件,使用 Antd 的 Table 组件的结构,但不使用这个组件
    <table style={{ width: '100%' }}>
      <thead className="ant-table-thead">
        <tr>
          <th className="ant-table-cell">产品封面</th>
          <th className="ant-table-cell">产品名称</th>
          <th className="ant-table-cell">产品价格</th>
          <th className="ant-table-cell">产品分类</th>
          <th className="ant-table-cell">产品数量</th>
          <th className="ant-table-cell">操作</th>
        </tr>
      </thead>
      <tbody className="ant-table-tbody">
        {cart.map(item => (
          <CartItem key={item._id} setCart={setCart} product={item} />
        ))}
      </tbody>
    </table>
  )

  return (
    <Layout title="购物车" subTitle="">
      <Row gutter={16}>
        <Col span="16">{showCart()}</Col>
        <Col span="8"></Col>
      </Row>
    </Layout>
  )
}

export default Cart

删除购物车中的产品

添加方法

// src\helpers\cart.ts
import { Product } from '../store/models/product'

export interface CartItem extends Product {
  count: number
}

...

// 删除购物车中的产品
export const deleteItem = (productId: string) => {
  const cart: CartItem[] = getCart()

  const cartIndex = cart.findIndex(product => productId === product._id)
  if (cartIndex !== -1) {
    cart.splice(cartIndex, 1)
    localStorage.setItem('cart', JSON.stringify(cart))
  }

  return cart
}

修改页面

// src\components\core\CartItem.tsx
import { Button, Image, Input } from 'antd'
import { API } from '../../config'
import { CartItem as CartItemModel, deleteItem, updateItem } from '../../helpers/cart'
import { ChangeEvent, FC } from 'react'

interface Props {
  product: CartItemModel
  setCart: (arg: CartItemModel[]) => void
}

const CartItem: FC<Props> = ({ product, setCart }) => {
  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
    const count = Math.max(parseInt(event.target.value), 1)
    setCart(updateItem(product._id, count))
  }

  return (
    <tr className="ant-table-row">
      <td className="ant-table-cell">
        <Image src={`${API}/product/photo/${product._id}`} width={120} alt={product.name} preview={false} />
      </td>
      <td className="ant-table-cell">{product.name}</td>
      <td className="ant-table-cell">{product.price}</td>
      <td className="ant-table-cell">{product.category.name}</td>
      <td className="ant-table-cell">
        <Input type="number" value={product.count} onChange={handleChange} />
      </td>
      <td className="ant-table-cell">
        <Button
          danger
          type="primary"
          onClick={() => {
            setCart(deleteItem(product._id))
          }}
        >
          删除
        </Button>
      </td>
    </tr>
  )
}

export default CartItem

计算商品总价

// src\components\core\TotalPrice.tsx
import { Typography } from 'antd'
import { FC, useEffect } from 'react'
import { CartItem } from '../../helpers/cart'

interface Props {
  cart: CartItem[]
  setTotalPrice: (price: number) => void
}

const { Title } = Typography

const TotalPrice: FC<Props> = ({ cart, setTotalPrice }) => {
  const getTotalPrice = () => {
    return cart
      .reduce((total, cartItem) => {
        return total + cartItem.price * cartItem.count
      }, 0)
      .toFixed(2)
  }

  useEffect(() => {
    setTotalPrice(parseFloat(getTotalPrice()))
  }, [cart])

  return <Title level={5}>商品总价:¥{getTotalPrice()}</Title>
}

export default TotalPrice

// src\components\core\Cart.tsx
import { Row, Col, Input, Divider } from 'antd'
import { ChangeEvent, useEffect, useState } from 'react'
import { CartItem as CartItemModel, getCart } from '../../helpers/cart'
import CartItem from './CartItem'
import Layout from './Layout'
import TotalPrice from './TotalPrice'

const Cart = () => {
  const [cart, setCart] = useState<CartItemModel[]>([])

  const [address, setAddress] = useState<string>('')

  const [totalPrice, setTotalPrice] = useState<number>(0)

  useEffect(() => {
    setCart(getCart())
  }, [])

  const showCart = () => (
    // 为了拆分组件,使用 Antd 的 Table 组件的结构,但不使用这个组件
    <table style={{ width: '100%' }}>
      <thead className="ant-table-thead">
        <tr>
          <th className="ant-table-cell">产品封面</th>
          <th className="ant-table-cell">产品名称</th>
          <th className="ant-table-cell">产品价格</th>
          <th className="ant-table-cell">产品分类</th>
          <th className="ant-table-cell">产品数量</th>
          <th className="ant-table-cell">操作</th>
        </tr>
      </thead>
      <tbody className="ant-table-tbody">
        {cart.map(item => (
          <CartItem key={item._id} setCart={setCart} product={item} />
        ))}
      </tbody>
    </table>
  )

  return (
    <Layout title="购物车" subTitle="">
      <Row gutter={16}>
        <Col span="16">{showCart()}</Col>
        <Col span="8">
          <Row>
            <Input
              placeholder="请输入收货地址"
              value={address}
              onChange={(event: ChangeEvent<HTMLInputElement>) => setAddress(event.target.value)}
            />
          </Row>
          <Divider />
          <Row>
            <TotalPrice cart={cart} setTotalPrice={setTotalPrice} />
          </Row>
        </Col>
      </Row>
    </Layout>
  )
}

export default Cart

添加提交订单或登录按钮

// src\components\core\Pay.tsx
import { Button } from 'antd'
import { Link } from 'react-router-dom'
import { isAuth } from '../../helpers/auth'

const Pay = () => {
  const showButton = () => {
    return isAuth() ? (
      <Button>提交订单</Button>
    ) : (
      <Button>
        <Link to="/signin">登录</Link>
      </Button>
    )
  }
  return <>{showButton()}</>
}

export default Pay

// src\components\core\Cart.tsx

<Row>
  <TotalPrice cart={cart} setTotalPrice={setTotalPrice} />
</Row>
<Row>
  <Pay />
</Row>

订单支付成功后的提示页面组件

// src\components\core\Success.tsx
import { Button } from 'antd'
import { Link } from 'react-router-dom'
import Layout from './Layout'

const Success = () => {
  return (
    <Layout title="支付完成" subTitle="">
      <Button>
        <Link to="/">继续购物</Link>
      </Button>
    </Layout>
  )
}

export default Success

// src\Routes.tsx
import { HashRouter, Route, Switch } from 'react-router-dom'
import AddCategory from './components/admin/AddCategory'
import AddProduct from './components/admin/AddProduct'
import AdminDashboard from './components/admin/AdminDashboard'
import AdminRoute from './components/admin/AdminRoute'
import Dashboard from './components/admin/Dashboard'
import PrivateRoute from './components/admin/PrivateRoute'
import Cart from './components/core/Cart'
import Home from './components/core/Home'
import Product from './components/core/Product'
import Shop from './components/core/Shop'
import Signin from './components/core/Signin'
import Signup from './components/core/Signup'
import Success from './components/core/Success'

const Routes = () => {
  return (
    <HashRouter>
      <Switch>
        <Route path="/" component={Home} exact />
        <Route path="/shop" component={Shop} />
        <Route path="/signin" component={Signin} />
        <Route path="/signup" component={Signup} />
        <PrivateRoute path="/user/dashboard" component={Dashboard} />
        <AdminRoute path="/admin/dashboard" component={AdminDashboard} />
        <AdminRoute path="/create/category" component={AddCategory} />
        <AdminRoute path="/create/product" component={AddProduct} />
        <Route path="/product/:productId" component={Product} />
        <Route path="/cart" component={Cart} />
        <Route path="/paysuccess" component={Success} />
      </Switch>
    </HashRouter>
  )
}

export default Routes

实现提交订单流程

  1. 获取支付宝收银台地址,并跳转
  2. 支付成功后跳转回客户端支付成功页面
  3. 支付宝服务器会向 ecommerce 服务端的 api 发送请求通知支付结果
    • 由于是支付宝服务器发送请求,所以不能指定本地地址(localhost),应该提供公网可以访问的地址,如本地服务的 IP,或部署服务端 API 的服务器的地址
// src\components\core\Pay.tsx
import { Button } from 'antd'
import axios from 'axios'
import { FC } from 'react'
import { Link } from 'react-router-dom'
import { API } from '../../config'
import { isAuth } from '../../helpers/auth'
import { CartItem } from '../../helpers/cart'
import { Jwt } from '../../store/models/auth'

interface Props {
  totalPrice: number
  address: string
  cart: CartItem[]
}

const Pay: FC<Props> = ({ totalPrice, address, cart }) => {
  const getPayUrl = () => {
    axios
      .post(`${API}/alipay`, {
        totalAmount: totalPrice,
        subject: '订单标题',
        body: '订单描述',
        // 测试时也要用 127.0.0.1
        returnUrl: 'http://127.0.0.1:3000/#/paysuccess',
        notifyUrl: '<服务端地址>/api/alipayNotifyUrl',
        address: address,
        products: cart.map(item => ({
          product: item._id,
          count: item.count
        })),
        userId: (isAuth() as Jwt).user._id
      })
      .then(response => {
        window.location.href = response.data.url
      })
      .catch(error => {
        console.error(error)
      })
  }

  const showButton = () => {
    return isAuth() ? (
      <Button onClick={getPayUrl}>提交订单</Button>
    ) : (
      <Button>
        <Link to="/signin">登录</Link>
      </Button>
    )
  }

  return <>{showButton()}</>
}

export default Pay

传递参数:

// src\components\core\Cart.tsx

<Pay totalPrice={totalPrice} address={address} cart={cart} />

在导航栏添加购物车链接

获取购物车产品数量的方法

// src\helpers\cart.ts
import { Product } from '../store/models/product'

...

// 获取购物车产品数量
export const itemCount = () => {
  const cart: CartItem[] = getCart()
  return cart.length
}

添加链接

// src\components\core\Navigation.tsx
import { Badge, Menu } from 'antd'
import { Link } from 'react-router-dom'
import { useSelector } from 'react-redux'
import { AppState } from '../../store/reducers'
import { RouterState } from 'connected-react-router'
import { isAuth } from '../../helpers/auth'
import { Jwt } from '../../store/models/auth'

// 判断选中类名的钩子函数
function useActive(currentPath: string, path: string): string {
  return currentPath === path ? 'ant-menu-item-selected' : ''
}

const Navigation = () => {
  const router = useSelector<AppState, RouterState>(state => state.router)
  const pathname = router.location.pathname

  const isHome = useActive(pathname, '/')
  const isShop = useActive(pathname, '/shop')
  const isSignin = useActive(pathname, '/signin')
  const isSignup = useActive(pathname, '/signup')
  const isCart = useActive(pathname, '/cart')
  const isDashboard = useActive(pathname, getDashboardUrl())

  function getDashboardUrl() {
    let url = '/user/dashboard'
    if (isAuth()) {
      const {
        user: { role }
      } = isAuth() as Jwt

      if (role === 1) {
        url = '/admin/dashboard'
      }
    }

    return url
  }

  return (
    <Menu mode="horizontal" selectable={false}>
      <Menu.Item className={isHome}>
        <Link to="/">首页</Link>
      </Menu.Item>
      <Menu.Item className={isShop}>
        <Link to="/shop">商城</Link>
      </Menu.Item>
      <Menu.Item className={isCart}>
        <Link to="/cart">
          购物车
          <Badge count={10} offset={[5, -10]} />
        </Link>
      </Menu.Item>
      {!isAuth() && (
        <>
          <Menu.Item className={isSignin}>
            <Link to="/signin">登录</Link>
          </Menu.Item>
          <Menu.Item className={isSignup}>
            <Link to="/signup">注册</Link>
          </Menu.Item>
        </>
      )}
      {isAuth() && (
        <Menu.Item className={isDashboard}>
          <Link to={getDashboardUrl()}>dashboard</Link>
        </Menu.Item>
      )}
    </Menu>
  )
}

export default Navigation

另一种方式存储共享状态

使用 createContext 创建共享状态的组件:

// src\anotherStore.tsx
import React, { Dispatch, FC, SetStateAction, useState } from 'react'
import { itemCount } from './helpers/cart'

export const TotalContext = React.createContext<[number, Dispatch<SetStateAction<number>>]>([0, () => null])

interface Props {
  children: React.ReactNode[] | React.ReactNode
}

const AnotherStore: FC<Props> = ({ children }) => {
  const [count, setCount] = useState(itemCount())
  return <TotalContext.Provider value={[count, setCount]}>{children}</TotalContext.Provider>
}

export default AnotherStore

包裹全部组件:

// src\index.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import 'antd/dist/antd.css'
import './style.css'
import Routes from './Routes'
import { Provider } from 'react-redux'
import store, { history } from './store'
import { ConnectedRouter } from 'connected-react-router'
import AnotherStore from './anotherStore'

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <ConnectedRouter history={history}>
        <AnotherStore>
          <Routes />
        </AnotherStore>
      </ConnectedRouter>
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
)

修改导航

// src\components\core\Navigation.tsx
import { Badge, Menu } from 'antd'
import { Link } from 'react-router-dom'
import { useSelector } from 'react-redux'
import { AppState } from '../../store/reducers'
import { RouterState } from 'connected-react-router'
import { isAuth } from '../../helpers/auth'
import { Jwt } from '../../store/models/auth'
import { useContext, useEffect } from 'react'
import { TotalContext } from '../../anotherStore'
import { itemCount } from '../../helpers/cart'

// 判断选中类名的钩子函数
function useActive(currentPath: string, path: string): string {
  return currentPath === path ? 'ant-menu-item-selected' : ''
}

const Navigation = () => {
  const router = useSelector<AppState, RouterState>(state => state.router)
  const pathname = router.location.pathname

  const isHome = useActive(pathname, '/')
  const isShop = useActive(pathname, '/shop')
  const isSignin = useActive(pathname, '/signin')
  const isSignup = useActive(pathname, '/signup')
  const isCart = useActive(pathname, '/cart')
  const isDashboard = useActive(pathname, getDashboardUrl())

  // 获取 anotherStore 中的状态
  const [count, setCount] = useContext(TotalContext)

  // 每轮渲染后都获取购物车数量
  useEffect(() => {
    setCount(itemCount())
  })

  function getDashboardUrl() {
    let url = '/user/dashboard'
    if (isAuth()) {
      const {
        user: { role }
      } = isAuth() as Jwt

      if (role === 1) {
        url = '/admin/dashboard'
      }
    }

    return url
  }

  return (
    <Menu mode="horizontal" selectable={false}>
      <Menu.Item className={isHome}>
        <Link to="/">首页</Link>
      </Menu.Item>
      <Menu.Item className={isShop}>
        <Link to="/shop">商城</Link>
      </Menu.Item>
      <Menu.Item className={isCart}>
        <Link to="/cart">
          购物车
          <Badge count={count} offset={[5, -10]} />
        </Link>
      </Menu.Item>
      {!isAuth() && (
        <>
          <Menu.Item className={isSignin}>
            <Link to="/signin">登录</Link>
          </Menu.Item>
          <Menu.Item className={isSignup}>
            <Link to="/signup">注册</Link>
          </Menu.Item>
        </>
      )}
      {isAuth() && (
        <Menu.Item className={isDashboard}>
          <Link to={getDashboardUrl()}>dashboard</Link>
        </Menu.Item>
      )}
    </Menu>
  )
}

export default Navigation

支付成功清空购物车

// src\helpers\cart.ts

...

// 清空购物车
export const clearCart = () => {
  if (typeof window !== 'undefined') {
    localStorage.removeItem('cart')
  }
}

// src\components\core\Success.tsx
import { Button } from 'antd'
import { useContext, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { TotalContext } from '../../anotherStore'
import { clearCart } from '../../helpers/cart'
import Layout from './Layout'

const Success = () => {
  const [count, setCount] = useContext(TotalContext)

  useEffect(() => {
    clearCart()
    setCount(0)
  })
  return (
    <Layout title="支付完成" subTitle="">
      <Button>
        <Link to="/">继续购物</Link>
      </Button>
    </Layout>
  )
}

export default Success

管理员订单列表页面

创建页面组件

// src\components\admin\Orders.tsx
import Layout from '../core/Layout'

const Orders = () => {
  return (
    <Layout title="订单" subTitle="当前订单的数量是 10">
      <Title level={4}>订单号:{order.out_trade_no}</Title>
      <table style={{ width: '100%' }}>
        <thead className="ant-table-thead">
          <tr>
            <th className="ant-table-cell">订单状态</th>
            <th className="ant-table-cell">订单号</th>
            <th className="ant-table-cell">总价</th>
            <th className="ant-table-cell">创建时间</th>
            <th className="ant-table-cell">邮寄地址</th>
            <th className="ant-table-cell">客户姓名</th>
          </tr>
        </thead>
        <tbody className="ant-table-tbody">
          <tr className="abt-table-row">
            <td className="ant-table-cell"></td>
            <td className="ant-table-cell"></td>
            <td className="ant-table-cell"></td>
            <td className="ant-table-cell"></td>
            <td className="ant-table-cell"></td>
            <td className="ant-table-cell"></td>
          </tr>
        </tbody>
      </table>
      <table style={{ width: '100%' }}>
        <thead className="ant-table-thead">
          <tr>
            <th className="ant-table-cell">产品名称</th>
            <th className="ant-table-cell">产品价格</th>
            <th className="ant-table-cell">产品数量</th>
            <th className="ant-table-cell">产品 ID</th>
          </tr>
        </thead>
        <tbody className="ant-table-tbody">
          <tr className="abt-table-row">
            <td className="ant-table-cell"></td>
            <td className="ant-table-cell"></td>
            <td className="ant-table-cell"></td>
            <td className="ant-table-cell"></td>
          </tr>
        </tbody>
      </table>
    </Layout>
  )
}

export default Orders

配置路由

// src\Routes.tsx
import { HashRouter, Route, Switch } from 'react-router-dom'
import AddCategory from './components/admin/AddCategory'
import AddProduct from './components/admin/AddProduct'
import AdminDashboard from './components/admin/AdminDashboard'
import AdminRoute from './components/admin/AdminRoute'
import Dashboard from './components/admin/Dashboard'
import Orders from './components/admin/Orders'
import PrivateRoute from './components/admin/PrivateRoute'
import Cart from './components/core/Cart'
import Home from './components/core/Home'
import Product from './components/core/Product'
import Shop from './components/core/Shop'
import Signin from './components/core/Signin'
import Signup from './components/core/Signup'
import Success from './components/core/Success'

const Routes = () => {
  return (
    <HashRouter>
      <Switch>
        <Route path="/" component={Home} exact />
        <Route path="/shop" component={Shop} />
        <Route path="/signin" component={Signin} />
        <Route path="/signup" component={Signup} />
        <Route path="/product/:productId" component={Product} />
        <Route path="/cart" component={Cart} />
        <Route path="/paysuccess" component={Success} />
        <PrivateRoute path="/user/dashboard" component={Dashboard} />
        <AdminRoute path="/admin/dashboard" component={AdminDashboard} />
        <AdminRoute path="/create/category" component={AddCategory} />
        <AdminRoute path="/create/product" component={AddProduct} />
        <AdminRoute path="/admin/orders" component={Orders} />
      </Switch>
    </HashRouter>
  )
}

export default Routes

配置连接

// src\components\admin\AdminDashboard.tsx
<Link to="/admin/orders">订单列表</Link>

获取订单列表数据

定义 interface

// src\store\models\order.ts
import { User } from './auth'
import { Product } from './product'

export interface OrderProduct {
  _id: string
  count: number
  product: Product
  snapshot: Product
}

export interface Order {
  status: string
  _id: string
  out_trade_no: string
  trade_no: string
  amount: number
  address: string
  products: OrderProduct[]
  user: User
  createdAt: string
}

定义订单状态枚举

// src\helpers\status.ts

// 订单状态
export const status: { [param: string]: string } = {
  Unpaid: '未付款',
  Paid: '已付款',
  Shipped: '运输中',
  Complete: '已完成',
  Cancelle: '已取消'
}

获取数据并展示

// src\components\admin\Orders.tsx
import { Divider, message, Select, Typography } from 'antd'
import axios from 'axios'
import moment from 'moment'
import React, { useEffect, useState } from 'react'
import { API } from '../../config'
import { isAuth } from '../../helpers/auth'
import { status } from '../../helpers/status'
import { Jwt } from '../../store/models/auth'
import { Order } from '../../store/models/order'
import Layout from '../core/Layout'

const { Title } = Typography

const Orders = () => {
  const {
    token,
    user: { _id: userId }
  } = isAuth() as Jwt

  const [orders, setOrders] = useState([])

  useEffect(() => {
    async function getOrders() {
      try {
        const response = await axios.get(`${API}/orders/${userId}`, {
          headers: {
            Authorization: `Bearer ${token}`
          }
        })

        setOrders(response.data)
      } catch (error) {
        console.dir(error)
        message.error(error.response.data.errors[0])
      }
    }

    getOrders()
  }, [])

  // 页面副标题
  const getOrderCount = () => {
    if (orders.length > 0) {
      return `当前订单的数量是 ${orders.length}`
    } else {
      return `还没有订单`
    }
  }

  return (
    <Layout title="订单" subTitle={getOrderCount()}>
      {orders.map((order: Order) => (
        <React.Fragment key={order._id}>
          <Title level={4}>订单号:{order.out_trade_no}</Title>
          <table style={{ width: '100%' }}>
            <thead className="ant-table-thead">
              <tr>
                <th className="ant-table-cell">订单状态</th>
                <th className="ant-table-cell">订单号</th>
                <th className="ant-table-cell">总价</th>
                <th className="ant-table-cell">创建时间</th>
                <th className="ant-table-cell">邮寄地址</th>
                <th className="ant-table-cell">客户姓名</th>
              </tr>
            </thead>
            <tbody className="ant-table-tbody">
              <tr className="abt-table-row">
                <td className="ant-table-cell">{status[order.status]}</td>
                <td className="ant-table-cell">{order.out_trade_no}</td>
                <td className="ant-table-cell">{order.amount}</td>
                <td className="ant-table-cell">{moment(order.createdAt).format('YYYY-MM-DD HH:mm:ss')}</td>
                <td className="ant-table-cell">{order.address}</td>
                <td className="ant-table-cell">{order.user.name}</td>
              </tr>
            </tbody>
          </table>
          <table style={{ width: '100%' }}>
            <thead className="ant-table-thead">
              <tr>
                <th className="ant-table-cell">产品名称</th>
                <th className="ant-table-cell">产品价格</th>
                <th className="ant-table-cell">产品数量</th>
                <th className="ant-table-cell">产品 ID</th>
              </tr>
            </thead>
            <tbody className="ant-table-tbody">
              {order.products.map(item => (
                <tr key={item._id} className="abt-table-row">
                  <td className="ant-table-cell">{item.product.name}</td>
                  <td className="ant-table-cell">{item.product.price}</td>
                  <td className="ant-table-cell">{item.count}</td>
                  <td className="ant-table-cell">{item.product._id}</td>
                </tr>
              ))}
            </tbody>
          </table>
          <Divider />
        </React.Fragment>
      ))}
    </Layout>
  )
}

export default Orders

更改订单状态

// src\components\admin\Orders.tsx
import { Divider, message, Select, Typography } from 'antd'
import axios from 'axios'
import moment from 'moment'
import React, { useEffect, useState } from 'react'
import { API } from '../../config'
import { isAuth } from '../../helpers/auth'
import { status } from '../../helpers/status'
import { Jwt } from '../../store/models/auth'
import { Order } from '../../store/models/order'
import Layout from '../core/Layout'

const { Title } = Typography

const Orders = () => {
  const {
    token,
    user: { _id: userId }
  } = isAuth() as Jwt

  const [orders, setOrders] = useState([])

  async function getOrders() {
    try {
      const response = await axios.get(`${API}/orders/${userId}`, {
        headers: {
          Authorization: `Bearer ${token}`
        }
      })

      setOrders(response.data)
    } catch (error) {
      console.dir(error)
      message.error(error.response.data.errors[0])
    }
  }

  useEffect(() => {
    getOrders()
  }, [])

  // 页面副标题
  const getOrderCount = () => {
    if (orders.length > 0) {
      return `当前订单的数量是 ${orders.length}`
    } else {
      return `还没有订单`
    }
  }

  // 变更订单状态
  const handleChange = (orderId: string) => (status: string) => {
    axios
      .put(
        `${API}/order/updateStatus/${userId}`,
        {
          orderId,
          status
        },
        {
          headers: {
            Authorization: `Bearer ${token}`
          }
        }
      )
      .then(() => {
        getOrders()
      })
  }

  return (
    <Layout title="订单" subTitle={getOrderCount()}>
      {orders.map((order: Order) => (
        <React.Fragment key={order._id}>
          <Title level={4}>订单号:{order.out_trade_no}</Title>
          <table style={{ width: '100%' }}>
            <thead className="ant-table-thead">
              <tr>
                <th className="ant-table-cell">订单状态</th>
                <th className="ant-table-cell">订单号</th>
                <th className="ant-table-cell">总价</th>
                <th className="ant-table-cell">创建时间</th>
                <th className="ant-table-cell">邮寄地址</th>
                <th className="ant-table-cell">客户姓名</th>
              </tr>
            </thead>
            <tbody className="ant-table-tbody">
              <tr className="abt-table-row">
                <td className="ant-table-cell">
                  <Select defaultValue={order.status} onChange={handleChange(order._id)}>
                    {Object.entries(status).map(([value, label]) => (
                      <Select.Option key={value} value={value}>
                        {label}
                      </Select.Option>
                    ))}
                  </Select>
                </td>
                <td className="ant-table-cell">{order.out_trade_no}</td>
                <td className="ant-table-cell">{order.amount}</td>
                <td className="ant-table-cell">{moment(order.createdAt).format('YYYY-MM-DD HH:mm:ss')}</td>
                <td className="ant-table-cell">{order.address}</td>
                <td className="ant-table-cell">{order.user.name}</td>
              </tr>
            </tbody>
          </table>
          <table style={{ width: '100%' }}>
            <thead className="ant-table-thead">
              <tr>
                <th className="ant-table-cell">产品名称</th>
                <th className="ant-table-cell">产品价格</th>
                <th className="ant-table-cell">产品数量</th>
                <th className="ant-table-cell">产品 ID</th>
              </tr>
            </thead>
            <tbody className="ant-table-tbody">
              {order.products.map(item => (
                <tr key={item._id} className="abt-table-row">
                  <td className="ant-table-cell">{item.product.name}</td>
                  <td className="ant-table-cell">{item.product.price}</td>
                  <td className="ant-table-cell">{item.count}</td>
                  <td className="ant-table-cell">{item.product._id}</td>
                </tr>
              ))}
            </tbody>
          </table>
          <Divider />
        </React.Fragment>
      ))}
    </Layout>
  )
}

export default Orders

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值