React项目实战(下)

581 阅读27分钟

笔记来源:拉勾教育 - 大前端就业集训营

文章内容:学习过程中的笔记、感悟、和经验

提示:项目实战类文章资源无法上传,仅供参考

拉勾教育图书电商项目实战 - 下

构建首页搜索框布局

core文件夹新建搜索框组件

使用Form组件,组件内部有三个解构:选择分类、文本框、搜索按钮

设置表单为横向、默认选择分类、去除空隙

最后把搜索组件作为home子组件

构建搜索结果布局

搜索框和搜索结果中间使用中横线组件隔开

搜索结果展现在行和列里面,需要引入布局 - row、col,设置行和列之间间距

每商品为一个组件,创一个新的商品组件,组件使用卡片组件构建

去除卡片多余元素,设置actions属性添加查看详情和加入购物车

添加标题、段落(不可超过两行)

设置其他展示内容(使用行和列组件,左右平分)

添加销量、价格、所属分类、上架时间

构建其他布局

添加最新上架、最受欢迎,和搜索结果类似

// src/Components/core/Home.js  Home组件添加搜索组件和最受欢迎和最新上架两个结构

import { Button, Card, Col, Row, Typography } from "antd";
import React from "react";
import { Link } from "react-router-dom/cjs/react-router-dom.min";
const { Title, Paragraph } = Typography;

function Wares() {
  return (
    <>
      {/* 使用卡片包裹 */}
      <Card
        cover={
          // 图片
          <img
            alt="example"
            src="https://os.alipayobjects.com/rmsportal/QBnOOoLaAfKPirc.png"
          />
        }
        // 底部操作按钮
        actions={[
          <Button type="link">
            <Link>查看详情</Link>
          </Button>,
          <Button type="link">加入购物车</Button>,
        ]}
      >
        {/* 卡片内容 */}
        <Title level={5}>测试标题</Title>
        <Paragraph ellipsis={{ rows: 2 }}>
          我们提供完善的设计原则、最佳实践和设计资源文件
        </Paragraph>
        <Row>
          <Col span={12}>价格:</Col>
          <Col span={12}>销量</Col>
        </Row>
        <Row>
          <Col span={12}>上架时间:</Col>
          <Col span={12}>所属分类:</Col>
        </Row>
      </Card>
    </>
  );
}

export default Wares;
// src/Components/core/Search.js  新建搜索组件

import React from "react";
import { Button, Col, Divider, Form, Input, Row, Select } from "antd";
import Wares from "./Wares";

// 搜索组件
function Search() {
  return (
    <>
      {/* 表单 - layout:横向布局,initialValues:默认值 */}
      <Form layout="inline" initialValues={{ category: "all" }}>
        {/* 使用Input.Group包裹,使用compact消除间距 */}
        <Input.Group compact>
          {/* 下拉菜单 */}
          <Form.Item name="category">
            <Select>
              <Select.Option value="all">请选择分类</Select.Option>
            </Select>
          </Form.Item>
          {/* 搜索输入框 */}
          <Form.Item name="search">
            <Input />
          </Form.Item>
          {/* 按钮 */}
          <Form.Item>
            <Button type="primary" htmlType="submit">
              搜索
            </Button>
          </Form.Item>
        </Input.Group>
      </Form>
      {/* 间隔线 */}
      <Divider />
      {/* 搜索展示区 */}
      <Row>
        <Col style={{ margin: "16px 16px" }} span={6}>
          <Wares />
        </Col>
      </Row>
    </>
  );
}

export default Search;
// src/Components/core/Wares.js  新建单个商品组件

import { Button, Card, Col, Row, Typography } from "antd";
import React from "react";
import { Link } from "react-router-dom/cjs/react-router-dom.min";
const { Title, Paragraph } = Typography;

// 单个商品组件
function Wares() {
  return (
    <>
      {/* 使用卡片包裹 */}
      <Card
        cover={
          // 图片
          <img
            alt="example"
            src="https://os.alipayobjects.com/rmsportal/QBnOOoLaAfKPirc.png"
          />
        }
        // 底部操作按钮
        actions={[
          <Button type="link">
            <Link>查看详情</Link>
          </Button>,
          <Button type="link">加入购物车</Button>,
        ]}
      >
        {/* 卡片内容 */}
        <Title level={5}>测试标题</Title>
        <Paragraph ellipsis={{ rows: 2 }}>
          我们提供完善的设计原则、最佳实践和设计资源文件
        </Paragraph>
        <Row>
          <Col span={12}>价格:</Col>
          <Col span={12}>销量</Col>
        </Row>
        <Row>
          <Col span={12}>上架时间:</Col>
          <Col span={12}>所属分类:</Col>
        </Row>
      </Card>
    </>
  );
}

export default Wares;

完成首页获取商品列表的redux流程

这里指获取最受欢迎和最新上架两个列表

在数据库可视化软件中导入事先准备好的商品数据

书写指令 - 书写reducer - 合并reducer - 书写saga - 合并saga - 调用指令

reducer需要添加最新商品、虽受欢迎两个数据

saga拦截指令进行异步请求然后触发新指令

Home组件获取指令和状态

在生命周期中触发指令分别获取最受欢迎和最新上架

// src/store/actions/products.action.js  新建指令文件

import { createAction} from 'redux-actions'

// 新建指令
export const productsList = createAction('productsList')
export const productsList_success = createAction('productsList_success')
// src/store/reducers/products.reducer.js  新建reducer文件

import { handleActions } from "redux-actions";
import {productsList_success} from '../actions/products.action'

// 原始状态
const productsListData = {
  // 最新和最受欢迎数据
  sold: [],
  createdAt: []
}

// 创建 reducer
const productsListReducer = handleActions({
  [productsList_success]: (state, action) => ({
    ...state,
    [action.payload.sortBy]: action.payload.list
  })
}, productsListData)

export default productsListReducer
/// src/store/reducers/index.js  合并reducer

// 引入模块方法
import { connectRouter } from "connected-react-router"
// 导入 reducer 合并方法
import { combineReducers } from "redux"
// 引入要合并的 reducer
import testReducer from "./text"
import logupReducer from './logup.reducer'
import productsListReducer from "./products.reducer"

// 合并全部 reducer
// createRootReducer是一个函数,不要更改名字
const createRootReducer = history => combineReducers({
  test: testReducer,
  // 路由信息
  router: connectRouter(history),
  logup: logupReducer,
  productsList: productsListReducer
})

// 导出
export default createRootReducer
// src/store/sagas/products.saga.js  新建saga处理文件

import axios from "axios";
import { takeEvery, put } from "redux-saga/effects";
import { productsList, productsList_success } from "../actions/products.action";
import { API } from "../../config";

// 拦截获取指令后的回调函数
function* handleproducts(action) {
  // 获取返回数据
  const {data} = yield axios.get(
    `${API}/products?sortBy=${action.payload}&order=asc&limit=4`
  );
  // 触发新指令
  yield put(productsList_success({
    // 对应名称
    sortBy: action.payload,
    // 值
    list: data
  }));
}
// 拦截指令
export default function* productsSaga() {
  yield takeEvery(productsList, handleproducts);
}

// src/store/sagas/root.saga.js  合并saga

// 引入 all 方法合并saga
import { all } from 'redux-saga/effects'
// 引入需要合并的 saga
import logupSaga from './logup.saga'
import productsSaga from './products.saga'

// 调用方法合并 saga 并导出
export default function* rootSaga() {
  yield all([logupSaga(), productsSaga()])
}
// src/Components/core/Home.js  Home组件获取状态和指令,并在钩子函数中调用指令

import React, { useEffect } from "react";
import {useDispatch, useSelector} from 'react-redux'
import Layout from "./Layout";
import Search from "./Search";
import { Col, Row, Typography } from "antd";
import Wares from "./Wares";
import {productsList} from '../../store/actions/products.action'
const { Title } = Typography;

// Home 组件
function Home() {
  // 获取指令
  const dispatch = useDispatch();
  // 获取撞坏数据
  const state = useSelector(state => state.productsList)
  console.log(state);
  // 租价挂载后触发指令获取数据
  useEffect(()=>{
    dispatch(productsList('sold'))
    dispatch(productsList('createdAt'))
  },[]) // eslint-disable-line 
  return (
    // 最外层是公共组件,组件是公共组件的子组件
    // 传递 title, subTitle
    <Layout title="首页" subTitle="开始你的选购之旅吧">
      {/* 搜索组件 */}
      <Search />
      <Title level={5}>最受欢迎</Title>
      <Row>
        <Col style={{ margin: "16px 16px" }} span={6}>
          {/* 商品组件 */}
          <Wares />
        </Col>
      </Row>
      <Title level={5}>最新上架</Title>
      <Row>
        <Col style={{ margin: "16px 16px" }} span={6}>
          {/* 商品组件 */}
          <Wares />
        </Col>
      </Row>
    </Layout>
  );
}

export default Home;

首页商品数据展示

获取状态数据

遍历数组,将商品数据传递给商品组件

商品组件使用数据

使用第三方包dateformat处理时间格式

使用Image组件加载图片

服务器并没有返回封面图片地址,我们需要根据接口文档处理图片地址

// src/Components/core/Home.js Home组件获取数据后遍历并传递给zi子组件

