《React后台管理系统实战:十》Redux项目实战(二):用redux管理用户状态 和登录、退出功能

49 篇文章 6 订阅

上接:https://blog.csdn.net/u010132177/article/details/105639035

一、登录/退出改用redux管理state

1.src/redux/action-type.js定义action-type常量

//包含n个action的type常量标识名称的模块
export const SET_HEAD_TITLE = 'set_head_title' // 设置头部标题
export const RECEIVE_USER = 'receive_user'  // 【1】接收用户信息
export const SHOW_ERROR_MSG = 'show_error_msg' // 【2】显示错误信息
export const RESET_USER = 'reset_user' // 【3】重置用户信息

2.src/redux/actions.js写action,并写异步登录请求函数

/*
包含n个action creator函数的模块
同步action: 对象 {type: 'xxx', data: 数据值}
异步action: 函数  dispatch => {}
 */
import {
  SET_HEAD_TITLE,
  RECEIVE_USER, //【0】引入以下3个action-type变量
  SHOW_ERROR_MSG,
  RESET_USER
} from './action-types'
import {reqLogin} from '../api' //【1】引入异步登录请求函数
import storageUtils from "../utils/storageUtils"; //【2】引入localstorage操作函数

//设置头部标题的同步action
export const setHeadTitle = (headTitle) => ({type: SET_HEAD_TITLE, data: headTitle})

//【3】接收用户的同步action
export const receiveUser = (user) => ({type: RECEIVE_USER, user})

//【4】显示错误信息同步action
export const showErrorMsg = (errorMsg) => ({type: SHOW_ERROR_MSG, errorMsg})

//【5】登陆的异步action
export const login = (username, password) => {
  return async dispatch => {
  
    // 1. 执行异步ajax请求
    const result = await reqLogin(username, password)  // {status: 0, data: user} {status: 1, msg: 'xxx'}
    // 2.1. 如果成功, 分发成功的同步action
    if(result.status===0) {
      const user = result.data
      // 保存local中
      storageUtils.saveUser(user)
      // 分发接收用户的同步action
      dispatch(receiveUser(user))
    } else { // 2.2. 如果失败, 分发失败的同步action
      const msg = result.msg
      // message.error(msg)
      dispatch(showErrorMsg(msg))
    }

  }
}

//【6】退出登陆的同步action
export const logout = () =>  {
  // 删除localstage中的user
  storageUtils.removeUser()
  // 返回action对象给reducer去清空用户state
  return {type: RESET_USER}
}

3.src/redux/reducer.js 登录退出reducer

/*根据老的state和指定的action生成并返回新的state的函数*/
import {combineReducers} from 'redux' //【1】用于合并多个reducer为一个,没有多个reducer则直接导出对应函数即可
import storageUtils from '../utils/storageUtils.js' //【2】引入localStorage管理函数
import {SET_HEAD_TITLE,RECEIVE_USER,SHOW_ERROR_MSG,RESET_USER} from './action-type.js' //【3】引入action-type

//用来控制头部显示标题的状态
const initHeadTitle=''
function headTitle(state=initHeadTitle,action){
    switch(action.type){
        //添加据action返回不同数据
        case SET_HEAD_TITLE:
            return action.data
        default:
            return state
    }
}

//【4】用来管理登录用户的reducer函数
const initUser=storageUtils.getUser() //从从localSorage读取user
function user(state=initUser,action){
    switch(action.type){
        case RECEIVE_USER: //如果收到的action是RECEIVE_USER则把用户数据返回
            return action.user 
        case SHOW_ERROR_MSG: //如果收到的是错误信息,则证明登录错误,就把错误信息加到原state里
            const errorMsg=action.errorMsg
            return {...state,errorMsg} // state.errorMsg = errorMsg 有人可能用这种,建议不要用这种方式直接修改原本状态数据
        case RESET_USER: //如果收到的action-Type是这个,即表示需要退出登录,把用户的state置空即可
            return {}
        default:
            return state
    }
}


/*【5】导出多个reducer函数:
向外默认暴露的是合并产生的总的reducer函数,管理的总的state的结构:
  {headTitle: '首页',user: {} } 
*/
export default combineReducers({
    headTitle,
    user
})

4. src/pages/login/login.jsx加如下代码(旧代码略)

//【0】以下3行用不到了,注释掉或删除
// import {reqLogin} from '../../api/' 
// import memoryUtils from '../../utils/memoryUtils'
// import storageUtils from '../../utils/storageUtils'

