一、登录页面以及相关信息存储
api->path->user.ts登陆人要请求的api
import { Get, Post } from "../server";
interface FcResponse<T> {
code: number;
message: string;
data: T;
}
type ApiResponse<T> = Promise<[any, FcResponse<T> | undefined]>;
// 获取验证码
export const getCaptchaImage = <
T = {
image: string;
uuid: string;
}
>(): ApiResponse<T> => {
return Get<T>("/captchaImage");
};
// 用户名密码校验
export const checkUser = <T = {}>(data: any): ApiResponse<T> => {
return Post<T>("/login", data);
};
// 手机验证码校验
export const checkCode = <T = { saTokenInfo: any }>(
data: any
): ApiResponse<T> => {
return Post<T>("/code", data);
};
export const userApi = {
getCaptchaImage,
checkUser,
checkCode,
};
登陆页面用户名、密码、图形验证码,点击登录手机发送短信填写验证码这个地方用测试环境虚拟的为1234,点击确定之后去获取用户信息,主要说的是点击‘确定’之后api获取到用户信息的逻辑。
1.store存储用户信息和菜单权限
存储在本地
store->features->userSlice.ts
import { createSlice } from "@reduxjs/toolkit";
import { Local } from "@/utils/storage";
export interface UserState {
token: any;
userInfo: any;
menusTree: any; //路由信息
menus: any; //接口返回的权限菜单
}
const initialState: UserState = {
token: Local.get("token"),
userInfo: Local.get("userInfo"),
menusTree: [],
menus: Local.get("menus"),
};
export const userSlice = createSlice({
name: "userSlice",
initialState,
reducers: {
setAccessToken: (state, { payload }) => {
Local.set("token", payload);
state.token = payload;
},
setUserInfo: (state, { payload }) => {
Local.set("userInfo", payload);
state.userInfo = payload;
},
setMenusTree: (state, { payload }) => {
state.menusTree = payload;
},
setMenus: (state, { payload }) => {
Local.set("menus", payload);
state.menus = payload;
},
userSliceClear: (state) => {
Local.clear();
state = {
token: null,
userInfo: null,
menus: null,
};
},
},
});
export const {
setAccessToken,
setUserInfo,
setMenusTree,
setMenus,
userSliceClear,
} = userSlice.actions;
export default userSlice.reducer;
2.点击登录获取登录信息和菜单权限api获取成功进行存储
import { useDispatch } from "react-redux";
import {
setAccessToken,
setUserInfo,
setMenus,
} from "@/store/features/userSlice";
interface Props {
ref: any;
refreshCaptchaImage: any;
}
const ComponentMessageAuth: React.FC<Props> = forwardRef((props, ref) => {
...............
................
.............
// 登录
const dispatch = useDispatch();
const navigate = useNavigate();
const toLogin = async () => {
setLoading(true);
const userInfo = await mesAuth();
// 获取到 token 登录成功
if (userInfo && userInfo.saTokenInfo) {
// 存储用户信息
dispatch(setUserInfo(userInfo));
// 存储 token
dispatch(setAccessToken(userInfo.saTokenInfo.tokenValue));
// 存储用户菜单权限
const [, perRes] = await api.getUserPermission();
if (perRes && perRes.data && perRes.data.length > 0) {
dispatch(setMenus(perRes.data));
}
navigate("/home");
}
setLoading(false);
};
......................
............
}
二、layout菜单模块
根据路由层级,菜单下面的子菜单路由需要占位符
layout–>components–>Parent–>index.tsx
import React from "react";
import { Outlet } from "react-router";
const ComponentParent: React.FC = () => {
return <Outlet />;
};
export default ComponentParent;
完整导航路由如下:
const navRoutes = [
{
path: "/home",
element: lazyLoad(<PageHome />),
meta: {
title: "首页",
icon: "HomeOutlined",
isPermission: true,
},
},
{
path: "/todo",
element: lazyLoad(<ComponentParent />),
meta: {
title: "待办任务",
icon: "CalendarOutlined",
},
children: [
{
path: "/todo/mytodo",
element: <PageMyTodo />,
meta: {
title: "我的待办",
icon: "ContainerOutlined",
},
},
{
path: "/todo/mytodo/detail",
element: <PageMyTodoDetail />,
meta: {
title: "待办详情",
isHide: true,
isPermission: true,
},
},
{
path: "/todo/mytodo/personDetail",
element: <PagePersonnelDetail />,
meta: {
title: "人员详情",
isHide: true,
isPermission: true,
},
},
{
path: "/todo/hastodo",
element: <PageHasTodo />,
meta: {
title: "我的已办",
icon: "AuditOutlined",
},
},
{
path: "/todo/hastodo/detail",
element: <PageHasTodoDetail />,
meta: {
title: "已办详情",
isHide: true,
isPermission: true,
},
},
{
path: "/todo/hastodo/personDetail",
element: <PagePersonnelDetail />,
meta: {
title: "人员详情",
isHide: true,
isPermission: true,
},
},
],
},
{
path: "/organization",
element: lazyLoad(<ComponentParent />),
meta: {
title: "组织管理",
icon: "ApartmentOutlined",
},
children: [
{
path: "/organization/structure",
element: <PageOrganizationStructure />,
meta: {
title: "组织结构",
icon: "ClusterOutlined",
},
},
],
},
{
path: "/company",
element: lazyLoad(<ComponentParent />),
meta: {
title: "公司管理",
icon: "SmileOutlined",
},
children: [
{
path: "/company/management",
element: <PageCompanyManagement />,
meta: {
title: "公司管理",
icon: "SolutionOutlined",
},
},
],
},
{
path: "/demand",
element: lazyLoad(<ComponentParent />),
meta: {
title: "需求管理",
icon: "BlockOutlined",
},
children: [
{
path: "/demand/management",
element: <PageDemandManagement />,
meta: {
title: "需求管理",
icon: "BookOutlined",
},
},
{
path: "/demand/management/startDemand",
element: <PageDemandAddEdit />,
meta: {
title: "发起需求",
isHide: true,
isPermission: true,
},
},
{
path: "/demand/issueDemandManage/issueDemand",
element: <PageIssueDemand />,
meta: {
title: "发布需求",
isHide: true,
isPermission: true,
},
},
{
path: "/demand/issueDemandManage",
element: <PageIssueDemandManage />,
meta: {
title: "需求发布管理",
icon: "UpSquareOutlined",
},
},
{
path: "/demand/management/demandDetail",
element: <PageDemandDetail />,
meta: {
title: "需求详情",
isHide: true,
isPermission: true,
},
},
{
path: "/demand/project",
element: <PageDemandProject />,
meta: {
title: "项目/业务管理",
icon: "FlagOutlined",
},
},
{
path: "/demand/panelrelease",
element: <PagePanelRelease />,
meta: {
title: "需求发布中",
icon: "UpSquareOutlined",
},
},
{
path: "/demand/panelfinished",
element: <PagePanelFinished />,
meta: {
title: "需求已结束",
icon: "UpSquareOutlined",
},
},
{
path: "/demand/panelrelease/paneldetail",
element: <PagePanelDetail />,
meta: {
title: "需求面板详情发布中",
isHide: true,
isPermission: true,
},
},
{
path: "/demand/panelfinished/paneldetail",
element: <PagePanelDetail />,
meta: {
title: "需求面板详情已结束",
isHide: true,
isPermission: true,
},
},
],
},
{
path: "/personnel",
element: lazyLoad(<ComponentParent />),
meta: {
title: "人员管理",
icon: "UserOutlined",
},
children: [
{
path: "/personnel/intransit",
element: <PagePersonnelIntransit />,
meta: {
title: "在途人员",
icon: "UserSwitchOutlined",
},
},
{
path: "/personnel/intransit/detail",
element: <PagePersonnelDetail />,
meta: {
title: "人员详情",
isHide: true,
isPermission: true,
},
},
{
path: "/personnel/information",
element: <PagePersonnelInformation />,
meta: {
title: "人员信息",
icon: "TeamOutlined",
},
},
{
path: "/personnel/information/detail",
element: <PagePersonnelDetail />,
meta: {
title: "人员详情",
isHide: true,
isPermission: true,
},
},
{
path: "/personnel/perService",
element: <PagePerService />,
meta: {
title: "服务人员",
icon: "UserSwitchOutlined",
},
},
{
path: "/personnel/perService/detail",
element: <PagePersonnelDetail />,
meta: {
title: "人员详情",
isHide: true,
isPermission: true,
},
},
{
path: "/personnel/perCompany",
element: <PagePerCompany />,
meta: {
title: "公司人员",
icon: "TeamOutlined",
},
},
{
path: "/personnel/perCompany/detail",
element: <PagePersonnelDetail />,
meta: {
title: "人员详情",
isHide: true,
isPermission: true,
},
},
{
path: "/personnel/informationChange",
element: <PageInformationChange />,
meta: {
title: "人员信息变更",
},
},
],
},
{
path: "/system",
element: lazyLoad(<ComponentParent />),
meta: {
title: "系统管理",
icon: "SettingOutlined",
},
children: [
{
path: "/system/user",
element: <PageUser />,
meta: {
title: "用户管理",
icon: "UserOutlined",
},
},
{
path: "/system/role",
element: <PageRole />,
meta: {
title: "角色管理",
icon: "TeamOutlined",
},
},
{
path: "/system/menu",
element: <PageMenu />,
meta: {
title: "菜单管理",
icon: "ReadOutlined",
},
},
{
path: "/system/process",
element: <PageProcess />,
meta: {
title: "流程参数",
icon: "ReadOutlined",
},
},
{
path: "/system/processDesign",
element: <PageProcessDesign />,
meta: {
title: "流程设计",
icon: "PullRequestOutlined",
},
},
{
path: "/system/operationLog",
element: <PageOperationLog />,
meta: {
title: "操作日志",
icon: "FileTextOutlined",
},
},
],
},
];
meta里isHide,isPermisson是自己加的参数,以后会用到
菜单则由接口返回来进行菜单权限控制,也就是之前存在的store里面的menus,主页每个用户都有,接口不会返回主页home的权限,所以根据接口的字段在store里手动加上
setMenus: (state, { payload }) => {
payload.unshift({
icon: "HomeOutlined",
menuName: "首页",
menuType: "C",
path: "/home",
status: "1",
visible: "1",
})
Local.set("menus", payload);
state.menus = payload;
},
!!!这里穿插写一个icon组件,由于需求菜单是可以配置的,所以图标也可以配置,提前把目前需要的icon都在组件里加载好
components—>IconSelect—>indxe.tsx
import {
...
MailOutlined,
} from "@ant-design/icons";
import React from "react";
import { ReactElement } from "react";
const IconMap: any = {
...
MailOutlined,
};
export const createAntdIcon = (iconName: string, style?: any): ReactElement => {
return IconMap[iconName] && React.createElement(IconMap[iconName], { style });
};
渲染menu菜单
component–>menu–>index.tsx
import React, { useEffect, useState } from "react";
import type { MenuProps } from "antd";
import { Menu } from "antd";
import { useSelector } from "react-redux";
import styles from "./menu.module.less";
import { createAntdIcon } from "@/components/IconSelect";
import { Link, useLocation } from "react-router-dom";
const ComponentMenu: React.FC = () => {
// 高亮
//直接输入地址跳转、404回到首页等情况保证高亮
const location = useLocation();
const selectedKeys = () =>
location.pathname === "/" ? "/home" : location.pathname;
//后端返回 的菜单权限
const { menus } = useSelector((store: any) => store.userSlice);
// 根据后端返回的数据进行处理成menu的格式 递归
const recursiveMenus = (list: any): any => {
return filterHide(list).map((item: any) => ({
key: item.path,
icon: item.icon && createAntdIcon(item.icon), //处理icon的方法
label:
item.menuType === "M" ? (
item.menuName
) : (
<Link to={item.path}>{item.menuName}</Link>
),
children: item.menuType === "M" && recursiveMenus(item.children),
}));
};
// 过滤菜单状态为1
const filterHide = (menus: any) =>
menus.filter((item: any) => item.status && item.status === "1");
// 菜单items
const [items, setItems] = useState<MenuProps["items"]>([]);
useEffect(() => {
menus && menus.length != 0 ? setItems(recursiveMenus(menus)) : [];
}, []);
return (
<Menu
mode="inline"
selectedKeys={[selectedKeys()]}
defaultSelectedKeys={[selectedKeys()]}
className={styles.menu}
items={items}
/>
);
};
export default ComponentMenu;
四、路由权限
鉴权组件放在登录页面外面和layout最外面
//是否需要登陆
{
path: "/login",
element: <ComponentAuth>{lazyLoad(<PageLogin />)}</ComponentAuth>,
meta: {
title: "登录",
},
},
//菜单渲染什么
{
path: "/",
element: (
<ComponentAuth>
<ComponentLayout />
</ComponentAuth>
),
children: navRoutes,
},
1、鉴权组件逻辑
------是否登录
(1)已经登录[token和menus已经获取]
获取路由白名单:后端接口返回菜单menu+主菜单下面的其他路由(比如,各个详情、404等)
先判断路由是否为:‘/',’/login‘,如果是,直接跳转到’/home‘
如果不是再判断
当前路由是否处于路由白名单里
a不处于白名单:跳转到403无权限页面
b处于白名单:return页面
-------未登录:路由是否处于登录页面
a未处于登录页面:直接跳转到登陆页面
b处于登录页面:直接返回登录页面
2、准备数据
(1)utils—>router.ts
// react获取不到路由实力,所以需要重新遍历获取实例用于鉴权
// 获取菜单路由信息
export const getNavRouter = (routes: any[]): any[] => {
return routes.map((route: any) => ({
path: route.path,
mete: route.meta,
children: route.children && getNavRouter(route.children),
}));
};
// 获取菜单树扁平化 path
export const flatMenusPath = (
menusTree: any[],
result: string[] = []
): string[] => {
menusTree.forEach((item) => {
if (item.children && item.children.length > 0) {
flatMenusPath(item.children, result);
}
result.push(item.path);
});
return result;
};
// 获取权限菜单 path
export const getPermissionPath = (
menusTree: any[],
result: string[] = []
): string[] => {
menusTree.forEach((item) => {
if (item.meta.isPermission) {
result.push(item.path);
}
if (item.children && item.children.length > 0) {
getPermissionPath(item.children, result);
}
});
return result;
};
在路由里面导出遍历的对象
router–>index.tsx
//在路由里面导出遍历的对象
export const localRouters=getNavRouter(navRoutes)
(2)上下文(Context)-进行获取全局/更新应用全局常量/变量
a.目前需要获取的全局变量menusTree,menus用于鉴权
hooks->useAppContext.tsx
上下文组件
import { useState, createContext, useContext, ReactElement } from "react";
import { localRouters } from "@/router";
import { Local } from "@/utils/storage";
interface iContext {
menusTree: any[];
menus: any[];
setMenus: Function;
}
interface iProps {
children: ReactElement;
}
// 初始化数据
const initContext = {
menusTree: [],
menus: [],
setMenus: () => {},
};
// 创建上下文组件
const AppContext = createContext<iContext>(initContext);
export const AppContextProvider: React.FC<iProps> = ({ children }) => {
// 用户菜单
//这里更新菜单可以用上下文里的setMenus(由于刚开始是直接在App.tsx通过store获取这
//些信息的,所以全部用了store的setMnus,后来改用了执行上下文比较美观,所以没有
//用上下文的setMenus
const [menus, setMenus] = useState(Local.get("menus"));
return (
<AppContext.Provider value={{ menusTree: localRouters, menus, setMenus }}>{children}</AppContext.Provider>
);
};
// 暴露上下文数据
const useAppContext = () => useContext(AppContext);
export default useAppContext;
b.渲染App.tsx的时候执行上下文
App.tsx
import { BrowserRouter } from "react-router-dom";
import { ConfigProvider } from "antd";
import zhCN from "antd/es/locale/zh_CN";
import "antd/dist/reset.css";
import RouterRender from "@/router";
import { AppContextProvider } from "./hooks/useAppContext";
function App() {
return (
<ConfigProvider locale={zhCN}>
<AppContextProvider>
<BrowserRouter>
<RouterRender />
</BrowserRouter>
</AppContextProvider>
</ConfigProvider>
);
}
export default App;
3.鉴权组件
components–>Auth–>index.tsx
import useAppContext from "@/hooks/useAppContext";
import { flatMenusPath, getPermissionPath } from "@/utils/router";
import Cookies from "js-cookie";
import React, { ReactElement } from "react";
import { useSelector } from "react-redux";
import { Navigate, useLocation } from "react-router-dom";
type Props = {
children: ReactElement;
};
const ComponentAuth: React.FC<Props> = ({ children }) => {
// 获取全局信息
const { menusTree } = useAppContext();
const { menus } = useSelector((store: any) => store.userSlice);
const token = Cookies.get("token");
const { pathname } = useLocation();
// 已登陆
if (token && menus && menus.length > 0) {
// 获取本地权限菜单
const localPermissionPath = getPermissionPath(menusTree);
// 组装白名单
const permissionPath = flatMenusPath(menus).concat(localPermissionPath);
// 登录状态时登陆页自动跳转到首页
if (pathname === "/" || pathname == "/login")
return <Navigate to="/home" replace />;
// 判断当前路由是否在用户信息的菜单中
const isRight = (): boolean => {
return permissionPath.includes(pathname);
};
if (!isRight()) {
return <Navigate to="/403" replace />;
}
return children;
}
// 未登录
if (pathname != "/login") {
return <Navigate to="/login" replace />;
} else {
return children;
}
};
export default ComponentAuth;
五、退出登录
退出登录就要清除登陆人的相关信息
存储的token,用户信息,menu,menuTree以及tablist标签栏
store–>features–>systemSlice.ts
mport { createSlice } from "@reduxjs/toolkit";
import { Session } from "@/utils/storage";
export interface iSystemState {
globalLoading: boolean;
tabsList: any[];
}
const initialState: iSystemState = {
globalLoading: true,
tabsList: Session.get("tabsList") || [{ label: "首页", key: "/home", closable: false }],
};
export const systemSlice = createSlice({
name: "systemSlice",
initialState,
reducers: {
setGlobalLoading: (state, { payload }) => {
state.globalLoading = payload;
},
setTabsList: (state, { payload }) => {
Session.set("tabsList", payload);
state.tabsList = payload;
},
systemSliceClear: (state) => {
Session.remove("tabsList");
state.tabsList = [{ label: "首页", key: "/home", closable: false }];
},
},
});
export const { setGlobalLoading, setTabsList, systemSliceClear } = systemSlice.actions;
export default systemSlice.reducer;
layout->Header->index.tsx
const { userInfo } = useSelector((store: any) => store.userSlice);
const dispatch = useDispatch();
const navigate = useNavigate();
// 退出登录
const loginOut = () => {
Modal.confirm({
title: "确认退出登录?",
icon: <ExclamationCircleOutlined />,
okText: "确认",
cancelText: "取消",
onOk: async () => {
const [err, res] = await api.logout();
if (!err && res?.code === 200) {
dispatch(systemSliceClear());
dispatch(userSliceClear());
navigate("/");
}
},
});
};
return (
<Header className={styles.header}>
<div>
<div className={styles.title}>
<img src={logo} alt="中国联通" />
</div>
</div>
<div className={styles.login}>
<span>
{userInfo.realName ?? "--"} ({userInfo.mobile ?? "--"})
</span>
<Button type="link" onClick={loginOut}>
退出
</Button>
</div>
</Header>
);
结尾
项目的基本功能到此结束,其中的路由页面可自行补充,前端工程化配置详情看https://blog.csdn.net/weixin_46533954/article/details/132027693