将产品添加到购物车
定义方法
// 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
实现提交订单流程
- 获取支付宝收银台地址,并跳转
- 支付成功后跳转回客户端支付成功页面
- 支付宝服务器会向 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