import React, { useLayoutEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import Layout from "./Layout";
import Search from "./Search";
import { Col, Row, Typography } from "antd";
import Wares from "./Wares";
import { productsList } from "../../store/actions/products.action";
const { Title } = Typography;

// Home 组件
function Home() {
  // 获取指令
  const dispatch = useDispatch();
  // 获取状态数据
  const state = useSelector((state) => state.productsList);
  // 组件挂载后触发指令获取数据
  useLayoutEffect(() => {
    dispatch(productsList("sold"));
    dispatch(productsList("createdAt"));
  }, []); // eslint-disable-line
  return (
    // 最外层是公共组件,组件是公共组件的子组件
    // 传递 title, subTitle
    <Layout title="首页" subTitle="开始你的选购之旅吧">
      {/* 搜索组件 */}
      <Search />
      <Title level={5}>最受欢迎</Title>
      <Row>
        {/* 遍历数据,将数据通过参数传递给子组件 */}
        {state.sold.map((item, index) => (
          <Col key={index} style={{ padding: "16px 16px" }} span={6}>
            {/* 商品组件 */}
            <Wares item={item} />
          </Col>
        ))}
      </Row>
      <Title level={5}>最新上架</Title>
      <Row>
        {state.createdAt.map((item, index) => (
          <Col key={index} style={{ padding: "16px 16px" }} span={6}>
            {/* 商品组件 */}
            <Wares item={item} />
          </Col>
        ))}
      </Row>
    </Layout>
  );
}

export default Home;
// src/Components/core/Wares.js  组件使用数据

import { Button, Card, Col, Row, Typography, Image } from "antd";
import React from "react";
import { Link } from "react-router-dom/cjs/react-router-dom.min";
import dateformat from "dateformat";
import { API } from "../../config";
const { Title, Paragraph } = Typography;

// 虚拟数据,为了避免因为第一次加载获取不到数据而报错
const defaulydata = {
  item: {
    sold: 0,
    _id: "",
    name: "",
    description: "",
    price: 0,
    category: {
      _id: "",
      name: "",
      createdAt: "",
      updatedAt: "",
      __v: 0,
    },
    quantity: 0,
    shipping: true,
    createdAt: "",
    updatedAt: "",
    __v: 0,
  },
};
// 单个商品组件
function Wares(props) {
  // 判断是否传入了数据,如果没传入使用虚拟数据,避免报错
  props = props.item ? props : defaulydata;
  return (
    <>
      {/* 使用卡片包裹 */}
      <Card
        cover={
          // 图片
          <Image src={`${API}/product/photo/${props.item._id}`} />
        }
        // 底部操作按钮
        actions={[
          <Button type="link">
            <Link>查看详情</Link>
          </Button>,
          <Button type="link">加入购物车</Button>,
        ]}
      >
        {/* 卡片内容,直接调用数据即可 */}
        <Title level={5} ellipsis>
          {props.item.name}
        </Title>
        <Paragraph ellipsis={{ rows: 2 }}>{props.item.description}</Paragraph>
        <Row>价格:{props.item.price}</Row>
        <Row>销量:{props.item.quantity}</Row>
        <Row>上架时间:{dateformat(props.item.updatedAt, "yyyy-mm-dd")}</Row>
        <Row>所属分类:{props.item.category.name}</Row>
      </Card>
    </>
  );
}

export default Wares;

获取商品分类单独抽离为一个公共逻辑

新建文件获取商品分类

书写获取逻辑,之前使用这个逻辑的替换掉,后期再使用直接引用即可

在搜索组件中使用这个解构

// src/method/GetSort.js  新建文件书写公共逻辑 - 获取分类

import axios from "axios";
import { API } from "../config";
import { useEffect, useState } from 'react'

// 获取并返回分类列表,以后再需要使用分类直接调用即可
export default function GetSort() {
  // 创建状态
  const [list, listSet] = useState([]);
  // 钩子函数
  useEffect(() => {
    // 处理异步函数,这里把异步过程写成一个单独的函数 
    async function loadList() {
      // 发起请求获取全部分类列表
      const { data } = await axios.get(`${API}/categories`);
      // 将获取到的列表放进状态
      listSet(data);
    }
    // 调用函数执行
    loadList();
  }, []);
  // 返回列表
  return list
}
// src/Components/core/Search.js  搜索组件调用方法获取分类并遍历创建结构

import React from "react";
import { Button, Col, Divider, Form, Input, Row, Select } from "antd";
import Wares from "./Wares";
import GetSort from "../../method/GetSort";

// 搜索组件
function Search() {
  // 引入公共方法获取分类列表
  const sortList = GetSort();
  return (
    <>
      {/* 表单 - layout:横向布局,initialValues:默认值 */}
      <Form layout="inline" initialValues={{ category: "all" }}>
        {/* 使用Input.Group包裹,使用compact消除间距 */}
        <Input.Group compact>
          {/* 下拉菜单 */}
          <Form.Item name="category">
            <Select>
              <Select.Option value="all">请选择分类</Select.Option>
              {/* 遍历分类列表创建结构 */}
              {sortList.map((item) => (
                <Select.Option key={item._id} value={item._id}>
                  {item.name}
                </Select.Option>
              ))}
            </Select>
          </Form.Item>
          {/* 搜索输入框 */}
          <Form.Item name="search">
            <Input />
          </Form.Item>
          {/* 按钮 */}
          <Form.Item>
            <Button type="primary" htmlType="submit">
              搜索
            </Button>
          </Form.Item>
        </Input.Group>
      </Form>
      {/* 间隔线 */}
      <Divider />
      {/* 搜索展示区 */}
      <Row>
        <Col style={{ margin: "16px 16px" }} span={6}>
          <Wares />
        </Col>
      </Row>
    </>
  );
}

export default Search;
// src/Components/admin/AddWares.js  改造之前写的代码,使用公共逻辑

import { Form, Input, Button, Upload, Select, Radio, message } from "antd";
import { UploadOutlined } from "@ant-design/icons";
import { useState } from "react";
import Layout from "../core/Layout";
import axios from "axios";
import { API } from "../../config";
import { loginOrNot } from "../../method/loginOrNot";
import { useHistory } from "react-router-dom/cjs/react-router-dom.min";
import GetSort from "../../method/GetSort";

// 添加商品组件
function AddWares() {
  // 直接引用公共方法获取分类列表
  const sortList = GetSort();
  // 上传封面
  let [flie, flieSet] = useState();
  // 获取表单
  const [form] = Form.useForm();
  // 获取路由跳转方法
  const history = useHistory();
  // 上传组件配置
  const props = {
    // 上传相关,参数为上传图片本身
    beforeUpload(e) {
      // 将图片信息存储起来
      flieSet(e);
      // 返回false避免自动上传
      return false;
    },
  };
  // 点击提交事件
  const onFinish = (value) => {
    // 获取用户信息
    const {
      token,
      user: { _id },
    } = loginOrNot();
    // 创建formdata实例,接口要求的
    const formData = new FormData();
    // 将表单数据添加进实例对象
    for (let key in value) {
      formData.append(key, value[key]);
    }
    // 将图片信息添加进实例对象
    formData.append("photo", flie);
    // 发起请求添加商品
    axios
      .post(`${API}/product/create/${_id}`, formData, {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      })
      .then((rse) => {
        // 成功弹出提示信息
        message.success(`添加 “${rse.data.name}” 商品成功`);
        // 清空表单
        form.resetFields();
        // 自动跳转回仪表盘
        history.push("/admin/dashboard");
      });
  };
  return (
    <Layout title="添加商品">
      {/* 设置 form用来获取From组件,initialValues设置默认值,onFinish提交是件憾事 */}
      <Form form={form} initialValues={{ category: "-1" }} onFinish={onFinish}>
        <Form.Item>
          {/* 添加props设置参数 */}
          <Upload {...props}>
            <Button icon={<UploadOutlined />}>上传封面</Button>
          </Upload>
        </Form.Item>
        <Form.Item label="名称" name="name">
          <Input />
        </Form.Item>
        <Form.Item label="描述" name="description">
          <Input />
        </Form.Item>
        <Form.Item label="价格" name="price">
          <Input />
        </Form.Item>
        <Form.Item label="所属分类" name="category">
          <Select>
            <Select.Option value="-1">请选择分类</Select.Option>
            {/* 遍历列表,创建结构 */}
            {sortList.map((item) => {
              return (
                <Select.Option key={item._id} value={item._id}>
                  {item.name}
                </Select.Option>
              );
            })}
          </Select>
        </Form.Item>
        <Form.Item label="数量" name="quantity">
          <Input />
        </Form.Item>
        <Form.Item label="是否需要运输" name="shipping">
          <Radio.Group>
            <Radio value={true}></Radio>
            <Radio value={false}></Radio>
          </Radio.Group>
        </Form.Item>
        <Form.Item>
          <Button type="primary" htmlType="submit">
            添加商品
          </Button>
        </Form.Item>
      </Form>
    </Layout>
  );
}

export default AddWares;

实现搜索功能并展示结果

搜索功能使用单独的redux流程,同上面获取商品列表逻辑相似

搜索组件获取指令,点击搜索触发搜索指令,然后获取数据并且展示数据

搜索完成后重置表单

// src/store/actions/search.action.js  创建文件书写新指令

import {createAction} from 'redux-actions'

// 新建指令
export const search = createAction('search')
export const search_success = createAction('search_success')
// src/store/reducers/search.reducer.js  创建文件书写reducer

import {handleActions} from 'redux-actions'
import {search_success} from '../actions/search.action'

// 创建搜索初始状态
const searchData = []

// 创建 reducer
const searchDataReducer = handleActions({
  [search_success]: (state, action) => ([
    // 直接把新值替换进去
    ...action.payload
  ])
},searchData)

export default searchDataReducer
// src/store/reducers/index.js  合并reducer

// 引入模块方法
import { connectRouter } from "connected-react-router"
// 导入 reducer 合并方法
import { combineReducers } from "redux"
// 引入要合并的 reducer
import testReducer from "./text"
import logupReducer from './logup.reducer'
import productsListReducer from "./products.reducer"
import searchDataReducer from "./search.reducer"

// 合并全部 reducer
// createRootReducer是一个函数,不要更改名字
const createRootReducer = history => combineReducers({
  test: testReducer,
  // 路由信息
  router: connectRouter(history),
  logup: logupReducer,
  productsList: productsListReducer,
  serachData: searchDataReducer
})

// 导出
export default createRootReducer
// src/store/sagas/serach.saga.js  新建文件书写saga

import axios from "axios";
import { takeEvery, put } from "redux-saga/effects";
import { search ,search_success } from "../actions/search.action";
import { API } from "../../config";

// 拦截指令后的回调函数
function* handleSearch(action) {
  // 获取请求结果
  const { data } = yield axios.get(
    `${API}/products/search?search=${action.payload.search}&category=${action.payload.category}`
  );
  // 触发新指令
  yield put(search_success(data))
}

// 拦截指令
export default function* serachSaga() {
  yield takeEvery(search, handleSearch);
}
// src/store/sagas/root.saga.js  合并saga

// 引入 all 方法合并saga
import { all } from "redux-saga/effects";
// 引入需要合并的 saga
import logupSaga from "./logup.saga";
import productsSaga from "./products.saga";
import serachSaga from "./serach.saga";

// 调用方法合并 saga 并导出
export default function* rootSaga() {
  yield all([logupSaga(), productsSaga(), serachSaga()]);
}
// src/Components/core/Search.js  搜索组件获取指令和状态数据,点击搜索触发指令

import React from "react";
import { Button, Col, Divider, Form, Input, Row, Select } from "antd";
import Wares from "./Wares";
import GetSort from "../../method/GetSort";
import { useDispatch, useSelector } from "react-redux";
import { search } from "../../store/actions/search.action";

// 搜索组件
function Search() {
  // 获取指令
  const dispatch = useDispatch();
  // 获取状态
  const state = useSelector((state) => state.serachData);
  // 获取form表单
  const [form] = Form.useForm();
  // 引入公共方法获取分类列表
  const sortList = GetSort();
  // 表单提交事件函数
  const onFinish = (value) => {
    // 触发指令
    dispatch(search(value));
    // 清空表单
    form.resetFields();
  };
  return (
    <>
      {/* 表单 - layout:横向布局,initialValues:默认值 */}
      <Form
        form={form}
        onFinish={onFinish}
        layout="inline"
        initialValues={{ category: "all" }}
      >
        {/* 使用Input.Group包裹,使用compact消除间距 */}
        <Input.Group compact>
          {/* 下拉菜单 */}
          <Form.Item name="category">
            <Select>
              <Select.Option value="all">请选择分类</Select.Option>
              {/* 遍历分类列表创建结构 */}
              {sortList.map((item) => (
                <Select.Option key={item._id} value={item._id}>
                  {item.name}
                </Select.Option>
              ))}
            </Select>
          </Form.Item>
          {/* 搜索输入框 */}
          <Form.Item name="search">
            <Input />
          </Form.Item>
          {/* 按钮 */}
          <Form.Item>
            <Button type="primary" htmlType="submit">
              搜索
            </Button>
          </Form.Item>
        </Input.Group>
      </Form>
      {/* 间隔线 */}
      <Divider />
      {/* 搜索展示区 */}
      <Row>
        {/* 遍历搜索结果传递参数 */}
        {state.map((item, index) => (
          <Col key={index} style={{ padding: "16px 16px" }} span={6}>
            {/* 商品组件 */}
            <Wares item={item} />
          </Col>
        ))}
      </Row>
    </>
  );
}

export default Search;

构建商城页面布局

左右两栏布局,左侧筛选条件(4),右侧筛选结果(20)

筛选条件分为两类:按照分类(复选)、按照价格(单选)

左右分别放在两个组件中

使用标题组件

按照分类筛选:获取分类列表创建结构(使用列表组件),列表使用多选框组件包裹

按价格筛选:使用列表组件,使用单选框包裹(使用互斥)

设置两个组件间距,使用space组件设置间距、纵向间距

//  src/Components/core/Shop.js  商城组件左右分布,引入两个筛选条件组件

import { Col, Row, Space } from 'antd'
import React from 'react'
// 引入公共组件
import Layout from './Layout'
import ScreenPrice from './ScreenPrice'
import ScreenSort from './ScreenSort'

// 商城组件
function Shop() {
  return (
    // 最外层是公共组件,组件是公共组件的子组件,传递 title, subTitle
    <Layout title='商品列表' subTitle='开始选购吧'>
      <Row>
        {/* 左侧筛选条件 */}
        <Col span={4}>
          {/* 调整间距、纵向间距、中等间距 */}
          <Space direction='vertical' size='middle'>
            {/* 分类筛选组件 */}
            <ScreenSort />
            {/* 价格筛选组件 */}
            <ScreenPrice />
          </Space>
        </Col>
        {/* 右侧筛选结果展示区 */}
        <Col span={20}>right</Col>
      </Row>
    </Layout>
  )
}

export default Shop
// src/Components/core/ScreenSort.js  新建按分类苏筛选组件

import React from 'react'
import { Typography, List, Checkbox } from 'antd'
import GetSort from '../../method/GetSort'
const { Title } = Typography

// 分类筛选
function ScreenSort() {
  // 获取分类列表
  const sortList = GetSort()
  // 分类组件状态修改是件憾事
  const onChange = value => {
    console.log(value)
  }
  return (
    <>
      {/* 标题 */}
      <Title level={5}>按分类筛选</Title>
      {/* 多选框容器 */}
      <Checkbox.Group onChange={onChange}>
        {/* 列表组件 */}
        <List
          dataSource={sortList}
          renderItem={item => (
            <List.Item>
              <Checkbox value={item._id}>{item.name}</Checkbox>
            </List.Item>
          )}
        />
      </Checkbox.Group>
    </>
  )
}

export default ScreenSort
// src/Components/core/ScreenPrice.js  新建按价格筛选组件

import React, { useState } from 'react'
import { Typography, List, Radio } from 'antd'
const { Title } = Typography

// 价格筛选
function ScreenPrice() {
  // 虚拟数据,后期会更改
  const list = ['0-50', '51-100', '101-150', '151-200']
  // 创建选中状态
  const [value, setValue] = useState(1)
  // 单选组件触发事件函数
  const onChange = e => {
    console.log('radio checked', e.target.value)
    setValue(e.target.value)
  }
  return (
    <>
      {/* 标题 */}
      <Title level={5}>按价格筛选</Title>
      {/* 单选组件 */}
      <Radio.Group onChange={onChange} value={value}>
        {/* 列表组件 */}
        <List
          dataSource={list}
          renderItem={(item, index) => (
            <List.Item>
              <Radio value={index}>{item}</Radio>
            </List.Item>
          )}
        />
      </Radio.Group>
    </>
  )
}

export default ScreenPrice

收集筛选条件

筛选条件要发往服务器端,所以需要参考服务器接口设置筛选参数

创建筛选条件 - useState

分类筛选组件使用checkbox.group包裹,使用onChange事件获取选择的分类

价格筛选把同理,使用radio.group的onChange事件获取选择价格(参数是时间对象)

从商城组件中,传递给两个筛选组件方法,此方法可以修改商城组件的筛选条件

//  src/Components/core/Shop.js  商城组件创建筛选条件,将修改筛选条件的函数传递给子组件

import { Col, Row, Space } from 'antd'
import React, { useState, useEffect } from 'react'
// 引入公共组件
import Layout from './Layout'
import ScreenPrice from './ScreenPrice'
import ScreenSort from './ScreenSort'

// 商城组件
function Shop() {
  // 创建筛选条件
  const [filter, setfilter] = useState({
    // 分类
    category: [],
    // 价格
    price: [],
  })
  // 修改筛选条件函数
  const Changefilter = ({ name, newValue }) => {
    setfilter({ ...filter, [name]: newValue })
  }
  // 钩子函数 - 当筛选条件改变时触发
  useEffect(() => {
    console.log(filter)
  }, [filter])
  return (
    // 最外层是公共组件,组件是公共组件的子组件,传递 title, subTitle
    <Layout title='商品列表' subTitle='开始选购吧'>
      <Row>
        {/* 左侧筛选条件 */}
        <Col span={4}>
          {/* 调整间距、纵向间距、中等间距 */}
          <Space direction='vertical' size='middle'>
            {/* 分类筛选组件,传递函数 */}
            <ScreenSort Changefilter={Changefilter} />
            {/* 价格筛选组件,传递函数 */}
            <ScreenPrice Changefilter={Changefilter} />
          </Space>
        </Col>
        {/* 右侧筛选结果展示区 */}
        <Col span={20}>right</Col>
      </Row>
    </Layout>
  )
}

export default Shop
// src/Components/core/ScreenSort.js  组件当条件变化的时候调用函数修改父组件条件

import React from 'react'
import { Typography, List, Checkbox } from 'antd'
import GetSort from '../../method/GetSort'
const { Title } = Typography

// 分类筛选
function ScreenSort(props) {
  // 获取分类列表
  const sortList = GetSort()
  // 分类组件状态修改事件函数
  const onChange = value => {
    // 调用父组件传递过来的函数修改筛选条件
    props.Changefilter({ name: 'category', newValue: value })
  }
  return (
    <>
      {/* 标题 */}
      <Title level={5}>按分类筛选</Title>
      {/* 多选框容器 */}
      <Checkbox.Group onChange={onChange}>
        {/* 列表组件 */}
        <List
          dataSource={sortList}
          renderItem={item => (
            <List.Item>
              <Checkbox value={item._id}>{item.name}</Checkbox>
            </List.Item>
          )}
        />
      </Checkbox.Group>
    </>
  )
}

export default ScreenSort
// src/Components/core/ScreenPrice.js  组件当条件变化的时候调用函数修改父组件条件

import React, { useState } from 'react'
import { Typography, List, Radio } from 'antd'
import pricesList from '../../method/price'
const { Title } = Typography

// 价格筛选
function ScreenPrice(props) {
  // 创建选中状态
  const [value, setValue] = useState(1)
  // 单选组件触发事件函数
  const onChange = e => {
    setValue(e.target.value)
    // 调用父组件传递过来的函数修改筛选条件
    props.Changefilter({ name: 'price', newValue: e.target.value })
  }
  return (
    <>
      {/* 标题 */}
      <Title level={5}>按价格筛选</Title>
      {/* 单选组件 */}
      <Radio.Group onChange={onChange} value={value}>
        {/* 列表组件 */}
        <List
          dataSource={pricesList}
          renderItem={item => (
            <List.Item>
              <Radio value={item.array}>{item.name}</Radio>
            </List.Item>
          )}
        />
      </Radio.Group>
    </>
  )
}

export default ScreenPrice
// src/method/price.js  新建文件书写价格筛选条件

// 价格筛选数据
const pricesList = [
  {
    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 pricesList

展示筛选结果

书写筛选数据redux逻辑,将筛选结果数据添加到状态数据库中

reducer中设置data的时候需要判断一下当前是想要获取结果还是获取更多结果

当筛选条件发生变化触发指令获取结果,同时传递筛选条件

注意,需要设置一个skip属性来表示加载更多,,通过useState创建,默认为0

使用useSelector获取数据

将数据传递给单个商品组件,每个商品6份,设置空隙

下方添加加载更多按钮,点击更改skip(+4),设置skip发生变化再次获取数据

设置筛选条件发生变化重置skip

修改整体样式

没有更多数据的时候显示没有更多数据(空状态组件)- size

// src/store/actions/filter.action.js  创建新指令

import { createAction } from 'redux-actions'

// 新建指令
export const filter = createAction('filter')
export const filter_success = createAction('filter_success')
// src/store/reducers/filter.reducer.js  创建reducer

import { handleActions } from 'redux-actions'
import { filter_success } from '../actions/filter.action'

// 新建仓库存放筛选数据
const filterData = {
  size: 0,
  data: [],
}

// 创建reducer
const filterDataReducer = handleActions(
  {
    [filter_success]: (state, action) => ({
      size: action.payload.size,
      data:
        // 判断是否是显示更多,是则向数组中添加数据,否则直接替换数据
        action.payload.skip === 0
          ? action.payload.data
          : [...state.data, ...action.payload.data],
    }),
  },
  filterData
)

export default filterDataReducer
// src/store/reducers/index.js 合并reducer

// 引入模块方法
import { connectRouter } from 'connected-react-router'
// 导入 reducer 合并方法
import { combineReducers } from 'redux'
// 引入要合并的 reducer
import testReducer from './text'
import logupReducer from './logup.reducer'
import productsListReducer from './products.reducer'
import searchDataReducer from './search.reducer'
import filterDataReducer from './filter.reducer'

// 合并全部 reducer
// createRootReducer是一个函数,不要更改名字
const createRootReducer = history =>
  combineReducers({
    test: testReducer,
    // 路由信息
    router: connectRouter(history),
    logup: logupReducer,
    productsList: productsListReducer,
    serachData: searchDataReducer,
    filterData: filterDataReducer,
  })

// 导出
export default createRootReducer
// src/store/sagas/filter.saga.js  创建新saga

import axios from 'axios'
import { takeEvery, put } from 'redux-saga/effects'
import { API } from '../../config'
import { filter, filter_success } from '../actions/filter.action'

// 拦截指令后的回调函数
function* hadlefilter(action) {
  // 获取筛选结果
  const { data } = yield axios.post(`${API}/products/filter`, action.payload)
  // 触发新指令传递参数
  yield put(filter_success({ skip: action.payload.skip, ...data }))
}

// 拦截指令
export default function* filterSaga() {
  yield takeEvery(filter, hadlefilter)
}
// src/store/sagas/root.saga.js  合并saga

// 引入 all 方法合并saga
import { all } from 'redux-saga/effects'
import filterSaga from './filter.saga'
// 引入需要合并的 saga
import logupSaga from './logup.saga'
import productsSaga from './products.saga'
import serachSaga from './serach.saga'

// 调用方法合并 saga 并导出
export default function* rootSaga() {
  yield all([logupSaga(), productsSaga(), serachSaga(), filterSaga()])
}
// src/Components/core/Shop.js  商城组件获取指令和数据,在筛选时触发指令,并添加加载更多功能

import { Button, Col, Row, Space, Empty } from 'antd'
import React, { useState, useEffect } from 'react'
// 引入公共组件
import Layout from './Layout'
import ScreenPrice from './ScreenPrice'
import ScreenSort from './ScreenSort'
import { useDispatch, useSelector } from 'react-redux'
import { filter } from '../../store/actions/filter.action'
import Wares from './Wares'

// 商城组件
function Shop() {
  // 获取筛选数据
  const state = useSelector(state => state.filterData)
  // 获取全部指令
  const dispatch = useDispatch()
  // 创建状态 - 是否为加载更多,不为0则代表加载更多
  const [skip, setskip] = useState(0)
  // 创建筛选条件
  const [filters, setfilters] = useState({
    // 分类
    category: [],
    // 价格
    price: [],
  })
  // 修改筛选条件函数
  const Changefilter = ({ name, newValue }) => {
    setfilters({ ...filters, [name]: newValue })
  }
  // 钩子函数 - 当筛选条件改变时触发
  useEffect(() => {
    // 只要修改筛选条件一律把获取更多取消
    setskip(0)
    // 触发指令,传递参数
    dispatch(filter({ filters: { ...filters }, skip: skip }))
  }, [filters]) // eslint-disable-line
  // 钩子函数 - 当加载更多时候触发
  useEffect(() => {
    // 触发指令(同上)
    dispatch(filter({ filters: { ...filters }, skip: skip }))
  }, [skip]) // eslint-disable-line
  return (
    // 最外层是公共组件,组件是公共组件的子组件,传递 title, subTitle
    <Layout title='商品列表' subTitle='开始选购吧'>
      <Row>
        {/* 左侧筛选条件 */}
        <Col span={4}>
          {/* 调整间距、纵向间距、中等间距 */}
          <Space direction='vertical' size='middle'>
            {/* 分类筛选组件,传递函数 */}
            <ScreenSort Changefilter={Changefilter} />
            {/* 价格筛选组件,传递函数 */}
            <ScreenPrice Changefilter={Changefilter} />
          </Space>
        </Col>
        {/* 右侧筛选结果展示区 */}
        <Col style={{ padding: '16px 0 50px 0' }} span={20}>
          <Row>
            {/* 遍历数组,创建结构、传递数据 */}
            {state.data.map(item => (
              <Col style={{ padding: '16px 16px' }} span={6} key={item._id}>
                <Wares item={item} />
              </Col>
            ))}
          </Row>
          {/* 判断返回结果是否为4,小于4代表没有更多数据了,否则还可以获取,没有更多数据展示空状态,否则展示获取更多按钮 */}
          {state.size === 4 ? (
            <Button
              // 点击后修改skip
              onClick={() => setskip(skip + 4)}
              style={{ margin: '0 auto' }}>
              加载更多
            </Button>
          ) : (
            <Empty description='没有更多了' />
          )}
        </Col>
      </Row>
    </Layout>
  )
}

export default Shop

构建商品详情页面布局

新建商品详情页组件,使用layout容器、写入路由规则

单个商品组件添加点击查看详情跳转详情页面,带着ID跳转

详情页依旧采用两列布局18:6

获取ID - usePaeams

// src/Components/core/WaresInfo.js  创建商品信息页组件

import { Col, Row } from 'antd'
import React from 'react'
import { useParams } from 'react-router-dom/cjs/react-router-dom.min'
import Layout from './Layout'

// 商品信息页面
function WaresInfo() {
  // 获取路由信息
  const { waresId } = useParams()
  return (
    // 容器
    <Layout title='商品名称' subTitle='商品简介'>
      {/* 左右布局 */}
      <Row>
        <Col span={18}>{waresId}</Col>
        <Col span={6}>right</Col>
      </Row>
    </Layout>
  )
}

export default WaresInfo
// src/Components/Routes.js  添加进路由规则

import React from 'react'
// 路由需要使用的方法: BrowserRouter类似于HashRouter,更推荐使用
import { Switch, Route } from 'react-router-dom'
import AddSort from './admin/AddSort'
import AddWares from './admin/AddWares'
import AdminDashboard from './admin/AdminDashboard'
import GuardAdminDashboard from './admin/GuardAdminDashboard'
import GuardUserDashboard from './admin/GuardUserDashboard'
import UserDashboard from './admin/UserDashboard'
// 引入路由组件
import Home from './core/Home'
import Login from './core/Login'
import Logup from './core/Logup'
import Shop from './core/Shop'
import WaresInfo from './core/WaresInfo'

function Routes() {
  return (
    // 包裹开启路由功能(这里因为处理路由到store的时候已经做了,这里不需要了)
    // {/* 避免书写错误出现相同路由 */}
    <Switch>
      {/* 路由,第一个路由为默认路由,精准匹配 */}
      <Route path='/' component={Home} exact />
      <Route path='/shop' component={Shop} />
      <Route path='/login' component={Login} />
      <Route path='/logup' component={Logup} />
      {/* 使用路由守卫 */}
      <GuardUserDashboard path='/user/dashboard' component={UserDashboard} />
      <GuardAdminDashboard path='/admin/dashboard' component={AdminDashboard} />
      <GuardAdminDashboard path='/admin/addsort' component={AddSort} />
      <GuardAdminDashboard path='/admin/addwares' component={AddWares} />
      <Route path='/waresinfo/:waresId' component={WaresInfo} />
    </Switch>
  )
}

export default Routes
// src/Components/core/Wares.js  商品组件设置点击跳转

import { Button, Card, Row, Typography, Image } from 'antd'
import React from 'react'
import { Link } from 'react-router-dom/cjs/react-router-dom.min'
import dateformat from 'dateformat'
import { API } from '../../config'
const { Title, Paragraph } = Typography

// 虚拟数据,为了避免因为第一次加载获取不到数据而报错
const defaulydata = {
  item: {
    sold: 0,
    _id: '',
    name: '',
    description: '',
    price: 0,
    category: {
      _id: '',
      name: '',
      createdAt: '',
      updatedAt: '',
      __v: 0,
    },
    quantity: 0,
    shipping: true,
    createdAt: '',
    updatedAt: '',
    __v: 0,
  },
}
// 单个商品组件
function Wares(props) {
  // 判断是否传入了数据,如果没传入使用虚拟数据,避免报错
  props = props.item ? props : defaulydata
  return (
    <>
      {/* 使用卡片包裹 */}
      <Card
        cover={
          // 图片
          <Image src={`${API}/product/photo/${props.item._id}`} />
        }
        // 底部操作按钮
        actions={[
          <Button type='link'>
            {/* 设置点击带参数跳转 */}
            <Link to={`/waresinfo/${props.item._id}`}>查看详情</Link>
          </Button>,
          <Button type='link'>加入购物车</Button>,
        ]}>
        {/* 卡片内容,直接调用数据即可 */}
        <Title level={5} ellipsis>
          {props.item.name}
        </Title>
        <Paragraph ellipsis={{ rows: 2 }}>{props.item.description}</Paragraph>
        <Row>价格:{props.item.price}</Row>
        <Row>销量:{props.item.quantity}</Row>
        <Row>上架时间:{dateformat(props.item.updatedAt, 'yyyy-mm-dd')}</Row>
        <Row>所属分类:{props.item.category.name}</Row>
      </Card>
    </>
  )
}

export default Wares

完成根据产品ID获取产品详情的Redux流程

创建指令 - 创建reducer - 合并reducer - 创建saga - 合并saga - 调用指令 - 获取状态数据

组件加载的时候触发指令获取产品信息

// src/store/actions/waresInfo.action.js  创建新指令

import { createAction } from 'redux-actions'

// 创建指令
export const waresInfo = createAction('waresInfo')
export const waresInfo_success = createAction('waresInfo_success')
// src/store/reducers/waresInfo.reducer.js  创建新reducer

import { handleActions } from 'redux-actions'
import { waresInfo_success } from '../actions/waresInfo.action'

// 创建初始状态
const waresInfoData = {}

// 创建reducer
const waresInfoDataReducer = handleActions(
  {
    [waresInfo_success]: (state, action) => ({ ...action.payload }),
  },
  waresInfoData
)

export default waresInfoDataReducer 
// src/store/reducers/index.js  合并reducer

// 引入模块方法
import { connectRouter } from 'connected-react-router'
// 导入 reducer 合并方法
import { combineReducers } from 'redux'
// 引入要合并的 reducer
import testReducer from './text'
import logupReducer from './logup.reducer'
import productsListReducer from './products.reducer'
import searchDataReducer from './search.reducer'
import filterDataReducer from './filter.reducer'
import waresInfoDataReducer from './waresInfo.reducer'

// 合并全部 reducer
// createRootReducer是一个函数,不要更改名字
const createRootReducer = history =>
  combineReducers({
    test: testReducer,
    // 路由信息
    router: connectRouter(history),
    logup: logupReducer,
    productsList: productsListReducer,
    serachData: searchDataReducer,
    filterData: filterDataReducer,
    waresInfoData: waresInfoDataReducer,
  })

// 导出
export default createRootReducer
// src/store/sagas/waresInfo.saga.js 创建新saga

import axios from 'axios'
import { takeEvery, put } from 'redux-saga/effects'
import { API } from '../../config'
import { waresInfo_success } from '../actions/waresInfo.action'

// 拦截指令后的回调函数
function* handlewaresInfo(action) {
  const { data } = yield axios.get(`${API}/product/${action.payload}`)
  yield put(waresInfo_success(data))
}

// 拦截指令
export default function* waresInfoSaga() {
  yield takeEvery('waresInfo', handlewaresInfo)
}
// src/store/sagas/root.saga.js  合并saga

// 引入 all 方法合并saga
import { all } from 'redux-saga/effects'
import filterSaga from './filter.saga'
// 引入需要合并的 saga
import logupSaga from './logup.saga'
import productsSaga from './products.saga'
import serachSaga from './serach.saga'
import waresInfoSaga from './waresInfo.saga'

// 调用方法合并 saga 并导出
export default function* rootSaga() {
  yield all([
    logupSaga(),
    productsSaga(),
    serachSaga(),
    filterSaga(),
    waresInfoSaga(),
  ])
}
// src/Components/core/WaresInfo.js  组件加载后自动触发指令获取产品详情

import { Col, Row } from 'antd'
import React from 'react'
import { useDispatch } from 'react-redux'
import { useParams } from 'react-router-dom/cjs/react-router-dom.min'
import { useEffect } from 'react/cjs/react.development'
import { waresInfo } from '../../store/actions/waresInfo.action'
import Layout from './Layout'

// 商品信息页面
function WaresInfo() {
  // 获取全部指令
  const dispatch = useDispatch()
  // 获取路由信息
  const { waresId } = useParams()
  // 组件挂载后钩子函数
  useEffect(() => {
    // 直接触发指令获取数据
    dispatch(waresInfo(waresId))
  })
  return (
    // 容器
    <Layout title='商品名称' subTitle='商品简介'>
      {/* 左右布局 */}
      <Row>
        <Col span={18}>{waresId}</Col>
        <Col span={6}>right</Col>
      </Row>
    </Layout>
  )
}

export default WaresInfo

展示商品详情

组件获取数据,根据商品信息数据新型结构创建和数据绑定

引入单个产品组件,把数据传递给他

因为第一次可能会出现空对象,所以需要进行判断一下,如果为空不渲染

因为图片尺寸过大,需要给组件传递样式属性,让组件获取然后使用

同时传递两个值控制查看详情和加入购物车两个按钮是否显示(默认true)

可以把两个按钮写在一个函数中最后调用返回出来

// src/Components/core/WaresInfo.js  详情组件获取数据,将数据传递给子组件渲染(需要判断一下)

import { Col, Row } from 'antd'
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useParams } from 'react-router-dom/cjs/react-router-dom.min'
import { useEffect } from 'react/cjs/react.development'
import { waresInfo } from '../../store/actions/waresInfo.action'
import Layout from './Layout'
import Wares from './Wares'

// 商品信息页面
function WaresInfo() {
  // 获取数据
  const state = useSelector(state => state.waresInfoData)
  // 获取全部指令
  const dispatch = useDispatch()
  // 获取路由信息
  const { waresId } = useParams()
  // 组件挂载后钩子函数
  useEffect(() => {
    // 直接触发指令获取数据
    dispatch(waresInfo(waresId))
  }, []) // eslint-disable-line
  return (
    // 容器
    <Layout title='商品名称' subTitle='商品简介'>
      {/* 左右布局 */}
      <Row>
        <Col span={18}>
          {/* 判断数据内容是否不为空,不为空再引入子组件渲染 */}
          {Object.keys(state).length > 0 && (
            // 传递数据 - imgstyle:数据,imgstyle:图片样式,lookInfo:查看详情按钮
            <Wares
              item={state}
              imgstyle={{ width: '50%', margin: '0 auto' }}
              lookInfo={false}
            />
          )}
        </Col>
        <Col span={6}>right</Col>
      </Row>
    </Layout>
  )
}

export default WaresInfo
// src/Components/core/Wares.js  单个商品组件获取数据渲染,另外新增加几条数据用来动态控制显示

import { Button, Card, Row, Typography, Image } from 'antd'
import React from 'react'
import { Link } from 'react-router-dom/cjs/react-router-dom.min'
import dateformat from 'dateformat'
import { API } from '../../config'
const { Title, Paragraph } = Typography

// 虚拟数据,为了避免因为第一次加载获取不到数据而报错
const defaulydata = {
  item: {
    sold: 0,
    _id: '',
    name: '',
    description: '',
    price: 0,
    category: {
      _id: '',
      name: '',
      createdAt: '',
      updatedAt: '',
      __v: 0,
    },
    quantity: 0,
    shipping: true,
    createdAt: '',
    updatedAt: '',
    __v: 0,
  },
}
// 单个商品组件,解构出可能需要单独使用的数据
function Wares({ lookInfo = true, addCart = true, ...props }) {
  // 判断是否传入了数据,如果没传入使用虚拟数据,避免报错
  props = props.item ? props : defaulydata
  // 根据传入的值动态返回卡片底部按钮
  function actionsHTML() {
    // 创建数组
    const buttons = []
    // 如果lookInfo为真添加查看详情
    if (lookInfo) {
      buttons.push(
        <Button type='link'>
          {/* 设置点击带参数跳转 */}
          <Link to={`/waresinfo/${props.item._id}`}>查看详情</Link>
        </Button>
      )
    }
    // 如果addCart为真添加加入购物车按钮
    if (addCart) {
      buttons.push(<Button type='link'>加入购物车</Button>)
    }
    // /返回数组
    return buttons
  }
  return (
    <>
      {/* 使用卡片包裹 */}
      <Card
        cover={
          // 图片
          <Image
            style={props.imgstyle}
            src={`${API}/product/photo/${props.item._id}`}
          />
        }
        // 底部操作按钮,直接调用函数返回结构
        actions={actionsHTML()}>
        {/* 卡片内容,直接调用数据即可 */}
        <Title level={5} ellipsis>
          {props.item.name}
        </Title>
        <Paragraph ellipsis={{ rows: 2 }}>{props.item.description}</Paragraph>
        <Row>价格:{props.item.price}</Row>
        <Row>销量:{props.item.quantity}</Row>
        <Row>上架时间:{dateformat(props.item.updatedAt, 'yyyy-mm-dd')}</Row>
        <Row>所属分类:{props.item.category.name}</Row>
      </Card>
    </>
  )
}

export default Wares

商品添加到购物车

购物车商品存储在本地的local storage中,所以添加购物车的商品只需要存储进local storage即可

新建一个公共文件用来书写添加购物车方法

购物车商品存储在一个数组中

需要先判断window是否存在,因为local storage存储在window中

判断当前是否存在购物车列表,如果存在添加,如果不存在新建

添加进数组的时候需要添加商品数量,默认为1

进行去重操作(利用商品ID),使用Set类

将新的购物车数组保存到local storage中

最后调用参数2的回调函数,跳转购物车页面

点击添加购物车按钮,触发公共方法,传递商品信息

参数2给一个跳转购物车页面的逻辑,使用push方法配合dispatch

// src/method/cart.js  新建文件书写添加购物车逻辑

// 添加购物车逻辑
// 参数1:商品信息
// 参数2:回调函数
export function addItem(item, cb) {
  // 创建购物车列表
  let cart =
    typeof window !== 'undefined' && localStorage.getItem('cart')
      ? JSON.parse(localStorage.getItem('cart'))
      : []
  // 将商品添加进数组
  cart.push({
    ...item,
    count: 1,
  })
  // 去重处理,Set是ES6新语法,类似于数组但内部没有重复
  cart = Array.from(new Set(cart.map(i => i._id))).map(id =>
    cart.find(i => i._id === id)
  )
  // 将新的购物车列表本地化
  localStorage.setItem('cart', JSON.stringify(cart))
  // 调用回调函数
  cb()
}
// src/Components/core/Wares.js  单个商品组件调用逻辑并传递参数

import { Button, Card, Row, Typography, Image } from 'antd'
import React from 'react'
import { Link } from 'react-router-dom/cjs/react-router-dom.min'
import dateformat from 'dateformat'
import { API } from '../../config'
import { useDispatch } from 'react-redux'
import { push } from 'connected-react-router'
import { addItem } from '../../method/cart'
const { Title, Paragraph } = Typography

// 虚拟数据,为了避免因为第一次加载获取不到数据而报错
const defaulydata = {
  item: {
    sold: 0,
    _id: '',
    name: '',
    description: '',
    price: 0,
    category: {
      _id: '',
      name: '',
      createdAt: '',
      updatedAt: '',
      __v: 0,
    },
    quantity: 0,
    shipping: true,
    createdAt: '',
    updatedAt: '',
    __v: 0,
  },
}
// 单个商品组件,解构出可能需要单独使用的数据
function Wares({ lookInfo = true, addCart = true, ...props }) {
  // 判断是否传入了数据,如果没传入使用虚拟数据,避免报错
  props = props.item ? props : defaulydata
  // 获取全部指令
  const dispatch = useDispatch()
  // 加入物购车按钮点击事件
  const hadleAddItem = () => {
    // 调用购物车添加函数,传递参数
    addItem(props.item, () => {
      // 路由跳转
      dispatch(push('/cart'))
    })
  }
  // 根据传入的值动态返回卡片底部按钮
  function actionsHTML() {
    // 创建数组
    const buttons = []
    // 如果lookInfo为真添加查看详情
    if (lookInfo) {
      buttons.push(
        <Button type='link'>
          {/* 设置点击带参数跳转 */}
          <Link to={`/waresinfo/${props.item._id}`}>查看详情</Link>
        </Button>
      )
    }
    // 如果addCart为真添加加入购物车按钮
    if (addCart) {
      buttons.push(
        // 添加点击事件
        <Button type='link' onClick={hadleAddItem}>
          加入购物车
        </Button>
      )
    }
    // /返回数组
    return buttons
  }
  return (
    <>
      {/* 使用卡片包裹 */}
      <Card
        cover={
          // 图片
          <Image
            style={props.imgstyle}
            src={`${API}/product/photo/${props.item._id}`}
          />
        }
        // 底部操作按钮,直接调用函数返回结构
        actions={actionsHTML()}>
        {/* 卡片内容,直接调用数据即可 */}
        <Title level={5} ellipsis>
          {props.item.name}
        </Title>
        <Paragraph ellipsis={{ rows: 2 }}>{props.item.description}</Paragraph>
        <Row>价格:{props.item.price}</Row>
        <Row>销量:{props.item.quantity}</Row>
        <Row>上架时间:{dateformat(props.item.updatedAt, 'yyyy-mm-dd')}</Row>
        <Row>所属分类:{props.item.category.name}</Row>
      </Card>
    </>
  )
}

export default Wares

展示本地购物车商品

在上面的cart文件中新建一个方法获取本地购物车商品

判断本地数据是否有,如果没有返回空数组

新建购物车组件,使用Layout布局容器、配置路由逻辑

购物车在挂载后自动获取购物车列表

可以使用table组件,但这里不使用,因为table要求把所有逻辑写在table组件中,这里我们只使用table的Html结构

购物车分为左右两部分16:8,设置左右间距

表格放在左侧,遍历数据创建tr结构(可以单独写在一个组件中)

// src/method/cart.js  创建方法获取购物车列表

// 添加购物车逻辑,参数1:商品信息,参数2:回调函数
export function addItem(item, cb) {
  // 创建购物车列表
  let cart =
    typeof window !== 'undefined' && localStorage.getItem('cart')
      ? JSON.parse(localStorage.getItem('cart'))
      : []
  // 将商品添加进数组
  cart.push({
    ...item,
    count: 1,
  })
  // 去重处理,Set是ES6新语法,类似于数组但内部没有重复
  cart = Array.from(new Set(cart.map(i => i._id))).map(id =>
    cart.find(i => i._id === id)
  )
  // 将新的购物车列表本地化
  localStorage.setItem('cart', JSON.stringify(cart))
  // 调用回调函数
  cb()
}

// 获取购物车列表
export function getCart() {
  return typeof window !== 'undefined' && localStorage.getItem('cart')
    ? JSON.parse(localStorage.getItem('cart'))
    : []
}
// src/Components/core/Cart.js  新建购物车组件

import { Col, Row } from 'antd'
import React, { useEffect, useState } from 'react'
import { getCart } from '../../method/cart'
import CartItem from './cartItem'
import Layout from './Layout'

// 购物车组件
function Cart() {
  // 创建状态 - 购物车列表,默认为空
  const [cart, setCart] = useState([])
  // 钩子函数
  useEffect(() => {
    // 调用函数获取购物车商品列表
    setCart(getCart())
  }, [])
  return (
    <Layout title='购物车' subTitle='您可以在这里购买您喜欢的产品了'>
      <Row gutter={16}>
        {/* 左右分栏 */}
        <Col span={18}>
          {/* 设置宽度撑满 */}
          <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>
            {/* 遍历列表 */}
            {cart.map(item => (
              // 引入单个购物车列表组件
              <CartItem key={item._id} item={item} />
            ))}
          </table>
        </Col>
        {/* 右侧 */}
        <Col span={6}>right</Col>
      </Row>
    </Layout>
  )
}

export default Cart
// src/Components/core/cartItem.js  新建单个购物车列表组件

import { Button, Image } from 'antd'
import React from 'react'
import { API } from '../../config'

// 单个购物车列表组件
function CartItem({ item }) {
  // 使用数据
  return (
    <tbody className='ant-table-tbody'>
      <tr className='ant-table-row'>
        <td className='ant-table-cell'>
          <Image width={100} src={`${API}/product/photo/${item._id}`} />
        </td>
        <td className='ant-table-cell'>{item.name}</td>
        <td className='ant-table-cell'>{item.price}</td>
        <td className='ant-table-cell'>{item.category.name}</td>
        <td className='ant-table-cell'>{item.count}</td>
        <td className='ant-table-cell'>
          {/* 删除按钮 */}
          <Button type='primary' danger>
            删除
          </Button>
        </td>
      </tr>
    </tbody>
  )
}

export default CartItem
// src/Components/Routes.js  添加购物车组件路由

import React from 'react'
// 路由需要使用的方法: BrowserRouter类似于HashRouter,更推荐使用
import { Switch, Route } from 'react-router-dom'
import AddSort from './admin/AddSort'
import AddWares from './admin/AddWares'
import AdminDashboard from './admin/AdminDashboard'
import GuardAdminDashboard from './admin/GuardAdminDashboard'
import GuardUserDashboard from './admin/GuardUserDashboard'
import UserDashboard from './admin/UserDashboard'
import Cart from './core/Cart'
// 引入路由组件
import Home from './core/Home'
import Login from './core/Login'
import Logup from './core/Logup'
import Shop from './core/Shop'
import WaresInfo from './core/WaresInfo'

function Routes() {
  return (
    // 包裹开启路由功能(这里因为处理路由到store的时候已经做了,这里不需要了)
    // {/* 避免书写错误出现相同路由 */}
    <Switch>
      {/* 路由,第一个路由为默认路由,精准匹配 */}
      <Route path='/' component={Home} exact />
      <Route path='/shop' component={Shop} />
      <Route path='/login' component={Login} />
      <Route path='/logup' component={Logup} />
      {/* 使用路由守卫 */}
      <GuardUserDashboard path='/user/dashboard' component={UserDashboard} />
      <GuardAdminDashboard path='/admin/dashboard' component={AdminDashboard} />
      <GuardAdminDashboard path='/admin/addsort' component={AddSort} />
      <GuardAdminDashboard path='/admin/addwares' component={AddWares} />
      <Route path='/waresinfo/:waresId' component={WaresInfo} />
      <Route path='/cart' component={Cart} />
    </Switch>
  )
}

export default Routes
// src/Components/core/Navgation.js  顶部导航蓝添加购物车

import React from 'react'
// 引入 导航组件
import { Menu } from 'antd'
// 引入获取 stre 状态方法
import { useSelector } from 'react-redux'
// 引入跳转路由方法
import { Link } from 'react-router-dom'
// 引入方法 - 判断是否登陆
import { loginOrNot } from '../../method/loginOrNot'

// 顶部导航组件
function Navgation() {
  // 调用方法返回信息(如果登录会返回登录信息,未登录返回false)
  const data = loginOrNot()
  // 获取状态 - 路由信息
  const state = useSelector(state => state.router)
  // 返回注册、登录结构
  const loginRegisterShow = () => {
    return (
      <>
        <Menu.Item key='/logIn'>
          <Link to='/logIn'>登录</Link>
        </Menu.Item>
        <Menu.Item key='/logUp'>
          <Link to='/logUp'>注册</Link>
        </Menu.Item>
      </>
    )
  }
  // 返回登录后的 dashboard 结构
  const DashboardShow = () => {
    // 根据返回的用户类型返设置不同的值
    const keyAndTo = data.user.role ? '/admin/dashboard' : '/user/dashboard'
    return (
      <>
        {/* 直接使用动态参数 */}
        <Menu.Item key={keyAndTo}>
          <Link to={keyAndTo}>仪表盘</Link>
        </Menu.Item>
      </>
    )
  }
  return (
    <div>
      {/* 使用导航组件,mode - 横向,selectedKeys - 设置选中(数组中使用当前路由信息) */}
      <Menu mode='horizontal' selectedKeys={[state.location.pathname]}>
        {/* key 对应着上面的 selectedKeys */}
        <Menu.Item key='/'>
          {/* 设置点击跳转路由 */}
          <Link to='/'>首页</Link>
        </Menu.Item>
        <Menu.Item key='/shop'>
          <Link to='/shop'>商品列表</Link>
        </Menu.Item>
        <Menu.Item key='/cart'>
          <Link to='/cart'>购物车</Link>
        </Menu.Item>
        {/* 动态结构,根据是否登陆判断 */}
        {data ? DashboardShow() : loginRegisterShow()}
      </Menu>
    </div>
  )
}

export default Navgation

更改购物车商品数量

再cart文件中书写更改数量方法

根据Id找到要修改的商品,修改数量,然后再重新保存

当数量修改的时候触发事件执行修改数量函数,传递参数

同步修改本地页面数量

注意数量应该是Number类型

注意同步修改父组件中的数据,直接把set传递下去即可

这里直接让方法返回cart,避免了还要请求一次的问题

// src/method/cart.js  添加修改数量方法,并返回修改后的cart

// 添加购物车逻辑,参数1:商品信息,参数2:回调函数
export function addItem(item, cb) {
  // 创建购物车列表
  let cart =
    typeof window !== 'undefined' && localStorage.getItem('cart')
      ? JSON.parse(localStorage.getItem('cart'))
      : []
  // 将商品添加进数组
  cart.push({
    ...item,
    count: 1,
  })
  // 去重处理,Set是ES6新语法,类似于数组但内部没有重复
  cart = Array.from(new Set(cart.map(i => i._id))).map(id =>
    cart.find(i => i._id === id)
  )
  // 将新的购物车列表本地化
  localStorage.setItem('cart', JSON.stringify(cart))
  // 调用回调函数
  cb()
}

// 获取购物车列表
export function getCart() {
  return typeof window !== 'undefined' && localStorage.getItem('cart')
    ? JSON.parse(localStorage.getItem('cart'))
    : []
}

// 修改数量方法
export function upDataCart(id, count) {
  let cart =
    typeof window !== 'undefined' && localStorage.getItem('cart')
      ? JSON.parse(localStorage.getItem('cart'))
      : []
  const my = cart.find(item => (item._id = id))
  my.count = count
  // 将新的购物车列表本地化
  localStorage.setItem('cart', JSON.stringify(cart))
  return cart
}
// src/Components/core/Cart.js  后五车组件将修改自己cart方法传递下去

import { Col, Row } from 'antd'
import React, { useEffect, useState } from 'react'
import { getCart } from '../../method/cart'
import CartItem from './cartItem'
import Layout from './Layout'

// 购物车组件
function Cart() {
  // 创建状态 - 购物车列表,默认为空
  const [cart, setCart] = useState([])
  // 钩子函数
  useEffect(() => {
    // 调用函数获取购物车商品列表
    setCart(getCart())
  }, [])
  return (
    <Layout title='购物车' subTitle='您可以在这里购买您喜欢的产品了'>
      <Row gutter={16}>
        {/* 左右分栏 */}
        <Col span={18}>
          {/* 设置宽度撑满 */}
          <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>
            {/* 遍历列表 */}
            {cart.map(item => (
              // 引入单个购物车列表组件,把修改cart的方法传递下去
              <CartItem key={item._id} item={item} setCart={setCart} />
            ))}
          </table>
        </Col>
        {/* 右侧 */}
        <Col span={6}>right</Col>
      </Row>
    </Layout>
  )
}

export default Cart
// src/Components/core/cartItem.js  单个购物车列表组件数量修改调用方法修改本地数据并修改父组件和自己的数据

import { Button, Image, Input } from 'antd'
import React, { useState } from 'react'
import { API } from '../../config'
import { upDataCart } from '../../method/cart'

// 单个购物车列表组件
function CartItem({ item, setCart }) {
  // 创建状态 - count
  const [count, setCount] = useState(item.count)
  // 计数器修改后触发回到函数
  function countChange(e) {
    // 修改数量
    setCount(e.target.value)
    // 调用方法修改数量,并将返回值设置给父组件
    setCart(upDataCart(item._id, e.target.value))
  }
  // 使用数据
  return (
    <tbody className='ant-table-tbody'>
      <tr className='ant-table-row'>
        <td className='ant-table-cell'>
          <Image width={100} src={`${API}/product/photo/${item._id}`} />
        </td>
        <td className='ant-table-cell'>{item.name}</td>
        <td className='ant-table-cell'>{item.price}</td>
        <td className='ant-table-cell'>{item.category.name}</td>
        <td className='ant-table-cell'>
          <Input
            type='Number'
            value={count}
            // 添加事件
            onChange={e => countChange(e)}
            min={1}
          />
        </td>
        <td className='ant-table-cell'>
          {/* 删除按钮 */}
          <Button type='primary' danger>
            删除
          </Button>
        </td>
      </tr>
    </tbody>
  )
}

export default CartItem
// 其实这里可以直接使用度组件传递过来的count,不需要再创建一个count状态

删除购物车中商品

cart添加删除商品逻辑,获取所有商品,然后获取要删除商品的索引,然后根据索引删除商品,最后保存商品列表

点击删除按钮的时候调用该方法,传递要删除的商品id,同样需要更新父元素数据

// src/method/cart.js  添加删除商品方法

// 添加购物车逻辑,参数1:商品信息,参数2:回调函数
export function addItem(item, cb) {
  // 创建购物车列表
  let cart =
    typeof window !== 'undefined' && localStorage.getItem('cart')
      ? JSON.parse(localStorage.getItem('cart'))
      : []
  // 将商品添加进数组
  cart.push({
    ...item,
    count: 1,
  })
  // 去重处理,Set是ES6新语法,类似于数组但内部没有重复
  cart = Array.from(new Set(cart.map(i => i._id))).map(id =>
    cart.find(i => i._id === id)
  )
  // 将新的购物车列表本地化
  localStorage.setItem('cart', JSON.stringify(cart))
  // 调用回调函数
  cb()
}

// 获取购物车列表
export function getCart() {
  return typeof window !== 'undefined' && localStorage.getItem('cart')
    ? JSON.parse(localStorage.getItem('cart'))
    : []
}

// 修改数量方法
export function upDataCart(id, count) {
  let cart =
    typeof window !== 'undefined' && localStorage.getItem('cart')
      ? JSON.parse(localStorage.getItem('cart'))
      : []
  // 获取修改目标
  const my = cart.find(item => (item._id = id))
  // 修改目标数量
  my.count = count
  // 将新的购物车列表本地化
  localStorage.setItem('cart', JSON.stringify(cart))
  // 返回cart
  return cart
}

// 删除购物车商品
export function deleteCart(id) {
  let cart =
    typeof window !== 'undefined' && localStorage.getItem('cart')
      ? JSON.parse(localStorage.getItem('cart'))
      : []
  // 获取要删除目标索引
  const index = cart.findIndex(item => item._id === id)
  // 删除
  cart.splice(index, 1)
  // 将新的购物车列表本地化
  localStorage.setItem('cart', JSON.stringify(cart))
  // 返回cart
  return cart
}
// src/Components/core/cartItem.js  添加点击事件,点击后调用方法修改本地和父元素数据

import { Button, Image, Input } from 'antd'
import React, { useState } from 'react'
import { API } from '../../config'
import { deleteCart, upDataCart } from '../../method/cart'

// 单个购物车列表组件
function CartItem({ item, setCart }) {
  // 创建状态 - count
  const [count, setCount] = useState(item.count)
  // 计数器修改后触发回到函数
  function countChange(e) {
    // 修改数量
    setCount(e.target.value)
    // 调用方法修改数量,并将返回值设置给父组件
    setCart(upDataCart(item._id, e.target.value))
  }
  // 删除商品事件函数
  function deleteItem(id) {
    // 调用读元素方法修改父元素cart
    setCart(deleteCart(id))
  }
  // 使用数据
  return (
    <tbody className='ant-table-tbody'>
      <tr className='ant-table-row'>
        <td className='ant-table-cell'>
          <Image width={100} src={`${API}/product/photo/${item._id}`} />
        </td>
        <td className='ant-table-cell'>{item.name}</td>
        <td className='ant-table-cell'>{item.price}</td>
        <td className='ant-table-cell'>{item.category.name}</td>
        <td className='ant-table-cell'>
          <Input
            type='Number'
            value={count}
            // 添加事件
            onChange={e => countChange(e)}
            min={1}
          />
        </td>
        <td className='ant-table-cell'>
          {/* 删除按钮,添加点击事件 */}
          <Button type='primary' danger onClick={() => deleteItem(item._id)}>
            删除
          </Button>
        </td>
      </tr>
    </tbody>
  )
}

export default CartItem

构建购物车右侧布局

添加收货地址(存储为状态)

添加总价结构(存储为状态)

当购物车中数据发生变化时,计算总价(reduce方法),保留两位小数点(toFixed)

// src/Components/core/Cart.js  添加地址和总价格,并可且自动计算和修改

import { Col, Input, Row, Typography } from 'antd'
import React, { useEffect, useState } from 'react'
import { getCart } from '../../method/cart'
import CartItem from './cartItem'
import Layout from './Layout'
const { Title } = Typography

// 购物车组件
function Cart() {
  // 创建状态 - 地址
  const [address, setaddress] = useState('')
  // 创建状态 - 总价
  const [TotalPrice, setTotalPrice] = useState(0)
  // 创建状态 - 购物车列表,默认为空
  const [cart, setCart] = useState([])
  // 钩子函数
  useEffect(() => {
    // 调用函数获取购物车商品列表
    setCart(getCart())
  }, [])
  // 钩子函数,当cart变化时触发
  useEffect(() => {
    let newTotal = cart
      // reduce - 参数1:执行函数,参数2:初始值
      .reduce((total, item) => (total += item.count * item.price), 0)
      // 保留两位小数
      .toFixed(2)
    // 修改总价格
    setTotalPrice(newTotal)
  }, [cart])
  return (
    <Layout title='购物车' subTitle='您可以在这里购买您喜欢的产品了'>
      <Row gutter={16}>
        {/* 左右分栏 */}
        <Col span={18}>
          {/* 设置宽度撑满 */}
          <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>
            {/* 遍历列表 */}
            {cart.map(item => (
              // 引入单个购物车列表组件,把修改cart的方法传递下去
              <CartItem key={item._id} item={item} setCart={setCart} />
            ))}
          </table>
        </Col>
        {/* 右侧 */}
        <Col span={6}>
          {/* 收货地址文本框,添加onChange事件 - 修改状态 */}
          <Input value={address} onChange={e => setaddress(e.target.value)} />
          {address}
          <Title level={5}>总价格:{TotalPrice}元</Title>
        </Col>
      </Row>
    </Layout>
  )
}

export default Cart

增加支付按钮或者登录按钮

右侧支付按钮需要根据用户是否登陆来决定,未登录显示登录按钮

可以把按钮放到新的组件中

点击登录按钮跳转到登录页面

在顶部菜单栏添加购物车导航,不登录也能访问

// src/Components/core/Cart.js  引入底部按钮组件

import { Col, Input, Row, Typography } from 'antd'
import React, { useEffect, useState } from 'react'
import { getCart } from '../../method/cart'
import CartItem from './cartItem'
import Layout from './Layout'
import PayOrLogin from './PayOrLogin'
const { Title } = Typography

// 购物车组件
function Cart() {
  // 创建状态 - 地址
  const [address, setaddress] = useState('')
  // 创建状态 - 总价
  const [TotalPrice, setTotalPrice] = useState(0)
  // 创建状态 - 购物车列表,默认为空
  const [cart, setCart] = useState([])
  // 钩子函数
  useEffect(() => {
    // 调用函数获取购物车商品列表
    setCart(getCart())
  }, [])
  // 钩子函数,当cart变化时触发
  useEffect(() => {
    let newTotal = cart
      // reduce - 参数1:执行函数,参数2:初始值
      .reduce((total, item) => (total += item.count * item.price), 0)
      // 保留两位小数
      .toFixed(2)
    // 修改总价格
    setTotalPrice(newTotal)
  }, [cart])
  return (
    <Layout title='购物车' subTitle='您可以在这里购买您喜欢的产品了'>
      <Row gutter={16}>
        {/* 左右分栏 */}
        <Col span={18}>
          {/* 设置宽度撑满 */}
          <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>
            {/* 遍历列表 */}
            {cart.map(item => (
              // 引入单个购物车列表组件,把修改cart的方法传递下去
              <CartItem key={item._id} item={item} setCart={setCart} />
            ))}
          </table>
        </Col>
        {/* 右侧 */}
        <Col span={6}>
          {/* 收货地址文本框,添加onChange事件 - 修改状态 */}
          <Input value={address} onChange={e => setaddress(e.target.value)} />
          {address}
          <Title level={5}>总价格:{TotalPrice}元</Title>
          {/* 底部按钮,引入组件 */}
          <PayOrLogin />
        </Col>
      </Row>
    </Layout>
  )
}

export default Cart
// src/Components/core/PayOrLogin.js  新建底部按钮组件,根据登录与否返回不同按钮

import { Button } from 'antd'
import React from 'react'
import { Link } from 'react-router-dom'
import { loginOrNot } from '../../method/loginOrNot'

// 按钮组件
function PayOrLogin() {
  // 根据是否登陆返回不同结构
  function showButton() {
    return loginOrNot() ? (
      <Button>支付</Button>
    ) : (
      <Button>
        <Link to='./login'>登录</Link>
      </Button>
    )
  }
  // 调用函数执行生成结构
  return <>{showButton()}</>
}

export default PayOrLogin

实现创建支付账单

由于项目原有线上服务器到期,线上接口地址目前改为 ecommerce.fed.lagou.com/api

是否支付成功,系统都会给我们创建订单

点击提交按钮触发支付

传递参数

请求成功后后台会传递给我们要跳转的支付地址,使用loaction.href跳转

支付宝不允许在开发环境访问,需要打包为线上环境才能访问

由于可能事件后到cooke影响,最好使用无痕浏览打开

使用老师提供的支付宝账号进行购买操作

购买完成后会自动重定向回项目页面

// src/Components/core/PayOrLogin.js  支付按钮添加点击事件

import { Button } from 'antd'
import React from 'react'
import { Link } from 'react-router-dom'
import { loginOrNot } from '../../method/loginOrNot'

// 按钮组件
function PayOrLogin(props) {
  // 根据是否登陆返回不同结构
  function showButton() {
    return loginOrNot() ? (
      // 支付按钮添加点击事件,触发父组件传递过来的方法
      <Button onClick={props.pay}>支付</Button>
    ) : (
      <Button>
        <Link to='./login'>登录</Link>
      </Button>
    )
  }
  // 调用函数执行生成结构
  return <>{showButton()}</>
}

export default PayOrLogin
// src/Components/core/Cart.js  书写点击事件逻辑

import { Col, Input, Row, Typography } from 'antd'
import axios from 'axios'
import React, { useEffect, useState } from 'react'
import { API } from '../../config'
import { getCart } from '../../method/cart'
import { loginOrNot } from '../../method/loginOrNot'
import CartItem from './cartItem'
import Layout from './Layout'
import PayOrLogin from './PayOrLogin'
const { Title } = Typography

// 购物车组件
function Cart() {
  // 创建状态 - 地址
  const [address, setaddress] = useState('')
  // 创建状态 - 总价
  const [TotalPrice, setTotalPrice] = useState(0)
  // 创建状态 - 购物车列表,默认为空
  const [cart, setCart] = useState([])
  // 钩子函数
  useEffect(() => {
    // 调用函数获取购物车商品列表
    setCart(getCart())
  }, [])
  // 钩子函数,当cart变化时触发
  useEffect(() => {
    let newTotal = cart
      // reduce - 参数1:执行函数,参数2:初始值
      .reduce((total, item) => (total += item.count * item.price), 0)
      // 保留两位小数
      .toFixed(2)
    // 修改总价格
    setTotalPrice(newTotal)
  }, [cart])
  // 支付按钮点击事件
  function pay() {
    // 发起请求
    axios
      .post(`${API}/alipay`, {
        totalAmount: TotalPrice,
        subject: '测试标题',
        body: '测试订单描述',
        products: [
          cart.map(item => ({ product: item._id, count: item.count })),
        ],
        address: address,
        userId: loginOrNot().user._id,
      })
      .then(res => {
        // 成功后修改地址跳转支付宝支付
        window.location.href = res.data.result
      })
  }
  return (
    <Layout title='购物车' subTitle='您可以在这里购买您喜欢的产品了'>
      <Row gutter={16}>
        {/* 左右分栏 */}
        <Col span={18}>
          {/* 设置宽度撑满 */}
          <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>
            {/* 遍历列表 */}
            {cart.map(item => (
              // 引入单个购物车列表组件,把修改cart的方法传递下去
              <CartItem key={item._id} item={item} setCart={setCart} />
            ))}
          </table>
        </Col>
        {/* 右侧 */}
        <Col span={6}>
          {/* 收货地址文本框,添加onChange事件 - 修改状态 */}
          <Input value={address} onChange={e => setaddress(e.target.value)} />
          {address}
          <Title level={5}>总价格:{TotalPrice}元</Title>
          {/* 底部按钮,引入组件 */}
          <PayOrLogin pay={pay} />
        </Col>
      </Row>
    </Layout>
  )
}

export default Cart

获取并展示订单列表数据

因为刚刚是线上付款所以本地没有支付订单数据,老师提供了测试使用的数据

再mangodb可视化软件 - 我们的项目中新建一个集合,然后倒入老师给的json数据

创建订单列表组件(admin)、引入Layout布局、书写路由规则

给仪表盘组件按钮添加点击点击跳转订单列表页

订单列表组件初始化后获取全部订单列表(useeffect、useatate),传递参数

使用获取到的订单信息设置页头文本

使用老师给的表格书写订单列表

添加表头

老师文档里提供了英文转换中文方法,接用即可

使用dateformat处理时间格式

微调样式

// src/Components/admin/orders.js  创建订单组件

import axios from 'axios'
import React, { useEffect, useState } from 'react'
import { API } from '../../config'
import { loginOrNot } from '../../method/loginOrNot'
import Layout from '../core/Layout'
import { Row, Typography } from 'antd'
import dateFormat from 'dateformat'

// 订单列表组件
function Orders() {
  // 创建初始状态数据
  const [orders, setorders] = useState([])
  // 钩子函数
  useEffect(() => {
    // 获取用户信息
    const userInfo = loginOrNot()
    // 发起请求,获取订单
    axios
      .get(`${API}/order/list/${userInfo.user._id}`, {
        headers: {
          Authorization: `Bearer ${userInfo.token}`,
        },
      })
      .then(res => {
        setorders(res.data)
      })
  }, [])
  // 用于页头描述信息
  function subTitle() {
    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 '已取消'
      default:
        return
    }
  }
  return (
    // 布局容器,传递参数
    <Layout title='订单列表' subTitle={subTitle()}>
      {/* 遍历全部订单 */}
      {orders.map(item => (
        <Row key={item._id} style={{ margin: '0 0 50px 0' }}>
          <Typography.Title level={5}>订单ID:{item._id}</Typography.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(item.status)}</td>
                <td className='ant-table-cell'>{item.trade_no}</td>
                <td className='ant-table-cell'>{item.amount}</td>
                <td className='ant-table-cell'>
                  {dateFormat(item.createdAt, 'yyyy-mm-dd')}
                </td>
                <td className='ant-table-cell'>{item.address}</td>
                <td className='ant-table-cell'>{item.user}</td>
              </tr>
            </tbody>
          </table>
          {/* 遍历全部商品 */}
          {item.products.map(i => (
            <table key={i._id} style={{ width: '100%' }}>
              <thead className='ant-table-thead'>
                <tr>
                  <th className='ant-table-cell'>商品名称</th>
                  <th className='ant-table-cell'>商品价格</th>
                  <th className='ant-table-cell'>商品数量</th>
                  <th className='ant-table-cell'>商品ID</th>
                </tr>
              </thead>
              <tbody className='ant-table-tbody'>
                <tr className='ant-table-row'>
                  <td className='ant-table-cell'>{i.product.name}</td>
                  <td className='ant-table-cell'>{i.product.price}</td>
                  <td className='ant-table-cell'>{i.count}</td>
                  <td className='ant-table-cell'>{i.product._id}</td>
                </tr>
              </tbody>
            </table>
          ))}
        </Row>
      ))}
    </Layout>
  )
}

