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 (
    <>
      <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

实现获取产品列表

定义相关 action

// 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

定义 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: 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

定义 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 } = 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>

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

控制 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 } = 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

你可能感兴趣的:(实战,typescript,react.js)