React+Redux+Ant Design+TypeScript 电子商务实战-客户端应用 03 分类、产品

创建添加分类组件

创建组件文件

// src\components\admin\AddCategory.tsx
import { Button, Form, Input } from 'antd'
import { Link } from 'react-router-dom'
import Layout from '../core/Layout'

const AddCategory = () => {
  const onFinish = (value: { name: string }) => {
    console.log(value)
  }

  return (
    <Layout title="添加分类" subTitle="">
      <Form onFinish={onFinish}>
        <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\Routes.tsx
import { HashRouter, Route, Switch } from 'react-router-dom'
import AddCategory from './components/admin/AddCategory'
import AdminDashboard from './components/admin/AdminDashboard'
import AdminRoute from './components/admin/AdminRoute'
import Dashboard from './components/admin/Dashboard'
import PrivateRoute from './components/admin/PrivateRoute'
import Home from './components/core/Home'
import Shop from './components/core/Shop'
import Signin from './components/core/Signin'
import Signup from './components/core/Signup'

const Routes = () => {
  return (
    <HashRouter>
      <Switch>
        <Route path="/" component={Home} exact />
        <Route path="/shop" component={Shop} />
        <Route path="/signin" component={Signin} />
        <Route path="/signup" component={Signup} />
        <PrivateRoute path="/user/dashboard" component={Dashboard} />
        <AdminRoute path="/admin/dashboard" component={AdminDashboard} />
        <AdminRoute path="/create/category" component={AddCategory} />
      </Switch>
    </HashRouter>
  )
}

export default Routes

配置链接入口

// src\components\admin\AdminDashboard.tsx
import { Col, Descriptions, Menu, Row, Typography } from 'antd'
import { Link } from 'react-router-dom'
import Layout from '../core/Layout'
import { ShoppingCartOutlined, UserOutlined, OrderedListOutlined } from '@ant-design/icons'
import { Jwt } from '../../store/models/auth'
import { isAuth } from '../../helpers/auth'

const { Title } = Typography

const AdminDashboard = () => {
  const {
    user: { name, email }
  } = isAuth() as Jwt

  const adminLinks = () => (
    <>
      <Title level={5}>管理员链接</Title>
      <Menu style={{ borderRight: 0 }}>
        <Menu.Item>
          <ShoppingCartOutlined style={{ marginRight: '5px' }} />
          <Link to="/create/category">添加分类</Link>
        </Menu.Item>
        <Menu.Item>
          <UserOutlined style={{ marginRight: '5px' }} />
          <Link to="">添加产品</Link>
        </Menu.Item>
        <Menu.Item>
          <OrderedListOutlined style={{ marginRight: '5px' }} />
          <Link to="">订单列表</Link>
        </Menu.Item>
      </Menu>
    </>
  )

  const adminInfo = () => (
    <Descriptions title="管理员信息" bordered>
      <Descriptions.Item label="昵称">{name}</Descriptions.Item>
      <Descriptions.Item label="邮箱">{email}</Descriptions.Item>
      <Descriptions.Item label="角色">管理员</Descriptions.Item>
    </Descriptions>
  )

  return (
    <Layout title="管理员 dashboard" subTitle="">
      <Row>
        <Col span="4">{adminLinks()}</Col>
        <Col span="20">{adminInfo()}</Col>
      </Row>
    </Layout>
  )
}

export default AdminDashboard

实现添加分类功能

// src\components\admin\AddCategory.tsx
import { Button, Form, Input, message } from 'antd'
import axios from 'axios'
import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { API } from '../../config'
import { isAuth } from '../../helpers/auth'
import { Jwt } from '../../store/models/auth'
import Layout from '../core/Layout'

const AddCategory = () => {
  const [name, setName] = useState<string>('')

  const { user, token } = isAuth() as Jwt

  useEffect(() => {
    async function addCategory() {
      try {
        const response = await axios.post<{ name: string }>(
          `${API}/category/create/${user._id}`,
          {
            name: name
          },
          {
            headers: {
              Authorization: `Bearer ${token}`
            }
          }
        )

        message.success(`[${response.data.name}] 分类添加成功`)
      } catch (error) {
        message.error(`${error.response.data.errors[0]}`)
      }
    }

    if (name) {
      addCategory()
    }
  }, [name])

  const onFinish = (value: { name: string }) => {
    setName(value.name)
  }

  return (
    <Layout title="添加分类" subTitle="">
      <Form onFinish={onFinish}>
        <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.tsx
import { Button, Form, Input, Select, Upload } from 'antd'
import Layout from '../core/Layout'

const AddProduct = () => {
  return (
    <Layout title="添加产品" subTitle="">
      <Form>
        <Form.Item label="上传封面">
          <Upload>
            <Button>上传产品封面</Button>
          </Upload>
        </Form.Item>
        <Form.Item name="name" label="产品名称">
          <Input />
        </Form.Item>
        <Form.Item name="description" label="产品描述">
          <Input />
        </Form.Item>
        <Form.Item name="price" label="产品价格">
          <Input />
        </Form.Item>
        <Form.Item name="category" label="所属分类">
          <Select>
            <Select.Option value="">请选择分类</Select.Option>
            <Select.Option value="1">测试分类</Select.Option>
          </Select>
        </Form.Item>
        <Form.Item name="quantity" label="产品库存">
          <Input />
        </Form.Item>
        <Form.Item name="shipping" label="是否需要运输">
          <Select>
            <Select.Option value={1}></Select.Option>
            <Select.Option value={0}></Select.Option>
          </Select>
        </Form.Item>
        <Form.Item>
          <Button type="primary" htmlType="submit">
            添加产品
          </Button>
        </Form.Item>
      </Form>
    </Layout>
  )
}

export default AddProduct

配置路由

// src\Routes.tsx
import { HashRouter, Route, Switch } from 'react-router-dom'
import AddCategory from './components/admin/AddCategory'
import AddProduct from './components/admin/AddProduct'
import AdminDashboard from './components/admin/AdminDashboard'
import AdminRoute from './components/admin/AdminRoute'
import Dashboard from './components/admin/Dashboard'
import PrivateRoute from './components/admin/PrivateRoute'
import Home from './components/core/Home'
import Shop from './components/core/Shop'
import Signin from './components/core/Signin'
import Signup from './components/core/Signup'

const Routes = () => {
  return (
    <HashRouter>
      <Switch>
        <Route path="/" component={Home} exact />
        <Route path="/shop" component={Shop} />
        <Route path="/signin" component={Signin} />
        <Route path="/signup" component={Signup} />
        <PrivateRoute path="/user/dashboard" component={Dashboard} />
        <AdminRoute path="/admin/dashboard" component={AdminDashboard} />
        <AdminRoute path="/create/category" component={AddCategory} />
        <AdminRoute path="/create/product" component={AddProduct} />
      </Switch>
    </HashRouter>
  )
}

export default Routes

配置链接入口

// src\components\admin\AdminDashboard.tsx
import { Col, Descriptions, Menu, Row, Typography } from 'antd'
import { Link } from 'react-router-dom'
import Layout from '../core/Layout'
import { ShoppingCartOutlined, UserOutlined, OrderedListOutlined } from '@ant-design/icons'
import { Jwt } from '../../store/models/auth'
import { isAuth } from '../../helpers/auth'

const { Title } = Typography

const AdminDashboard = () => {
  const {
    user: { name, email }
  } = isAuth() as Jwt

  const adminLinks = () => (
    <>
      <Title level={5}>管理员链接</Title>
      <Menu style={{ borderRight: 0 }}>
        <Menu.Item>
          <ShoppingCartOutlined style={{ marginRight: '5px' }} />
          <Link to="/create/category">添加分类</Link>
        </Menu.Item>
        <Menu.Item>
          <UserOutlined style={{ marginRight: '5px' }} />
          <Link to="/create/product">添加产品</Link>
        </Menu.Item>
        <Menu.Item>
          <OrderedListOutlined style={{ marginRight: '5px' }} />
          <Link to="">订单列表</Link>
        </Menu.Item>
      </Menu>
    </>
  )

  const adminInfo = () => (
    <Descriptions title="管理员信息" bordered>
      <Descriptions.Item label="昵称">{name}</Descriptions.Item>
      <Descriptions.Item label="邮箱">{email}</Descriptions.Item>
      <Descriptions.Item label="角色">管理员</Descriptions.Item>
    </Descriptions>
  )

  return (
    <Layout title="管理员 dashboard" subTitle="">
      <Row>
        <Col span="4">{adminLinks()}</Col>
        <Col span="20">{adminInfo()}</Col>
      </Row>
    </Layout>
  )
}

export default AdminDashboard

获取分类列表

创建分类相关的 action

// src\store\models\category.ts
export interface Category {
  _id: string
  name: string
}

// src\store\actions\category.action.ts
import { Category } from '../models/category'

export const GET_CATEGORY = 'GET_CATEGORY'
export const GET_CATEGORY_SUCCESS = 'GET_CATEGORY_SUCCESS'

export interface GetCategoryAction {
  type: typeof GET_CATEGORY
}

export interface GetCategorySuccessAction {
  type: typeof GET_CATEGORY_SUCCESS
  payload: Category[]
}

export const getCategory = (): GetCategoryAction => ({
  type: GET_CATEGORY
})

export const getCategorySuccess = (payload: Category[]): GetCategorySuccessAction => ({
  type: GET_CATEGORY_SUCCESS,
  payload
})

// action 的联合类型
export type CategoryUnionType = GetCategoryAction | GetCategorySuccessAction

定义 reducer

// src\store\reducers\category.reducer.ts
import { CategoryUnionType, GET_CATEGORY, GET_CATEGORY_SUCCESS } from '../actions/category.action'
import { Category } from '../models/category'

export interface CategoryState {
  category: {
    loaded: boolean
    success: boolean
    result: Category[]
  }
}

const initialState: CategoryState = {
  category: {
    loaded: false,
    success: false,
    result: []
  }
}

export default function categoryReducer(state = initialState, action: CategoryUnionType) {
  switch (action.type) {
    case GET_CATEGORY:
      return {
        ...state,
        category: {
          loaded: false,
          success: false,
          result: []
        }
      }
    case GET_CATEGORY_SUCCESS:
      return {
        ...state,
        category: {
          loaded: true,
          success: true,
          result: action.payload
        }
      }
    default:
      return state
  }
}

// src\store\reducers\index.ts
import { connectRouter, RouterState } from 'connected-react-router'
import { History } from 'history'
import { combineReducers } from 'redux'
import authReducer, { AuthState } from './auth.reducer'
import categoryReducer, { CategoryState } from './category.reducer'
// import testReducer from './test.reducer'

// 定义一个包含 router 的 store 类型接口 供外部使用
export interface AppState {
  router: RouterState
  auth: AuthState
  category: CategoryState
}

const createRootReducer = (history: History) =>
  combineReducers({
    // test: testReducer,
    router: connectRouter(history),
    auth: authReducer,
    category: categoryReducer
  })

export default createRootReducer

定义 sage

// src\store\sagas\category.sage.ts
import axios, { AxiosResponse } from 'axios'
import { put, takeEvery } from 'redux-saga/effects'
import { API } from '../../config'
import { getCategorySuccess, GET_CATEGORY } from '../actions/category.action'
import { Category } from '../models/category'

function* handleGetCategory() {
  const response: AxiosResponse = yield axios.get<Category[]>(`${API}/categories`)

  yield put(getCategorySuccess(response.data))
}

export default function* categorySage() {
  // 获取分类列表
  yield takeEvery(GET_CATEGORY, handleGetCategory)
}

// src\store\sagas\index.ts
import { all } from 'redux-saga/effects'
import authSaga from './auth.saga'
import categorySage from './category.sage'

export default function* rootSaga() {
  yield all([authSaga(), categorySage()])
}

修改组件

// src\components\admin\AddProduct.tsx
import { Button, Form, Input, Select, Upload } from 'antd'
import Layout from '../core/Layout'
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { getCategory } from '../../store/actions/category.action'
import { AppState } from '../../store/reducers'
import { CategoryState } from '../../store/reducers/category.reducer'

const AddProduct = () => {
  const dispatch = useDispatch()

  const category = useSelector<AppState, CategoryState>(state => state.category)

  useEffect(() => {
    dispatch(getCategory())
  }, [])

  return (
    <Layout title="添加产品" subTitle="">
      <Form
        initialValues={{
          category: '',
          shipping: 0
        }}
      >
        <Form.Item>
          <Upload label="上传封面">
            <Button>上传产品封面</Button>
          </Upload>
        </Form.Item>
        <Form.Item name="name" label="产品名称">
          <Input />
        </Form.Item>
        <Form.Item name="description" label="产品描述">
          <Input />
        </Form.Item>
        <Form.Item name="price" label="产品价格">
          <Input />
        </Form.Item>
        <Form.Item name="category" label="所属分类">
          <Select>
            <Select.Option value="">请选择分类</Select.Option>
            {category.category.result.map(item => (
              <Select.Option key={item._id} value={item._id}>
                {item.name}
              </Select.Option>
            ))}
          </Select>
        </Form.Item>
        <Form.Item name="quantity" label="产品库存">
          <Input />
        </Form.Item>
        <Form.Item name="shipping" label="是否需要运输">
          <Select>
            <Select.Option value={1}></Select.Option>
            <Select.Option value={0}></Select.Option>
          </Select>
        </Form.Item>
        <Form.Item>
          <Button type="primary" htmlType="submit">
            添加产品
          </Button>
        </Form.Item>
      </Form>
    </Layout>
  )
}

export default AddProduct

实现添加产品功能

// src\components\admin\AddProduct.tsx
import { Button, Form, Input, message, Select, Upload } from 'antd'
import { PlusOutlined } from '@ant-design/icons'
import Layout from '../core/Layout'
import { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { getCategory } from '../../store/actions/category.action'
import { AppState } from '../../store/reducers'
import { CategoryState } from '../../store/reducers/category.reducer'
import { RcFile, UploadProps } from 'antd/lib/upload'
import axios from 'axios'
import { API } from '../../config'
import { isAuth } from '../../helpers/auth'
import { Jwt } from '../../store/models/auth'

// 将图片转化为 Data Url
function getBase64(blob: RcFile, callback: (e: string) => void) {
  const reader = new FileReader()
  reader.addEventListener('load', () => callback(reader.result as string))
  reader.readAsDataURL(blob)
}

const AddProduct = () => {
  const dispatch = useDispatch()
  const [file, setFile] = useState<RcFile>()

  const category = useSelector<AppState, CategoryState>(state => state.category)

  useEffect(() => {
    dispatch(getCategory())
  }, [])

  // 产品封面 Data Url
  const [imgUrl, setImgUrl] = useState<string>()
  // 上传产品封面按钮
  const uploadButton = (
    <div>
      <PlusOutlined />
      <div style={{ marginTop: 8 }}>Upload</div>
    </div>
  )
  // Upload 组件属性
  const props: UploadProps = {
    accept: 'image/*',
    showUploadList: false,
    listType: 'picture-card',
    beforeUpload(file: RcFile) {
      // 获取文件保存下来等待和表单一起提交
      setFile(file)
      // 阻止默认上传行为(发送请求)
      return false
    },
    onChange({ file }) {
      // 获取图片的 Data Url 显示在页面
      getBase64(file as RcFile, (dataUrl: string) => {
        setImgUrl(dataUrl)
      })
    }
  }

  const addProductForm = () => {
    const { user, token } = isAuth() as Jwt

    const onFinish = (product: any) => {
      // 要将二进制图片和普通文本字段结合在一起发送需要用到 FormData
      const formData = new FormData()
      for (let attr in product) {
        formData.set(attr, product[attr])
      }
      if (typeof file !== 'undefined') {
        formData.set('photo', file)
      }

      axios
        .post(`${API}/product/create/${user._id}`, formData, {
          headers: {
            Authorization: `Bearer ${token}`
          }
        })
        .then(() => {
          message.success('产品添加成功')
        })
        .catch(() => {
          message.success('产品添加失败')
        })
    }

    return (
      <Form
        onFinish={onFinish}
        initialValues={{
          category: '',
          shipping: 0
        }}
      >
        <Form.Item label="上传封面">
          <Upload {...props}>
            {imgUrl ? (
              <img src={imgUrl} alt="产品封面" style={{ maxWidth: '100%', maxHeight: '100%' }} />
            ) : (
              uploadButton
            )}
          </Upload>
        </Form.Item>
        <Form.Item name="name" label="产品名称">
          <Input />
        </Form.Item>
        <Form.Item name="description" label="产品描述">
          <Input />
        </Form.Item>
        <Form.Item name="price" label="产品价格">
          <Input />
        </Form.Item>
        <Form.Item name="category" label="所属分类">
          <Select>
            <Select.Option value="">请选择分类</Select.Option>
            {category.category.result.map(item => (
              <Select.Option key={item._id} value={item._id}>
                {item.name}
              </Select.Option>
            ))}
          </Select>
        </Form.Item>
        <Form.Item name="quantity" label="产品库存">
          <Input />
        </Form.Item>
        <Form.Item name="shipping" label="是否需要运输">
          <Select>
            <Select.Option value={1}></Select.Option>
            <Select.Option value={0}></Select.Option>
          </Select>
        </Form.Item>
        <Form.Item>
          <Button type="primary" htmlType="submit">
            添加产品
          </Button>
        </Form.Item>
      </Form>
    )
  }

  return (
    <Layout title="添加产品" subTitle="">
      {addProductForm()}
    </Layout>
  )
}

export default AddProduct

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值