点击查看【bilibili】
代码地址:https://github.com/xiumubai/syt-admin

添加权限管理页面

image.png
在这里添加了三个页面:用户管理、角色管理、菜单管理,用户对不同的用户权限做控制。
src/pages下面添加了acl目录,此为权限管理的页面
image.png

修改路由

在菜单管理中,可以对菜单进行添加,所有需要设置权限的菜单必须要在这里进行配置
image.png
注意这里的功能权限值需要跟路由定义的name一致,比如:

  1. {
  2. name: "Acl/User",
  3. path: "/syt/acl/user",
  4. meta: {
  5. title: "用户管理",
  6. },
  7. element: load(User),
  8. },

这里也可以对按钮增加,因为一个页面中的按钮也可以增加权限。
image.png
设置好了路由以后,在info接口当中,会返回当前用户所拥有的路由权限和按钮权限:
image.png
在渲染路由之前,需要对路由做筛选:

  1. // 控制router的业务逻辑
  2. import { FilterRouter, TreeRouterFilter } from "./types";
  3. // 递归处理
  4. const treeRouterFilter: TreeRouterFilter = ({
  5. routeHash,
  6. allAsyncRoutes,
  7. lv = 0
  8. }) => {
  9. // TODO
  10. return allAsyncRoutes.filter(router => {
  11. const { children = [] } = router;
  12. router.children = treeRouterFilter({
  13. routeHash,
  14. allAsyncRoutes: children,
  15. lv: lv+1
  16. })
  17. return (lv === 0) || routeHash[router.name]
  18. })
  19. }
  20. // 处理权限控制的函数
  21. export const filterRouter: FilterRouter = ({
  22. allAsyncRoutes,
  23. routes
  24. }) => {
  25. // hash表结构化, 以优化时间复杂度
  26. const routeHash = (()=>{
  27. const hash: Record<string, any> = {};
  28. routes.forEach((route)=>{hash[route] = true});
  29. return hash
  30. })()
  31. return treeRouterFilter({
  32. routeHash,
  33. allAsyncRoutes,
  34. })
  35. }
  1. import { useRoutes, } from "react-router-dom";
  2. import type { SRoutes } from "./types";
  3. import cloneDeep from 'lodash/cloneDeep'
  4. import { allAsyncRoutes, anyRoute, constantRoutes } from "./routes";
  5. import { useAppSelector } from "@/app/hooks";
  6. import { selectUser } from "@/pages/login/slice";
  7. import { filterRouter } from "./effect";
  8. const allRoutes = cloneDeep(allAsyncRoutes);
  9. /*
  10. 自定义hook: 注册应用的所有路由
  11. */
  12. export const useAppRoutes = () => {
  13. const {routes} = useAppSelector(selectUser);
  14. const resultRouter = routes?.length ? filterRouter({
  15. allAsyncRoutes: allRoutes,
  16. routes: routes
  17. }) : constantRoutes
  18. return useRoutes([...resultRouter, ...anyRoute]);
  19. };
  20. // 找到要渲染成左侧菜单的路由
  21. export const findSideBarRoutes = () => {
  22. const currentIndex = allRoutes.findIndex((route) => route.path === "/syt");
  23. return allRoutes[currentIndex].children as SRoutes;
  24. };
  25. export default allAsyncRoutes;

强制登陆控制

