React+Redux+Ant Design+TypeScript 电子商务实战-客户端应用 02 登录注册

创建导航菜单

// src\components\core\Navigation.tsx
import { Menu } from 'antd'
import { Link } from 'react-router-dom'
import { useSelector } from 'react-redux'
import { AppState } from '../../store/reducers'
import { RouterState } from 'connected-react-router'

// 判断选中类名的钩子函数
function useActive(currentPath: string, path: string): string {
  return currentPath === path ? 'ant-menu-item-selected' : ''
}

const Navigation = () => {
  const router = useSelector<AppState, RouterState>(state => state.router)
  const pathname = router.location.pathname

  const isHome = useActive(pathname, '/')
  const isShop = useActive(pathname, '/shop')

  return (
    <Menu mode="horizontal" selectable={false}>
      <Menu.Item className={isHome}>
        <Link to="/">首页</Link>
      </Menu.Item>
      <Menu.Item className={isShop}>
        <Link to="/shop">商城</Link>
      </Menu.Item>
    </Menu>
  )
}

export default Navigation

// src\components\core\Layout.tsx
import React, { FC } from 'react'
import Navigation from './Navigation'

// 定义 Layout 组件参数类型的接口
interface Props {
  children: React.ReactNode
}

// FC 表示函数型组件类型
const Layout: FC<Props> = ({ children }) => {
  return (
    <div>
      <Navigation></Navigation>
      <div style={{ width: '85%', minWidth: '980px', margin: '0 auto' }}>{children}</div>
    </div>
  )
}

export default Layout

创建页头

页头组件

// src\components\core\Layout.tsx
import { PageHeader } from 'antd'
import React, { FC } from 'react'
import Navigation from './Navigation'

// 定义 Layout 组件参数类型的接口
interface Props {
  children: React.ReactNode
  title: string
  subTitle: string
}

// FC 表示函数型组件类型
const Layout: FC<Props> = ({ children, title, subTitle }) => {
  return (
    <div>
      <Navigation></Navigation>
      <PageHeader className="jumbotron" title={title} subTitle={subTitle} />
      <div style={{ width: '85%', minWidth: '980px', margin: '0 auto' }}>{children}</div>
    </div>
  )
}

export default Layout

页头样式

/* src\style.css */
.jumbotron {
  background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
  background-size: 400% 400%;
  -webkit-animation: Gradient 15s ease infinite;
  -moz-animation: Gradient 15s ease infinite;
  animation: Gradient 15s ease infinite;
  margin-bottom: 25px;
}

.jumbotron span {
  color: #fff;
}

@-webkit-keyframes Gradient {
  0% {
    background-position: 0% 50%;
  }
  50% {
    background-position: 100% 50%;
  }
  100% {
    background-position: 0% 50%;
  }
}

@-moz-keyframes Gradient {
  0% {
    background-position: 0% 50%;
  }
  50% {
    background-position: 100% 50%;
  }
  100% {
    background-position: 0% 50%;
  }
}

@keyframes Gradient {
  0% {
    background-position: 0% 50%;
  }
  50% {
    background-position: 100% 50%;
  }
  100% {
    background-position: 0% 50%;
  }
}

引入样式表

// src\index.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import 'antd/dist/antd.css'
import './style.css'
import Routes from './Routes'
import { Provider } from 'react-redux'
import store, { history } from './store'
import { ConnectedRouter } from 'connected-react-router'

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <ConnectedRouter history={history}>
        <Routes />
      </ConnectedRouter>
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
)

页面组件传递 title 和 subTitle

// src\components\core\Home.tsx
import { useSelector } from 'react-redux'
import Layout from './Layout'

const Home = () => {
  const state = useSelector(state => state)
  return (
    <Layout title="RM商城" subTitle="优享品质 惊喜价格">
      Home {JSON.stringify(state)}
    </Layout>
  )
}

export default Home

// src\components\core\Shop.tsx
import { useSelector } from 'react-redux'
import Layout from './Layout'

const Shop = () => {
  const state = useSelector(state => state)
  return (
    <Layout title="RM商城" subTitle="挑选你喜欢的商品把">
      Shop {JSON.stringify(state)}
    </Layout>
  )
}

export default Shop

构建注册和登录表单

注册页面组件

