diff --git a/src/router/guards/beforeEach.ts b/src/router/guards/beforeEach.ts index 891a316..18c60db 100644 --- a/src/router/guards/beforeEach.ts +++ b/src/router/guards/beforeEach.ts @@ -110,14 +110,22 @@ async function handleRouteGuard( return } - // 尝试刷新路由重新注册 + // 路由未匹配时,判断是 404 还是 403 if (userStore.isLogin) { - isRouteRegistered.value = false - await handleDynamicRoutes(to, router, next) - return + // 检查路由是否存在于 asyncRoutes 中 + const routeExistsInAsync = checkRouteExistsInAsyncRoutes(to.path, asyncRoutes) + console.log(`[路由检查] 目标路径: ${to.path}, 是否存在于asyncRoutes: ${routeExistsInAsync}`) + + if (routeExistsInAsync) { + // 路由存在于 asyncRoutes 但未注册,说明用户没有权限 + console.warn('路由存在但用户无权限访问:', to.path) + next(RoutesAlias.Exception403 || '/exception/403') + return + } } - // 如果以上都不匹配,跳转到404 + // 路由不存在,跳转到 404 + console.log(`[路由检查] 路径 ${to.path} 不存在于asyncRoutes,跳转404`) next(RoutesAlias.Exception404) } @@ -374,7 +382,7 @@ function convertBackendMenuToRoute( } } - // 递归处理所有层级的 children + // 递归处理后端返回的 children if (menu.children && menu.children.length > 0) { const children = menu.children .map((child: any) => convertBackendMenuToRoute(child, routeMap, menuUrl)) @@ -453,6 +461,88 @@ function isValidMenuList(menuList: AppRouteRecord[]): boolean { return Array.isArray(menuList) && menuList.length > 0 } +/** + * 检查路由是否存在于 asyncRoutes 中 + * 支持动态参数匹配,如 /account-management/enterprise-customer/customer-accounts/3000 匹配 enterprise-customer/customer-accounts/:id + * @param targetPath 目标路由路径,如 /account-management/enterprise-customer/customer-accounts/3000 + * @param routes 路由配置数组 + * @param parentPath 父路由路径 + */ +function checkRouteExistsInAsyncRoutes( + targetPath: string, + routes: AppRouteRecord[], + parentPath = '' +): boolean { + for (const route of routes) { + // 构建完整路径 + const fullPath = route.path.startsWith('/') + ? route.path + : parentPath + ? `${parentPath}/${route.path}`.replace(/\/+/g, '/') + : `/${route.path}` + + // 检查当前路由是否匹配(支持动态参数) + const isMatch = matchRoutePath(targetPath, fullPath) + + if (isMatch) { + console.log(`[路由匹配成功] 目标: ${targetPath}, 匹配到: ${fullPath}`) + return true + } + + // 递归检查子路由 + if (route.children && route.children.length > 0) { + if (checkRouteExistsInAsyncRoutes(targetPath, route.children, fullPath)) { + return true + } + } + } + + return false +} + +/** + * 匹配路由路径,支持动态参数 + * @param targetPath 实际路径,如 /account-management/enterprise-customer/customer-accounts/3000 + * @param routePath 路由定义路径,如 /account-management/enterprise-customer/customer-accounts/:id + */ +function matchRoutePath(targetPath: string, routePath: string): boolean { + // 移除查询参数 + const cleanTargetPath = targetPath.split('?')[0] + const cleanRoutePath = routePath.split('?')[0] + + // 如果完全匹配,直接返回 + if (cleanTargetPath === cleanRoutePath) { + return true + } + + // 将路径分割成段 + const targetSegments = cleanTargetPath.split('/').filter(Boolean) + const routeSegments = cleanRoutePath.split('/').filter(Boolean) + + // 段数必须相同 + if (targetSegments.length !== routeSegments.length) { + return false + } + + // 逐段比较 + for (let i = 0; i < routeSegments.length; i++) { + const routeSegment = routeSegments[i] + const targetSegment = targetSegments[i] + + // 如果是动态参数(以 : 开头),跳过比较 + if (routeSegment.startsWith(':')) { + continue + } + + // 如果不是动态参数,必须完全匹配 + if (routeSegment !== targetSegment) { + return false + } + } + + return true +} + /** * 重置路由相关状态 */ diff --git a/src/router/guards/permission.ts b/src/router/guards/permission.ts index c02762c..46c4353 100644 --- a/src/router/guards/permission.ts +++ b/src/router/guards/permission.ts @@ -107,10 +107,12 @@ export const hasRoutePermission = ( */ function checkMenuAccess(path: string, menus: any[]): boolean { for (const menu of menus) { - // 检查当前菜单的 URL 是否匹配 - if (menu.url && (path === menu.url || path.startsWith(menu.url + '/'))) { + // 检查当前菜单的 URL 是否匹配(支持动态参数) + if (menu.url && matchMenuPath(path, menu.url)) { + console.log(`[菜单权限检查] 路径 ${path} 匹配菜单 ${menu.url}`) return true } + // 递归检查子菜单 if (menu.children && menu.children.length > 0) { if (checkMenuAccess(path, menu.children)) { @@ -118,9 +120,53 @@ function checkMenuAccess(path: string, menus: any[]): boolean { } } } + console.log(`[菜单权限检查] 路径 ${path} 未找到匹配的菜单`) return false } +/** + * 匹配菜单路径,支持动态参数 + * @param actualPath 实际访问路径,如 /account-management/enterprise-customer/customer-accounts/3000 + * @param menuPath 菜单定义路径,如 /account-management/enterprise-customer/customer-accounts/:id + */ +function matchMenuPath(actualPath: string, menuPath: string): boolean { + // 移除查询参数 + const cleanActualPath = actualPath.split('?')[0] + const cleanMenuPath = menuPath.split('?')[0] + + // 如果完全匹配,直接返回 + if (cleanActualPath === cleanMenuPath) { + return true + } + + // 将路径分割成段 + const actualSegments = cleanActualPath.split('/').filter(Boolean) + const menuSegments = cleanMenuPath.split('/').filter(Boolean) + + // 段数必须相同 + if (actualSegments.length !== menuSegments.length) { + return false + } + + // 逐段比较 + for (let i = 0; i < menuSegments.length; i++) { + const menuSegment = menuSegments[i] + const actualSegment = actualSegments[i] + + // 如果是动态参数(以 : 开头),跳过比较 + if (menuSegment.startsWith(':')) { + continue + } + + // 如果不是动态参数,必须完全匹配 + if (menuSegment !== actualSegment) { + return false + } + } + + return true +} + /** * 检查 Token 是否有效 * 简单检查,真实项目中应该验证 JWT 或者调用后端接口 diff --git a/src/views/account-management/account/index.vue b/src/views/account-management/account/index.vue index f663a56..9e80a9a 100644 --- a/src/views/account-management/account/index.vue +++ b/src/views/account-management/account/index.vue @@ -95,18 +95,6 @@ 分配角色
{{ currentAccountName }} - - 超级管理员不能分配角色 - - - 平台用户只能分配一个平台角色 - - - 代理账号只能分配一个客户角色 - - - 企业账号只能分配一个客户角色 -
diff --git a/src/views/account-management/enterprise-cards/index.vue b/src/views/account-management/enterprise-cards/index.vue index 961c210..5f41f97 100644 --- a/src/views/account-management/enterprise-cards/index.vue +++ b/src/views/account-management/enterprise-cards/index.vue @@ -67,6 +67,7 @@ @size-change="handleSizeChange" @current-change="handleCurrentChange" @selection-change="handleSelectionChange" + @row-contextmenu="handleRowContextMenu" > @@ -776,18 +768,26 @@ border-color: var(--el-border-color); } - .role-info { - display: flex; - align-items: center; - gap: 8px; - flex: 1; - } - :deep(.el-checkbox) { width: 100%; .el-checkbox__label { width: 100%; + display: flex; + + .role-info { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + flex: 1; + width: 100%; + + > span:first-child { + flex: 1; + min-width: 0; + } + } } } } @@ -800,6 +800,18 @@ .role-info { flex: 1; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + + > span:first-child { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } } .el-button { diff --git a/src/views/package-management/package-list/index.vue b/src/views/package-management/package-list/index.vue index ae9b54a..68ef589 100644 --- a/src/views/package-management/package-list/index.vue +++ b/src/views/package-management/package-list/index.vue @@ -480,7 +480,7 @@ { prop: 'real_data_mb', label: '真流量', - width: 100, + width: 120, formatter: (row: PackageResponse) => `${row.real_data_mb}MB` }, {