export default Orders
// src/Components/Routes.js  添加订单路由

import React from 'react'
// 路由需要使用的方法: BrowserRouter类似于HashRouter,更推荐使用
import { Switch, Route } from 'react-router-dom'
import AddSort from './admin/AddSort'
import AddWares from './admin/AddWares'
import AdminDashboard from './admin/AdminDashboard'
import GuardAdminDashboard from './admin/GuardAdminDashboard'
import GuardUserDashboard from './admin/GuardUserDashboard'
import Orders from './admin/orders'
import UserDashboard from './admin/UserDashboard'
import Cart from './core/Cart'
// 引入路由组件
import Home from './core/Home'
import Login from './core/Login'
import Logup from './core/Logup'
import Shop from './core/Shop'
import WaresInfo from './core/WaresInfo'

function Routes() {
  return (
    // 包裹开启路由功能(这里因为处理路由到store的时候已经做了,这里不需要了)
    // {/* 避免书写错误出现相同路由 */}
    <Switch>
      {/* 路由,第一个路由为默认路由,精准匹配 */}
      <Route path='/' component={Home} exact />
      <Route path='/shop' component={Shop} />
      <Route path='/login' component={Login} />
      <Route path='/logup' component={Logup} />
      <Route path='/orders' component={Orders} />
      {/* 使用路由守卫 */}
      <GuardUserDashboard path='/user/dashboard' component={UserDashboard} />
      <GuardAdminDashboard path='/admin/dashboard' component={AdminDashboard} />
      <GuardAdminDashboard path='/admin/addsort' component={AddSort} />
      <GuardAdminDashboard path='/admin/addwares' component={AddWares} />
      <Route path='/waresinfo/:waresId' component={WaresInfo} />
      <Route path='/cart' component={Cart} />
    </Switch>
  )
}