// src\components\core\Signup.tsx
import Layout from './Layout'
import { Button, Form, Input } from 'antd'

const Signup = () => {
  return (
    <Layout title="注册" subTitle="还没有账号?注册一个吧">
      <Form>
        <Form.Item name="name" label="昵称">
          <Input />
        </Form.Item>
        <Form.Item name="password" label="密码">
          <Input.Password />
        </Form.Item>
        <Form.Item name="email" label="邮箱">
          <Input />
        </Form.Item>
        <Form.Item>
          <Button type="primary" htmlType="submit">
            注册
          </Button>
        </Form.Item>
      </Form>
    </Layout>
  )
}

export default Signup

登录页面组件

// src\components\core\Signin.tsx
import Layout from './Layout'
import { Button, Form, Input } from 'antd'

const Signin = () => {
  return (
    <Layout title="登录" subTitle="">
      <Form>
        <Form.Item name="email" label="邮箱">
          <Input />
        </Form.Item>
        <Form.Item name="password" label="密码">
          <Input.Password />
        </Form.Item>
        <Form.Item>
          <Button type="primary" htmlType="submit">
            登录
          </Button>
        </Form.Item>
      </Form>
    </Layout>
  )
}

export default Signin

配置路由

// src\Routes.tsx
import { HashRouter, Route, Switch } from 'react-router-dom'
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} />
      </Switch>
    </HashRouter>
  )
}

export default Routes

配置导航菜单

// src\components\core\Navigation.tsx
import { Menu } from 'antd'
import { Link } from 'react-router-dom'
import { useSelector } from 'react-redux'
import { AppState } from '../../store/reducers'
import { RouterState } from 'connected-react-router'

// 判断选中类名的钩子函数
function useActive(currentPath: string, path: string): string {
  return currentPath === path ? 'ant-menu-item-selected' : ''
}

const Navigation = () => {
  const router = useSelector<AppState, RouterState>(state => state.router)
  const pathname = router.location.pathname

  const isHome = useActive(pathname, '/')
  const isShop = useActive(pathname, '/shop')
  const isSignin = useActive(pathname, '/signin')
  const isSignup = useActive(pathname, '/signup')

  return (
    <Menu mode="horizontal" selectable={false}>
      <Menu.Item className={isHome}>
        <Link to="/">首页</Link>
      </Menu.Item>
      <Menu.Item className={isShop}>
        <Link to="/shop">商城</Link>
      </Menu.Item>
      <Menu.Item className={isSignin}>
        <Link to="/signin">登录</Link>
      </Menu.Item>
      <Menu.Item className={isSignup}>
        <Link to="/signup">注册</Link>
      </Menu.Item>
    </Menu>
  )
}

export default Navigation

实现注册的 Redux 流程

和注册相关的 action

// src\store\actions\auth.action.ts
// action.type 常量
export const SIGNUP = 'SIGNUP' // 发送注册请求
export const SIGNUP_SUCCESS = 'SIGNUP_SUCCESS' // 注册成功
export const SIGNUP_FAIL = 'SIGNUP_FAIL' // 注册失败

// action.payload 接口类型
export interface SignupPayload {
  email: string
  name: string
  password: string
}

// action 对象接口类型
export interface SignupAction {
  type: typeof SIGNUP
  payload: SignupPayload
}
export interface SignupSuccessAction {
  type: typeof SIGNUP_SUCCESS
}
export interface SignupFailAction {
  type: typeof SIGNUP_FAIL
  message: string
}

// actionCreator
export const signup = (payload: SignupPayload): SignupAction => ({
  type: SIGNUP,
  payload
})
export const signupSuccess = (): SignupSuccessAction => ({
  type: SIGNUP_SUCCESS
})
export const signupFail = (message: string): SignupFailAction => ({
  type: SIGNUP_FAIL,
  message
})

// action 的联合类型
export type AuthUnionType = SignupAction | SignupSuccessAction | SignupFailAction

定义 reducer

// src\store\reducers\auth.reducer.ts
import { AuthUnionType, SIGNUP, SIGNUP_FAIL, SIGNUP_SUCCESS } from '../actions/auth.action'

// state 接口类型
export interface AuthState {
  signup: {
    loaded: boolean
    success: boolean
    message: string
  }
}