当用户在刷新页面的时候,会强制请求info接口,获取用户信息和权限列表,这里定义了一个高阶组件withAuthorization

  1. import { FC } from "react";
  2. import { useLocation, Navigate } from "react-router-dom";
  3. import { Spin } from "antd";
  4. import { useAppDispatch, useAppSelector } from "@/app/hooks";
  5. import { getUserInfoAsync, selectUser, setToken } from "@/pages/login/slice";
  6. function withAuthorization(WrappedComponent: FC) { // FunctionComponent
  7. return () => {
  8. // 读取redux中管理的token和用户名 ==> 只要状态数据发生了改变, 当前组件函数就会自动重新执行
  9. const { token, name } = useAppSelector(selectUser);
  10. // 获取当前请求的路由地址
  11. const { pathname } = useLocation();
  12. const dispatch = useAppDispatch();
  13. // 如果有token, 说明至少登录过
  14. if (token) {
  15. // 如果要去的是登陆页面或根路径路由, 自动访问首页
  16. if (pathname === "/login" || pathname === "/") {
  17. return <Navigate to="/syt/dashboard" />;
  18. }
  19. // 访问的某个管理页面
  20. // 如果有用户名, 说明已经登陆, 直接渲染目标组件 LayoutComponent/xxx组件
  21. if (name) {
  22. return <WrappedComponent />;
  23. }
  24. // 还没有登陆, 分发请求获取用户信息的异步action
  25. dispatch(getUserInfoAsync());
  26. return <Spin size="large" />;
  27. } else { // 没有登录过 => 都得去登陆页面
  28. // 判断是否是微信扫码登陆
  29. const params = new URLSearchParams(document.location.search.substring(1));
  30. const token = params.get('token');
  31. if (token) {
  32. // 微信扫码登陆,获取到token,直接跳转到首页
  33. dispatch(setToken(token))
  34. return <Navigate to="/syt/dashboard" />;
  35. }
  36. // 如果访问的是登陆页面, 直接显示对应的组件
  37. if (pathname === "/login") {
  38. return <WrappedComponent />;
  39. }
  40. // // 如果访问不是登录页面, 自动跳转到登陆页面
  41. return <Navigate to="/login" />;
  42. }
  43. };
  44. }
  45. export default withAuthorization;

这个组件中会判断登录逻辑,最后需要在App.tsx中使用,这样就能保证每次页面一刷新都会走登陆逻辑

  1. import { ConfigProvider } from "antd";
  2. import zhCN from "antd/lib/locale/zh_CN";
  3. import enUS from "antd/lib/locale/en_US";
  4. import { useAppSelector } from "@/app/hooks";
  5. import { selectLang } from "@/app/appSlice";
  6. import { useAppRoutes } from "./router";
  7. import withAuthorization from "./components/withAuthorization";
  8. function App() {
  9. const lang = useAppSelector(selectLang);
  10. return <ConfigProvider locale={lang === "zh_CN" ? zhCN : enUS}>{useAppRoutes()}</ConfigProvider>;
  11. }
  12. export default withAuthorization(App);

定义store

我们的routes和buttons都需要存放在store中,便于在组件中取值。

  1. ...
  2. const initialState: UserState = {
  3. ...
  4. routes: [],
  5. buttons: []
  6. }
  7. // 创建当前redux模块的管理对象slice
  8. const userSlice = createSlice({
  9. name: 'user', // 标识名称
  10. initialState, // 初始状态
  11. // 配置同步action对应的reducer => 同步action会自动生成
  12. reducers: {
  13. setToken: (state, action) => {
  14. const token = action.payload;
  15. state.token = token
  16. localStorage.setItem('token_key', token)
  17. }
  18. },
  19. // 为前面定义的异步action, 定义对应的reducer
  20. extraReducers(builder) {
  21. builder
  22. // 获取用户信息请求成功后的reducer处理
  23. .addCase(getUserInfoAsync.fulfilled, (state, action) => {
  24. // 将返回的name和avatar只保存到redux
  25. console.log(action);
  26. const {name, avatar, routes, buttons} = action.payload
  27. state.name = name
  28. state.avatar = avatar
  29. state.routes = routes;
  30. state.buttons = buttons;
  31. })
  32. },
  33. })

按钮权限

按钮的控制就更加的简单,只要根据buttons列表中去筛选当前传的key是否存在,来决定展示与否

  1. import { useAppSelector } from "@/app/hooks";
  2. import { selectUser } from "@/pages/login/slice"
  3. const AuthButton = (props: { authKey: any; children: any; }) => {
  4. const { authKey, children } = props;
  5. const {buttons} = useAppSelector(selectUser);
  6. const authorized = buttons?.includes(authKey);
  7. return (
  8. authorized ? children : null
  9. )
  10. }
  11. export default AuthButton

使用方式:

  1. import AuthButton from '@/components/authButton'
  2. <AuthButton authKey="btn.User.assgin">
  3. <Button
  4. type="primary"
  5. icon={<UserOutlined />}
  6. onClick={() => handleAddUser(3, row)}
  7. ></Button>
  8. </AuthButton>

注意authKey的值需要跟后台配置的值相同。