export default Routes
// src/Components/admin/AdminDashboard.js  添加点击跳转订单页面

import { Col, Row, Menu, Typography, Descriptions } from 'antd'
import React from 'react'
// 布局容器
import Layout from '../core/Layout'
import {
  PlusOutlined,
  AppstoreAddOutlined,
  UnorderedListOutlined,
} from '@ant-design/icons'
import { loginOrNot } from '../../method/loginOrNot'
import { Link } from 'react-router-dom/cjs/react-router-dom.min'
const { Title } = Typography

// 管理员信息组件 - 仪表盘
function AdminDashboard() {
  // 左侧展示函数
  const leftShow = () => {
    return (
      <>
        <Title level={5}>功能管理</Title>
        <Menu>
          {/* 点击跳转添加分类组件 */}
          <Menu.Item>
            <Link to='/admin/addsort'>
              <PlusOutlined />
              添加分类
            </Link>
          </Menu.Item>
          {/* 点击跳转添加商品组件 */}
          <Menu.Item>
            <Link to='/admin/addwares'>
              <AppstoreAddOutlined />
              添加商品
            </Link>
          </Menu.Item>
          {/* 点击跳转订单列表 */}
          <Menu.Item>
            <Link to='/orders'>
              <UnorderedListOutlined />
              订单列表
            </Link>
          </Menu.Item>
        </Menu>
      </>
    )
  }
  // 右侧展示函数
  const rightShow = () => {
    // 获取用户登录信息
    const {
      user: { name, email },
    } = loginOrNot()
    return (
      <Descriptions title='个人信息' bordered>
        <Descriptions.Item label='昵称'>{name}</Descriptions.Item>
        <Descriptions.Item label='电子邮箱'>{email}</Descriptions.Item>
        <Descriptions.Item label='角色'>管理员</Descriptions.Item>
      </Descriptions>
    )
  }
  return (
    // 使用布局容器,传递数据
    <Layout title='用户信息' subTitle='辛苦了,管理员同学'>
      {/* 行 */}
      <Row>
        {/* 列,调用函数渲染 */}
        <Col span={4}>{leftShow()}</Col>
        <Col span={20}>{rightShow()}</Col>
      </Row>
    </Layout>
  )
}

