文章目录
- 前景
- 登录组件编写
- 登录逻辑
- 菜单的时机
- 动态路由
- 页面刷新
- 手动修改地址
前景
不同用户拥有不同的菜单权限,现在我们实现登录动态获取权限菜单。
登录组件编写
//当我们需要使用dva的dispatch函数时,除了通过@connect函数包裹组件还可以使用这种方式来实现
import { getDvaApp } from 'umi';
const Login = ({ form ,dispatch}) => {
const { getFieldDecorator, validateFields } = form; // 从 props 中解构出 form 方法
/**
* export default Form.create()(Login);
*/
const onSubmit = () => {
validateFields((err, values) => {
if (!err) {
// 处理登录逻辑
getDvaApp()._store.dispatch({
type:"globalModel/login",
payload:{
username: values.username,
password: values.password,
}
});
} else {
console.log('Validation Failed:', err);
}
});
};
return (
<div className="login-page">
<div className="login-container" style ={{
marginTop: '7%',
marginRight: '13%'
}}>
<div className="image-container">
<img src={loginImage} alt="Login" />
</div>
<div className="form-container">
<h2>欢迎回来</h2>
<Form
name="login"
layout="vertical"
>
<Form.Item label="用户名">
{getFieldDecorator('username', {
rules: [{ required: true, message: '请输入用户名!' }],
})(<Input placeholder="请输入用户名" />)}
</Form.Item>
<Form.Item label="密码">
{getFieldDecorator('password', {
rules: [{ required: true, message: '请输入密码!' }],
})(<Input.Password placeholder="请输入密码" />)}
</Form.Item>
<Form.Item>
<Button type="primary" onClick={onSubmit} className="login-button">
登录
</Button>
</Form.Item>
</Form>
</div>
</div>
</div>
);
};
// 使用 Form.create 包裹 Login 组件 从而获取form中相关的函数 否则需要使用队Form组件设置ref 通过ref来实现详见loginbak.js
export default Form.create()(Login);
效果如下所示
登录逻辑
上面登录组件我们点击登录后,使用dispatch函数发送了一个访问请求,该请求处理逻辑处理来自dva中一个namespace叫做globalModel的model,代码如下所示:
getDvaApp()._store.dispatch({
type:"globalModel/login",
payload:{
username: values.username,
password: values.password,
}
});
在globalModel这个model中,我们在effect块中定义了一个login函数,他主要做一下操作:
- 访问后端接口
- 存储登录的数据
- 跳转到/index页面
menuList: 是当前用户拥有的菜单权限
//import {routerRedux} from 'dva/router'
//import * as requestUtil from "../utils/request";
//globalModel文件内
state: {
menuList: [],//当前用户拥有的菜单权限
},
*login({payload:{username,password}}, { select, call, put }) {
const {data, code, msg} = yield call(globalModelService.login,{username,password});
if (code === 200){
requestUtil.save(data);
yield put({
type: 'updateState',
payload:{
menuList: data.menuList,
}
});
//去首页信息
yield put(routerRedux.push('/index'))
} else {
message.error("登陆失败!");
}
},
//requestUtil内容
export function save({accessToken,refreshToken,menuList}){
sessionStorage.setItem("accessToken",accessToken);
sessionStorage.setItem("refreshToken",refreshToken);
sessionStorage.setItem("menuList",JSON.stringify(menuList));
}
菜单的时机
上面我们将数据存储在了sessionStorage中,下面我们来看如何使用这些数据,首先我们看看路由信息。
//src/routes 这个是定义的全局路由,各个模块的路由信息将在这里汇总
module.exports = [
{
path: "/",
exact: true,
redirect:"/login",//跳转到登录页
},
{
path: "/login",
exact: true,
component: "@/layouts/login/login.js",
},
{
//不能加exact=true
path: "/",
component: "@/layouts/index.js",
routes: [//routes将会作为 index.js 中BasicRoute组件的props信息
{
path: '/index',
component: '@/pages/atscript/basic.tsx'
},
...routeConsole(practice_routes),
...routeConsole(lesson_routes)
]
}
];
这个routes将在
.umirc.ts
作为系统的路由配置
登录完成后页面被重定向到/index
中,通过路由可以发现首先会加载父路径/
下的@/layout/index.js
的资源,相关代码如下所示,在该组件中我有一个SysMenu
组件,所需的参数是当前用户获取的菜单权限信息
。
const BasicRoute = (props) => {
const {globalModel,dispatch} = props;
const historyHook = useHistory();
let {
menuList
} = globalModel;
useEffect(()=>{
const curMenuList = (!!menuList && menuList.length > 0 )? menuList :
(!!sessionStorage.getItem("menuList")? JSON.parse(sessionStorage.getItem("menuList")) : []);
dispatch({
type: 'globalModel/updateState',
payload:{
menuList: curMenuList,
}
});
},[menuList]);
const sysMenuProps ={
menuList: menuList
}
return (
<SysMenu {...sysMenuProps}>
{props.children}
</SysMenu>
);
}
// 指定订阅数据,这里关联了 model的namespace = globalModel
function mapStateToProps({globalModel }) {
return {globalModel};
}
// 建立数据关联关系 对于关联组件名称
export default connect(mapStateToProps)(BasicRoute);
useEffect 一是为了防止页面刷新数据导致菜单信息丢失,二是可以避免menuList数据有延迟导致渲染问题(第一次默认值为[])
下面我们来到SysMenu
组件,最终生成菜单信息来自MenuTree
组件。
<Layout>
<Sider trigger={null} collapsible collapsed={collapsed}>
<div className={layoutModule.logo} >
学习系统 !!!
</div>
<MenuTree {...menuTreeProps} />
</Sider>
<Layout>
<Header style={{ background: '#fff', padding: 0 }}>
<Icon
className={layoutModule.trigger}
type={collapsed ? 'menu-unfold' : 'menu-fold'}
onClick={onCollapse}
/>
</Header>
<Content className={layoutModule.content}>
<Breadcrumb separator=">" style={{ margin: '16px 0' }}>
<Breadcrumb.Item>User</Breadcrumb.Item>
<Breadcrumb.Item>Bill</Breadcrumb.Item>
</Breadcrumb>
<div style={{ padding: 24, background: '#fff', minHeight: '90%' }}>
{props.children}
</div>
</Content>
<Footer style={{ textAlign: 'center' }}>Ant Design ©2018 Created by Ant UED</Footer>
</Layout>
</Layout>
动态路由
前面我们配置了路由地址,实际情况会根据用户的不同权限展示不同的菜单,下面我们将菜单抽取出一个专门的组件来动态处理。文件目录如下所示:
其中SysMenu
文件中菜单配置变化如下:
目前我们指定菜单属性只有5个属性(后台已封装为父子结构),分别为name
,url
,icon
,key
,children
,按照顺序依次表现为菜单名称,菜单路由,菜单图标,唯一key,菜单的子级节点。数据结构如下所示:
public static List<Menu> getMenuList() {
List<Menu> menuList = new ArrayList<>();
menuList.addAll(Arrays.asList(
new Menu("学习模块", "#", "build", "lesson", new ArrayList<>())
.addChild(new Menu("Ref学习", "/lesson/reftest", "skin", "/lesson/reftest", new ArrayList<>())),
new Menu("练习程序", "#", "book", "practice", new ArrayList<>())
.addChild(new Menu("计算程序", "/practice/calculate", "snippets", "/practice/calculate", new ArrayList<>()))
.addChild(new Menu("中央空调", "/practice/air", "skin", "/practice/air", new ArrayList<>()))
.addChild(new Menu("流程管理", "/practice/activiti", "user", "/practice/activiti", new ArrayList<>())),
// new Menu("文件操作", "#", "build", "fileManage", new ArrayList<>())
// .addChild(new Menu("整体上传", "/fileManage/commonUploadFile", "user", "/fileManage/commonUploadFile", new ArrayList<>()))
// .addChild(new Menu("分片上传", "/fileManage/filePartUploadFile", "user", "/fileManage/filePartUploadFile", new ArrayList<>()))
// .addChild(new Menu("文件秒传", "/fileManage/flashUploadFile", "user", "/fileManage/flashUploadFile", new ArrayList<>()))
// .addChild(new Menu("断点续传", "/fileManage/breakPointUploadFile", "user", "/fileManage/breakPointUploadFile", new ArrayList<>())),
new Menu("支付对接", "#", "build", "pay", new ArrayList<>())
.addChild(new Menu("支付宝web支付", "/pay/AliWebPay", "user", "/pay/AliWebPay", new ArrayList<>())),
new Menu("二维码", "/qrcode/qrcode", "build", "/qrcode/qrcode", new ArrayList<>())));
return menuList;
}
MenuTree
组件整体结构如下,其中extractMenus方法
则是将后台返回的权限菜单进行转化为对应的配置。
const MenuTree = ()=>{
return(
<Menu theme="dark" mode="inline" defaultSelectedKeys={[menuList[0].key]}>
{extractMenus(menuList)}
</Menu>
);
}
export default MenuTree;
const extractMenus = (list) =>{
return list.map((item, index) => (
doExtractMenus(item)
));
}
//显示菜单项
const doExtractMenus = (item)=>{
if(item.children.length == 0)
{
return (
<Menu.Item key={item.key}>
<Icon type={item.icon}/>
<span>{item.name}</span>
<Link to={item.url}/>
</Menu.Item>
);
}
else
{
return (
<SubMenu key={item.key} title={
<span>
<Icon type={item.icon}/>
<span>{item.name}</span>
</span>
}>
{extractMenus(item.children)}
</SubMenu>
);
}
}
页面刷新
当我们刷新页面后丢失了菜单的选中信息,实际上需要还是在对应选中的菜单节点,下面我们来避免这个问题,我们需要记录以下信息:
- 原来展开的菜单节点信息
- 原来选中的菜单
//import { useHistory } from 'react-router-dom';
//import {useEffect, useState} from "react";
const MenuTree = (props)=>{
const historyHook = useHistory();
const menuList = props.menuList;
const [state,setState] = useState ({
visible:false,
menuTree:[],
defaultOpenKeys:[],
defaultSelectedKeys:[historyHook.location.pathname]
});
useEffect(()=>{
const defaultOpenKeys = [historyHook.location.pathname];
const menuTree = extractMenus(menuList,null,defaultOpenKeys);
console.log("defaultOpenKeys",defaultOpenKeys);
setState({
...state,
visible: true,
defaultOpenKeys: defaultOpenKeys,
menuTree:menuTree,
})
},[menuList])
//显示菜单列表
const extractMenus = (list, parent,curOpenKeys) =>{
return list.map((item, index) => doExtractMenus(item, parent,curOpenKeys));
}
//显示菜单项
const doExtractMenus = (item, parent,curOpenKeys)=> {
//需要展开父节点
if (!!parent && item.url == state.defaultSelectedKeys[0]) {
console.log("parent", parent);
curOpenKeys.push( parent.key);
}
//没有子节点
if(item.children.length == 0)
{
return (
<Menu.Item key={item.key}>
<Icon type={item.icon}/>
<span>{item.name}</span>
<Link to={item.url}/>
</Menu.Item>
);
}
//当前时父节点
else
{
return (
<SubMenu key={item.key} title={
<span>
<Icon type={item.icon}/>
<span>{item.name}</span>
</span>
}>
{extractMenus(item.children, item,curOpenKeys)}
</SubMenu>
);
}
}
console.log("state",state);
/**
* defaultOpenKeys 的使用:defaultOpenKeys 只在组件首次渲染时生效。组件执行顺序
* render => useEffect 所以利用state.visible来控制
*/
return(
state.visible &&
<Menu theme="dark" mode="inline" defaultSelectedKeys={state.defaultSelectedKeys} defaultOpenKeys={state.defaultOpenKeys}>
{state.menuTree}
</Menu>
);
}
上面我们通过监听菜单信息menuList(第一次进来为[])将原来
extractMenus
方法新增parent
,curOpenKeys
,前者是为了处理选中节点,后者是为了记录需要展开的父节点信息
当第一次进来时menuList为[],导致defaultOpenKeys一直为[] ,为了避免Menu 第一次挂在后,后续刷新defaultOpenKeys将不再生效,组件使用state.visible 来控制挂载时机
手动修改地址
登录完成后避免可以通过修改浏览器直接访问/login
,也就是跳转到登录页面,我们需要配合globalModel
中的监听函数subscriptions
函数
//globalModel.js中
subscriptions: {
setup({ dispatch, history}) {
return history.listen(location => {
//如果有登录信息,直接访问/login则重定向到/index页面
if (!!sessionStorage.getItem("refreshToken") && history.location.pathname === "/login"){
history.push("/index");
}
}
});
},
},