// state 默认值
const initialState: AuthState = {
  signup: {
    loaded: false, // 注册请求是否结束
    success: false, // 注册是否成功
    message: '' // 注册失败提示
  }
}

export default function authReducer(state = initialState, action: AuthUnionType) {
  switch (action.type) {
    // 发送注册请求
    case SIGNUP:
      return {
        ...state,
        signup: {
          loaded: false,
          success: false
        }
      }
    // 注册成功
    case SIGNUP_SUCCESS:
      return {
        ...state,
        signup: {
          loaded: true,
          success: true
        }
      }
    // 注册失败
    case SIGNUP_FAIL:
      return {
        ...state,
        signup: {
          loaded: true,
          success: false,
          message: action.message
        }
      }
    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 testReducer from './test.reducer'

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

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

export default createRootReducer

定义 saga 接收请求

// src\store\sagas\auth.saga.ts
import axios from 'axios'
import { takeEvery, put } from 'redux-saga/effects'
import { API } from '../../config'
import { SIGNUP, SignupAction, signupFail, signupSuccess } from '../actions/auth.action'

function* handleSignup(action: SignupAction) {
  try {
    const response = yield axios.post(`${API}/signup`, action.payload)
    yield put(signupSuccess())
  } catch (error) {
    yield put(signupFail(error.response.data.errors[0]))
  }
}

export default function* authSaga() {
  yield takeEvery(SIGNUP, handleSignup)
}

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

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

// src\store\index.ts
import { applyMiddleware, createStore } from 'redux'
import createRootReducer from './reducers'
import { createHashHistory } from 'history'
import { routerMiddleware } from 'connected-react-router'
import { composeWithDevTools } from 'redux-devtools-extension'
import createSagaMiddleware from '@redux-saga/core'
import rootSaga from './sagas'

export const history = createHashHistory()

const sagaMiddleware = createSagaMiddleware()

const store = createStore(
  createRootReducer(history),
  composeWithDevTools(applyMiddleware(routerMiddleware(history), sagaMiddleware))
)

sagaMiddleware.run(rootSaga)

export default store

修改页面组件

// src\components\core\Signup.tsx
import Layout from './Layout'
import { Button, Form, Input } from 'antd'
import { signup, SignupPayload } from '../../store/actions/auth.action'
import { useDispatch } from 'react-redux'

const Signup = () => {
  // 获取 dispatch 方法
  const dispatch = useDispatch()

  // 注册表单提交
  const onFinish = (value: SignupPayload) => {
    // 发送注册请求
    dispatch(signup(value))
  }

  return (
    <Layout title="注册" subTitle="还没有账号?注册一个吧">
      <Form onFinish={onFinish}>
        <Form.Item name="name" label="昵称">
          <Input />
        </Form.Item>
        <Form.Item name="password" label="密码">
          <Input.Password />
        </Form.Item>
        <Form.Item name="email" label="邮箱">
          <Input />
        </Form.Item>
        <Form.Item>
          <Button type="primary" htmlType="submit">
            注册
          </Button>
        </Form.Item>
      </Form>
    </Layout>
  )
}

export default Signup

处理注册结果

实现内容

  1. 注册成功 清空表单
  2. 注册成功 显示成功的提示信息
  3. 注册失败 显示失败的提示信息
  4. 离开页面之前 重置状态
    • 注册状态如果未重置,返回该页面仍然会显示注册结果和提示

刷新表单和显示提示信息

// src\components\core\Signup.tsx
import Layout from './Layout'
import { Button, Form, Input, Result } from 'antd'
import { signup, SignupPayload } from '../../store/actions/auth.action'
import { useDispatch, useSelector } from 'react-redux'
import { AppState } from '../../store/reducers'
import { AuthState } from '../../store/reducers/auth.reducer'
import { useEffect } from 'react'
import { Link } from 'react-router-dom'

const Signup = () => {
  // 获取 dispatch 方法
  const dispatch = useDispatch()

  // 获取注册结果
  const auth = useSelector<AppState, AuthState>(state => state.auth)

  // 创建表单数据域
  // 用于绑定到 Form 组件上操作表单
  const [form] = Form.useForm()

  // 注册表单提交
  const onFinish = (value: SignupPayload) => {
    // 发送注册请求
    dispatch(signup(value))
  }

  // 监听状态
  useEffect(() => {
    // 1. 注册成功 清空表单
    if (auth.signup.loaded && auth.signup.success) {
      form.resetFields()
    }
  }, [auth])

  // 2. 注册成功 显示成功的提示信息
  const showSuccess = () => {
    if (auth.signup.loaded && auth.signup.success) {
      return (
        <Result
          status="success"
          title="注册成功"
          extra={[
            <Button type="primary">
              <Link to="/signin">登录</Link>
            </Button>
          ]}
        />
      )
    }
  }

  // 3. 注册失败 显示失败的提示信息
  const showError = () => {
    if (auth.signup.loaded && !auth.signup.success) {
      return <Result status="warning" title="注册失败" subTitle={auth.signup.message} />
    }
  }

  // 4. 离开页面之前 重置页面状态
  useEffect(() => {
    // 离开页面时会执行这个方法
    return () => {
      // 稍后写
    }
  }, [])

  // 注册表单
  const signupForm = () => (
    <Form form={form} onFinish={onFinish}>
      <Form.Item name="name" label="昵称">
        <Input />
      </Form.Item>
      <Form.Item name="password" label="密码">
        <Input.Password />
      </Form.Item>
      <Form.Item name="email" label="邮箱">
        <Input />
      </Form.Item>
      <Form.Item>
        <Button type="primary" htmlType="submit">
          注册
        </Button>
      </Form.Item>
    </Form>
  )

  return (
    <Layout title="注册" subTitle="还没有账号?注册一个吧">
      {showSuccess()}
      {showError()}
      {signupForm()}
    </Layout>
  )
}

export default Signup

重置注册状态

定义 action

// src\store\actions\auth.action.ts
// action.type 常量
export const SIGNUP = 'SIGNUP' // 发送注册请求
export const SIGNUP_SUCCESS = 'SIGNUP_SUCCESS' // 注册成功
export const SIGNUP_FAIL = 'SIGNUP_FAIL' // 注册失败
export const RESET_SIGNUP = 'RESET_SIGNUP' // 重置注册状态

// action.payload 接口类型
export interface SignupPayload {
  email: string
  name: string
  password: string
}

// action 对象接口类型
export interface SignupAction {
  type: typeof SIGNUP
  payload: SignupPayload
}
export interface SignupSuccessAction {
  type: typeof SIGNUP_SUCCESS
}
export interface SignupFailAction {
  type: typeof SIGNUP_FAIL
  message: string
}
export interface ResetSignupAction {
  type: typeof RESET_SIGNUP
}

// actionCreator
export const signup = (payload: SignupPayload): SignupAction => ({
  type: SIGNUP,
  payload
})
export const signupSuccess = (): SignupSuccessAction => ({
  type: SIGNUP_SUCCESS
})
export const signupFail = (message: string): SignupFailAction => ({
  type: SIGNUP_FAIL,
  message
})
export const resetSignup = (): ResetSignupAction => ({
  type: RESET_SIGNUP
})

// action 的联合类型
export type AuthUnionType = SignupAction | SignupSuccessAction | SignupFailAction | ResetSignupAction

定义 reducer

// src\store\reducers\auth.reducer.ts
import { AuthUnionType, RESET_SIGNUP, SIGNUP, SIGNUP_FAIL, SIGNUP_SUCCESS } from '../actions/auth.action'

// state 接口类型
export interface AuthState {
  signup: {
    loaded: boolean
    success: boolean
    message: string
  }
}

// state 默认值
const initialState: AuthState = {
  signup: {
    loaded: false, // 注册请求是否结束
    success: false, // 注册是否成功
    message: '' // 注册失败提示
  }
}

export default function authReducer(state = initialState, action: AuthUnionType) {
  switch (action.type) {
    // 发送注册请求
    case SIGNUP:
      return {
        ...state,
        signup: {
          loaded: false,
          success: false
        }
      }
    // 注册成功
    case SIGNUP_SUCCESS:
      return {
        ...state,
        signup: {
          loaded: true,
          success: true
        }
      }
    // 注册失败
    case SIGNUP_FAIL:
      return {
        ...state,
        signup: {
          loaded: true,
          success: false,
          message: action.message
        }
      }
    // 重置注册状态
    case RESET_SIGNUP:
      return {
        ...state,
        signup: {
          loaded: false,
          success: false,
          message: ''
        }
      }
    default:
      return state
  }
}

执行重置操作

// src\components\core\Signup.tsx
import Layout from './Layout'
import { Button, Form, Input, Result } from 'antd'
import { resetSignup, signup, SignupPayload } from '../../store/actions/auth.action'
import { useDispatch, useSelector } from 'react-redux'
import { AppState } from '../../store/reducers'
import { AuthState } from '../../store/reducers/auth.reducer'
import { useEffect } from 'react'
import { Link } from 'react-router-dom'

const Signup = () => {
  // 获取 dispatch 方法
  const dispatch = useDispatch()

  // 获取注册结果
  const auth = useSelector<AppState, AuthState>(state => state.auth)

  // 创建表单数据域
  // 用于绑定到 Form 组件上操作表单
  const [form] = Form.useForm()

  // 注册表单提交
  const onFinish = (value: SignupPayload) => {
    // 发送注册请求
    dispatch(signup(value))
  }

  // 监听状态
  useEffect(() => {
    // 1. 注册成功 清空表单
    if (auth.signup.loaded && auth.signup.success) {
      form.resetFields()
    }
  }, [auth])

  // 2. 注册成功 显示成功的提示信息
  const showSuccess = () => {
    if (auth.signup.loaded && auth.signup.success) {
      return (
        <Result
          status="success"
          title="注册成功"
          extra={[
            <Button type="primary">
              <Link to="/signin">登录</Link>
            </Button>
          ]}
        />
      )
    }
  }

  // 3. 注册失败 显示失败的提示信息
  const showError = () => {
    if (auth.signup.loaded && !auth.signup.success) {
      return <Result status="warning" title="注册失败" subTitle={auth.signup.message} />
    }
  }

  // 4. 离开页面之前 重置页面状态
  useEffect(() => {
    // 离开页面时会执行这个方法
    return () => {
      dispatch(resetSignup())
    }
  }, [])

  // 注册表单
  const signupForm = () => (
    <Form form={form} onFinish={onFinish}>
      <Form.Item name="name" label="昵称">
        <Input />
      </Form.Item>
      <Form.Item name="password" label="密码">
        <Input.Password />
      </Form.Item>
      <Form.Item name="email" label="邮箱">
        <Input />
      </Form.Item>
      <Form.Item>
        <Button type="primary" htmlType="submit">
          注册
        </Button>
      </Form.Item>
    </Form>
  )

  return (
    <Layout title="注册" subTitle="还没有账号?注册一个吧">
      {showSuccess()}
      {showError()}
      {signupForm()}
    </Layout>
  )
}

export default Signup

实现登录的 Redux 流程

和登录相关的 action

// src\store\actions\auth.action.ts

/**
 * 注册
 */

...

/**
 * 登录
 */

export const SIGNIN = 'SIGNIN' // 发送登录请求
export const SIGNIN_SUCCESS = 'SIGNIN_SUCCESS' // 登录成功
export const SIGNIN_FAIL = 'SIGNIN_FAIL' // 登录失败

export interface SigninPayload {
  email: string
  password: string
}

export interface SigninAction {
  type: typeof SIGNIN
  payload: SigninPayload
}

export interface SigninSuccessAction {
  type: typeof SIGNIN_SUCCESS
}

export interface SigninFailAction {
  type: typeof SIGNIN_FAIL
  message: string
}

export const signin = (payload: SigninPayload): SigninAction => ({
  type: SIGNIN,
  payload
})

export const signinSuccess = (): SigninSuccessAction => ({
  type: SIGNIN_SUCCESS
})

export const signinFail = (message: string): SigninFailAction => ({
  type: SIGNIN_FAIL,
  message
})

// action 的联合类型
export type AuthUnionType =
  | SignupAction
  | SignupSuccessAction
  | SignupFailAction
  | ResetSignupAction
  | SigninAction
  | SigninSuccessAction
  | SigninFailAction

定义 reducer

// src\store\reducers\auth.reducer.ts
import {
  AuthUnionType,
  RESET_SIGNUP,
  SIGNIN,
  SIGNIN_FAIL,
  SIGNIN_SUCCESS,
  SIGNUP,
  SIGNUP_FAIL,
  SIGNUP_SUCCESS
} from '../actions/auth.action'

// state 接口类型
export interface AuthState {
  signup: {
    loaded: boolean
    success: boolean
    message: string
  }
  signin: {
    loaded: boolean
    success: boolean
    message: string
  }
}

// state 默认值
const initialState: AuthState = {
  signup: {
    loaded: false, // 注册请求是否结束
    success: false, // 注册是否成功
    message: '' // 注册失败提示
  },
  signin: {
    loaded: false, // 登录请求是否结束
    success: false, // 登录是否成功
    message: '' // 登录失败提示
  }
}

export default function authReducer(state = initialState, action: AuthUnionType) {
  switch (action.type) {
    ...
    
    // 发送登录请求
    case SIGNIN:
      return {
        ...state,
        signin: {
          loaded: false,
          success: false,
          message: ''
        }
      }
    // 登录成功
    case SIGNIN_SUCCESS:
      return {
        ...state,
        signin: {
          loaded: true,
          success: true,
          message: ''
        }
      }
    // 登录失败
    case SIGNIN_FAIL:
      return {
        ...state,
        signin: {
          loaded: true,
          success: false,
          message: action.message
        }
      }
    default:
      return state
  }
}

定义 sage 接收请求

// src\store\sagas\auth.saga.ts
import axios, { AxiosResponse } from 'axios'
import { takeEvery, put } from 'redux-saga/effects'
import { API } from '../../config'
import {
  SIGNIN,
  SigninAction,
  signinFail,
  signinSuccess,
  SIGNUP,
  SignupAction,
  signupFail,
  signupSuccess
} from '../actions/auth.action'

function* handleSignup(action: SignupAction) {
  try {
    yield axios.post(`${API}/signup`, action.payload)
    yield put(signupSuccess())
  } catch (error) {
    yield put(signupFail(error.response.data.errors[0]))
  }
}

function* handleSignin(action: SigninAction) {
  try {
    const response: AxiosResponse = yield axios.post(`${API}/signin`, action.payload)

    // 存储令牌
    localStorage.setItem('jwt', JSON.stringify(response.data))

    yield put(signinSuccess())
  } catch (error) {
    yield put(signinFail(error.response.data.errors[0]))
  }
}

export default function* authSaga() {
  // 注册
  yield takeEvery(SIGNUP, handleSignup)
  // 登录
  yield takeEvery(SIGNIN, handleSignin)
}

修改页面组件

// src\components\core\Signin.tsx
import Layout from './Layout'
import { Button, Form, Input } from 'antd'
import { signin, SigninPayload } from '../../store/actions/auth.action'
import { useDispatch } from 'react-redux'

const Signin = () => {
  // 获取 dispatch
  const dispatch = useDispatch()

  const onFinish = (value: SigninPayload) => {
    dispatch(signin(value))
  }

  return (
    <Layout title="登录" subTitle="">
      <Form onFinish={onFinish}>
        <Form.Item name="email" label="邮箱">
          <Input />
        </Form.Item>
        <Form.Item name="password" label="密码">
          <Input.Password />
        </Form.Item>
        <Form.Item>
          <Button type="primary" htmlType="submit">
            登录
          </Button>
        </Form.Item>
      </Form>
    </Layout>
  )
}

export default Signin

处理登录结果

实现内容

  1. 获取登录结果
  2. 登录失败 显示错误信息
  3. 登录成功 根据角色跳转到对应的管理页面
  4. 处理导航链接
    • 已登录
    • 隐藏[登录,注册]
    • 显示[dashboard]

获取登录结果和显示失败信息

// src\components\core\Signin.tsx
import Layout from './Layout'
import { Button, Form, Input, Result } from 'antd'
import { signin, SigninPayload } from '../../store/actions/auth.action'
import { useDispatch, useSelector } from 'react-redux'
import { AppState } from '../../store/reducers'
import { AuthState } from '../../store/reducers/auth.reducer'

const Signin = () => {
  // 获取 dispatch
  const dispatch = useDispatch()

  const onFinish = (value: SigninPayload) => {
    dispatch(signin(value))
  }

  // 1. 获取登录结果
  const auth = useSelector<AppState, AuthState>(state => state.auth)
  // 2. 登录失败 显示错误信息
  const showError = () => {
    if (auth.signin.loaded && !auth.signin.success) {
      return <Result status="warning" title="登录失败" subTitle={auth.signin.message} />
    }
  }
  // 3. 登录成功 根据角色跳转到对应的管理页面
  // 4. 处理导航链接: 已登录 隐藏[登录,注册] 显示[dashboard]

  // 登录表单
  const singinForm = () => (
    <Form onFinish={onFinish}>
      <Form.Item name="email" label="邮箱">
        <Input />
      </Form.Item>
      <Form.Item name="password" label="密码">
        <Input.Password />
      </Form.Item>
      <Form.Item>
        <Button type="primary" htmlType="submit">
          登录
        </Button>
      </Form.Item>
    </Form>
  )

  return (
    <Layout title="登录" subTitle="">
      {showError()}
      {singinForm()}
    </Layout>
  )
}

export default Signin

登录成功跳转管理页面

定义一个判断是否登录的方法

首先定义一个 User 接口和 Jwt 接口便于其它地方使用:

// src\store\models\auth.ts
export interface User {
  _id: string
  name: string
  email: string
  role: number
}

export interface Jwt {
  token: string
  user: User
}

定义判断方法:

// src\helpers\auth.ts
import { Jwt } from '../store/models/auth'

// 是否登录
export function isAuth(): boolean | Jwt {
  const jwt = localStorage.getItem('jwt')
  if (jwt) {
    return JSON.parse(jwt)
  }

  return false
}

跳转页面

// src\components\core\Signin.tsx
import Layout from './Layout'
import { Button, Form, Input, Result } from 'antd'
import { signin, SigninPayload } from '../../store/actions/auth.action'
import { useDispatch, useSelector } from 'react-redux'
import { AppState } from '../../store/reducers'
import { AuthState } from '../../store/reducers/auth.reducer'
import { isAuth } from '../../helpers/auth'
import { Jwt } from '../../store/models/auth'
import { Redirect } from 'react-router-dom'

const Signin = () => {
  // 获取 dispatch
  const dispatch = useDispatch()

  const onFinish = (value: SigninPayload) => {
    dispatch(signin(value))
  }

  // 1. 获取登录结果
  const auth = useSelector<AppState, AuthState>(state => state.auth)
  // 2. 登录失败 显示错误信息
  const showError = () => {
    if (auth.signin.loaded && !auth.signin.success) {
      return <Result status="warning" title="登录失败" subTitle={auth.signin.message} />
    }
  }
  // 3. 登录成功 根据角色跳转到对应的管理页面
  const redirectToDashboard = () => {
    const auth = isAuth()
    if (auth) {
      const {
        user: { role }
      } = auth as Jwt

      if (role === 0) {
        // 普通用户
        return <Redirect to="/user/dashboard" />
      } else {
        // 管理员
        return <Redirect to="/admin/dashboard" />
      }
    }
  }
  // 4. 处理导航链接: 已登录 隐藏[登录,注册] 显示[dashboard]

  // 登录表单
  const singinForm = () => (
    <Form onFinish={onFinish}>
      <Form.Item name="email" label="邮箱">
        <Input />
      </Form.Item>
      <Form.Item name="password" label="密码">
        <Input.Password />
      </Form.Item>
      <Form.Item>
        <Button type="primary" htmlType="submit">
          登录
        </Button>
      </Form.Item>
    </Form>
  )

  return (
    <Layout title="登录" subTitle="">
      {showError()}
      {redirectToDashboard()}
      {singinForm()}
    </Layout>
  )
}

export default Signin

处理导航链接

// src\components\core\Navigation.tsx
import { Menu } from 'antd'
import { Link } from 'react-router-dom'
import { useSelector } from 'react-redux'
import { AppState } from '../../store/reducers'
import { RouterState } from 'connected-react-router'
import { isAuth } from '../../helpers/auth'
import { Jwt } from '../../store/models/auth'

// 判断选中类名的钩子函数
function useActive(currentPath: string, path: string): string {
  return currentPath === path ? 'ant-menu-item-selected' : ''
}

const Navigation = () => {
  const router = useSelector<AppState, RouterState>(state => state.router)
  const pathname = router.location.pathname

  const isHome = useActive(pathname, '/')
  const isShop = useActive(pathname, '/shop')
  const isSignin = useActive(pathname, '/signin')
  const isSignup = useActive(pathname, '/signup')
  const isDashboard = useActive(pathname, getDashboardUrl())

  function getDashboardUrl() {
    let url = '/user/dashboard'
    if (isAuth()) {
      const {
        user: { role }
      } = isAuth() as Jwt

      if (role === 1) {
        url = '/admin/dashboard'
      }
    }

    return url
  }

  return (
    <Menu mode="horizontal" selectable={false}>
      <Menu.Item className={isHome}>
        <Link to="/">首页</Link>
      </Menu.Item>
      <Menu.Item className={isShop}>
        <Link to="/shop">商城</Link>
      </Menu.Item>
      {!isAuth() && (
        <>
          <Menu.Item className={isSignin}>
            <Link to="/signin">登录</Link>
          </Menu.Item>
          <Menu.Item className={isSignup}>
            <Link to="/signup">注册</Link>
          </Menu.Item>
        </>
      )}
      {isAuth() && (
        <Menu.Item className={isDashboard}>
          <Link to={getDashboardUrl()}>dashboard</Link>
        </Menu.Item>
      )}
    </Menu>
  )
}

export default Navigation

创建 Dashboard 用户管理页面

创建普通用户管理页面

// src\components\admin\Dashboard.tsx
import Layout from '../core/Layout'

const Dashboard = () => {
  return (
    <Layout title="用户 dashboard" subTitle="">
      Dashboard
    </Layout>
  )
}

export default Dashboard

配置路由

// src\Routes.tsx
import { HashRouter, Route, Switch } from 'react-router-dom'
import Dashboard from './components/admin/Dashboard'
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} />
        <Route path="/user/dashboard" component={Dashboard} />
      </Switch>
    </HashRouter>
  )
}

