上接: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"}