export default AdminDashboard

修改订单状态

给订单状态后面添加一个下拉菜单(删除原来的),提供几种状态可选,可以实现// eslint-disable-line修改状态

当下拉菜单状态变化(onChange)时修改订单状态,传递参数

发起请求后如果请求成功重新请求订单状态

// src/Components/admin/orders.js  组件添加下拉菜单,修改下拉菜单执行修改状态并重新获取数据

import axios from 'axios'
import React, { useEffect, useState } from 'react'
import { API } from '../../config'
import { loginOrNot } from '../../method/loginOrNot'
import Layout from '../core/Layout'
import { Row, Select, Typography } from 'antd'
import dateFormat from 'dateformat'

// 订单列表组件
function Orders() {
  // 创建初始状态数据
  const [orders, setorders] = useState([])
  // 获取用户信息
  const userInfo = loginOrNot()
  // 单独提取获取数据方法
  async function getorders() {
    // 发起请求,获取订单
    const { data } = await axios.get(`${API}/order/list/${userInfo.user._id}`, {
      headers: {
        Authorization: `Bearer ${userInfo.token}`,
      },
    })
    // 修改状态
    setorders(data)
  }
  // 钩子函数
  useEffect(() => {
    // 直接调用
    getorders()
  }, []) // eslint-disable-line
  // 用于页头描述信息
  function subTitle() {
    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 '已取消'
      default:
        return
    }
  }
  // 修改状态方法,默认传递了一个status,我们自己添加了一个orderId
  const statusChange = orderId => status => {
    axios
      .put(
        `${API}/order/status/${userInfo.user._id}`,
        {
          orderId,
          status,
        },
        {
          headers: {
            Authorization: `Bearer ${userInfo.token}`,
          },
        }
      )
      .then(() => {
        // 成功后直接调用
        getorders()
      })
  }
  return (
    // 布局容器,传递参数
    <Layout title='订单列表' subTitle={subTitle()}>
      {/* 遍历全部订单 */}
      {orders.map(item => (
        <Row key={item._id} style={{ margin: '0 0 50px 0' }}>
          <Typography.Title level={5}>订单ID:{item._id}</Typography.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(item.status)}
                  {/* 修改状态下拉菜单,状态修改触发事件 */}
                  <Select
                    defaultValue={item.status}
                    onChange={statusChange(item._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'>{item.trade_no}</td>
                <td className='ant-table-cell'>{item.amount}</td>
                <td className='ant-table-cell'>
                  {dateFormat(item.createdAt, 'yyyy-mm-dd')}
                </td>
                <td className='ant-table-cell'>{item.address}</td>
                <td className='ant-table-cell'>{item.user}</td>
              </tr>
            </tbody>
          </table>
          {/* 遍历全部商品 */}
          {item.products.map(i => (
            <table key={i._id} style={{ width: '100%' }}>
              <thead className='ant-table-thead'>
                <tr>
                  <th className='ant-table-cell'>商品名称</th>
                  <th className='ant-table-cell'>商品价格</th>
                  <th className='ant-table-cell'>商品数量</th>
                  <th className='ant-table-cell'>商品ID</th>
                </tr>
              </thead>
              <tbody className='ant-table-tbody'>
                <tr className='ant-table-row'>
                  <td className='ant-table-cell'>{i.product.name}</td>
                  <td className='ant-table-cell'>{i.product.price}</td>
                  <td className='ant-table-cell'>{i.count}</td>
                  <td className='ant-table-cell'>{i.product._id}</td>
                </tr>
              </tbody>
            </table>
          ))}
        </Row>
      ))}
    </Layout>
  )
}

export default Orders