动态路由,用户权限,按钮权限简单案例

动态路由,用户权限,按钮权限简单案例

学会搭建layout布局

【Vue3+TS项目】硅谷甄选day03–layout组件搭建+路由配置+左侧菜单搭建_鱼仔是个NaN的博客-CSDN博客

硅谷甄选(项目主体) (yuque.com)

动态路由

后台管理系统,需要根据角色拥有页面信息也就是路由信息,然后由前端动态展示出来。

推荐项目可参考:ruoyi-vue-plus,可以一步一步调试了解。

案例分享:

在main.ts中配置路由

/**
 * 初始化事件
 */
async function setupSynchronized() {
  setupPlugins(app);
  setupRegisterGlobComp(app);
  await setupRouter(app);
}

setupSynchronized().then(() => {
  app.mount("#app");
});

在这里插入图片描述

index.ts这里创建了一个路由

路由的配置参数等可以看官网:

入门 | Vue Router (vuejs.org)

import { App } from "vue";
import { createRouter, createWebHashHistory } from "vue-router";
import autoload from "./autoload";
import guard from "./guard";
import routes from "./routes";

const router = createRouter({
  history: createWebHashHistory(),
  routes: [...routes]
});

export async function setupRouter(app: App) {
  await autoload(router);
  guard(router);
  app.use(router);
}


export default router;

前面创建的路由首先是从routers.ts中获取

as RouteRecordRaw[] 导出为routers

import { RouteRecordRaw } from "vue-router";

const routes = [
  {
    path: "/",
    name: "HomePage",
    redirect: { name: "Dashboard" },
    component: () => import("@/layouts/common-page.vue"),
    meta: { auth: true, menu: { title: "router.home", icon: "Monitor" } },
    children: [
      {
        path: "dashboard",
        name: "Dashboard",
        meta: { menu: { title: "router.dashboard" } },
        component: () => import("@/views/HomePage.vue")
      }
    ]
  },
  {
    path: "/login",
    name: "LoginPage",
    meta: { guest: true },
    component: () => import("@/views/auth/LoginPage.vue")
  },
  {
    path: "/:any(.*)",
    name: "notFound",
    component: () => import("@/views/errors/404.vue")
  }
] as RouteRecordRaw[];

export default routes;

然后执行了autoload/index.ts 自动装配路由 本地已有的路由+后端获取的动态路由

import { fetchRouteInfo } from "@/api/route";
import { userStore } from "@/store/user";
import { getToken } from "@/utils/auth";
import { Recordable } from "vite-plugin-mock";
import { Router, RouteRecordRaw } from "vue-router";
import autoloadModuleRoutes from "./module";
let routes: RouteRecordRaw[] = autoloadModuleRoutes();
let dynamicViewsModules: Record<string, () => Promise<Recordable>>;
const LayoutMap = new Map<string, () => Promise<typeof import("*.vue")>>();
LayoutMap.set("common-page", () => import("@/layouts/common-page.vue"));

/**
 * 收集嵌套路由元信息并组装
 * @param children 嵌套的子路由
 * @returns 路由元信息
 */
function filterNestedChildren(children: RouteRecordRaw[]) {
  const user = userStore();
  if (user.roleList === undefined) return children;
  return children.filter((r) => {
    const permissions = r.meta?.permissions;

    if (r.children) {
      r.children = filterNestedChildren(r.children);
    }

    return permissions ? permissions?.includes(user.roleList[0]) : true;
  });
}

/**
 * 根据已有的页面模块匹配路由
 * @param dynamicViewsModules 动态路由模块
 * @param r 路由元信息
 * @returns 匹配结果
 */
function matchDynamicComponent(
  dynamicViewsModules: Record<string, () => Promise<Recordable>>,
  r: RouteRecordRaw
) {
  const keys = Object.keys(dynamicViewsModules);
  return keys.filter((key) => {
    const k = key.replace("/src/views", "");
    const startIndex = 0;
    const lastIndex = k.length;
    return k.substring(startIndex, lastIndex) === `${r.component}`;
  });
}

/**
 * 过滤从服务器回传的路由元数据
 * @param route 远程路由
 * @returns 过滤后的路由
 */
function filterRemoteRoute(route: RouteRecordRaw[]): RouteRecordRaw[] {
  dynamicViewsModules = dynamicViewsModules || import.meta.glob("@/views/**/*.{vue,tsx}");
  return route.map((r) => {
    if (r.children) {
      r.children = filterRemoteRoute(r.children);
    }

    // 获取匹配路由文件
    const path = `${r.component}`;
    if (LayoutMap.has(path)) {
      r.component = LayoutMap.get(path);
    }

    // 获取页面路由模块
    const matchKeys = matchDynamicComponent(dynamicViewsModules, r);
    if (matchKeys?.length === 1) {
      const matchKey = matchKeys[0];
      r.component = dynamicViewsModules[matchKey];
    }
    return r;
  });
}

