动态路由,用户权限,按钮权限简单案例
学会搭建layout布局
【Vue3+TS项目】硅谷甄选day03–layout组件搭建+路由配置+左侧菜单搭建_鱼仔是个NaN的博客-CSDN博客
动态路由
后台管理系统,需要根据角色拥有页面信息也就是路由信息,然后由前端动态展示出来。
推荐项目可参考:ruoyi-vue-plus,可以一步一步调试了解。
案例分享:
在main.ts中配置路由
/**
* 初始化事件
*/
async function setupSynchronized() {
setupPlugins(app);
setupRegisterGlobComp(app);
await setupRouter(app);
}
setupSynchronized().then(() => {
app.mount("#app");
});
index.ts这里创建了一个路由
路由的配置参数等可以看官网:
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文件就是在路由走前和路由走后进行的一些操作。
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();