创建导航菜单
// 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
处理注册结果
实现内容
- 注册成功 清空表单
- 注册成功 显示成功的提示信息
- 注册失败 显示失败的提示信息
- 离开页面之前 重置状态
- 注册状态如果未重置,返回该页面仍然会显示注册结果和提示
刷新表单和显示提示信息
// 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
处理登录结果
实现内容
- 获取登录结果
- 登录失败 显示错误信息
- 登录成功 根据角色跳转到对应的管理页面
- 处理导航链接
- 已登录
- 隐藏[登录,注册]
- 显示[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