/**
 * 获取远程路由元数据
 * @returns 远程路由
 */
async function fetchRemoteRoute() {
  const res = await fetchRouteInfo();
  console.log(res);
  return filterRemoteRoute(res.data);
}

/**
 * 路由自动装配
 * @param router 路由实例
 */
// let a = 1;
async function autoload(router: Router) {
  if (remoteFlag) {
    const remoteRoutes = await fetchRemoteRoute();
    routes = [...routes, ...remoteRoutes];
  }
  routes = routes.map((route) => {
    route.children = filterNestedChildren(route.children!);
    return route;
  });

  routes.forEach((r) => router.addRoute(r));
}

export default autoload;

这里面routes = [...routes, ...remoteRoutes]; routes代表 autoload/module.ts 下的路由信息
let routes: RouteRecordRaw[] = autoloadModuleRoutes();

autoload/module.ts

import { RouteRecordRaw } from "vue-router";
export default function autoloadModuleRoutes() {
  const modules: Record<
    string,
    {
      [key: string]: any;
    }
  > = import.meta.glob("../module/**/*.ts", { eager: true });

  const routes = [] as RouteRecordRaw[];

  Object.keys(modules).forEach((key) => {
    routes.push(modules[key].default);
  });

  return routes;
}

这里有一个要分清 就是 公共路由是前面三个,后面的是装配的路由,装配的路由(有本地+后端传来的)。

guard.ts文件就是在路由走前和路由走后进行的一些操作。

导航守卫 | Vue Router (vuejs.org)

import { userStore } from "@/store/user";
import { getToken } from "@/utils/auth";
import redirectService from "@/hooks/useRedirect";
import { RouteLocationNormalized, Router } from "vue-router";
import NProgress from "@/utils/progress";
import autoload from "./autoload";
import { useMessage } from "@/hooks/useMessage";

class Guard {
  constructor(private router: Router) {}

  public run() {
    this.router.beforeEach(this.beforeEach.bind(this));
    this.router.afterEach(this.afterEach.bind(this));
  }

  private async beforeEach(to: RouteLocationNormalized, from: RouteLocationNormalized) {
    const userState = userStore();
    NProgress.start();

    if (to.meta.auth && !this.token()) {
      redirectService.addRedirect(to.name as unknown as string);
      console.log(1111);
      return { name: "LoginPage" };
    }

    if (from.name === "LoginPage") {
      console.log(2222);
      await autoload(this.router);
    }
    if (this.token() && userState.roleList[0] == null) {
      console.log(3333);
      const res = await userState.getUserInfo();
      // console.log(res);
      if (res?.code === 401) {
        useMessage("error", res.msg + ",请重新登录");
        userState.roleList[0] = "guest";
      } else {
        console.log(4444);
        await autoload(this.router);
      }
    }
    // console.log(userState.permission);
    if (userState.roleList) {
      const permissions = to.meta.permissions;
      // console.log(permissions);
      if (permissions && !permissions.includes(userState.roleList[0])) {
        console.log(5555);
        return { name: "404" };
      }
    }
    if (to.meta.guest && this.token()) {
      console.log(6666);
      return from;
    }
  }

  private async afterEach() {
    NProgress.done();
  }

  private token(): string | null {
    return getToken();
  }
}

export default (router: Router) => {
  new Guard(router).run();
};

通过这个看layout路由加载

 {
    path: "/",
    name: "HomePage",
    redirect: { name: "Dashboard" },
    component: () => import("@/layouts/common-page.vue"),
    meta: { auth: true, menu: { title: "router.home", icon: "Monitor" } },
    children: [
      {
        path: "dashboard",
        name: "Dashboard",
        meta: { menu: { title: "router.dashboard" } },
        component: () => import("@/views/HomePage.vue")
      }
    ]
  },

component: () => import(“@/layouts/common-page.vue”)

这个就是页面的布局 监听路由变化。

<script lang="ts" setup>
import Menu from "@/layouts/default/menu/index.vue";
import Header from "@/layouts/default/header/index.vue";
import History from "@/layouts/default/history/index.vue";
import tabService from "@/hooks/useTab";
import PageView from "./pages/index.vue";

const route = useRoute();
watch(
  route,
  () => {
    tabService.addHistoryTab(route);
  },
  { immediate: true }
);
</script>
<template>
  <el-container class="h-screen w-full font-sans">
    <Menu />
    <el-container direction="vertical">
      <Header />
      <History />
      <PageView />
    </el-container>
  </el-container>
</template>

<style scoped></style>

import { IMenu } from "#/menu";
import router from "@/router";
import { RouteLocationNormalized, RouteRecordRaw } from "vue-router";
import { useStorage } from "@vueuse/core";
import { CacheEnum } from "@/enum/cacheEnum";

