基于Geeker-Admin后台管理模版实现动态路由,权限管理

2024-05-26  本文已影响0人  风中凌乱的男子
- 前言:最近公司在研发新的项目,由于vue2.x已经停止维护更新,作为这个这个决策者,决定采用vue3.x+vite+pinia+element-plus+ts的最新技术栈进行开发。
- 但是呢,自己搭开发模版费时费力,几番周折下来,决定采用坊间比较靠谱的Geeker-Admin来进行开发,苦于全网都没个像样的教程,故花了几天的时间研究了一下这个模版,发现确实还挺好用的,就基于这个模版实现了登录、获取个人信息、在不破坏主体架构的基础上,略微改造,实现了权限管理、动态路由。
- 之前也写过vue2.x基于vue-element-admin开发模版实现的权限管理,并且录制了视频教程,帮助了数以百计的学员吧,小赚了2万来块,哈哈,也算有点小经验,希望这篇教程能帮到你,让你少走点弯路。
- 废话不多说了,下面开始表演。
第一步:下载模版,打开gitee地址:https://gitee.com/HalseySpicy/Geeker-Admin
git clone https://gitee.com/HalseySpicy/Geeker-Admin.git
下面vscode打开代码
Geeker-Admin
├─ .husky                  # husky 配置文件
├─ .vscode                 # VSCode 推荐配置
├─ build                   # Vite 配置项
├─ public                  # 静态资源文件(该文件夹不会被打包)
├─ src
│  ├─ api                  # API 接口管理
│  ├─ assets               # 静态资源文件
│  ├─ components           # 全局组件
│  ├─ config               # 全局配置项
│  ├─ directives           # 全局指令文件
│  ├─ enums                # 项目常用枚举
│  ├─ hooks                # 常用 Hooks 封装
│  ├─ languages            # 语言国际化 i18n
│  ├─ layouts              # 框架布局模块
│  ├─ routers              # 路由管理
│  ├─ stores               # pinia store
│  ├─ styles               # 全局样式文件
│  ├─ typings              # 全局 ts 声明
│  ├─ utils                # 常用工具库
│  ├─ views                # 项目所有页面
│  ├─ App.vue              # 项目主组件
│  ├─ main.ts              # 项目入口文件
│  └─ vite-env.d.ts        # 指定 ts 识别 vue
├─ .editorconfig           # 统一不同编辑器的编码风格
├─ .env                    # vite 常用配置
├─ .env.development        # 开发环境配置
├─ .env.production         # 生产环境配置
├─ .env.test               # 测试环境配置
├─ .eslintignore           # 忽略 Eslint 校验
├─ .eslintrc.cjs           # Eslint 校验配置文件
├─ .gitignore              # 忽略 git 提交
├─ .prettierignore         # 忽略 Prettier 格式化
├─ .prettierrc.cjs         # Prettier 格式化配置
├─ .stylelintignore        # 忽略 stylelint 格式化
├─ .stylelintrc.cjs        # stylelint 样式格式化配置
├─ CHANGELOG.md            # 项目更新日志
├─ commitlint.config.cjs   # git 提交规范配置
├─ index.html              # 入口 html
├─ LICENSE                 # 开源协议文件
├─ lint-staged.config.cjs  # lint-staged 配置文件
├─ package-lock.json       # 依赖包包版本锁
├─ package.json            # 依赖包管理
├─ postcss.config.cjs      # postcss 配置
├─ README.md               # README 介绍
├─ tsconfig.json           # typescript 全局配置
└─ vite.config.ts          # vite 全局配置文件
以上是项目的工程目录
第二步:安装依赖,run起项目
pnpm install //安装依赖
pnpm dev  //启动项目
下面是启动后的登录页面和登录后的首页
登录页
首页
第三步:下面当然老规矩,先来分析下,登录逻辑!只有掌握了登录逻辑,才会熟悉整个项目。
image.png
image.png
image.png
const userStore = useUserStore();
const authStore = useAuthStore();
image.png
// 1.NProgress 开始
  NProgress.start();

  // 2.动态设置标题
  const title = import.meta.env.VITE_GLOB_APP_TITLE;
  document.title = to.meta.title ? `${to.meta.title} - ${title}` : title;

  // 3.判断是访问登陆页,有 Token 就在当前页面,没有 Token 重置路由到登陆页
  if (to.path.toLocaleLowerCase() === LOGIN_URL) {
    if (userStore.token) return next(from.fullPath);
    resetRouter();
    return next();
  }

  // 4.判断访问页面是否在路由白名单地址(静态路由)中,如果存在直接放行
  if (ROUTER_WHITE_LIST.includes(to.path)) return next();

  // 5.判断是否有 Token,没有重定向到 login 页面
  if (!userStore.token) return next({ path: LOGIN_URL, replace: true });

  // 6.如果没有菜单列表,就重新请求菜单列表并添加动态路由
  if (!authStore.authMenuListGet.length) {
    await initDynamicRouter();
    return next({ ...to, replace: true });
  }

  // 7.存储 routerName 做按钮权限筛选
  authStore.setRouteName(to.name as string);

  // 8.正常访问页面
  next();