export default Routes

创建受保护的路由

dashboard 页面需要登陆后才能访问,要添加访问权限。

创建一个受保护的路由组件,限制只有登录后才能访问的页面:

// src\components\admin\PrivateRoute.tsx
import { Redirect, Route, RouteProps } from 'react-router-dom'
import { FC } from 'react'
import { isAuth } from '../../helpers/auth'

interface PrivateRouteProps extends RouteProps {
  component: React.ComponentType<any>
}

const PrivateRoute: FC<PrivateRouteProps> = ({ component: Component, ...rest }) => {
  return (
    <Route
      {...rest}
      render={props => {
        const auth = isAuth()
        if (auth) {
          return <Component {...props} />
        }

        return <Redirect to="/signin" />
      }}
    />
  )
}

export default PrivateRoute

替换原来的路由:

// src\Routes.tsx
import { HashRouter, Route, Switch } from 'react-router-dom'
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} />
      </Switch>
    </HashRouter>
  )
}

export default Routes

创建管理员管理页面

// src\components\admin\AdminDashboard.tsx
import Layout from '../core/Layout'

const AdminDashboard = () => {
  return (
    <Layout title="管理员 dashboard" subTitle="">
      Dashboard
    </Layout>
  )
}

export default AdminDashboard

创建管理员身份判断路由

// src\components\admin\AdminRoute.tsx
import { Redirect, Route, RouteProps } from 'react-router-dom'
import { FC } from 'react'
import { isAuth } from '../../helpers/auth'
import { Jwt } from '../../store/models/auth'

interface PrivateRouteProps extends RouteProps {
  component: React.ComponentType<any>
}

const AdminRoute: FC<PrivateRouteProps> = ({ component: Component, ...rest }) => {
  return (
    <Route
      {...rest}
      render={props => {
        const auth = isAuth() as Jwt
        if (auth && auth.user.role === 1) {
          return <Component {...props} />
        }

        return <Redirect to="/signin" />
      }}
    />
  )
}

export default AdminRoute

配置管理员管理页面路由

// src\Routes.tsx
import { HashRouter, Route, Switch } from 'react-router-dom'
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} />
      </Switch>
    </HashRouter>
  )
}

export default Routes

完善管理员 Dashboard 页面

// 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="">添加分类</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

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