class Tab {
  public history = useStorage<IMenu[]>(CacheEnum.HISTORY_MENU, []);
  public route = ref(null as null | RouteLocationNormalized);
  public isRouterAlive = ref<boolean>(true);

  constructor() {
    this.history.value = this.getHistoryTab();
  }

  removeHistoryTab(menu: IMenu) {
    const index = this.history.value.indexOf(menu);
    this.history.value.splice(index, 1);
  }

  addHistoryTab(route: RouteLocationNormalized) {
    if (!route.meta?.menu) return;
    this.route.value = route;

    const menu: IMenu = { ...route.meta?.menu, route: route.name as string };
    const isHas = this.history.value.some((menu) => menu.route == route.name);
    if (!isHas) this.history.value.unshift(menu);
    if (this.history.value.length > 10) {
      this.history.value.pop();
    }
  }

  /**
   * 重载页面
   */
  reload() {
    this.isRouterAlive.value = false;
    nextTick(() => {
      this.isRouterAlive.value = true;
    });
  }

  /**
   * 关闭当前页面
   */
  closeSelf() {
    const menu = this.history.value.find((m) => m.route == this.route.value?.name);
    const length = this.history.value.length;
    if (menu && length > 1) {
      this.removeHistoryTab(menu);
      const prevRouter: IMenu = this.history.value[length - 2];
      router.push({ name: prevRouter.route });
    }
  }

  /**
   * 索引计算
   * @returns 当前页面在历史记录中的索引
   */
  calcalateHistoryIndex() {
    return this.history.value.findIndex((m) => m.route == this.route.value?.name);
  }

  isTop() {
    return computed(() => this.calcalateHistoryIndex() === 0);
  }

  isBottom() {
    return computed(() => {
      const length = this.history.value.length;
      return this.calcalateHistoryIndex() === length - 1;
    });
  }

  /**
   * 关闭左边的页面
   */
  closeLeft() {
    if (!this.isTop().value) {
      const index = this.calcalateHistoryIndex();
      this.history.value.splice(0, index);
    }
  }

  /**
   * 关闭右边的页面
   */
  closeRight() {
    if (!this.isBottom().value) {
      const index = this.calcalateHistoryIndex();
      this.history.value.splice(index + 1);
    }
  }

  /**
   * 关闭其他页面
   */
  closeOther() {
    this.closeLeft();
    this.closeRight();
  }

  /**
   * 关闭所有页面
   */
  closeAll() {
    router.push("/");
    this.history.value = [
      {
        route: "Dashboard",
        title: "router.dashboard"
      }
    ];
  }

  private getHistoryTab() {
    const routes = [] as RouteRecordRaw[];
    router.getRoutes().map((r) => routes.push(...r.children));

    return this.history.value.filter((m) => {
      return routes.some((r) => r.name == m.route);
    });
  }
}

export default new Tab();

按钮权限

vue权限管理—按钮权限如何写?_丑小鸭变黑天鹅的博客-CSDN博客

<script setup lang="ts">
// import permissionService from "@/hooks/usePermission";
import { useI18n } from "vue-i18n";

const { t } = useI18n();
</script>

<template>
  <div class="relative w-full">
    <div class="p-5 absolute w-full">
      <el-button v-permission="'*:*:*'">{{ t("page.common.btn.edit") }}</el-button>
      <el-button v-permission="'*:*:*'" type="primary">{{ t("page.common.btn.add") }}</el-button>
      <el-button v-permission="'*:*:*'" type="danger">{{ t("page.common.btn.delete") }}</el-button>
      <el-button v-permission="'*:*:*'" type="danger">{{
        t("page.common.btn.batchDelete")
      }}</el-button>
    </div>
  </div>
</template>

<style scoped></style>

import type { Directive, DirectiveBinding } from "vue";

import permissionService from "@/hooks/usePermission";

function isAuth(el: Element, binding: any) {
  const { hasPermission } = permissionService;

  const value = binding.value;
  if (!value) return;
  if (!hasPermission(value)) {
    el.parentNode?.removeChild(el);
  }
}

const mounted = (el: Element, binding: DirectiveBinding<any>) => {
  isAuth(el, binding);
};

const authDirective: Directive = {
  mounted
};

export default authDirective;

import { refreshWindow } from "@/utils/web";
import { useStorage } from "@vueuse/core";
import { userStore } from "@/store/user";

class Permission {
  public defaultPermission = useStorage<string>("role", "superadmin");

  public togglePermission = async () => {
    this.defaultPermission.value =
      this.defaultPermission.value === "superadmin" ? "editor" : "superadmin";
    refreshWindow();
  };

  public hasPermission(value: string): boolean {
    const userState = userStore();
    // console.log(userState.permission!.includes(value));
    return userState.permission!.includes(value);
  }
}

export default new Permission();

项目后端地址

项目前端地址

前端项目参考地址

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值