image.png
image.png
第四步:下面就开始分析,点击登录触发的方法,打开src/views/login/components/LoginForm.vue文件,找到login方法,
const login = (formEl: FormInstance | undefined) => {
  if (!formEl) return;
  formEl.validate(async valid => {
    if (!valid) return;
    loading.value = true;
    try {
      // 1.执行登录接口
      const { data } = await loginApi({ ...loginForm, password: md5(loginForm.password) });
      userStore.setToken(data.access_token);

      // 2.添加动态路由
      await initDynamicRouter();

      // 3.清空 tabs、keepAlive 数据
      tabsStore.setTabs([]);
      keepAliveStore.setKeepAliveName([]);

      // 4.跳转到首页
      router.push(HOME_URL);
      ElNotification({
        title: getTimeState(),
        message: "欢迎登录 Geeker-Admin",
        type: "success",
        duration: 3000
      });
    } finally {
      loading.value = false;
    }
  });
};
image.png
/**
 * @description 初始化动态路由
 */
export const initDynamicRouter = async () => {
  const userStore = useUserStore();
  const authStore = useAuthStore();

  try {
    // 1.获取菜单列表 && 按钮权限列表
    await authStore.getAuthMenuList();
    await authStore.getAuthButtonList();

    // 2.判断当前用户有没有菜单权限
    if (!authStore.authMenuListGet.length) {
      ElNotification({
        title: "无权限访问",
        message: "当前账号无任何菜单权限,请联系系统管理员!",
        type: "warning",
        duration: 3000
      });
      userStore.setToken("");
      router.replace(LOGIN_URL);
      return Promise.reject("No permission");
    }

    // 3.添加动态路由
    authStore.flatMenuListGet.forEach(item => {
      item.children && delete item.children;
      if (item.component && typeof item.component == "string") {
        item.component = modules["/src/views" + item.component + ".vue"];
      }
      if (item.meta.isFull) {
        router.addRoute(item as unknown as RouteRecordRaw);
      } else {
        router.addRoute("layout", item as unknown as RouteRecordRaw);
      }
    });
  } catch (error) {
    // 当按钮 || 菜单请求出错时,重定向到登陆页
    userStore.setToken("");
    router.replace(LOGIN_URL);
    return Promise.reject(error);
  }
};
 const userStore = useUserStore();
 const authStore = useAuthStore();
 // 1.获取菜单列表 && 按钮权限列表
 await authStore.getAuthMenuList();
 await authStore.getAuthButtonList();
 // Get AuthButtonList
    async getAuthButtonList() {
      const { data } = await getAuthButtonListApi();
      this.authButtonList = data;
    },
    // Get AuthMenuList
    async getAuthMenuList() {
      const { data } = await getAuthMenuListApi();
      this.authMenuList = data;
    },
