vite+react搭建人力管理系统项目(2)

一、登录页面以及相关信息存储
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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值