mirror of
https://github.com/un-pany/v3-admin-vite.git
synced 2025-04-22 03:49:19 +08:00
feat: 实现动态路由与动态菜单
This commit is contained in:
parent
0f140a61b2
commit
1fd876de72
87
src/api/hook-demo/use-dynamic-route.ts
Normal file
87
src/api/hook-demo/use-dynamic-route.ts
Normal file
@ -0,0 +1,87 @@
|
||||
/** 菜单类型 */
|
||||
export enum MenuType {
|
||||
Menu = "menu",
|
||||
Page = "page",
|
||||
Button = "button",
|
||||
Link = "link"
|
||||
}
|
||||
|
||||
/** 菜单元数据 */
|
||||
export interface MenuMeta {
|
||||
/** 菜单标题 */
|
||||
title: string
|
||||
/** 菜单图标 */
|
||||
icon?: string
|
||||
/** 菜单总是可见 */
|
||||
alwaysShow?: boolean
|
||||
/** 菜单是否可用 */
|
||||
roles?: string[]
|
||||
}
|
||||
|
||||
/** 菜单详情 */
|
||||
export interface MenuItem {
|
||||
/** 菜单名称 */
|
||||
name: string
|
||||
/** 菜单类型 */
|
||||
type: MenuType
|
||||
/** 菜单路径 */
|
||||
path: string
|
||||
/** 菜单元数据 */
|
||||
meta: MenuMeta
|
||||
/** 子菜单 */
|
||||
children?: MenuItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态路由
|
||||
* 用来放置有权限 (Roles 属性) 的路由
|
||||
* 必须带有 Name 属性
|
||||
*/
|
||||
const dynamicRoutes: MenuItem[] = [
|
||||
{
|
||||
path: "/permission",
|
||||
name: "Permission",
|
||||
type: MenuType.Menu,
|
||||
meta: {
|
||||
title: "权限",
|
||||
icon: "lock",
|
||||
roles: ["admin", "editor"], // 可以在根路由中设置角色
|
||||
alwaysShow: true // 将始终显示根菜单
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "page",
|
||||
name: "PagePermission",
|
||||
type: MenuType.Page,
|
||||
meta: {
|
||||
title: "页面级",
|
||||
roles: ["admin"] // 或者在子导航中设置角色
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "directive",
|
||||
name: "DirectivePermission",
|
||||
type: MenuType.Page,
|
||||
meta: {
|
||||
title: "按钮级" // 如果未设置角色,则表示:该页面不需要权限,但会继承根路由的角色
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
/** 模拟加载菜单接口 */
|
||||
export function getMenuDataApi() {
|
||||
return new Promise<typeof dynamicRoutes>((resolve, reject) => {
|
||||
// 模拟接口响应时间 1s
|
||||
setTimeout(() => {
|
||||
// 模拟接口调用成功
|
||||
if (Math.random() < 0.8) {
|
||||
resolve(dynamicRoutes)
|
||||
} else {
|
||||
// 模拟接口调用出错
|
||||
reject(new Error("接口发生错误"))
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
}
|
@ -2,7 +2,7 @@ import { type RouteRecordRaw, createRouter } from "vue-router"
|
||||
import { history, flatMultiLevelRoutes } from "./helper"
|
||||
import routeSettings from "@/config/route"
|
||||
|
||||
const Layouts = () => import("@/layouts/index.vue")
|
||||
export const Layouts = () => import("@/layouts/index.vue")
|
||||
|
||||
/**
|
||||
* 常驻路由
|
||||
|
@ -38,10 +38,11 @@ router.beforeEach(async (to, _from, next) => {
|
||||
// 否则要重新获取权限角色
|
||||
try {
|
||||
await userStore.getInfo()
|
||||
await userStore.getMenu()
|
||||
// 注意:角色必须是一个数组! 例如: ["admin"] 或 ["developer", "editor"]
|
||||
const roles = userStore.roles
|
||||
// 生成可访问的 Routes
|
||||
routeSettings.dynamic ? permissionStore.setRoutes(roles) : permissionStore.setAllRoutes()
|
||||
routeSettings.dynamic ? permissionStore.setRoutes(roles, userStore.menus) : permissionStore.setAllRoutes()
|
||||
// 将 "有访问权限的动态路由" 添加到 Router 中
|
||||
permissionStore.addRoutes.forEach((route) => router.addRoute(route))
|
||||
// 确保添加路由已完成
|
||||
|
@ -2,9 +2,12 @@ import { ref } from "vue"
|
||||
import store from "@/store"
|
||||
import { defineStore } from "pinia"
|
||||
import { type RouteRecordRaw } from "vue-router"
|
||||
import { constantRoutes, dynamicRoutes } from "@/router"
|
||||
import { constantRoutes, dynamicRoutes, Layouts } from "@/router"
|
||||
import { flatMultiLevelRoutes } from "@/router/helper"
|
||||
import routeSettings from "@/config/route"
|
||||
import { MenuItem, MenuType } from "@/api/hook-demo/use-dynamic-route"
|
||||
|
||||
const modules = import.meta.glob(["@/views/*.vue", "@/views/**/*.vue"])
|
||||
|
||||
const hasPermission = (roles: string[], route: RouteRecordRaw) => {
|
||||
const routeRoles = route.meta?.roles
|
||||
@ -25,6 +28,35 @@ const filterDynamicRoutes = (routes: RouteRecordRaw[], roles: string[]) => {
|
||||
return res
|
||||
}
|
||||
|
||||
function transformMenuToRoute(menuItem: MenuItem, rootItem: boolean, parentPath: string = ""): RouteRecordRaw {
|
||||
const childrenRoute: RouteRecordRaw[] = []
|
||||
const vuePath = "/src/views" + parentPath + "/" + menuItem.path + ".vue"
|
||||
const vuePage = rootItem ? Layouts : modules[vuePath]
|
||||
|
||||
// 如果有 children,则需要递归添加 children 到 route
|
||||
if (menuItem.children && menuItem.children.length > 0) {
|
||||
for (let i = 0; i < menuItem.children.length; i++) {
|
||||
if (menuItem.children[i].type == MenuType.Page) {
|
||||
childrenRoute.push(transformMenuToRoute(menuItem.children[i], false, menuItem.path))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const routeItem: RouteRecordRaw = {
|
||||
path: menuItem.path,
|
||||
name: menuItem.name,
|
||||
component: vuePage,
|
||||
meta: {
|
||||
title: menuItem.meta.title,
|
||||
svgIcon: menuItem.meta?.icon,
|
||||
roles: menuItem.meta?.roles
|
||||
},
|
||||
children: childrenRoute.length > 0 ? childrenRoute : undefined
|
||||
}
|
||||
|
||||
return routeItem
|
||||
}
|
||||
|
||||
export const usePermissionStore = defineStore("permission", () => {
|
||||
/** 可访问的路由 */
|
||||
const routes = ref<RouteRecordRaw[]>([])
|
||||
@ -32,8 +64,13 @@ export const usePermissionStore = defineStore("permission", () => {
|
||||
const addRoutes = ref<RouteRecordRaw[]>([])
|
||||
|
||||
/** 根据角色生成可访问的 Routes(可访问的路由 = 常驻路由 + 有访问权限的动态路由) */
|
||||
const setRoutes = (roles: string[]) => {
|
||||
const accessedRoutes = filterDynamicRoutes(dynamicRoutes, roles)
|
||||
const setRoutes = (roles: string[], menus: MenuItem[]) => {
|
||||
const menuRoute: RouteRecordRaw[] = []
|
||||
for (let i = 0; i < menus.length; i++) {
|
||||
menuRoute.push(transformMenuToRoute(menus[i], true))
|
||||
}
|
||||
|
||||
const accessedRoutes = filterDynamicRoutes(menuRoute, roles)
|
||||
_set(accessedRoutes)
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ref } from "vue"
|
||||
import { ref, reactive } from "vue"
|
||||
import store from "@/store"
|
||||
import { defineStore } from "pinia"
|
||||
import { useTagsViewStore } from "./tags-view"
|
||||
@ -7,11 +7,13 @@ import { getToken, removeToken, setToken } from "@/utils/cache/cookies"
|
||||
import { resetRouter } from "@/router"
|
||||
import { loginApi, getUserInfoApi } from "@/api/login"
|
||||
import { type LoginRequestData } from "@/api/login/types/login"
|
||||
import { MenuItem, getMenuDataApi } from "@/api/hook-demo/use-dynamic-route"
|
||||
import routeSettings from "@/config/route"
|
||||
|
||||
export const useUserStore = defineStore("user", () => {
|
||||
const token = ref<string>(getToken() || "")
|
||||
const roles = ref<string[]>([])
|
||||
const menus = reactive<MenuItem[]>([])
|
||||
const username = ref<string>("")
|
||||
|
||||
const tagsViewStore = useTagsViewStore()
|
||||
@ -30,6 +32,13 @@ export const useUserStore = defineStore("user", () => {
|
||||
// 验证返回的 roles 是否为一个非空数组,否则塞入一个没有任何作用的默认角色,防止路由守卫逻辑进入无限循环
|
||||
roles.value = data.roles?.length > 0 ? data.roles : routeSettings.defaultRoles
|
||||
}
|
||||
/** 获取菜单 */
|
||||
const getMenu = async () => {
|
||||
const data = await getMenuDataApi()
|
||||
if (data && data.length > 0) {
|
||||
menus.push(...data)
|
||||
}
|
||||
}
|
||||
/** 模拟角色变化 */
|
||||
const changeRoles = async (role: string) => {
|
||||
const newToken = "token-" + role
|
||||
@ -60,7 +69,7 @@ export const useUserStore = defineStore("user", () => {
|
||||
}
|
||||
}
|
||||
|
||||
return { token, roles, username, login, getInfo, changeRoles, logout, resetToken }
|
||||
return { token, roles, menus, username, login, getInfo, getMenu, changeRoles, logout, resetToken }
|
||||
})
|
||||
|
||||
/** 在 setup 外使用 */
|
||||
|
Loading…
x
Reference in New Issue
Block a user