// src\components\core\Search.tsx
import { Button, Col, Divider, Form, Input, Row, Select } from 'antd'
import ProductItem from './ProductItem'
const Search = () => {
return (
<>
<Form
layout="inline"
initialValues={{
category: ''
}}
>
<Input.Group compact>
<Form.Item name="category">
<Select>
<Select.Option value="">所有分类</Select.Option>
</Select>
</Form.Item>
<Form.Item name="search">
<Input placeholder="请输入搜索关键字" />
</Form.Item>
<Form.Item>
<Button htmlType="submit">搜索</Button>
</Form.Item>
</Input.Group>
</Form>
<Divider />
<Row gutter={[16, 16]}>
<Col span="6">
<ProductItem />
</Col>
</Row>
</>
)
}
export default Search
// src\components\core\ProductItem.tsx
import { Button, Card, Col, Row, Typography } from 'antd'
import { Link } from 'react-router-dom'
const { Title, Paragraph } = Typography
const ProductItem = () => {
return (
<Card
cover={<img alt="example" src="https://os.alipayobjects.com/rmsportal/QBnOOoLaAfKPirc.png" />}
actions={[
<Button type="link">
<Link to="">查看详情</Link>
</Button>,
<Button type="link">
<Link to="">加入购物车</Link>
</Button>
]}
>
<Title level={5}>产品标题</Title>
<Paragraph ellipsis={{ rows: 2 }}>产品描述</Paragraph>
<Row>
<Col span="12">销量</Col>
<Col span="12" style={{ textAlign: 'right' }}>
价格
</Col>
</Row>
<Row>
<Col span="12">上架时间</Col>
<Col span="12" style={{ textAlign: 'right' }}>
所属分类
</Col>
</Row>
</Card>
)
}
export default ProductItem
// src\components\core\Home.tsx
import { Col, Row, Typography } from 'antd'
import Layout from './Layout'
import ProductItem from './ProductItem'
import Search from './Search'
const { Title } = Typography
const Home = () => {
return (
<Layout title="RM商城" subTitle="优享品质 惊喜价格">
{/* 搜索列表 */}
<Search />
{/* 最新上架列表 */}
<Title level={5}>最新上架</Title>
<Row gutter={[16, 16]}>
<Col span="6">
<ProductItem />
</Col>
</Row>
{/* 最受欢迎列表 */}
<Title level={5}>最受欢迎</Title>
<Row gutter={[16, 16]}>
<Col span="6">
<ProductItem />
</Col>
</Row>
</Layout>
)
}
export default Home
// src\store\models\product.ts
import { Category } from './category'
export interface Product {
_id: string
name: string
price: number
description: string
category: Category
quantity: number
sold: number
photo: FormData
shipping: boolean
createdAt: string
}
import { Product } from '../models/product'
// src\store\actions\product.action.ts
export const GET_PRODUCT = 'GET_PRODUCT'
export const GET_PRODUCT_SUCCESS = 'GET_PRODUCT_SUCCESS'
// {
// "sortBy": "_id",
// "order": "desc",
// "limit": 10,
// "skip": 0,
// "search": "JavaScript",
// "filters": {
// "category": ["610a3d79458ef7766805473d"],
// "price": [50, 100]
// }
// }
export interface GetProductAction {
type: typeof GET_PRODUCT
sortBy: string
order: string
limit: number
skip: number
search: string
filters: {
[param: string]: any[]
}
}
export interface GetProductSuccessAction {
type: typeof GET_PRODUCT_SUCCESS
sortBy: string
payload: Product[]
}
export const getProduct = (
sortBy: string = 'createdAt',
order: string = 'asc',
limit: number = 4,
skip: number = 0,
search: string = '',
filters: {
[param: string]: any[]
} = {}
): GetProductAction => ({
type: GET_PRODUCT,
sortBy,
order,
limit,
skip,
search,
filters
})
export const getProductSuccess = (payload: Product[], sortBy: string): GetProductSuccessAction => ({
type: GET_PRODUCT_SUCCESS,
payload,
sortBy
})
// action 的联合类型
export type ProductUnionType = GetProductAction | GetProductSuccessAction
// src\store\reducers\product.reducer.ts
import { GET_PRODUCT, GET_PRODUCT_SUCCESS, ProductUnionType } from '../actions/product.action'
import { Product } from '../models/product'
export interface ProductState {
// 最新上架列表(上架时间)
createdAt: {
loaded: boolean
success: boolean
products: Product[]
}
// 最受欢迎列表(销量)
sold: {
loaded: boolean
success: boolean
products: Product[]
}
}
const initialState = {
createdAt: {
loaded: false,
success: false,
products: []
},
sold: {
loaded: false,
success: false,
products: []
}
}
export default function productReducer(state = initialState, action: ProductUnionType) {
switch (action.type) {
case GET_PRODUCT:
return {
...state,
[action.sortBy]: {
// 避免每次查询时都清空
...state[action.sortBy === 'createdAt' ? 'createdAt' : 'sold'],
loaded: false,
success: false
}
}
case GET_PRODUCT_SUCCESS:
return {
...state,
[action.sortBy]: {
loaded: true,
success: true,
products: action.payload
}
}
default:
return state
}
}
// src\store\reducers\index.ts
import { connectRouter, RouterState } from 'connected-react-router'
import { History } from 'history'
import { combineReducers } from 'redux'
import authReducer, { AuthState } from './auth.reducer'
import categoryReducer, { CategoryState } from './category.reducer'
import productReducer, { ProductState } from './product.reducer'
// import testReducer from './test.reducer'
// 定义一个包含 router 的 store 类型接口 供外部使用
export interface AppState {
router: RouterState
auth: AuthState
category: CategoryState
product: ProductState
}
const createRootReducer = (history: History) =>
combineReducers({
// test: testReducer,
router: connectRouter(history),
auth: authReducer,
category: categoryReducer,
product: productReducer
})
export default createRootReducer
// src\store\sagas\product.saga.ts
import axios, { AxiosResponse } from 'axios'
import { put, takeEvery } from 'redux-saga/effects'
import { API } from '../../config'
import { GetProductAction, getProductSuccess, GET_PRODUCT } from '../actions/product.action'
import { Product } from '../models/product'
function* handleGetProduct({ sortBy, order, limit, skip, search, filters }: GetProductAction) {
const response: AxiosResponse = yield axios.post<Product[]>(`${API}/products`, {
sortBy,
order,
limit,
skip,
search,
filters
})
yield put(getProductSuccess(response.data, sortBy))
}
export default function* productSaga() {
yield takeEvery(GET_PRODUCT, handleGetProduct)
}
// src\store\sagas\index.ts
import { all } from 'redux-saga/effects'
import authSaga from './auth.saga'
import categorySage from './category.sage'
import productSaga from './product.saga'
export default function* rootSaga() {
yield all([authSaga(), categorySage(), productSaga()])
}
// src\components\core\Home.tsx
import { useEffect } from 'react'
import { Col, Row, Typography } from 'antd'
import { useDispatch, useSelector } from 'react-redux'
import Layout from './Layout'
import ProductItem from './ProductItem'
import Search from './Search'
import { getProduct } from '../../store/actions/product.action'
import { AppState } from '../../store/reducers'
import { ProductState } from '../../store/reducers/product.reducer'
const { Title } = Typography
const Home = () => {
const dispatch = useDispatch()
const { createdAt, sold } = useSelector<AppState, ProductState>(state => state.product)
useEffect(() => {
dispatch(getProduct('createdAt', 'desc', 4))
dispatch(getProduct('sold', 'desc', 4))
}, [])
return (
<Layout title="RM商城" subTitle="优享品质 惊喜价格">
{/* 搜索列表 */}
<Search />
{/* 最新上架列表 */}
<Title level={5}>最新上架</Title>
<Row gutter={[16, 16]}>
{createdAt.products.map(item => (
<Col span="6" key={item._id}>
<ProductItem product={item} />
</Col>
))}
</Row>
{/* 最受欢迎列表 */}
<Title level={5} style={{marginTop: '20px'}}>最受欢迎</Title>
<Row gutter={[16, 16]}>
{sold.products.map(item => (
<Col span="6" key={item._id}>
<ProductItem product={item} />
</Col>
))}
</Row>
</Layout>
)
}
export default Home
// src\components\core\ProductItem.tsx
import { Button, Card, Col, Image, Row, Typography } from 'antd'
import moment from 'moment'
import { Link } from 'react-router-dom'
import { FC } from 'react'
import { API } from '../../config'
import { Product } from '../../store/models/product'
const { Title, Paragraph } = Typography
interface Props {
product: Product
}
const ProductItem: FC<Props> = ({ product }) => {
return (
<Card
cover={<Image src={`${API}/product/photo/${product._id}`} alt={product.name} preview={false} />}
actions={[
<Button type="link">
<Link to="">查看详情</Link>
</Button>,
<Button type="link">
<Link to="">加入购物车</Link>
</Button>
]}
>
<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
Search 组件中的 ProductItem 还没有添加 product 属性,为了页面正常显示,先注释掉 Search 组件中的 ProductItem 组件。
// src\components\core\Search.tsx
import { Button, Col, Divider, Form, Input, Row, Select } from 'antd'
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { getCategory } from '../../store/actions/category.action'
import { AppState } from '../../store/reducers'
import { CategoryState } from '../../store/reducers/category.reducer'
import ProductItem from './ProductItem'
const Search = () => {
const dispatch = useDispatch()
// 获取 state 中的分类列表
const { category } = useSelector<AppState, CategoryState>(state => state.category)
useEffect(() => {
// 页面首次加载时获取分类列表
dispatch(getCategory())
}, [])
return (
<>
<Form
layout="inline"
initialValues={{
category: ''
}}
>
<Input.Group compact>
<Form.Item name="category">
<Select>
<Select.Option value="">所有分类</Select.Option>
{category.result.map(item => (
<Select.Option value={item._id} key={item._id}>
{item.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item name="search">
<Input placeholder="请输入搜索关键字" />
</Form.Item>
<Form.Item>
<Button htmlType="submit">搜索</Button>
</Form.Item>
</Input.Group>
</Form>
<Divider />
<Row gutter={[16, 16]}>
<Col span="6">{/* */}</Col>
</Row>
</>
)
}
export default Search
// src\components\core\Search.tsx
import { Button, Col, Divider, Form, Input, message, Row, Select } from 'antd'
import axios from 'axios'
import { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { API } from '../../config'
import { getCategory } from '../../store/actions/category.action'
import { Product } from '../../store/models/product'
import { AppState } from '../../store/reducers'
import { CategoryState } from '../../store/reducers/category.reducer'
import ProductItem from './ProductItem'
const Search = () => {
const dispatch = useDispatch()
// 获取 state 中的分类列表
const { category } = useSelector<AppState, CategoryState>(state => state.category)
useEffect(() => {
// 页面首次加载时获取分类列表
dispatch(getCategory())
}, [])
// 搜索结果
const [products, setProducts] = useState<Product[]>([])
const onFinish = async (value: { category: string; search: string }) => {
try {
const response = await axios.post<Product[]>(`${API}/products`, {
sortBy: 'createdAt',
order: 'desc',
limit: 10,
skip: 0,
search: value.search,
filters: value.category
? {
category: [value.category]
}
: {}
})
setProducts(response.data)
} catch (error) {
message.error('搜索产品失败')
}
}
return (
<>
<Form
layout="inline"
initialValues={{
category: ''
}}
onFinish={onFinish}
>
<Input.Group compact>
<Form.Item name="category">
<Select>
<Select.Option value="">所有分类</Select.Option>
{category.result.map(item => (
<Select.Option value={item._id} key={item._id}>
{item.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item name="search">
<Input placeholder="请输入搜索关键字" />
</Form.Item>
<Form.Item>
<Button htmlType="submit">搜索</Button>
</Form.Item>
</Input.Group>
</Form>
<Divider />
<Row gutter={[16, 16]}>
{products.map(item => (
<Col span="6" key={item._id}>
<ProductItem product={item} />
</Col>
))}
</Row>
</>
)
}
export default Search
// src\components\core\CategoryFilter.tsx
import { Checkbox, List, Typography } from 'antd'
const { Title } = Typography
const CategoryFilter = () => {
const categories = [{name: '化妆品'}, {name: '书籍'}]
return (
<>
<Title level={4}>按照分类筛选</Title>
<Checkbox.Group onChange={onChange} style={{ width: '100%' }}>
<List
dataSource={categories}
renderItem={item => (
<List.Item>
<Checkbox>{item.name}</Checkbox>
</List.Item>
)}
/>
</Checkbox.Group>
</>
)
}
export default CategoryFilter
// src\components\core\PriceFilter.tsx
import { List, Radio, Typography } from 'antd'
const { Title } = Typography
const PriceFilter = () => {
const prices = [{ price: '0 - 50' }, { price: '50 - 100' }]
return (
<>
<Title level={4}>按照价格筛选</Title>
<Radio.Group>
<List
dataSource={prices}
renderItem={item => (
<List.Item>
<Radio>{item.price}</Radio>
</List.Item>
)}
/>
</Radio.Group>
</>
)
}
export default PriceFilter
// src\components\core\Shop.tsx
import { Col, Row, Space } from 'antd'
import CategoryFilter from './CategoryFilter'
import Layout from './Layout'
import PriceFilter from './PriceFilter'
const Shop = () => {
const filterDOM = () => (
<Space size="middle" direction="vertical">
<CategoryFilter />
<PriceFilter />
</Space>
)
return (
<Layout title="RM商城" subTitle="挑选你喜欢的商品把">
<Row>
<Col span="4">{filterDOM()}</Col>
<Col span="20">right</Col>
</Row>
</Layout>
)
}
export default Shop
// src\components\core\CategoryFilter.tsx
import { Checkbox, List, Typography } from 'antd'
import { CheckboxValueType } from 'antd/lib/checkbox/Group'
import { FC, useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { getCategory } from '../../store/actions/category.action'
import { AppState } from '../../store/reducers'
import { CategoryState } from '../../store/reducers/category.reducer'
const { Title } = Typography
interface Props {
handleFilter: (arg: string[]) => void
}
const CategoryFilter: FC<Props> = ({ handleFilter }) => {
const dispatch = useDispatch()
const category = useSelector<AppState, CategoryState>(state => state.category)
useEffect(() => {
dispatch(getCategory())
}, [])
const onChange = (checkedValue: CheckboxValueType[]) => {
handleFilter(checkedValue as string[])
}
return (
<>
<Title level={4}>按照分类筛选</Title>
<Checkbox.Group onChange={onChange} style={{ width: '100%' }}>
<List
dataSource={category.category.result}
renderItem={item => (
<List.Item>
<Checkbox value={item._id}>{item.name}</Checkbox>
</List.Item>
)}
/>
</Checkbox.Group>
</>
)
}
export default CategoryFilter
// src\components\core\Shop.tsx
import { Col, Row, Space } from 'antd'
import { useEffect, useState } from 'react'
import CategoryFilter from './CategoryFilter'
import Layout from './Layout'
import PriceFilter from './PriceFilter'
const Shop = () => {
const [myFilters, setMyFilters] = useState<{
category: string[]
price: number[]
}>({ category: [], price: [] })
useEffect(() => {
console.log(myFilters)
}, [myFilters])
const filterDOM = () => (
<Space size="middle" direction="vertical">
<CategoryFilter
handleFilter={(filters: string[]) => {
setMyFilters({ ...myFilters, category: filters })
}}
/>
<PriceFilter />
</Space>
)
return (
<Layout title="RM商城" subTitle="挑选你喜欢的商品把">
<Row>
<Col span="4">{filterDOM()}</Col>
<Col span="20">right</Col>
</Row>
</Layout>
)
}
export default Shop
定义筛选条件:
// src\store\models\product.ts
import { Category } from './category'
export interface Product {
_id: string
name: string
price: number
description: string
category: Category
quantity: number
sold: number
photo: FormData
shipping: boolean
createdAt: string
}
export interface Price {
id: number
name: string
array: [number?, number?]
}
// src\helpers\price.ts
import { Price } from '../store/models/product'
const prices: Price[] = [
{
id: 0,
name: '不限制价格',
array: []
},
{
id: 1,
name: '1 - 50',
array: [1, 50]
},
{
id: 2,
name: '51 - 100',
array: [51, 100]
},
{
id: 3,
name: '101 - 150',
array: [101, 150]
},
{
id: 4,
name: '151 - 200',
array: [151, 200]
},
{
id: 5,
name: '201 - 500',
array: [201, 500]
}
]
export default prices
修改组件:
// src\components\core\PriceFilter.tsx
import { List, Radio, RadioChangeEvent, Typography } from 'antd'
import { FC } from 'react'
import prices from '../../helpers/price'
const { Title } = Typography
interface Props {
handleFilter: (arg: number[]) => void
}
const PriceFilter: FC<Props> = ({ handleFilter }) => {
const onChange = (event: RadioChangeEvent) => {
handleFilter(event.target.value)
}
return (
<>
<Title level={4}>按照价格筛选</Title>
<Radio.Group onChange={onChange}>
<List
dataSource={prices}
renderItem={item => (
<List.Item>
<Radio value={item.array}>{item.name}</Radio>
</List.Item>
)}
/>
</Radio.Group>
</>
)
}
export default PriceFilter
// src\components\core\Shop.tsx
import { Col, Row, Space } from 'antd'
import { useEffect, useState } from 'react'
import CategoryFilter from './CategoryFilter'
import Layout from './Layout'
import PriceFilter from './PriceFilter'
const Shop = () => {
const [myFilters, setMyFilters] = useState<{
category: string[]
price: number[]
}>({ category: [], price: [] })
useEffect(() => {
console.log(myFilters)
}, [myFilters])
const filterDOM = () => (
<Space size="middle" direction="vertical">
<CategoryFilter
handleFilter={(filters: string[]) => {
setMyFilters({ ...myFilters, category: filters })
}}
/>
<PriceFilter
handleFilter={(filters: number[]) => {
setMyFilters({ ...myFilters, price: filters })
}}
/>
</Space>
)
return (
<Layout title="RM商城" subTitle="挑选你喜欢的商品把">
<Row>
<Col span="4">{filterDOM()}</Col>
<Col span="20">right</Col>
</Row>
</Layout>
)
}
export default Shop
// src\components\core\Shop.tsx
import { Col, message, Row, Space } from 'antd'
import axios from 'axios'
import { useEffect, useState } from 'react'
import { API } from '../../config'
import { Product } from '../../store/models/product'
import CategoryFilter from './CategoryFilter'
import Layout from './Layout'
import PriceFilter from './PriceFilter'
import ProductItem from './ProductItem'
const Shop = () => {
const [myFilters, setMyFilters] = useState<{
category: string[]
price: number[]
}>({ category: [], price: [] })
const [products, setProducts] = useState<Product[]>([])
useEffect(() => {
async function filterProduct() {
try {
const response = await axios.post<Product[]>(`${API}/products`, {
sortBy: 'createdAt',
order: 'desc',
limit: 4,
skip: 0,
search: '',
filters: myFilters
})
setProducts(response.data)
} catch (error) {
message.error(error.response.data.errors[0])
}
}
filterProduct()
}, [myFilters])
// 右侧产品列表
const productDOM = () => (
<Row gutter={[16, 16]}>
{products.map(item => (
<Col span="6" key={item._id}>
<ProductItem product={item} />
</Col>
))}
</Row>
)
// 左侧筛选条件
const filterDOM = () => (
<Space size="middle" direction="vertical">
<CategoryFilter
handleFilter={(filters: string[]) => {
setMyFilters({ ...myFilters, category: filters })
}}
/>
<PriceFilter
handleFilter={(filters: number[]) => {
setMyFilters({ ...myFilters, price: filters })
}}
/>
</Space>
)
return (
<Layout title="RM商城" subTitle="挑选你喜欢的商品把">
<Row>
<Col span="4">{filterDOM()}</Col>
<Col span="20">{productDOM()}</Col>
</Row>
</Layout>
)
}
export default Shop
// src\components\core\Shop.tsx
import { Button, Col, Empty, message, Row, Space } from 'antd'
import axios from 'axios'
import { useEffect, useState } from 'react'
import { API } from '../../config'
import { Product } from '../../store/models/product'
import CategoryFilter from './CategoryFilter'
import Layout from './Layout'
import PriceFilter from './PriceFilter'
import ProductItem from './ProductItem'
const Shop = () => {
const [myFilters, setMyFilters] = useState<{
category: string[]
price: number[]
}>({ category: [], price: [] })
// 筛选结果
const [products, setProducts] = useState<Product[]>([])
// 查询跳过条数
const [skip, setSkip] = useState<number>(0)
// 是否还有更多数据
const [isNoData, setIsNoData] = useState<boolean>(false)
// 筛选条件变化 重置skip
useEffect(() => {
setSkip(0)
}, [myFilters])
useEffect(() => {
async function filterProduct() {
try {
const response = await axios.post<Product[]>(`${API}/products`, {
sortBy: 'createdAt',
order: 'desc',
limit: 4,
skip,
search: '',
filters: myFilters
})
if (skip === 0) {
// 重新筛选 清空数据
setProducts(response.data)
} else {
// 加载更多 追加数据
setProducts([...products, ...response.data])
}
setIsNoData(response.data.length === 0)
} catch (error) {
message.error(error.response.data.errors[0])
}
}
filterProduct()
}, [myFilters, skip])
// 右侧产品列表
const productDOM = () => (
<Row gutter={[16, 16]}>
{products.map(item => (
<Col span="6" key={item._id}>
<ProductItem product={item} />
</Col>
))}
</Row>
)
// 加载更多按钮
const loadMoreButton = () => {
return (
!isNoData && (
<Row style={{ margin: '20px 0' }} justify="center">
<Button onClick={loadMore}>加载更多</Button>
</Row>
)
)
}
// 没有更多提示
const noData = () => {
return (
isNoData && (
<Row style={{ margin: '20px 0' }} justify="center">
<Empty description="暂无数据" image={Empty.PRESENTED_IMAGE_SIMPLE} />
</Row>
)
)
}
// 加载更多数据
const loadMore = () => {
setSkip(skip + 4)
}
// 左侧筛选条件
const filterDOM = () => (
<Space size="middle" direction="vertical">
<CategoryFilter
handleFilter={(filters: string[]) => {
setMyFilters({ ...myFilters, category: filters })
}}
/>
<PriceFilter
handleFilter={(filters: number[]) => {
setMyFilters({ ...myFilters, price: filters })
}}
/>
</Space>
)
return (
<Layout title="RM商城" subTitle="挑选你喜欢的商品把">
<Row>
<Col span="4">{filterDOM()}</Col>
<Col span="20">
{productDOM()}
{loadMoreButton()}
{noData()}
</Col>
</Row>
</Layout>
)
}
export default Shop
// src\components\core\Product.tsx
import { Col, Row } from 'antd'
import Layout from './Layout'
const Product = () => {
return (
<Layout title="产品名称" subTitle="产品描述">
<Row gutter={36}>
<Col span="18">
{/* 当前产品信息 */}
</Col>
<Col span="6">
{/* 与其相关的产品 */}
</Col>
</Row>
</Layout>
)
}
export default Product
添加路由:
// 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 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'
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} />
</Switch>
</HashRouter>
)
}
export default Routes
添加跳转链接:
// src\components\core\ProductItem.tsx
<Link to={`/product/${product._id}`}>查看详情</Link>
// src\components\core\Product.tsx
import { Col, message, Row } from 'antd'
import axios from 'axios'
import { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { API } from '../../config'
import { Product as ProductModel } from '../../store/models/product'
import Layout from './Layout'
const Product = () => {
// 获取路由上的 productId
const { productId } = useParams<{ productId: string }>()
const [product, setProduct] = useState<ProductModel>({
_id: '',
name: '',
price: 0,
description: '',
category: {
_id: '',
name: ''
},
quantity: 0,
sold: 0,
photo: new FormData(),
shipping: false,
createdAt: ''
})
useEffect(() => {
async function getProductById() {
try {
const response = await axios.get(`${API}/product/${productId}`)
setProduct(response.data)
} catch (error) {
message.error(error.response.data.errors[0])
}
}
getProductById()
}, [])
return (
<Layout title="产品名称" subTitle="产品描述">
<Row gutter={36}>
<Col span="18">{/* 当前产品信息 */}</Col>
<Col span="6">{/* 与其相关的产品 */}</Col>
</Row>
</Layout>
)
}
export default Product
// src\components\core\Product.tsx
import { Col, message, Row } from 'antd'
import axios from 'axios'
import { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { API } from '../../config'
import { Product as ProductModel } from '../../store/models/product'
import Layout from './Layout'
import ProductItem from './ProductItem'
const Product = () => {
// 获取路由上的 productId
const { productId } = useParams<{ productId: string }>()
const [product, setProduct] = useState<ProductModel>({
_id: '',
name: '',
price: 0,
description: '',
category: {
_id: '',
name: ''
},
quantity: 0,
sold: 0,
photo: new FormData(),
shipping: false,
createdAt: ''
})
useEffect(() => {
async function getProductById() {
try {
const response = await axios.get(`${API}/product/${productId}`)
setProduct(response.data)
} catch (error) {
message.error(error.response.data.errors[0])
}
}
getProductById()
}, [])
return (
<Layout title={product.name} subTitle={product.description}>
<Row gutter={36}>
<Col span="18" className="productDetail">
{/* 当前产品信息 */}
<ProductItem product={product} showViewBtn={false} showCartBtn={false} />
</Col>
<Col span="6">{/* 与其相关的产品 */}</Col>
</Row>
</Layout>
)
}
export default Product
/* src\style.css */
...
/* 产品详情页面封面样式 */
.productDetail .ant-image-img {
width: auto;
max-width: 100%;
}
// src\components\core\ProductItem.tsx
import { Button, Card, Col, Image, Row, Typography } from 'antd'
import moment from 'moment'
import { Link } from 'react-router-dom'
import { FC } from 'react'
import { API } from '../../config'
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 showButtons = () => {
const buttonArray = []
if (showViewBtn) {
buttonArray.push(
<Button type="link">
<Link to={`/product/${product._id}`}>查看详情</Link>
</Button>
)
}
if (showCartBtn) {
buttonArray.push(
<Button type="link">
<Link to="">加入购物车</Link>
</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\components\core\Product.tsx
import { Col, message, Row, Space } from 'antd'
import axios from 'axios'
import { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { API } from '../../config'
import { Product as ProductModel } from '../../store/models/product'
import Layout from './Layout'
import ProductItem from './ProductItem'
const Product = () => {
// 获取路由上的 productId
const { productId } = useParams<{ productId: string }>()
// 产品详情
const [product, setProduct] = useState<ProductModel>({
_id: '',
name: '',
price: 0,
description: '',
category: {
_id: '',
name: ''
},
quantity: 0,
sold: 0,
photo: new FormData(),
shipping: false,
createdAt: ''
})
// 相关产品
const [relatedProducts, setRelatedProducts] = useState<ProductModel[]>([])
useEffect(() => {
// 获取产品详情
async function getProductById() {
try {
const response = await axios.get(`${API}/product/${productId}`)
setProduct(response.data)
} catch (error) {
message.error(error.response.data.errors[0])
}
}
// 获取相关产品
async function getRelatedProducts() {
try {
const response = await axios.get(`${API}/products/related/${productId}`)
setRelatedProducts(response.data)
} catch (error) {
message.error(error.response.data.errors[0])
}
}
getProductById()
getRelatedProducts()
}, [])
return (
<Layout title={product.name} subTitle={product.description}>
<Row gutter={36}>
<Col span="18" className="productDetail">
{/* 当前产品信息 */}
<ProductItem product={product} showViewBtn={false} showCartBtn={false} />
</Col>
<Col span="6">
{/* 与其相关的产品 */}
<Space direction="vertical">
{relatedProducts.map(item => (
<ProductItem product={item} />
))}
</Space>
</Col>
</Row>
</Layout>
)
}
export default Product