fetch(modify):修改bug
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m20s
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m20s
This commit is contained in:
@@ -20,7 +20,7 @@ import { isInWhiteList, hasRoutePermission, isTokenValid, buildLoginRedirect } f
|
||||
const isRouteRegistered = ref(false)
|
||||
|
||||
// 临时开发模式:跳过所有权限验证(开发静态页面时使用)
|
||||
const DEV_MODE_SKIP_AUTH = true
|
||||
const DEV_MODE_SKIP_AUTH = false
|
||||
|
||||
/**
|
||||
* 路由全局前置守卫
|
||||
@@ -232,20 +232,125 @@ async function processFrontendMenu(router: Router): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理后端控制模式的菜单逻辑
|
||||
* 处理后端控制模式的菜单
|
||||
*/
|
||||
async function processBackendMenu(router: Router): Promise<void> {
|
||||
const closeLoading = loadingService.showLoading()
|
||||
|
||||
try {
|
||||
const { menuList } = await menuService.getMenuList()
|
||||
await registerAndStoreMenu(router, menuList, closeLoading)
|
||||
const userStore = useUserStore()
|
||||
const backendMenus = userStore.menus || []
|
||||
const routeMap = buildRouteMap(asyncRoutes)
|
||||
|
||||
const menuList = backendMenus
|
||||
.map((menu) => convertBackendMenuToRoute(menu, routeMap))
|
||||
.filter((route) => route !== null)
|
||||
|
||||
const finalMenuList = mergeDefaultMenus(menuList, routeMap)
|
||||
await registerAndStoreMenu(router, finalMenuList, closeLoading)
|
||||
} catch (error) {
|
||||
closeLoading()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并默认菜单
|
||||
* 将配置的默认菜单合并到后端返回的菜单中
|
||||
*/
|
||||
function mergeDefaultMenus(
|
||||
backendMenus: AppRouteRecord[],
|
||||
routeMap: Map<string, AppRouteRecord>
|
||||
): AppRouteRecord[] {
|
||||
const defaultMenuPaths = ['/dashboard']
|
||||
|
||||
const defaultMenus: AppRouteRecord[] = defaultMenuPaths
|
||||
.map((path) => {
|
||||
const route = routeMap.get(path)
|
||||
return route ? menuDataToRouter(route) : null
|
||||
})
|
||||
.filter((menu): menu is AppRouteRecord => menu !== null)
|
||||
|
||||
const backendPaths = new Set(backendMenus.map((m) => m.path))
|
||||
const filteredDefaultMenus = defaultMenus.filter((m) => !backendPaths.has(m.path))
|
||||
|
||||
return [...filteredDefaultMenus, ...backendMenus]
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 URL 到路由的映射表
|
||||
*/
|
||||
function buildRouteMap(routes: AppRouteRecord[], parentPath = ''): Map<string, AppRouteRecord> {
|
||||
const map = new Map<string, AppRouteRecord>()
|
||||
|
||||
routes.forEach((route) => {
|
||||
// 构建完整路径
|
||||
const fullPath = route.path.startsWith('/')
|
||||
? route.path
|
||||
: parentPath
|
||||
? `${parentPath}/${route.path}`.replace(/\/+/g, '/')
|
||||
: `/${route.path}`
|
||||
|
||||
// 存储路由映射
|
||||
map.set(fullPath, route)
|
||||
|
||||
// 递归处理子路由
|
||||
if (route.children && route.children.length > 0) {
|
||||
const childMap = buildRouteMap(route.children, fullPath)
|
||||
childMap.forEach((childRoute, childPath) => {
|
||||
map.set(childPath, childRoute)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return map
|
||||
}
|
||||
|
||||
/**
|
||||
* 将后端菜单数据转换为路由格式
|
||||
*/
|
||||
function convertBackendMenuToRoute(
|
||||
menu: any,
|
||||
routeMap: Map<string, AppRouteRecord>,
|
||||
parentPath = ''
|
||||
): AppRouteRecord | null {
|
||||
const menuUrl = menu.url || '/'
|
||||
const matchedRoute = routeMap.get(menuUrl)
|
||||
|
||||
if (!matchedRoute) {
|
||||
console.warn(`未找到与菜单 URL "${menuUrl}" 匹配的路由定义: ${menu.name}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const route: AppRouteRecord = {
|
||||
path: menuUrl,
|
||||
name: matchedRoute.name,
|
||||
component: matchedRoute.component,
|
||||
meta: {
|
||||
...matchedRoute.meta,
|
||||
title: menu.name,
|
||||
permission: menu.perm_code,
|
||||
sort: menu.sort || matchedRoute.meta?.sort || 0,
|
||||
icon: menu.icon || matchedRoute.meta?.icon,
|
||||
// 清除前端定义的 roles 和 permissions,使用后端权限控制
|
||||
roles: undefined,
|
||||
permissions: undefined
|
||||
}
|
||||
}
|
||||
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
const children = menu.children
|
||||
.map((child: any) => convertBackendMenuToRoute(child, routeMap, menuUrl))
|
||||
.filter((child: AppRouteRecord | null) => child !== null)
|
||||
|
||||
if (children.length > 0) {
|
||||
route.children = children
|
||||
}
|
||||
}
|
||||
|
||||
return route
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册路由并存储菜单数据
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import type { UserInfo } from '@/types/api'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
/**
|
||||
* 不需要登录的路由白名单
|
||||
@@ -33,6 +34,7 @@ export const isInWhiteList = (path: string): boolean => {
|
||||
|
||||
/**
|
||||
* 检查用户是否有权限访问路由
|
||||
* 根据文档要求:菜单访问基于 URL,按钮访问基于权限标识
|
||||
*/
|
||||
export const hasRoutePermission = (
|
||||
route: RouteLocationNormalized,
|
||||
@@ -40,7 +42,30 @@ export const hasRoutePermission = (
|
||||
): boolean => {
|
||||
const { roles = [], permissions = [] } = userInfo
|
||||
|
||||
// 如果路由没有设置权限要求,直接通过
|
||||
// 如果是超级管理员,直接通过
|
||||
if (userInfo.user_type === 1) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 默认始终可访问的路由(不需要菜单权限)
|
||||
const defaultAllowedPaths = ['/dashboard']
|
||||
const isDefaultRoute = defaultAllowedPaths.some(
|
||||
(allowedPath) => route.path === allowedPath || route.path.startsWith(allowedPath + '/')
|
||||
)
|
||||
|
||||
// 检查菜单访问权限(基于 URL)
|
||||
const userStore = useUserStore()
|
||||
const userMenus = userStore.menus || []
|
||||
|
||||
// 如果是默认路由,跳过菜单权限检查
|
||||
if (!isDefaultRoute && userMenus.length > 0) {
|
||||
const hasMenuAccess = checkMenuAccess(route.path, userMenus)
|
||||
if (!hasMenuAccess) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 如果路由没有设置额外的权限要求,直接通过
|
||||
if (!route.meta?.roles && !route.meta?.permissions) {
|
||||
return true
|
||||
}
|
||||
@@ -58,11 +83,9 @@ export const hasRoutePermission = (
|
||||
if (route.meta.permissions) {
|
||||
const routePermissions = route.meta.permissions as string[]
|
||||
const hasPermission = routePermissions.some((permission) => {
|
||||
// 支持通配符权限 *:*:*
|
||||
if (permissions.includes('*:*:*')) {
|
||||
return true
|
||||
}
|
||||
// 精确匹配或前缀匹配
|
||||
return permissions.some((userPermission) => {
|
||||
if (userPermission.endsWith('*')) {
|
||||
const prefix = userPermission.slice(0, -1)
|
||||
@@ -79,6 +102,25 @@ export const hasRoutePermission = (
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归检查菜单访问权限(基于 URL)
|
||||
*/
|
||||
function checkMenuAccess(path: string, menus: any[]): boolean {
|
||||
for (const menu of menus) {
|
||||
// 检查当前菜单的 URL 是否匹配
|
||||
if (menu.url && (path === menu.url || path.startsWith(menu.url + '/'))) {
|
||||
return true
|
||||
}
|
||||
// 递归检查子菜单
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
if (checkMenuAccess(path, menu.children)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 Token 是否有效
|
||||
* 简单检查,真实项目中应该验证 JWT 或者调用后端接口
|
||||
|
||||
@@ -306,32 +306,32 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
keepAlive: true,
|
||||
isHideTab: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'menu',
|
||||
name: 'Menus',
|
||||
component: RoutesAlias.Menu,
|
||||
meta: {
|
||||
title: 'menus.system.menu',
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER'],
|
||||
authList: [
|
||||
{
|
||||
title: '新增',
|
||||
auth_mark: 'add'
|
||||
},
|
||||
{
|
||||
title: '编辑',
|
||||
auth_mark: 'edit'
|
||||
},
|
||||
{
|
||||
title: '删除',
|
||||
auth_mark: 'delete'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
// {
|
||||
// path: 'menu',
|
||||
// name: 'Menus',
|
||||
// component: RoutesAlias.Menu,
|
||||
// meta: {
|
||||
// title: 'menus.system.menu',
|
||||
// keepAlive: true,
|
||||
// roles: ['R_SUPER'],
|
||||
// authList: [
|
||||
// {
|
||||
// title: '新增',
|
||||
// auth_mark: 'add'
|
||||
// },
|
||||
// {
|
||||
// title: '编辑',
|
||||
// auth_mark: 'edit'
|
||||
// },
|
||||
// {
|
||||
// title: '删除',
|
||||
// auth_mark: 'delete'
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
// {
|
||||
// path: 'nested',
|
||||
// name: 'Nested',
|
||||
// component: '',
|
||||
@@ -741,6 +741,26 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/shop-management',
|
||||
name: 'ShopManagement',
|
||||
component: RoutesAlias.Home,
|
||||
meta: {
|
||||
title: 'menus.product.shop',
|
||||
icon: ''
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
name: 'Shop',
|
||||
component: RoutesAlias.Shop,
|
||||
meta: {
|
||||
title: 'menus.product.shop',
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/account-management',
|
||||
name: 'AccountManagement',
|
||||
@@ -816,71 +836,71 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/product',
|
||||
name: 'Product',
|
||||
component: RoutesAlias.Home,
|
||||
meta: {
|
||||
title: 'menus.product.title',
|
||||
icon: ''
|
||||
},
|
||||
children: [
|
||||
// {
|
||||
// path: 'sim-card',
|
||||
// name: 'SimCardManagement',
|
||||
// component: RoutesAlias.SimCardManagement,
|
||||
// meta: {
|
||||
// title: 'menus.product.simCard',
|
||||
// keepAlive: true
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// path: 'sim-card-assign',
|
||||
// name: 'SimCardAssign',
|
||||
// component: RoutesAlias.SimCardAssign,
|
||||
// meta: {
|
||||
// title: 'menus.product.simCardAssign',
|
||||
// keepAlive: true
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// path: 'package-series',
|
||||
// name: 'PackageSeries',
|
||||
// component: RoutesAlias.PackageSeries,
|
||||
// meta: {
|
||||
// title: 'menus.product.packageSeries',
|
||||
// keepAlive: true
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// path: 'package-list',
|
||||
// name: 'PackageList',
|
||||
// component: RoutesAlias.PackageList,
|
||||
// meta: {
|
||||
// title: 'menus.product.packageList',
|
||||
// keepAlive: true
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// path: 'package-assign',
|
||||
// name: 'PackageAssign',
|
||||
// component: RoutesAlias.PackageAssign,
|
||||
// meta: {
|
||||
// title: 'menus.product.packageAssign',
|
||||
// keepAlive: true
|
||||
// }
|
||||
// },
|
||||
{
|
||||
path: 'shop',
|
||||
name: 'Shop',
|
||||
component: RoutesAlias.Shop,
|
||||
meta: {
|
||||
title: 'menus.product.shop',
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// {
|
||||
// path: '/product',
|
||||
// name: 'Product',
|
||||
// component: RoutesAlias.Home,
|
||||
// meta: {
|
||||
// title: 'menus.product.title',
|
||||
// icon: ''
|
||||
// },
|
||||
// children: [
|
||||
// // {
|
||||
// // path: 'sim-card',
|
||||
// // name: 'SimCardManagement',
|
||||
// // component: RoutesAlias.SimCardManagement,
|
||||
// // meta: {
|
||||
// // title: 'menus.product.simCard',
|
||||
// // keepAlive: true
|
||||
// // }
|
||||
// // },
|
||||
// // {
|
||||
// // path: 'sim-card-assign',
|
||||
// // name: 'SimCardAssign',
|
||||
// // component: RoutesAlias.SimCardAssign,
|
||||
// // meta: {
|
||||
// // title: 'menus.product.simCardAssign',
|
||||
// // keepAlive: true
|
||||
// // }
|
||||
// // },
|
||||
// // {
|
||||
// // path: 'package-series',
|
||||
// // name: 'PackageSeries',
|
||||
// // component: RoutesAlias.PackageSeries,
|
||||
// // meta: {
|
||||
// // title: 'menus.product.packageSeries',
|
||||
// // keepAlive: true
|
||||
// // }
|
||||
// // },
|
||||
// // {
|
||||
// // path: 'package-list',
|
||||
// // name: 'PackageList',
|
||||
// // component: RoutesAlias.PackageList,
|
||||
// // meta: {
|
||||
// // title: 'menus.product.packageList',
|
||||
// // keepAlive: true
|
||||
// // }
|
||||
// // },
|
||||
// // {
|
||||
// // path: 'package-assign',
|
||||
// // name: 'PackageAssign',
|
||||
// // component: RoutesAlias.PackageAssign,
|
||||
// // meta: {
|
||||
// // title: 'menus.product.packageAssign',
|
||||
// // keepAlive: true
|
||||
// // }
|
||||
// // },
|
||||
// {
|
||||
// path: 'shop',
|
||||
// name: 'Shop',
|
||||
// component: RoutesAlias.Shop,
|
||||
// meta: {
|
||||
// title: 'menus.product.shop',
|
||||
// keepAlive: true
|
||||
// }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
{
|
||||
path: '/asset-management',
|
||||
name: 'AssetManagement',
|
||||
|
||||
Reference in New Issue
Block a user