// 获取菜单列表
export const getAuthMenuListApi = () => {
  return http.get<Menu.MenuOptions[]>(PORT1 + `/menu/list`, {}, { loading: false });
  // 如果想让菜单变为本地数据,注释上一行代码,并引入本地 authMenuList.json 数据
  return authMenuList;
};

// 获取按钮权限
export const getAuthButtonListApi = () => {
  return http.get<Login.ResAuthButtons>(PORT1 + `/auth/buttons`, {}, { loading: false });
  // 如果想让按钮权限变为本地数据,注释上一行代码,并引入本地 authButtonList.json 数据
  return authButtonList;
};
// 2.判断当前用户有没有菜单权限
if (!authStore.authMenuListGet.length) {
      ElNotification({
        title: "无权限访问",
        message: "当前账号无任何菜单权限,请联系系统管理员!",
        type: "warning",
        duration: 3000
      });
      userStore.setToken("");
      router.replace(LOGIN_URL);
      return Promise.reject("No permission");
    }
image.png
// 3.添加动态路由
    authStore.flatMenuListGet.forEach(item => {
      item.children && delete item.children;
      if (item.component && typeof item.component == "string") {
        item.component = modules["/src/views" + item.component + ".vue"];
      }
      if (item.meta.isFull) {
        router.addRoute(item as unknown as RouteRecordRaw);
      } else {
        router.addRoute("layout", item as unknown as RouteRecordRaw);
      }
    });
// 3.清空 tabs、keepAlive 数据
      tabsStore.setTabs([]);
      keepAliveStore.setKeepAliveName([]);

      // 4.跳转到首页
      router.push(HOME_URL);
      ElNotification({
        title: getTimeState(),
        message: "欢迎登录 Geeker-Admin",
        type: "success",
        duration: 3000
      });
image.png

继续往下!

第五步:既然要开发项目嘛,先把无用的多余的页面删了。
image.png
image.png
{
  "code": 200,
  "data": [
    {
      "path": "/home/index",
      "name": "home",
      "component": "/home/index",
      "meta": {
        "icon": "HomeFilled",
        "title": "首页",
        "isLink": "",
        "isHide": false,
        "isFull": false,
        "isAffix": true,
        "isKeepAlive": true
      }
    }
  ],
  "msg": "成功"
}
image.png
image.png
第六步:开始分析这个authMenuList.json,为啥return这个json文件就可以渲染出来页面呢?
image.png
// 菜单权限列表 ==> 扁平化之后的一维数组菜单,主要用来添加动态路由
    flatMenuListGet: state => getFlatMenuList(state.authMenuList),
image.png
{
  "code": 200,
  "data": [
    {
      "path": "/home/index",
      "name": "home",
      "component": "/home/index",
      "meta": {
        "icon": "HomeFilled",
        "title": "首页",
        "isLink": "",
        "isHide": false,
        "isFull": false,
        "isAffix": true,
        "isKeepAlive": true
      }
    },
    {
      "path": "/assembly",
      "name": "assembly",
      "redirect": "/assembly/guide",
      "meta": {
        "icon": "Briefcase",
        "title": "测试菜单",
        "isLink": "",
        "isHide": false,
        "isFull": false,
        "isAffix": false,
        "isKeepAlive": true
      },
      "children": [
        {
          "path": "/assembly/guide",
          "name": "guide",
          "component": "/home/index",
          "meta": {
            "icon": "Menu",
            "title": "测试页面",
            "isLink": "",
            "isHide": false,
            "isFull": false,
            "isAffix": false,
            "isKeepAlive": true
          }
        }
      ]
    }
  ],
  "msg": "成功"
}
image.png
image.png
image.png
 // 3.添加动态路由
    authStore.flatMenuListGet.forEach(item => {
      item.children && delete item.children;
      if (item.component && typeof item.component == "string") {
        item.component = modules["/src/views" + item.component + ".vue"];
      }
      if (item.meta.isFull) {
        router.addRoute(item as unknown as RouteRecordRaw);
      } else {
        router.addRoute("layout", item as unknown as RouteRecordRaw);
      }
    });
