React+Redux+Ant Design+TypeScript 电子商务实战-客户端应用 04 首页产品展示、搜索、筛选和产品详情

本文主要是介绍React+Redux+Ant Design+TypeScript 电子商务实战-客户端应用 04 首页产品展示、搜索、筛选和产品详情,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

构建首页布局

搜索布局

// src\components\core\Search.tsx
import { Button, Col, Divider, Form, Input, Row, Select } from 'antd'
import ProductItem from './ProductItem'const Search = () => {return (<><Formlayout="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 } = Typographyconst ProductItem = () => {return (<Cardcover={<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 } = Typographyconst 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

实现获取产品列表

定义相关 action

// src\store\models\product.ts
import { Category } from './category'export interface Product {_id: stringname: stringprice: numberdescription: stringcategory: Categoryquantity: numbersold: numberphoto: FormDatashipping: booleancreatedAt: 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_PRODUCTsortBy: stringorder: stringlimit: numberskip: numbersearch: stringfilters: {[param: string]: any[]}
}export interface GetProductSuccessAction {type: typeof GET_PRODUCT_SUCCESSsortBy: stringpayload: 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

定义 reducer

// 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: booleansuccess: booleanproducts: Product[]}// 最受欢迎列表(销量)sold: {loaded: booleansuccess: booleanproducts: 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: RouterStateauth: AuthStatecategory: CategoryStateproduct: ProductState
}const createRootReducer = (history: History) =>combineReducers({// test: testReducer,router: connectRouter(history),auth: authReducer,category: categoryReducer,product: productReducer})export default createRootReducer

定义 saga

// 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 } = Typographyconst 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 } = Typographyinterface Props {product: Product
}const ProductItem: FC<Props> = ({ product }) => {return (<Cardcover={<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 (<><Formlayout="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">{/* <ProductItem /> */}</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 (<><Formlayout="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 } = Typographyconst CategoryFilter = () => {const categories = [{name: '化妆品'}, {name: '书籍'}]return (<><Title level={4}>按照分类筛选</Title><Checkbox.Group onChange={onChange} style={{ width: '100%' }}><ListdataSource={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 } = Typographyconst PriceFilter = () => {const prices = [{ price: '0 - 50' }, { price: '50 - 100' }]return (<><Title level={4}>按照价格筛选</Title><Radio.Group><ListdataSource={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 } = Typographyinterface 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%' }}><ListdataSource={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"><CategoryFilterhandleFilter={(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: stringname: stringprice: numberdescription: stringcategory: Categoryquantity: numbersold: numberphoto: FormDatashipping: booleancreatedAt: string
}export interface Price {id: numbername: stringarray: [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 } = Typographyinterface 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}><ListdataSource={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"><CategoryFilterhandleFilter={(filters: string[]) => {setMyFilters({ ...myFilters, category: filters })}}/><PriceFilterhandleFilter={(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"><CategoryFilterhandleFilter={(filters: string[]) => {setMyFilters({ ...myFilters, category: filters })}}/><PriceFilterhandleFilter={(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)// 筛选条件变化 重置skipuseEffect(() => {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"><CategoryFilterhandleFilter={(filters: string[]) => {setMyFilters({ ...myFilters, category: filters })}}/><PriceFilterhandleFilter={(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>

根据产品ID获取产品详情

// 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 = () => {// 获取路由上的 productIdconst { 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 = () => {// 获取路由上的 productIdconst { 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%;
}

控制 ProductItem 按钮展示

// 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 } = Typographyinterface Props {product: ProductshowViewBtn?: booleanshowCartBtn?: 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 (<Cardcover={<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 = () => {// 获取路由上的 productIdconst { 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

这篇关于React+Redux+Ant Design+TypeScript 电子商务实战-客户端应用 04 首页产品展示、搜索、筛选和产品详情的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1097937

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

Vue3 的 shallowRef 和 shallowReactive:优化性能

大家对 Vue3 的 ref 和 reactive 都很熟悉,那么对 shallowRef 和 shallowReactive 是否了解呢? 在编程和数据结构中,“shallow”(浅层)通常指对数据结构的最外层进行操作,而不递归地处理其内部或嵌套的数据。这种处理方式关注的是数据结构的第一层属性或元素,而忽略更深层次的嵌套内容。 1. 浅层与深层的对比 1.1 浅层(Shallow) 定义

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

中文分词jieba库的使用与实景应用(一)

知识星球:https://articles.zsxq.com/id_fxvgc803qmr2.html 目录 一.定义: 精确模式(默认模式): 全模式: 搜索引擎模式: paddle 模式(基于深度学习的分词模式): 二 自定义词典 三.文本解析   调整词出现的频率 四. 关键词提取 A. 基于TF-IDF算法的关键词提取 B. 基于TextRank算法的关键词提取

水位雨量在线监测系统概述及应用介绍

在当今社会,随着科技的飞速发展,各种智能监测系统已成为保障公共安全、促进资源管理和环境保护的重要工具。其中,水位雨量在线监测系统作为自然灾害预警、水资源管理及水利工程运行的关键技术,其重要性不言而喻。 一、水位雨量在线监测系统的基本原理 水位雨量在线监测系统主要由数据采集单元、数据传输网络、数据处理中心及用户终端四大部分构成,形成了一个完整的闭环系统。 数据采集单元:这是系统的“眼睛”,

【 html+css 绚丽Loading 】000046 三才归元阵

前言:哈喽,大家好,今天给大家分享html+css 绚丽Loading!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏+关注哦 💕 目录 📚一、效果📚二、信息💡1.简介:💡2.外观描述:💡3.使用方式:💡4.战斗方式:💡5.提升:💡6.传说: 📚三、源代码,上代码,可以直接复制使用🎥效果🗂️目录✍️

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

认识、理解、分类——acm之搜索

普通搜索方法有两种:1、广度优先搜索;2、深度优先搜索; 更多搜索方法: 3、双向广度优先搜索; 4、启发式搜索(包括A*算法等); 搜索通常会用到的知识点:状态压缩(位压缩,利用hash思想压缩)。

hdu1240、hdu1253(三维搜索题)

1、从后往前输入,(x,y,z); 2、从下往上输入,(y , z, x); 3、从左往右输入,(z,x,y); hdu1240代码如下: #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#inc

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置