创建应用路由及 Layput 组件
src├─ components│ ├─ Routes.js ------------------- 路由组件│ ├─ admin ----------------------- 后台文件夹│ └─ core ------------------------ 前台文件夹│ ├─ Home.js ------------------ 首页│ ├─ Layout.js ---------------- 公共组件│ └─ Shop.js ------------------ 商品列表├─ config.js ---------------------- 全局变量配置文件└─ index.js ----------------------- 入口文件
全局 store 初始化
src/store/reducers/test.js
export default function testReducer(state = 0) {return state}
src/store/reducers/index.js ```javascript import { combineReducers } from “redux”; import testReducer from “./test”;
const rootReducer = combineReducers({ test: testReducer })
export default rootReducer
- src/store/index.js```javascriptimport { createStore } from "redux";import rootReducer from "./reducers";const store = createStore(rootReducer)export default store
将路由状态同步到全局 store
- 安装插件
npm install --save connected-react-router src/store/reducers/index.js ```javascript import { connectRouter } from “connected-react-router”; import { combineReducers } from “redux”; import testReducer from “./test”;
const createRootReducer = (history) => combineReducers({ test: testReducer, router: connectRouter(history) })
export default createRootReducer
- src/store/index.js```javascriptimport { applyMiddleware , createStore } from "redux";import { createBrowserHistory } from 'history'import { routerMiddleware } from 'connected-react-router'import createRootReducer from "./reducers";export const history = createBrowserHistory()const store = createStore(createRootReducer(history),applyMiddleware(routerMiddleware(history)))export default store
- src/index.js ```jsx import React from ‘react’; import ReactDOM from ‘react-dom’; import Routes from ‘./components/Routes’; import { Provider } from ‘react-redux’ import store, {history} from ‘./store’; import { ConnectedRouter } from ‘connected-react-router’
ReactDOM.render(
- src/components/Routers.js```jsxfunction Routes() {return (<Switch><Route path='/' component={Home} exact/><Route path='/shop' component={Shop} /></Switch>)}export default Routes
创建菜单栏
- src/components/core/Navigation.js ```jsx import { Menu } from “antd” import { useSelector } from “react-redux” import { Link } from ‘react-router-dom’ function Navigation() { const router = useSelector(state => state.router) return ( ) }
export default Navigation
- src/components/core/Layout.js```jsximport Navigation from "./Navigation";function Layout({children}) {return (<div><Navigation />{children}</div>)}export default Layout
创建页头
- src/components/core/Layout.js ```javascript import { PageHeader } from “antd”; import Navigation from “./Navigation”;
function Layout({children, title, subTitle}) { return (
export default Layout
- src/components/core/Home.js```jsximport Layout from "./Layout"function Shop() {return <Layout title="拉勾严选商城列表" subTitle="挑选你喜欢的商品吧">Shop</Layout>}export default Shop
构建注册和登陆表单
- src/components/core/Signup.js
```jsx
import {Button, Form, Input }from ‘antd’
import Layout from ‘./Layout’
function Signup() {
return (
) }<Form><Form.Item name='name' label='昵称'><Input /></Form.Item><Form.Item name='password' label='密码'><Input.Password /></Form.Item><Form.Item name='email' label='邮箱'><Input /></Form.Item><Form.Item><Button type='primary' htmlType='submit'>注册</Button></Form.Item></Form>
export default Signup
- src/components/core/Signim.js```jsximport { Button, Form, Input } from 'antd'import Layout from './Layout'function Signin() {return (<Layout title='登陆' subTitle=''><Form><Form.Item name='email' label='邮箱'><Input /></Form.Item><Form.Item name='password' label='密码'><Input.Password /></Form.Item><Form.Item><Button type='primary' htmlType='submit'>登陆</Button></Form.Item></Form></Layout>)}export default Signin
- src/components/core/Layout.js ```jsx import { PageHeader } from “antd”; import Navigation from “./Navigation”;
function Layout({children, title, subTitle}) { return (
export default Layout
- src/components/Routes.js```jsximport { Switch, Route} from 'react-router-dom'import Home from './core/Home'import Shop from './core/Shop'import Signin from './core/Signin'import Signup from './core/Signup'function Routes() {return (<Switch><Route path='/' component={Home} exact/><Route path='/shop' component={Shop} /><Route path='/signin' component={Signin} /><Route path='/signup' component={Signup} /></Switch>)}export default Routes
- src/components/core/Navigation.js ```jsx import { Menu } from “antd” import { useSelector } from “react-redux” import { Link } from ‘react-router-dom’ function Navigation() { const router = useSelector(state => state.router) return ( ) }
export default Navigation
<a name="Fpled"></a>## 实现注册 Redux工作流<a name="HdMA2"></a>### 创建 Action和 Reducer- src/store/actions/signup.js```javascriptimport { createAction } from 'redux-actions'// 发起注册请求export const signup = createAction('signup')// 注册成功export const signup_success = createAction('signup_success')// 注册失败export const signup_fail = createAction('signup_fail')// 重置注册状态export const signup_reset = createAction('signup_reset')
- src/store/reducers/signup.js ```javascript import { handleActions } from “redux-actions” import { signup, signup_fail, signup_reset, signup_success } from “../actions/signup”
const initialState = { loading: false, loaded: false, success: false, message: “” }
const signupReducer = handleActions( { [signup]: () => ({ loading: true, loaded: false, success: false, message: “” }), [signup_success]: () => ({ loading: false, loaded: true, success: true, message: “” }), [signup_fail]: (state, action) => ({ loading: false, loaded: true, success: false, message: action.payload.message }), [signup_reset]: () => ({ loading: false, loaded: false, success: false, message: “” }) }, initialState )
export default signupReducer
- src/store/reducers/index.js```javascriptimport signupReducer from "./signup";const createRootReducer = (history) =>combineReducers({signup: signupReducer})export default createRootReducer
配置 sagaMiddleware
启动本地服务器
# 进入本地服务器更目录cd server# 安装依赖npm install# 启动数据库npm run serve# 启动服务端引用程序npm start
- src/store/sagas/signup.js ```javascript import axios from ‘axios’ import { takeEvery, put} from ‘redux-saga/effects’ import {signup, signup_fail, signup_success} from ‘../actions/signup’ import { API } from ‘../../config’
function * handleSignup(action) {
try {
yield axios.post(${API}/signup, action.payload)
yield put(signup_success())
} catch(ex) {
yield put(signup_fail({message: ex.response.data.error}))
}
}
export default function * signupSaga() { yield takeEvery(signup, handleSignup) }
- src/store/sagas/rootSaga.js```javascriptimport { all } from 'redux-saga/effects'import signupSaga from './signup'export default function * rootSaga() {yield all([signupSaga()])}
- src/store/index.js ```javascript import { applyMiddleware, createStore } from “redux” import { createBrowserHistory } from “history” import createRootReducer from “./reducers” import { routerMiddleware } from “connected-react-router” import createSagaMiddleware from “redux-saga” import rootSaga from “./sagas/rootSaga”
export const history = createBrowserHistory()
const sagaMiddleware = createSagaMiddleware()
const store = createStore( createRootReducer(history), applyMiddleware(routerMiddleware(history), sagaMiddleware)
)
sagaMiddleware.run(rootSaga)
export default store
<a name="vsnBR"></a>## 验证注册 Redux工作流是否可以跑通- src/components/core/signup.js```jsximport {Button, Form, Input }from 'antd'import Layout from './Layout'import { useDispatch } from 'react-redux'import {signup} from '../../store/actions/signup'function Signup() {const dispatch = useDispatch()// 获取状态// const {loading, loaded, sunccess, message} = useSelector(state => state.Signup)// 表单提交const handleOnFinish = value => {dispatch(signup(value))}return (<Layout title='注册' subTitle=""><Form onFinish={handleOnFinish}></Form></Layout>)}export default Signup
- 添加 redux 调试工具所需代码 src/store/index.js ```javascript
import { composeWithDevTools } from ‘redux-devtools-extension’
const store = createStore(
composeWithDevTools( applyMiddleware(routerMiddleware(history), sagaMiddleware) ) )
<a name="LOBT7"></a>##<a name="w2s9O"></a>## 将状态与注册组件进行结合- src/components/core/Signup.js```jsximport {Button, Form, Input, Result, Spin }from 'antd'import Layout from './Layout'import { useDispatch, useSelector } from 'react-redux'import {signup, signup_reset} from '../../store/actions/signup'import { useEffect } from 'react'import { Link } from 'react-router-dom'function Signup() {const dispatch = useDispatch()// 获取状态const {loading, loaded, success, message} = useSelector(state => state.signup)// 表单提交const handleOnFinish = value => {dispatch(signup(value))}// 获取表单实例对象const [form] = Form.useForm()// 正在发送注册请求,显示loadingconst showLoading = () => {if (loading) return <Spin />}//注册成功 显示提示信息useEffect(() => {if (loaded && success) form.resetFields()}, [loaded, success])// 注册成功 显示成功的提示信息const showSuccess = () => {if(loaded && success) {return (<Resultstatus='success'title="注册成功"subTitle=""extra={[<Button type='primary'><Link to='/singin'>登陆</Link></Button>]}/>)}}// 注册失败 显示失败提示信息const showError = () => {if(loaded && !success) {return <Result status='warning' title="注册失败" subTitle={message} />}}// 离开页面 重置注册useEffect(() => {return () => {dispatch(signup_reset())}}, [])// 注册表单const signupForm = () => (<Form form={form} onFinish={handleOnFinish}><Form.Item name='name' label='昵称'><Input /></Form.Item><Form.Item name='password' label='密码'><Input.Password /></Form.Item><Form.Item name='email' label='邮箱'><Input /></Form.Item><Form.Item><Button type='primary' htmlType='submit'>注册</Button></Form.Item></Form>)return (<Layout title='注册' subTitle="">{showLoading()}{showSuccess()}{showError()}{signupForm()}</Layout>)}export default Signup
实现用户登陆功能
- src/components/core/Signin.js ```jsx import { Button, Form, Input } from “antd”; import Layout from “./Layout”; import axios from “axios”; import { API } from “../../config”; import { useHistory } from “react-router-dom”;
function Signin() {
const history = useHistory()
// 用户登陆
const handleOnFinish = async (value) => {
// 发送登陆请求
let response = await axios.post(${API}/signin, value);
// 将服务器端返回的信息存储到本地
localStorage.setItem(“jwt”, JSON.stringify(response.data));
// 根据用户角色返回跳转地址
const url =
response.data.user.role === 1 ? “/admin/dashboard” : “/user/dashboard”;
// 跳转页面
history.push(url);
};
return (
export default Signin;
<a name="N8CN4"></a>## 根据用户登陆状态显示菜单栏选项- src/helpers/auth.js```javascriptexport function isAuth() {const jwt = localStorage.getItem('jwt')if(jwt) return JSON.parse(jwt)return false}
src/components/core/Navigation.js ```jsx import { Menu } from “antd” import { useSelector } from “react-redux” import { Link } from ‘react-router-dom’ import { isAuth } from ‘../../helpers/auth’ function Navigation() { const router = useSelector(state => state.router) const auth = isAuth() const showAuth = () => ( <>
<Menu.Item key='/signin'><Link to='/signin'>登陆</Link></Menu.Item><Menu.Item key='/signup'><Link to='/signup'>注册</Link></Menu.Item>
</> )
const showDashboard = () => { const url = auth.user.role === 1 ? ‘/admin/dashboard’ : ‘/user/dashboard’ return (
<Menu.Item key={url}><Link to={url}>Dashboard</Link></Menu.Item>
) }
return (
) }
export default Navigation
<a name="w0aYG"></a>## 创建受保护的 Dashboad组件- src/components/admin/PrivateUser.js```jsximport { Redirect, Route} from 'react-router'import { isAuth } from '../../helpers/auth'function PrivateUser({component: Component, ...rest}) {return (<Route{...rest}render={props => {console.log(props)if (isAuth()) return <Component {...props} />return <Redirect to='/signin' />}}></Route>)}export default PrivateUser
- src/components/admin/UserDashboard.js ```jsx import Layout from ‘../core/Layout’
function UserDashboard() {
return
export default UserDashboard
- src/components/admin/PrivateAdmin.js```jsximport { Redirect, Route } from "react-router"import { isAuth } from "../../helpers/auth"function PrivateAdmin({ component: Component, ...rest }) {return (<Route{...rest}render={props => {const auth = isAuth()if (auth && auth.user.role === 1) return <Component {...props} />return <Redirect to="/signin" />}}></Route>)}export default PrivateAdmin
- src/components/admin/AdminDashboard.js ```jsx import Layout from ‘../core/Layout’
function AdminDashboard() {
return
export default AdminDashboard
- src/components/Routes.js```jsximport { Switch, Route} from 'react-router-dom'import AdminDashboard from './admin/AdminDashboard'import PrivateAdmin from './admin/PrivateAdmin'import PrivateUser from './admin/PrivateUser'import UserDashboard from './admin/UserDashboard'import Home from './core/Home'import Shop from './core/Shop'import Signin from './core/Signin'import Signup from './core/Signup'function Routes() {return (<Switch><Route path='/' component={Home} exact/><Route path='/shop' component={Shop} /><Route path='/signin' component={Signin} /><Route path='/signup' component={Signup} /><PrivateUser path='/user/dashboard' component={UserDashboard} /><PrivateAdmin path='/admin/dashboard' component={AdminDashboard} /></Switch>)}export default Routes
管理员 Dashboard 组件添加链接和管理员信息
src/components/admin/AdminDashboard.js ```jsx import { Col, Descriptions, Menu, Row, Typography } from ‘antd’ import { Link } from ‘react-router-dom’ import Layout from ‘../core/Layout’ import { OrderedListOutlined, ShoppingCartOutlined, UserOutlined } from ‘@ant-design/icons’ import { isAuth } from ‘../../helpers/auth’ const { Title } = Typography function AdminDashboard() {
const {user: {name, email}} = isAuth() const adminLinks = () => ( <>
<Title level={5}>管理员链接</Title><Menu><Menu.Item><ShoppingCartOutlined /><Link to='/create/category'>添加分类</Link></Menu.Item><Menu.Item><UserOutlined /><Link to='/create/product'>添加商品</Link></Menu.Item><Menu.Item><OrderedListOutlined /><Link to='/admin/order'>订单列表</Link></Menu.Item></Menu>
</> )
const adminInfo = () => (
<Descriptions.Item label='昵称'>{name}</Descriptions.Item><Descriptions.Item label='邮箱'>{email}</Descriptions.Item><Descriptions.Item label="角色">管理员</Descriptions.Item>
)
return
<Col span='4'>{adminLinks()}</Col><Col span='20'>{adminInfo()}</Col>
}
export default AdminDashboard
<a name="dbIO0"></a>## 创建添加分类组件- src/components/admin/AddCategory.js```jsximport {Button, Form, Input} from 'antd'import Layout from '../core/Layout'function AddCategory() {// 表单提交const handleOnFinish = value => {console.log(value)}return (<Layout title="添加分类"><Form onFinish={handleOnFinish}><Form.Item name="name" label="分类名称"><Input /></Form.Item><Form.Item><Button type="primary" htmlType="submit">添加分类</Button></Form.Item></Form></Layout>)}export default AddCategory
- src/components/Routes.js ```javascript
import AddCategory from ‘./admin/AddCategory’
function Routes() {
return (
<PrivateAdmin path='/create/category' component={AddCategory} /></Switch>
) }
export default Routes
<a name="dusfP"></a>## 实现添加分类功能- src/components/admin/AddCategory.js```jsximport {Button, Form, Input, message} from 'antd'import Layout from '../core/Layout'import axios from 'axios'import {API} from '../../config'import { isAuth } from '../../helpers/auth'import { Link } from 'react-router-dom'function AddCategory() {const {user:{_id}, token} = isAuth()// 表单提交const handleOnFinish = value => {if (value.name) {axios.post(`${API}/category/create/${_id}`,value, {headers: {Authorization: `Bearer ${token}`}}).then(response => {message.success(`${response.data.name} 添加成功`)})}}return (<Layout title="添加分类"><Form onFinish={handleOnFinish}><Form.Item name="name" label="分类名称"><Input /></Form.Item><Form.Item><Button type="primary" htmlType="submit">添加分类</Button></Form.Item></Form><Button><Link to="/admin/dashboard">返回 Dashboard</Link></Button></Layout>)}export default AddCategory
创建添加商品组件
- src/components/admin/AddProduct.js ```jsx import { UploadOutlined } from ‘@ant-design/icons’ import {Button, Form, Input, Select, Upload} from ‘antd’ import Layout from ‘../core/Layout’
function AddProduct() {
return (
export default AddProduct
- src/components/Router.js```javascriptimport AddProduct from './admin/AddProduct'function Routes() {return (<Switch><PrivateAdmin path='/create/product' component={AddProduct} /></Switch>)}export default Routes
获取分类列表
- src/components/admin/AddProduct.js ```jsx import { UploadOutlined } from ‘@ant-design/icons’ import {Button, Form, Input, Select, Upload} from ‘antd’ import Layout from ‘../core/Layout’
import { useEffect, useState} from ‘react’ import axios from ‘axios’ import {API} from ‘../../config’
function AddProduct() {
const [categories, setCategories ] = useState([])
useEffect(() => {
async function getCategories() {
let response = await axios.get(${API}/categories)
setCategories(response.data)
}
getCategories()
}, [])
return (
export default Search
- src/components/core/Home.js```jsximport { useSelector } from "react-redux"import Layout from "./Layout"import Search from './Search'function Home() {const state = useSelector(state => state)return (<Layout title="拉勾严选首页" subTitle="尽情享受吧"><Search /></Layout>)}export default Home
构建搜索结果布局
- src/components/core/ProductItem.js ```jsx import { Button, Card, Col, Row, Typography} from ‘antd’ import {Link} from ‘react-router-dom’
const {Title, Paragraph } = Typography
function ProductItem() {
return (
export default ProductItem
- src/components/core/Search.js```jsximport { Button, Col, Divider, Form, Input, Row, Select } from 'antd'import ProductItem from './ProductItem'function Search() {return (<><Form layout="inline" initialValues={{category: "All"}}></Form><Divider /><Row gutter={[16, 16]}><Col span="6"><ProductItem /></Col></Row></>)}export default Search
- src/components/core/Home.js ```jsx import Layout from “./Layout” import Search from ‘./Search’ import { Col, Row, Typography } from ‘antd’ import ProductItem from ‘./ProductItem’
const {Title} = Typography
function Home() {
return (
export default Home
<a name="kYxtA"></a>## 完成首页获取商品列表的 redux 流程- src/store/actions/products.js```javascriptimport { createAction } from 'redux-actions'export const get_products = createAction('get_products')export const get_products_success = createAction("get_products_success")
- src/store/reducers/products.js ```javascript import { handleActions } from ‘redux-actions’ import { get_products_success } from ‘../actions/products’
const initialState = { sold: [], createdAt: [] } const productsReducer = handleActions( { [get_products_success]: (state, action) => ({ …state,
[action.payload.sortBy]: action.payload.products})
}, initialState )
export default productsReducer
- src/store/reducers/index.js```javascriptimport productsReducer from "./products";const createRootReducer = (history) =>combineReducers({products: productsReducer})export default createRootReducer
- src/store/sagas/products.js ```javascript import axios from ‘axios’ import { takeEvery, put} from ‘redux-saga/effects’ import {API} from ‘../../config’ import { get_products, get_products_success } from ‘../actions/products’
function * handleGetProducts(action) {
let response = yield axios.get(${API}/products, {params: action.payload})
yield put( get_products_success({ sortBy: action.payload.sortBy, products: response.data }) ) }
export default function * productsSaga() { yield takeEvery(get_products, handleGetProducts) }
- src/store/sagas/rootsaga.js```javascriptimport { all } from 'redux-saga/effects'import productsSaga from './products'import signupSaga from './signup'export default function * rootSaga() {yield all([signupSaga(),productsSaga()])}
商品列表数据展示
- src/components/core/Home.js ```jsx
import Layout from “./Layout” import Search from ‘./Search’ import { Col, Row, Typography } from ‘antd’ import ProductItem from ‘./ProductItem’
import { useDispatch, useSelector} from ‘react-redux’ import { useEffect } from ‘react’ import { get_products } from “../../store/actions/products”
const {Title} = Typography
function Home() { const dispatch = useDispatch()
const {sold, createdAt} = useSelector(state => state.products)
useEffect(() => { dispatch(get_products({sortBy: ‘sold’, limit: 4, order: ‘desc’})) dispatch(get_products({sortBy: ‘createdAt’, limit: 4, order: ‘desc’})) }, [])
return (
export default Home
- src/components/core/ProductItem.js```jsximport { Button, Card, Col, Row, Typography, Image} from 'antd'import {Link} from 'react-router-dom'import {API} from '../../config'import dateformat from 'dateformat'const {Title, Paragraph } = Typographyfunction ProductItem({product}) {const { name, description, price, sold, category, createdAt, _id } = productreturn (<Cardcover={<Imagealt="example"src={`${API}/product/photo/${_id}`}/>}actions={[<Button type="link"><Link to="">查看详情</Link></Button>,<Button type="link">加入购物车</Button>]}><Title level={5}>{name}</Title><Paragraph ellipsis={{rows: 2}}>{description}</Paragraph><Row style={{fontSize: 12}}><Col span="12">价格:{price}</Col><Col span="12">销量:{sold}</Col></Row><Row><Col span="12">所属分类:{category.name}</Col><Col span="12">上架时间:{dateformat(createdAt, "yyyy-mm-dd")}</Col></Row></Card>)}export default ProductItem
将获取商品分类列表抽离为公共逻辑
- src/helpers/getCategories.js ```javascript import { useState, useEffect} from ‘react’ import axios from ‘axios’ import {API} from ‘../config’
export default function useGetCategories() { const [categories, setCategories] = useState([])
useEffect(() => {
async function getCategories() {
let response = await axios.get(${API}/categories)
setCategories(response.data)
}
getCategories()
}, [])
return categories }
- src/components/admin/AddProduct.js```jsximport useGetCategories from '../../helpers/getCategories'function AddProduct() {const [file, setFile] = useState([])const categories = useGetCategories()const history = useHistory()const props = {beforeUpload (file) {setFile(file)return false}}const {user, token} = isAuth()const [form] = Form.useForm()const onFinish = value => {}return ()}export default AddProduct
- src/components/core/Search.js ```jsx import { Button, Col, Divider, Form, Input, Row, Select } from ‘antd’ import useGetCategories from “../../helpers/getCategories” import ProductItem from ‘./ProductItem’
function Search() {
const categories = useGetCategories()
return ( <>
export default Search
<a name="pCNMT"></a>## 实现搜索功能- src/store/actions/search.js```javascriptimport { createAction } from 'redux-actions'export const search_products = createAction("search_products")export const search_products_success = createAction("search_products_success")
- src/store/reducers/search.js ```javascript import { handleActions } from ‘redux-actions’ import { search_products_success } from ‘../actions/search’
const initialState = { results: [] }
const searchReducer = handleActions( { [search_products_success]: (state, action) => ({ results: action.payload.results }) }, initialState )
export default searchReducer
- src/store/reducers/index.js```javascriptimport searchReducer from "./search";const createRootReducer = (history) =>combineReducers({search: searchReducer})export default createRootReducer
- src/store/sagas/search.js ```javascript import {takeEvery, put} from ‘redux-saga/effects’ import axios from ‘axios’ import {API} from ‘../../config’ import { search_products, search_products_success } from ‘../actions/search’
function * handleSearchProducts(action) {
const response = yield axios.get(${API}/products/search, {params: action.payload})
yield put(search_products_success({results: response.data})) }
export default function * searchSaga() { yield takeEvery(search_products, handleSearchProducts) }
- src/store/sagas/rootsaga.js```javascriptimport { all } from 'redux-saga/effects'import productsSaga from './products'import searchSaga from './search'import signupSaga from './signup'export default function * rootSaga() {yield all([signupSaga(),productsSaga(),searchSaga()])}
- src/components/core/Search.js ```jsx import { Button, Col, Divider, Form, Input, Row, Select } from ‘antd’ import useGetCategories from “../../helpers/getCategories” import ProductItem from ‘./ProductItem’
import {useDispatch, useSelector} from ‘react-redux’ import { search_products } from ‘../../store/actions/search’
function Search() {
const dispatch = useDispatch() const { results } = useSelector(state => state.search) const categories = useGetCategories() const [form] = Form.useForm() const onFinish = value => { dispatch(search_products(value)) form.resetFields() }
return ( <>
export default Search
<a name="J3Hzw"></a>## 构建商城页面布局- src/components/core/Shop.js```jsximport Layout from "./Layout"import { Col, Row, Space } from 'antd'import FilterByCategory from "./FilterByCategory"import FilterByPrice from "./FilterByPrice"function Shop() {return (<Layout title="拉勾严选商城列表" subTitle="挑选你喜欢的商品吧"><Row><Col span="4"><Space size="middle" direction="vertical"><FilterByCategory /><FilterByPrice /></Space></Col><Col span="20">right</Col></Row></Layout>)}export default Shop
- src/components/core/FilterByCategory.js ```jsx
import {Checkbox, Typography, List} from ‘antd’ import useGetCategories from ‘../../helpers/getCategories’
const { Title } = Typography
function FilterByCategory() {
const categories = useGetCategories()
return (
<>
(
export default FilterByCategory
- src/components/core/FirlterByPrice.js```jsximport {Radio, Typography, List} from 'antd'const { Title } = Typographyfunction FilterByPrice() {const prices = [{name: '0-20', id:1}, {name: '20-40', id:2}]return (<><Title level={5}>按照价格筛选</Title><Radio.Group><ListdataSource={prices}renderItem={item => (<List.Item><Radio value={item.id}>{item.name}</Radio></List.Item>)}/></Radio.Group></>)}export default FilterByPrice
收集用户选择的筛选条件
- src/components/core/Shop.js ```jsx import Layout from “./Layout” import { Col, Row, Space } from ‘antd’ import FilterByCategory from “./FilterByCategory” import FilterByPrice from “./FilterByPrice” import { useEffect, useState } from “react”
function Shop() {
const [filters, setFilters] = useState({category: [], price: []})
useEffect(() => {
console.log(filters)
}, [filters])
return (
export default Shop
- src/components/core/FilterByCategory.js```jsximport {Checkbox, Typography, List} from 'antd'import useGetCategories from '../../helpers/getCategories'const { Title } = Typographyfunction FilterByCategory({ handleFilters }) {const categories = useGetCategories()const onChange = checkedValue => handleFilters(checkedValue)return (<><Title level={5}>按照分类筛选</Title><Checkbox.Group onChange={onChange}><ListdataSource={categories}renderItem={item => (<List.Item><Checkbox value={item._id}>{item.name}</Checkbox></List.Item>)}/></Checkbox.Group></>)}export default FilterByCategory
- src/components/core/FirlterByPrice.js ```jsx import {Radio, Typography, List} from ‘antd’ import prices from ‘../../helpers/price’
const { Title } = Typography
function FilterByPrice({handleFilters}) {
const onChange = event => handleFilters(event.target.value)
return (
<>
(
export default FilterByPrice
<a name="zfy5V"></a>## 展示筛选结果- src/store/actions/filter.js```javascriptimport { createAction } from 'redux-actions'export const filter_products = createAction("filter_products")export const filter_products_success = createAction("filter_products_success")
- src/store/reducers/filter.js ```javascript import { handleActions } from ‘redux-actions’ import { filter_products_success } from ‘../actions/filter’
const initialState = { size: 0, data: [] }
const filterReducer = handleActions( { [filter_products_success]: (state, action) => ({ size: action.payload.size, data: action.payload.skip === 0 ? action.payload.data : […state.data, …action.payload.data] }) }, initialState )
export default filterReducer
- src/store/reducers/index.js```javascriptimport filterReducer from "./filter";const createRootReducer = (history) =>combineReducers({filter: filterReducer})export default createRootReducer
- src/store/sagas/filter.js ```javascript import axios from ‘axios’ import {takeEvery, put} from ‘redux-saga/effects’ import {API} from ‘../../config’ import { filter_products, filter_products_success } from ‘../actions/filter’
function * handleFilterProducts(action) {
const response = yield axios.post(${API}/products/filter, action.payload)
yield put(filter_products_success({skip: action.payload.skip, …response.data})) }
export default function * filterSaga() { yield takeEvery(filter_products, handleFilterProducts) }
- src/store/sagas/rootsaga.js```javascriptimport { all } from 'redux-saga/effects'import filterSaga from './filter'import productsSaga from './products'import searchSaga from './search'import signupSaga from './signup'export default function * rootSaga() {yield all([signupSaga(),productsSaga(),searchSaga(),filterSaga()])}
- src/components/core/Shop.js ```jsx import Layout from “./Layout” import { Button, Col, Empty, Row, Space } from ‘antd’ import FilterByCategory from “./FilterByCategory” import FilterByPrice from “./FilterByPrice” import ProductItem from ‘./ProductItem’ import { useEffect, useState } from “react” import { useDispatch, useSelector} from ‘react-redux’ import { filter_products } from “../../store/actions/filter”
function Shop() { const [filters, setFilters] = useState({category: [], price: []}) const [skip, setSkip] = useState(0) const dispatch = useDispatch() const { data, size } = useSelector(state => state.filter)
useEffect(() => { setSkip(0) }, [filters])
useEffect(() => { dispatch(filter_products({filters, skip})) }, [filters, skip])
const loadMore = () => {
setSkip(skip + 4)
}
return (
export default Shop
<a name="WBHpt"></a>## 构建商品详情组件布局- src/components/core/Product.js```jsximport Layout from './Layout'import { Col, Row } from 'antd'import { useParams } from 'react-router-dom'function Product() {const { productId } = useParams()return (<Layout title="商品名称" subTitle="商品描述"><Row><Col span="18">{productId}</Col><Col span="6">right</Col></Row></Layout>)}export default Product
- src/components/Router.js ```jsx
import Product from ‘./core/Product’
function Routes() {
return (
<PrivateAdmin path='/product/:productId' component={Product} /></Switch>
) }
export default Routes
- src/components/core/ProductItem.js```jsx<Button type="link"><Link to={`product/${_id}`}>查看详情</Link></Button>
根据产品 ID 获取产品详情 redux 流程
- src/store/actions/product.js ```javascript import { createAction } from ‘redux-actions’
export const get_product_by_id = createAction(“get_product_by_id”) export const get_product_by_id_success = createAction(“get_product_by_id_success”)
- src/store/reducers/product.js```javascriptimport { handleActions } from 'redux-actions'import { get_product_by_id_success } from '../actions/product'const initialState = {}const productReducer = handleActions({[get_product_by_id_success]: (state, action) => action.payload},initialState)export default productReducer
- src/store/reducers/index.js ```javascript
import productReducer from “./product”;
const createRootReducer = (history) => combineReducers({
product: productReducer
})
export default createRootReducer
- src/store/sagas/product.js```javascriptimport axios from 'axios'import { takeEvery, put } from 'redux-saga/effects'import { API } from '../../config'import { get_product_by_id, get_product_by_id_success } from '../actions/product'function * handleProductById(action) {const response = yield axios.get(`${API}/product/${action.payload.productId}`)yield put(get_product_by_id_success(response.data))}export default function * productSaga() {yield takeEvery(get_product_by_id, handleProductById)}
- src/store/sagas/rootsaga.js ```javascript import { all } from ‘redux-saga/effects’ import filterSaga from ‘./filter’ import productSaga from ‘./product’ import productsSaga from ‘./products’ import searchSaga from ‘./search’ import signupSaga from ‘./signup’
export default function * rootSaga() { yield all([ signupSaga(), productsSaga(), searchSaga(), filterSaga(), productSaga() ]) }
<a name="SkLKw"></a>## 展示商品详情- src/components/core/Product.js```jsximport Layout from './Layout'import { Col, Row } from 'antd'import { useParams } from 'react-router-dom'import { useDispatch, useSelector } from 'react-redux'import { useEffect } from 'react'import { get_product_by_id } from '../../store/actions/product'import ProductItem from './ProductItem'function Product() {const { productId } = useParams()const dispatch = useDispatch()const product = useSelector(state => state.product)useEffect(() => {dispatch(get_product_by_id({ productId }))}, [])return (<Layout title="商品名称" subTitle="商品描述"><Row><Col span="18">{Object.keys(product).length > 0 && (<ProductItem product={product}imgStyle={{width: '50%', margin: "0 auto"}}showView={false}/>)}</Col><Col span="6">right</Col></Row></Layout>)}export default Product
- src/components/core/ProductItem.js ```jsx
function ProductItem({product, imgStyle, showView=true, showChart=true}) { const { name, description, price, sold, category, createdAt, _id } = product
const showButtons = () => { const button = [] if(showView) { button.push() } if(showChart) { button.push() }
return button
}
return (
</Card>
) }
export default ProductItem
<a name="sPyZS"></a>## 将商品添加到购物车中- src/helpers/cart.js```javascriptexport function addItem(item, next) {let cart = []if(typeof window !== "undefined") {if (localStorage.getItem("cart")) {cart = JSON.parse(localStorage.getItem('cart'))}cart.push({...item, count: 1})cart = Array.from(new Set(cart.map(product => product._id))).map(id => cart.find(product => product._id === id))localStorage.setItem('cart', JSON.stringify(cart))next && next()}}
- src/components/core/Productitem.js ```jsx
import { addItem } from ‘../../helpers/cart’ import { useDispatch } from ‘react-redux’ import { push } from ‘connected-react-router’
const {Title, Paragraph } = Typography
function ProductItem({product, imgStyle, showView=true, showChart=true}) { const { name, description, price, sold, category, createdAt, _id } = product
const dispatch = useDispatch() const addToCart = () => { addItem(product, () => dispatch(push(‘/cart’))) }
const showButtons = () => { const button = [] if(showView) { button.push() } if(showChart) { button.push() }
return button
} return (
) }
export default ProductItem
<a name="Mc57k"></a>## 展示本地购物车中商品- src/helpers/cart.js```javascriptexport function getCart() {if (typeof window !== "undefined") {if (localStorage.getItem("cart")) {return JSON.parse(localStorage.getItem("cart"))}}return []}
- src/components/core/Cart.js ```jsx import { useEffect, useState } from ‘react’ import { getCart } from ‘../../helpers/cart’ import Layout from ‘./Layout’ import { Col, Row } from ‘antd’ import CartItem from ‘./CartItem’
function Cart() { const [cart, setCart] = useState([])
useEffect(() => { setCart(getCart()) }, [])
const cartTable = () => (
{cart.map(product => (| 商品封面 | 商品名称 | 商品价格 | 商品分类 | 商品数量 | 操作 |
|---|
export default Cart
- src/components/Routers.js```jsximport Cart from './core/Cart'function Routes() {return (<Switch><Route path='/cart' component={Cart}></Route></Switch>)}export default Routes
- src/components/core/CartItem.js ```jsx import { Button, Image, Input } from ‘antd’ import {API} from ‘../../config’
function CartItem({product, setCart}) {
return (
{product.name}
{product.price}
{product.category.name}
)
}
export default CartItem
<a name="rLG2w"></a>## 更改购物车中商品的数量- src/helpers/cart.js```javascriptexport function updateItem(productId, count) {let cart = []if (typeof window !== "undefined") {if (localStorage.getItem("cart")) {cart = JSON.parse(localStorage.getItem("cart"))const target = cart.find(product => product._id === productId)target.count = countlocalStorage.setItem("cart", JSON.stringify(cart))}}return cart}
- src/components/core/CartItem.js ```jsx import { Button, Image, Input } from ‘antd’ import { useState } from ‘react’ import {API} from ‘../../config’ import { updateItem } from ‘../../helpers/cart’
function CartItem({product, setCart}) { const [count, setCount] = useState(product.count)
const handleChange = event => {
const count = parseInt(event.target.value)
setCount(count)
setCart(updateItem(product._id, count))
}
return (
{product.name}
{product.price}
{product.category.name}
)
}
export default CartItem
<a name="w0i0f"></a>## 删除购物车中的商品- src/helpers/cart.js```javascriptexport function deleteItem(productId) {let cart = []if (typeof window !== "undefined") {if (localStorage.getItem("cart")) {cart = JSON.parse(localStorage.getItem("cart"))const index = cart.findIndex(product => product._id === productId)cart.splice(index, 1)localStorage.setItem("cart", JSON.stringify(cart))}}return cart}
- src/components/core/CartItem.js
<Button danger type="primary" onClick={() => setCart(deleteItem(product._id))}>删除</Button>
构建购物车右侧布局
- src/components/core/Cart.js ```jsx import { useEffect, useState } from ‘react’ import { getCart } from ‘../../helpers/cart’ import Layout from ‘./Layout’ import { Col, Input, Row, Typography, Form} from ‘antd’ import CartItem from ‘./CartItem’
const { Title } = Typography
function Cart() { const [cart, setCart] = useState([]) const [address, setAddress] = useState(“”) const [totalPrice, setTotalPrice] = useState(0)
useEffect(() => { setCart(getCart()) }, [])
useEffect(() => { let totalPrice = cart.reduce((currentValue, nextValue) => { return (currentValue += nextValue.count * nextValue.price) }, 0).toFixed(2) setTotalPrice(totalPrice) }, [cart])
return (
export default Cart
<a name="hV6GZ"></a>## 增加支付按钮或登陆按钮- src/components/core/PayButtom.js```jsximport { Button } from 'antd'import { Link } from 'react-router-dom'import { isAuth } from '../../helpers/auth'function PayButton ({address, cart, totalPrice}) {const showButton = () => {return isAuth() ? (<Button>提交</Button>) : (<Button><Link to='/signin'>登陆</Link></Button>)}return <div>{showButton()}</div>}export default PayButton
- src/components/core/Cart.js
<Layout title='购物车' subTitle="付款吧,我就是你的了"><Row gutter={16}><Col span="18">{cartTable()}</Col><Col span="6"><Form.Item label="收获地址"><Inputvalue={address}onChange={event => setAddress(event.target.value)}placeholder="请输入收货地址"></Input></Form.Item><Title level={5}>总价:{totalPrice}</Title><PayButton address={address} cart={cart} totalPrice={totalPrice} /></Col></Row></Layout>
实现支付创建订单
src/components/core/PayButton.js ```jsx import { Button } from ‘antd’ import axios from ‘axios’ import { Link } from ‘react-router-dom’ import { isAuth } from ‘../../helpers/auth’ import {API} from ‘../../config’ function PayButton ({address, cart, totalPrice}) {
const handlePay = () => { axios.post(
${API}/alipay,{totalAmount: totalPrice,subject: '测试订单标题',body: '测试订单描述',products: cart.map(product => ({product: product._id,count: product.count})),address: address,userId: isAuth().user._id
}).then(response => {
window.location.href = response.data.result
}) }
const showButton = () => { return isAuth() ? () : () } return
{showButton()}}
export default PayButton
<a name="hHt6X"></a>## 获取并展示订单列表数据- src/components/admin/Order.js```jsximport Layout from "../core/Layout"import { useState, useEffect } from "react"import axios from "axios"import { API } from "../../config"import { isAuth } from "../../helpers/auth"import { Divider, Typography, Select } from "antd"import dateformat from "dateformat"const { Title } = Typographyfunction Order() {const [orders, setOrders] = useState([])const { user, token } = isAuth()async function loadOrders() {let { data } = await axios.get(`${API}/order/list/${user._id}`, {headers: {Authorization: `Bearer ${token}`}})setOrders(data)}useEffect(() => {loadOrders()}, [])const getOrderNumber = () => {return orders.length > 0? `您的订单数量是${orders.length}`: "您还没有订单"}const showStatus = status => {switch (status) {case "Unpaid":return "未付款"case "Paid":return "已付款"case "Shipped":return "运输中"case "Completed":return "已完成"case "Cancelled":return "已取消"}}const handleStatus = orderId => status => {axios.put(`${API}/order/status/${user._id}`,{ orderId, status },{headers: {Authorization: `Bearer ${token}`}}).then(() => loadOrders())}return (<Layout title="订单列表" subTitle={getOrderNumber()}>{orders.map(order => (<><Title level={5}>订单ID: {order._id}</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="ant-table-row"><td className="ant-table-cell">{showStatus(order.status)}<SelectdefaultValue={order.status}onChange={handleStatus(order._id)}><Select.Option value="Unpaid">未付款</Select.Option><Select.Option value="Paid">已付款</Select.Option><Select.Option value="Shipped">运输中</Select.Option><Select.Option value="Completed">已完成</Select.Option><Select.Option value="Cancelled">已取消</Select.Option></Select></td><td className="ant-table-cell">{order.trade_no}</td><td className="ant-table-cell">{order.amount}</td><td className="ant-table-cell">{dateformat(order.createdAt, "yyyy-mm-dd")}</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(product => (<tr className="ant-table-row"><td className="ant-table-cell">{product.product.name}</td><td className="ant-table-cell">{product.product.price}</td><td className="ant-table-cell">{product.count}</td><td className="ant-table-cell">{product.product._id}</td></tr>))}</tbody></table><Divider /></>))}<pre>{JSON.stringify(orders, null, 2)}</pre></Layout>)}export default Order