1115009958
接上篇 基于Geeker-Admin后台管理模版实现动态路由,权限管理01
server: {
      host: "0.0.0.0",
      port: viteEnv.VITE_PORT,
      open: viteEnv.VITE_OPEN,
      cors: true,
      // Load proxy configuration from .env.development
      proxy: createProxy(viteEnv.VITE_PROXY)
    },
image.png
image.png
config.headers.set("x-access-token", userStore.token);
image.png
image.png
image.png
image.png
image.png
import authMenuList from "@/assets/json/authMenuList.json";
import authButtonList from "@/assets/json/authButtonList.json";
/**
 * @name 登录模块
 */
// 用户登录
export const loginApi = (params: Login.ReqLoginForm) => {
  return http.post<Login.ResLogin>(PORT1 + `/login`, params, { loading: false }); // 正常 post json 请求  ==>  application/json
};

下篇再讲对接真实登录接口,累了,想睡觉了。

接上篇 基于Geeker-Admin后台管理模版实现动态路由,权限管理02
{
    "code": 200,
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpX5cCI6IkpX5cCI6IkpX",
    "message": "登录成功"
}
第一步:既然知道接口地址了,首先先来配置项目反向代理。
VITE_PROXY = [["/api","https://demo.it98k.cn"]]
第二步:打开src/api/modules/login.ts文件,找到loginApi,修改下接口地址
export const loginApi = (params: Login.ReqLoginForm) => {
  return http.post<Login.ResLogin>(PORT1 + `/api/login`, params, { loading: false }); // 正常 post json 请求  ==>  application/json
  // return http.post<Login.ResLogin>(PORT1 + `/login`, params, { loading: false }); // 控制当前请求不显示 loading
  // return http.post<Login.ResLogin>(PORT1 + `/login`, {}, { params }); // post 请求携带 query 参数  ==>  ?username=admin&password=123456
  // return http.post<Login.ResLogin>(PORT1 + `/login`, qs.stringify(params)); // post 请求携带表单参数  ==>  application/x-www-form-urlencoded
  // return http.get<Login.ResLogin>(PORT1 + `/login?${qs.stringify(params, { arrayFormat: "repeat" })}`); // get 请求可以携带数组等复杂参数
};
<el-input v-model="loginForm.username" placeholder="用户名:admin / user">
const loginRules = reactive({
  username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
  password: [{ required: true, message: "请输入密码", trigger: "blur" }]
});
const loginForm = reactive<Login.ReqLoginForm>({
  username: "",
  password: ""
});
<el-input v-model="loginForm.loginame" placeholder="用户名:admin / user">
const loginRules = reactive({
  loginame: [{ required: true, message: "请输入用户名", trigger: "blur" }],
  password: [{ required: true, message: "请输入密码", trigger: "blur" }]
});
const loginForm = reactive<Login.ReqLoginForm>({
  loginame: "",
  password: ""
});
image.png
  export interface ReqLoginForm {
    username: string;
    password: string;
  }
  export interface ReqLoginForm {
    loginame: string;
    password: string;
  }
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
// 1.执行登录接口
const { token } = await loginApi({ ...loginForm, password: loginForm.password });

userStore.setToken(token);
image.png
image.png
image.png
 export interface ResLogin {
    token: string;
  }
image.png
image.png
image.png
image.png
image.png
image.png
接上篇:基于Geeker-Admin后台管理模版实现动态路由,权限管理03
image.png
image.png
image.png
image.png
上一篇 下一篇

猜你喜欢

热点阅读