diff --git a/app/api/entry-modules/entry-modules.ts b/app/api/entry-modules/entry-modules.ts index 976905e..2d84cb0 100644 --- a/app/api/entry-modules/entry-modules.ts +++ b/app/api/entry-modules/entry-modules.ts @@ -23,6 +23,7 @@ export interface EntryModule { name: string; description?: string | null; path?: string | null; // logo图片路径 + route_path?: string | null; // 前端跳转路径 areas?: AreaConfig[] | null; // 地区配置列表 created_at?: string; updated_at?: string; diff --git a/app/hooks/usePermission.tsx b/app/hooks/usePermission.tsx index 98d56b9..8cd9af7 100644 --- a/app/hooks/usePermission.tsx +++ b/app/hooks/usePermission.tsx @@ -23,6 +23,7 @@ */ import { useRouteLoaderData, useLocation } from "@remix-run/react"; +import { normalizeRoutePathForPermission } from "~/utils/route-alias"; interface RootLoaderData { permissions?: string[]; @@ -74,7 +75,7 @@ export function usePermission() { const userArea = rootData?.userArea || ''; // 🔑 根据当前路由获取权限列表 - const currentPath = location.pathname; + const currentPath = normalizeRoutePathForPermission(location.pathname); // console.log('currentPath', currentPath) // 获取当前路由的权限:优先使用 permissionMap,否则使用交叉评查默认配置 @@ -118,13 +119,7 @@ export function usePermission() { return legacyPermissions.includes(permissionKey); } - // 降级方案:如果没有权限数据,使用userRole判断(兼容现有系统) - // 包含'provin'的角色拥有所有权限 - if (userRole.toLowerCase().includes('provin')) { - return true; - } - - // 默认只有查看权限 + // 降级方案:没有权限映射时绝不默认放开写权限,只保留只读能力。 if (permissionKey.includes(':read')) { return true; } @@ -174,8 +169,8 @@ export function usePermission() { return legacyPermissions.some(p => p.startsWith(`${module}:`)); } - // 降级方案 - return userRole.toLowerCase().includes('provin'); + // 降级方案只保留只读模块识别,避免 0/6 权限时被角色名放大。 + return hasPermission(`${module}:list:read`) || hasPermission(`${module}:detail:read`); }; /** diff --git a/app/routes/entry-modules._index.tsx b/app/routes/entry-modules._index.tsx index ef0688d..7e2ba80 100644 --- a/app/routes/entry-modules._index.tsx +++ b/app/routes/entry-modules._index.tsx @@ -46,6 +46,7 @@ interface LoaderData { pageSize: number; currentPage: number; error?: string; + accessDenied?: boolean; } function resolveModuleLogoUrl(path?: string | null): string | null { @@ -92,16 +93,19 @@ export async function clientLoader({ request }: ClientLoaderFunctionArgs) { modules: modulesResult, total: totalCount, pageSize, - currentPage: page + currentPage: page, + accessDenied: false, }; } catch (error) { + const errorMessage = error instanceof Error ? error.message : "加载入口模块列表失败"; console.error("加载入口模块列表失败:", error); return { modules: [], total: 0, pageSize: 10, currentPage: 1, - error: error instanceof Error ? error.message : "加载入口模块列表失败" + error: errorMessage, + accessDenied: errorMessage.includes('无权限') || errorMessage.includes('权限') || errorMessage.includes('403'), }; } } @@ -127,7 +131,7 @@ export default function EntryModulesList() { // 获取加载器数据 const loaderData = useLoaderData(); - const { modules, total, error } = loaderData; + const { modules, total, error, accessDenied } = loaderData; // ✅ 使用权限 Hook const { canCreate, canUpdate, canDelete, canView } = usePermission(); @@ -145,9 +149,13 @@ export default function EntryModulesList() { // 处理loader加载数据的时候的错误 useEffect(() => { if (error) { + if (accessDenied) { + toastService.warning('您当前只有入口模块页面可见权限,没有列表读取权限'); + return; + } toastService.error(error); } - }, [error]); + }, [error, accessDenied]); // 处理名称搜索 const handleNameSearch = (value: string) => { @@ -413,17 +421,30 @@ export default function EntryModulesList() { /> - {/* 表格 */} - + {accessDenied ? ( +
+
+ +
+
+ 当前账号没有入口模块列表权限 +
+
+ 请在“角色权限管理”中为当前角色授予 `entry_module:list:read` 及相关权限后再访问。 +
+
+ ) : ( +
+ )} {/* 分页 */} - {total > 0 && ( + {!accessDenied && total > 0 && ( !isAllPermission(p)); }; + const getPermissionShortLabel = (permission: ApiPermission): string => { + const [, resource = '', action = ''] = permission.permission_key.split(':'); + const labelMap: Record = { + 'list:read': '列表读取', + 'detail:read': '详情查看', + 'create:write': '创建', + 'update:write': '更新', + 'delete:delete': '删除', + 'image:write': '图标上传', + 'roles:read': '角色列表', + 'roles:update': '角色编辑', + 'permissions:read': '权限点读取', + 'role_permissions:write': '角色权限保存', + 'role_routes:write': '角色菜单保存', + 'users:read': '用户列表', + 'user_roles:write': '用户角色分配', + }; + return labelMap[`${resource}:${action}`] || permission.display_name; + }; + + const getPermissionHint = (route: RouteInfo, permission: ApiPermission): string => { + const key = permission.permission_key; + if (route.route_path === '/entry-modules') { + const hintMap: Record = { + 'entry_module:list:read': '决定能否读取入口模块列表并展示表格数据。', + 'entry_module:detail:read': '决定能否进入编辑页查看模块详情。', + 'entry_module:create:write': '决定能否新建入口模块。', + 'entry_module:update:write': '决定能否保存入口模块修改。', + 'entry_module:delete:delete': '决定能否删除入口模块。', + 'entry_module:image:write': '决定能否上传或替换入口模块图标。', + }; + return hintMap[key] || ''; + } + if (route.route_path === '/role-permissions') { + const hintMap: Record = { + 'rbac:roles:read': '读取角色列表与角色详情。', + 'rbac:roles:update': '创建、编辑、删除角色基础信息。', + 'rbac:permissions:read': '读取页面右侧的接口权限定义。', + 'rbac:role_permissions:write': '保存 API 权限勾选结果。', + 'rbac:role_routes:write': '保存菜单/路由勾选结果。', + 'rbac:users:read': '读取角色下的用户列表。', + 'rbac:user_roles:write': '给用户分配或移除角色。', + }; + return hintMap[key] || ''; + } + return ''; + }; + + const getRoutePermissionGuide = (route: RouteInfo, permissions: ApiPermission[]): string | null => { + if (route.route_path === '/entry-modules' && permissions.length > 0) { + return '这 6 项是完整闭环:先勾“列表读取/详情查看”,再按需勾“创建、更新、删除、图标上传”4 个写权限。'; + } + if (route.route_path === '/role-permissions' && permissions.length > 0) { + return '角色权限页建议至少保留“角色列表、权限点读取、角色菜单保存、角色权限保存”,否则页面容易只看得到却保存不了。'; + } + return null; + }; + // v3.7: 切换单个API权限(支持通用权限同步) const handleTogglePermission = (permission: ApiPermission, checked: boolean) => { const permissionId = permission.id; @@ -1638,10 +1696,29 @@ export default function RolePermissions() { const renderPermissionsList = () => { if (!hasPermissions || !isExpanded) return null; + const routeGuide = getRoutePermissionGuide(route, permissions); + return (
+ {routeGuide && ( +
+ 勾选说明:{routeGuide} +
+ )} {permissions.map(permission => { const isShared = isSharedPermission(permission); + const permissionHint = getPermissionHint(route, permission); // 获取通用权限关联的路由名称(排除当前路由) const relatedRouteNames = (() => { @@ -1681,8 +1758,16 @@ export default function RolePermissions() {
- {permission.display_name} + {getPermissionShortLabel(permission)} +
+ {permission.display_name} +
+ {permissionHint && ( +
+ {permissionHint} +
+ )} {isShared && relatedRouteNames.length > 0 && (
@@ -1693,9 +1778,10 @@ export default function RolePermissions() {
)}
- - {permission.api_path} - +
+
{permission.api_path}
+
{permission.permission_key}
+
); })} @@ -1872,15 +1958,16 @@ export default function RolePermissions() { 角色权限管理 - {/*
+
-
*/} +
@@ -2012,7 +2099,7 @@ export default function RolePermissions() { onClick={handleSavePermissions} disabled={!isProvincialAdmin || savingPermissions} > - {savingPermissions ? '保存中...' : '保存权限'} + {savingPermissions ? '保存中...' : '保存菜单与接口权限'}
@@ -2025,6 +2112,22 @@ export default function RolePermissions() { ) : ( <> +
+ 当前角色 {selectedRole.role_name} 已勾选 {selectedRouteIds.length} 个菜单路由、 + {selectedPermissionIds.length} 个接口权限。 + 保存时会同时提交“左侧菜单勾选 + 展开后的接口权限勾选”,请以接口权限说明为准。 +
{/* v3.8: 路由树容器 - 可滚动区域 */}