import {connect} from 'react-redux' //【1】引入连接react和redux互联组件
import {login} from '../../redux/actions.js' //【2】引入action动作

//【3】把以下函数修改成这样,其它部分删除
handleSubmit = (event) => {
    // 阻止事件的默认行为
    event.preventDefault()
    // 对所有表单字段进行检验
    this.props.form.validateFields(async (err, values) => {
      // 检验成功
      if (!err) {
        // console.log('提交登陆的ajax请求', values)
        // 请求登陆
        const {username, password} = values

        // 【4】调用分发异步action的函数 => 发登陆的异步请求, 有了结果后更新状态
        this.props.login(username, password)

      } else {
        console.log('检验失败!')
      }
    });

...render(){之下
// 【5】如果用户已经登陆, 自动跳转到管理界面
    const user = this.props.user
    if(user && user._id) {
      return <Redirect to='/home'/>
    }

...return之内(
return (
      <div className="login">
        <header className="login-header">
          <img src={logo} alt="logo"/>
          <h1>React项目: 后台管理系统</h1>
        </header>
        <section className="login-content">
        {/*【6】如果错误信息存在,则显示错误信息*/}
          <div>{this.props.user.errorMsg}</div>


...最下方
const WrapLogin = Form.create()(Login)
//【7】用connect把用户信息传给当前组件,并把登录函数传给当前组件
export default connect(
  state => ({user: state.user}),
  {login}
)(WrapLogin)

5. src/pages/admin/header/index.jsx

只写新增和redux相关代码,其它略过

import {connect} from 'react-redux' 
// 【1】以下两行用不到去掉
// import memoryUtils from '../../../utils/memoryUtils' //内存中存取用户信息工具 默认导出,不用加花括号
// import storageUtils from '../../../utils/storageUtils' //删除localstorage中的用户登录数据
import {logout} from '../../../redux/actions' //【2】引入logout action


  //【4】使用3处传入的logout退出登陆this.props.logout()
  loginOut = () => {
    // 显示确认框
    Modal.confirm({
      content: '确定退出吗?',
      onOk: () => {
      this.props.logout() //【5】用action的退出函数; 之下的行全部删除
      
        //console.log('OK', this)
        //删除localstorage中登录信息。及内存中登录信息
              //storageUtils.removeUser()
              //memoryUtils.user={}
              //跳转到登录页面,用替换因为无需退回 ; 因为组件更新会自动跳转,所以跳转删除
              //this.props.history.replace('/login')
               
      }
    })
  }



...render(){之下
		//【5】用3处传用的state,改成从props读取当前用户名
        // const username = memoryUtils.user.username
        const username=this.props.user.username


..最下方
//把headTitle传给当前组件
//【3】把user的state,退出action函数(logout)传入当前组件的props里备用
export default connect(
  state =>({headTitle:state.headTitle,user:state.user}),
  {logout}
)(withRouter(Header))

二、其它部分改redux管理状态

1. admin/admin.jsx

import React,{Component} from 'react'
import {Redirect,Route,Switch} from 'react-router-dom' //引入路由组件
// import memoryUtils from '../../utils/memoryUtils' 【1】去除此行
import { Layout } from 'antd'; //引入antd的页面布局
import LeftNav from './left' //因为文件名是index所以可省略
import Header from './header/index' 

//引入需要配置路由的页面
import Home from './home'

import Category from './category' //产品分类
import Product from './product'

import Role from './role' //角色管理页面
import User from './user/user' //用户管理页面

import Bar from './charts/bar' //图表页面
import Pie from './charts/pie'
import Line from './charts/line'
import { connect } from 'react-redux' //【2】引入


const { Footer, Sider, Content } = Layout;

class Admin extends Component{
    // constructor(props){
    //     super(props);
    // }

    render(){
        // 读取memoryUtils里的user数据,如果不存在就跳转到登录页面
        const user=this.props.user  //memoryUtils.user 【4】改用3处传入的用户数据
        if(!user || !user._id){
            return <Redirect to='/login'/>
        }
        return(
          
            <Layout style={{minHeight:'100%'}}>
                <Sider>
                    <LeftNav/>
                </Sider>
                <Layout>
                    <Header/>
                    {/*路由配置在要显示的位置,即内容里 */}
                    <Content style={{backgroundColor:'#fff',margin:20,height:'100%'}}>
                        <Switch>
                            <Route path='/home' component={Home}/>
                            <Route path='/category' component={Category}/>
                            <Route path='/product' component={Product}/>
                            <Route path='/role' component={Role}/>
                            <Route path='/user' component={User}/>
                            <Route path='/charts/bar' component={Bar}/>
                            <Route path='/charts/line' component={Line}/>
                            <Route path='/charts/pie' component={Pie}/>

                            {/*如果以上都不匹配跳转到home页 */}
                            <Redirect to='/home'/>
                        </Switch>
                        
                    </Content>
                    <Footer style={{textAlign:'center',color:'#333'}}>版权所有@pasaulis</Footer>
                </Layout>
            </Layout>
           
        )
    }
}
//【3】connect把用户数据传入当前组件备用
export default connect(
    state=>({user:state.user}),
    {}
)(Admin)

2.左导航用redux admin/left/index.js

import React,{Component} from 'react'
import {connect} from 'react-redux' //【1】引入连接函数
import {Link,withRouter} from 'react-router-dom' //withRouter:高阶函数,用于把非路由组件包装成路由组件
import './left.less'
import logo from '../../../assets/images/logo.png'
import { Menu, Icon } from 'antd' //引入antd组件
import menuList from '../../../config/menuConfig.js'  //保存左导航菜单
// import memoryUtils from '../../../utils/memoryUtils' 【2】此行删除
import {setHeadTitle} from '../../../redux/actions.js'//【3】引入action,用于管理右侧头部标题


const { SubMenu } = Menu;
class LeftNav extends Component{
    state = {
        collapsed: false, //控制左导航收缩状态
      };
      
    //   控制左侧导航收缩
      toggleCollapsed = () => {
        this.setState({
          collapsed: !this.state.collapsed,
        });
      };



    // 用map函数写的:根据配置文件自动写入左侧导航到页面
    getMenuItem_map=(menuList)=>{
       ...
    }



    //判断当前登陆用户对item是否有权限
    hasAuth = (item) => {
    const {key, isPublic} = item //取出key,菜单是否是公共的(无需权限也可见)

    const menus = this.props.user.role.menus //得到对应角色拥有的菜单【5】改memoryUtils.user.role.menus
    const username = this.props.user.username //得到当前登录用户名 【6】改改memoryUtils.user.role.menus
    /*
    1. 如果当前用户是admin
    2. 如果当前item是公开的
    3. 当前用户有此item的权限: key有没有存在于menus中
        */
    if(username==='admin' || isPublic || menus.indexOf(key)!==-1) {
        return true
    } else if(item.children){ // 4. 如果当前用户有此item的某个子item的权限
        return !!item.children.find(child =>  menus.indexOf(child.key)!==-1) //!!:强制转换成bool类型值
    }

    return false
    }



    //使用reduce() + 递归调用写的根据menu的数据数组生成对应的标签数组:getMenuItem用reduce函数重写方便对每一条进行控制
    getMenuItem=(menuList)=>{
        const path=this.props.location.pathname //得到当前请求路径
        return menuList.reduce((pre,item)=>{

            // 如果当前用户有item对应的权限, 才需要显示对应的菜单项
            if (this.hasAuth(item)) {
                
                //判断item是否是当前对应的item
                if (item.key===path || path.indexOf(item.key)===0) {
                    // 更新redux中的headerTitle状态
                    this.props.setHeadTitle(item.title)
                }


                if(!item.children){//1.没有子菜单添加:
                    pre.push((
                        <Menu.Item key={item.key}>
                            {/**点击时回调action去reducer更新state */}
                            <Link to={item.key} onClick={()=>this.props.setHeadTitle(item.title)}>
                                <Icon type={item.icon}/>
                                <span>{item.title}</span>
                            </Link>
                        </Menu.Item>
                    ))
                }else{//2.有子菜单
    
                    // 查找一个与当前请求路径,是否匹配的子Item
                    const cItem = item.children.find(cItem => path.indexOf(cItem.key)===0)
                    // 如果存在, 说明当前item的子列表需要展开
                    if (cItem) {
                        this.openKey = item.key
                    }
    
                    // 向pre添加<SubMenu>
                    pre.push((
                        <SubMenu
                        key={item.key}
                        title={
                            <span>
                        <Icon type={item.icon}/>
                        <span>{item.title}</span>
                        </span>
                        }
                        >
                        {this.getMenuItem(item.children)}
                        </SubMenu>
                    ))
                }
            }

            return pre
        },[])
    }




    /*
    在第一次render()之前执行一次
    为第一个render()准备数据(必须同步的)
    */
    componentWillMount () {
        this.menuNodes = this.getMenuItem(menuList)
    }

    render(){
        // 得到当前请求的路由路径
        let path=this.props.location.pathname
        console.log('render()', path)
        if(path.indexOf('/product')===0) { // 当前请求的是商品或其子路由界面
            path = '/product'
        }
        
        // 得到需要打开菜单项的key
        const openKey = this.openKey

        return (
        <div className='left'>
            <Link to='/home' className='left-header'>
                <img src={logo} alt='logo' />
                <h1>深蓝管理后台</h1>
            </Link>
            
        <Menu
          selectedKeys={[path]}
          defaultOpenKeys={[openKey]} 
          mode="inline"
          theme="dark"          
         >{/*inlineCollapsed={this.state.collapsed}*/}
            {this.menuNodes}
          
        </Menu>
        </div>
        ) 
    }
}

/*用withRouter高阶组件:
包装非路由组件, 返回一个新的组件
新的组件向非路由组件传递3个属性: history/location/match
 */
//【4】容器组件,把action传给当前组件,用于通过reducer更改state
export default connect(
    state=>({user:state.user}),
    {setHeadTitle}
)(withRouter(LeftNav))

3.角色管理pages/admin/role/index.jsx

import React,{Component} from 'react'
import {
    Card,
    Button,
    Table,
    Modal, //弹窗
    message
} from 'antd'
import {PAGE_SIZE} from '../../../utils/constans'
import {reqRoles,reqAddRole,reqUpdateRole} from '../../../api' //引入更新角色函数requpdaterole;   添加角色api
import AddForm from './addForm' //添加角色弹窗的表单
import AuthForm from './authForm' //设置权限弹窗的表单
import {formateDate} from '../../../utils/dateUtils' //时间格式化

import {connect} from 'react-redux' //【1】引入,删除以下两行
// import memoryUtils from '../../../utils/memoryUtils' //引入记忆模块用于显示用户名
// import storageUtils from '../../../utils/storageUtils' //引入记忆模块用于显示用户名
import {logout} from '../../../redux/actions' //【2】引入

class Role extends Component{   
    constructor (props) {
        super(props)
        //创建一个auth的ref用于父子组件传值
        this.auth = React.createRef()
      }

    state={
         roles:[], //所有角色列表:连接Table datasource
         role:{},//选中的role
         isShowAdd: false, //是否显示添加角色弹窗
         isShowAuth:false, //是否显示设置权限弹窗
    }

    //点击角色列表对应行的行为
    onRow=(role)=>{
        return{
            onClick: event => { //点击行时执行以下
                console.log('row onClick()', role)
                this.setState({ //把当前点击的行赋值到state里的role
                    role
                })
            }
        }
    }

    //获取角色列表数据,设置到state中
    getRoles=async()=>{
        const result=await reqRoles()
        if(result.status===0){
            const roles=result.data
            this.setState({
                roles
            })
        }
    }

    //初始化表格列标题,及对应的数据源,dataIndex:对应api返回的数据名
    initColumns=()=>{
        //调用函数格式化时间戳
        this.columns=[
            {title:'角色名称',dataIndex:'name'},
            {title:'创建时间',dataIndex:'create_time',render:(create_time)=>formateDate(create_time)},
            {title:'授权时间',dataIndex:'auth_time',render:formateDate},
            {title:'授权人',dataIndex:'auth_name'},           
        ]
    }
    //点添加角色弹窗的ok按钮:添加角色
    addRole=()=>{
        this.form.validateFields(async(err,value)=>{
            if(!err){
                console.log(value)
                //隐藏确认框
                this.setState({isShowAdd:false})
                //收集数据
                const {roleName}=value
                this.form.resetFields()//清空表单内数据,方便下次使用

                //添加角色请求
                const result=await reqAddRole(roleName)
                if(result.status===0){
                    message.success('角色添加成功')
                    //取出返回的新增role值
                    const role=result.data
                    //更新roles状态,使新增的角色显示出来(基于原本状态数据更新)
                    this.setState(state=>({
                        roles:[...state.roles,role]
                    }))
                    
                }else{
                    message.error('角色添加失败')
                }
            }
        })
    }


    // 更新角色:点设置权限弹窗里的ok操作
    updateRole=async()=>{//加async
        // 隐藏确认框
        this.setState({isShowAuth: false})

        const role=this.state.role
        //得到最新的menus => 到authForm.jsx里传值过来(getMenus = () => this.state.checkedKeys)
        const menus=this.auth.current.getMenus()
        //把接收过来的菜单传给当前role.menus => 到api/index.js里写更新角色接口函数
        role.menus=menus 

        //添加授权时间及授权人
        role.auth_time=Date.now()
        role.auth_name = this.props.user.username //【4】改this.props memoryUtils

        //发送更新请求
        console.log(role)
        const result = await reqUpdateRole(role)
        /*if (result.status===0){     //老写法       
            message.success('设置角色权限成功!')
            this.getRoles()
        }else{
            message.error('更新角色权限失败')
        }*/

        if (result.status===0) {
            // this.getRoles()
            // 如果当前更新的是自己角色的权限, 强制退出
            if (role._id === this.props.user.role_id) { //【5】memoryUtils
            //   memoryUtils.user = {} 注销以下三行
            //   storageUtils.removeUser()
            //   this.props.history.replace('/login')
              this.props.logout() //【6】退出登录action
              message.success('当前用户角色权限成功,请重新登录')
            } else {
              message.success('设置角色权限成功')
              this.setState({
                roles: [...this.state.roles]
              })
            }
      
          }


    }

    componentWillMount(){
        this.initColumns() //函数:运行初始表格列标题,及对应的数据源函数,把表格列数据赋值到this.columus上
    }

    componentDidMount(){
        this.getRoles() //函数:获取角色列表设置到state中
    }


    render(){
        const {roles,role,isShowAdd,isShowAuth}=this.state //娶出role; 取出isShowAuth

        //card的左侧 (Button的disabled:按钮不可用)
        const title=(
            <span>
                {/* 点创设置权限:显示对应弹窗;      点创建角色:显示创建角色的弹窗 */}
                <Button type='primary' style={{marginRight:8}} onClick={()=>{this.setState({isShowAdd:true})}}>创建角色</Button>
                <Button type='primary' disabled={!role._id} onClick={()=>{this.setState({isShowAuth:true})}}>设置角色权限</Button>
            </span>
        )
                
        return(
            <Card title={title}>
                <Table
                    bordered /**边框 */
                    rowKey='_id' /**表格行 key 的取值,可以是字符串或一个函数 */
                    dataSource={roles} /**数据源 */
                    columns={this.columns} /**列标题,及对应的数据源 */
                    pagination={{defaultPageSize:PAGE_SIZE}} /**分页设置默认分页数量 */
                    rowSelection={{type:'radio',
                    selectedRowKeys: [role._id],
                        onSelect: (role) => { // 选择某个radio时回调
                        this.setState({
                            role
                        })
                        }
                    } } /**selectedRowKeys根据4确定哪个是被选中状态;   第行前面加一个单选框antd文档找使用方法 */
                    onRow={this.onRow} /**控制点击当前行的行为 */
                 />

                {/* 添加角色弹窗 */}
                 <Modal 
                 title='添加角色'
                 visible={isShowAdd} /*弹窗可见状态*/
                 onOk={this.addRole} /*点ok提交信息*/
                 onCancel={()=>{
                     this.setState({isShowAdd:false})
                     this.form.resetFields()//取消时顺便清空表单方便下次使用
                    }} /*点取消*/
                 >
                     {/* 传递子组件form的函数setForm:(接收一个参数form,令当前组件的form=传过来的form) */}
                     <AddForm setForm={(form) => this.form = form} />
                 </Modal>


                 {/* 设置权限弹窗 */}
                 <Modal 
                 title='设置权限'
                 visible={isShowAuth} /*弹窗可见状态*/
                 onOk={this.updateRole} /*点ok提交信息*/
                 onCancel={()=>{this.setState({isShowAuth:false})}} /*点取消*/
                 >
                     {/*把this.auth传给子组件 把role传递给子组件 */}
                     <AuthForm ref={this.auth} role={role}  />
                 </Modal>
            </Card>
        )
    }
}
//【3】connect
export default connect(
    state =>({user:state.user}),
    {logout}
)(Role)

附件

扩展知识redux简化:https://zhuanlan.zhihu.com/p/61863127

三点扩展运算符的作用

const state={a:'b',b:'c'}
const state2={...state,c:'hello'}
state2

输出:{a: "b", b: "c", c: "hello"}

其它扩展124-133

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值