1. 为什么我劝你别再手写 RBAC 权限代码
React Admin 后台系统里,权限控制是最容易写成一团乱麻的部分。我见过太多项目,用户、角色、菜单三张表在数据库里躺得好好的,一到前端就退化成if (user.role === 'admin')这种硬编码判断。加一个角色要改代码、重新部署,按钮级权限靠role === 'admin' || role === 'editor'拼字符串,路由守卫只判断有没有 token 不判断有没有权限——直接输入 URL 就能绕过。
这套东西的本质问题在于:权限是数据,不是逻辑。用户拥有哪些角色、角色拥有哪些权限、权限对应哪个菜单或按钮,这些都应该由后端下发、前端动态渲染,而不是写死在组件里。RBAC(Role-Based Access Control)模型就是干这个的:用户 → 角色 → 权限,三层解耦,改权限只改数据不改代码。
但真要从零搭一套完整的 RBAC 前端骨架,涉及 TypeScript 类型定义、Zustand 状态管理、动态菜单树构建、路由守卫、按钮级权限组件、权限树勾选分配……光是理清这些模块的依赖关系就得花两三天。这也是为什么我把 Claude Code 拉进来:它能根据一段结构化的提示词,一次性生成类型定义、状态管理、权限组件、路由配置这一整套互相咬合的代码,而且 TypeScript 类型是连贯的,不会出现 A 文件定义了Permission接口、B 文件又自己写一个不兼容的版本。
这篇要交付的是一套能直接跑起来的 React Admin 后台骨架,技术栈是 React 18 + TypeScript 5 + Ant Design 5 + React Router 6 + Zustand 4 + Axios。核心目标有三个:RBAC 权限模型(用户-角色-权限三级)、动态菜单(根据角色生成侧边栏)、三层权限控制(路由级 + 按钮级 + 逻辑级)。适合正在做后台系统、被权限问题反复折磨的前端同学,也适合想看看 Claude Code 在真实工程里怎么用的开发者。
我会把每一步的提示词、生成的代码、验证命令都写清楚,你跟着敲就能在本地跑通。权限配置片段、角色-菜单映射表、验证步骤都会给全,不玩虚的。
2. 用 TaoToken 给 Claude Code 接上稳定通道
Claude Code 本身是个命令行工具,它需要调用 Claude 的模型能力来生成代码。如果你直接用它默认的接入方式,在国内网络环境下经常会遇到请求超时、连接中断的问题,尤其是生成大段代码的时候,一次断连就得重来。我试过在生成UserList.tsx这种几百行的组件时被中断三次,体验很差。
解决办法是给 Claude Code 配置一个稳定的 API 通道。TaoToken 提供的就是这个能力:它兼容 Anthropic 的 API 协议,你只需要把 Claude Code 的 Base URL 指向 TaoToken 的接口地址,再配一个 API Key,就能稳定调用 Claude 模型。整个过程不涉及任何网络工具,就是标准的 API 配置。
具体操作分三步。第一步,去 TaoToken 控制台创建一个 API Key。打开 https://taotoken.net/api-keys ,登录后点「创建密钥」,复制生成的 Key(形如sk-xxxxxxxx),这个 Key 只显示一次,记得存好。
第二步,配置 Claude Code 的环境变量。Claude Code 读取的是ANTHROPIC_BASE_URL和ANTHROPIC_API_KEY这两个变量。在终端里执行:
export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_API_KEY="sk-你的密钥"如果你想让配置持久化,把这两行写进~/.bashrc或~/.zshrc,然后source一下。Windows 用户可以在系统环境变量里添加,或者用 PowerShell 的$env:ANTHROPIC_BASE_URL="https://taotoken.net/api"。
第三步,验证配置是否生效。执行claude --version确认 Claude Code 已安装,然后随便问一句claude "你好",如果能正常返回内容,说明通道通了。如果报 401,多半是 Key 复制错了或者没生效,重新source一下配置文件。
这里有个细节要注意:Base URL 填的是https://taotoken.net/api,不要带多余的路径。有些同学会习惯性加上/v1,反而会导致 404。TaoToken 的接口已经做了路径兼容,直接填这个地址就行。
配好之后,Claude Code 的响应速度会明显稳定很多。我实测下来,生成一个 300 行的 React 组件,从发出提示词到完整返回,基本在 20 秒内完成,中途不会断。这对于需要连续生成多个文件的场景很关键——你总不希望生成到一半连接断了,还得重新描述需求。
另外,如果你后续要做更复杂的 Agent 任务(比如让 Claude Code 自动跑测试、自动修 bug),可以考虑 TaoToken 的 Coding Plan,它在长会话和连续任务上的稳定性更好。不过对于本篇这种「生成一套后台骨架」的需求,基础的 API Key 就够了。
3. 可复制的 RBAC 类型定义与权限配置片段
这一步是整个系统的地基。RBAC 的核心是三个实体:User(用户)、Role(角色)、Permission(权限)。用户通过roleIds关联角色,角色通过permissionIds关联权限,权限本身是一棵树(菜单有子菜单,菜单下有按钮)。
先建项目。用 Vite 起一个 React + TypeScript 的架子,比 CRA 快很多:
npm create vite@latest react-admin -- --template react-ts cd react-admin npm install antd @ant-design/icons react-router-dom zustand axios dayjs然后让 Claude Code 生成类型定义。提示词要写清楚三件事:实体关系、权限分类、需要模拟数据。我是这么写的:
请帮我定义 React Admin 系统的 RBAC 权限模型:1. 用户、角色、权限三个核心实体;2. 权限分为菜单权限和按钮权限,用 type 字段区分;3. 定义完整的 TypeScript 类型,包含分页请求和响应类型;4. 生成模拟数据,包含管理员、编辑者、查看者三种角色。
Claude Code 生成的src/types/auth.ts核心部分如下。注意Permission接口里的parentId和children,这是构建权限树的关键:
// src/types/auth.ts export interface User { id: string; username: string; nickname: string; email: string; phone: string; status: 'active' | 'disabled'; roleIds: string[]; roles?: Role[]; createdAt: string; updatedAt: string; } export interface Role { id: string; name: string; displayName: string; description: string; permissionIds: string[]; permissions?: Permission[]; status: 'active' | 'disabled'; createdAt: string; } export type PermissionType = 'menu' | 'button' | 'api'; export interface Permission { id: string; parentId: string | null; name: string; displayName: string; type: PermissionType; path?: string; component?: string; icon?: string; sort: number; visible: boolean; children?: Permission[]; } export interface LoginResponse { token: string; refreshToken: string; user: User; permissions: string[]; menus: Permission[]; }接下来是模拟数据。这里有个设计要点:权限的name字段用冒号分隔的命名空间,比如system:user:create表示「系统管理-用户管理-新增」。这种命名方式在后续做按钮级权限判断时非常直观,hasPermission('system:user:create')一眼就能看出是哪个按钮。
角色-权限的映射关系我用一张表说清楚,这也是你后续对接真实后端时的数据结构参考:
| 角色标识 | 显示名称 | 拥有的权限(name) | 说明 |
|---|---|---|---|
| admin | 超级管理员 | 全部权限(*) | 拥有所有菜单和按钮 |
| editor | 内容编辑 | dashboard、content:article、content:article:create、content:article:update、system:user(仅查看) | 能管内容,能看用户但不能改 |
| viewer | 普通查看者 | dashboard、content:article | 只能看,不能做任何写操作 |
对应的模拟数据片段:
// src/mock/data.ts export const mockRoles: Role[] = [ { id: 'r001', name: 'admin', displayName: '超级管理员', description: '拥有系统所有权限', permissionIds: mockPermissions.map(p => p.id), status: 'active', createdAt: '2024-01-01', }, { id: 'r002', name: 'editor', displayName: '内容编辑', description: '负责内容管理,可查看用户但不能修改', permissionIds: [ 'p001', 'p200', 'p210', 'p211', 'p212', 'p213', 'p214', 'p100', 'p110', ], status: 'active', createdAt: '2024-01-02', }, { id: 'r003', name: 'viewer', displayName: '普通查看者', description: '只能查看数据,不能修改', permissionIds: ['p001', 'p200', 'p210'], status: 'active', createdAt: '2024-01-03', }, ];权限树的数据结构也要定义好。菜单权限有path和component,按钮权限没有path但visible为false(不显示在菜单里)。这样一棵树既能渲染侧边栏,又能作为权限判断的数据源:
export const mockPermissions: Permission[] = [ { id: 'p001', parentId: null, name: 'dashboard', displayName: '仪表盘', type: 'menu', path: '/dashboard', component: 'Dashboard', icon: 'DashboardOutlined', sort: 1, visible: true }, { id: 'p100', parentId: null, name: 'system', displayName: '系统管理', type: 'menu', path: '/system', component: 'Layout', icon: 'SettingOutlined', sort: 100, visible: true }, { id: 'p110', parentId: 'p100', name: 'system:user', displayName: '用户管理', type: 'menu', path: '/system/users', component: 'UserList', icon: 'UserOutlined', sort: 1, visible: true }, { id: 'p111', parentId: 'p110', name: 'system:user:create', displayName: '新增用户', type: 'button', sort: 1, visible: false }, { id: 'p112', parentId: 'p110', name: 'system:user:update', displayName: '编辑用户', type: 'button', sort: 2, visible: false }, { id: 'p113', parentId: 'p110', name: 'system:user:delete', displayName: '删除用户', type: 'button', sort: 3, visible: false }, // ... 角色管理、菜单管理、内容管理、日志管理同理 ];写完这些,跑一下验证命令,确认数据没问题:
npx ts-node -e " const { mockPermissions, mockRoles, mockUsers } = require('./src/mock/data'); console.log('权限总数:', mockPermissions.length); console.log('角色数:', mockRoles.length); console.log('管理员权限数:', mockRoles[0].permissionIds.length); console.log('编辑者权限数:', mockRoles[1].permissionIds.length); console.log('查看者权限数:', mockRoles[2].permissionIds.length); "预期输出是权限总数 21、角色数 3、管理员 21、编辑者 10、查看者 3。如果数字对不上,检查一下mockPermissions数组是不是漏了项。
这一步的产物是类型定义 + 模拟数据,它们是后面所有模块的基础。类型定义保证了后续代码的连贯性——Claude Code 在生成 Store 和组件时,会引用这些类型,不会出现字段名对不上的情况。
4. 权限状态管理与路由守卫的完整配置
类型定义好了,接下来是状态管理。RBAC 系统里,登录后需要把用户信息、权限列表、菜单树存到全局状态里,供各个组件读取。我用 Zustand,因为它比 Redux 轻量得多,而且配合persist中间件能自动做 localStorage 持久化,刷新页面不掉登录态。
提示词这样写:
请帮我用 Zustand 创建权限状态管理:1. 存储当前用户信息、权限列表、菜单树;2. 登录/登出操作;3. 权限判断方法(hasPermission、hasAnyPermission、hasAllPermissions);4. 菜单树构建方法(将扁平权限转为树形结构);5. Token 持久化到 localStorage。
Claude Code 生成的src/store/auth-store.ts里,有两个函数是核心。一个是buildPermissionTree,把后端返回的扁平权限数组转成树;另一个是filterVisibleMenus,过滤出type === 'menu'且visible === true的节点,用于渲染侧边栏:
// src/store/auth-store.ts function buildPermissionTree(permissions: Permission[]): Permission[] { const map = new Map<string, Permission>(); const tree: Permission[] = []; permissions.forEach(p => { map.set(p.id, { ...p, children: [] }); }); permissions.forEach(p => { const node = map.get(p.id)!; if (p.parentId && map.has(p.parentId)) { map.get(p.parentId)!.children!.push(node); } else if (!p.parentId) { tree.push(node); } }); const sortTree = (nodes: Permission[]) => { nodes.sort((a, b) => a.sort - b.sort); nodes.forEach(node => { if (node.children?.length) sortTree(node.children); }); }; sortTree(tree); return tree; }权限判断方法里,有个细节要处理:超级管理员。如果用户的权限列表里包含*,说明是超管,所有权限判断都返回true。这样就不用给超管逐个分配权限了:
hasPermission: (permission: string) => { const { permissions } = get(); if (permissions.includes('*')) return true; return permissions.includes(permission); },Store 定义好之后,路由守卫是下一个关键点。路由守卫要做两件事:未登录跳登录页,已登录但无权限显示 403。AuthGuard组件接收permission或permissions参数,内部调用 Store 的判断方法:
// src/components/auth/AuthGuard.tsx export const AuthGuard: React.FC<AuthGuardProps> = ({ children, permission, permissions, requireAll = false, fallback, }) => { const { isLoggedIn, hasPermission, hasAnyPermission, hasAllPermissions } = useAuthStore(); const location = useLocation(); if (!isLoggedIn) { return <Navigate to="/login" state={{ from: location }} replace />; } let authorized = true; if (permission) { authorized = hasPermission(permission); } else if (permissions && permissions.length > 0) { authorized = requireAll ? hasAllPermissions(permissions) : hasAnyPermission(permissions); } if (!authorized) { return fallback ? <>{fallback}</> : <ForbiddenPage />; } return <>{children}</>; };路由配置里,每个需要权限的页面都用AuthGuard包一层。注意permission参数填的是权限的name,比如用户管理页填system:user,角色管理页填system:role:
// src/router/index.tsx { path: 'system/users', element: ( <AuthGuard permission="system:user"> {LazyLoad(UserListPage)} </AuthGuard> ), }, { path: 'system/roles', element: ( <AuthGuard permission="system:role"> {LazyLoad(RoleListPage)} </AuthGuard> ), },按钮级权限用PermissionButton组件。它接收permission参数,无权限时默认隐藏(fallback="hide"),也可以设为禁用(fallback="disable")并显示 Tooltip 提示:
// src/components/auth/PermissionButton.tsx export const PermissionButton: React.FC<PermissionButtonProps> = ({ permission, fallback = 'hide', tooltipText = '您没有此操作的权限', children, ...buttonProps }) => { const hasPermission = useAuthStore(state => state.hasPermission); const authorized = hasPermission(permission); if (!authorized && fallback === 'hide') return null; if (!authorized && fallback === 'disable') { return ( <Tooltip title={tooltipText}> <Button {...buttonProps} disabled>{children}</Button> </Tooltip> ); } return <Button {...buttonProps}>{children}</Button>; };用的时候很直观,比如用户列表页的「新增用户」按钮:
<PermissionButton permission="system:user:create" type="primary" icon={<PlusOutlined />} onClick={() => openModal()} > 新增用户 </PermissionButton>如果当前用户没有system:user:create权限,这个按钮直接不渲染。编辑按钮用permission="system:user:update",删除按钮用permission="system:user:delete",一目了然。
这里要提醒一个容易踩的坑:PermissionButton的permission参数必须和权限数据里的name完全一致,包括大小写和冒号。我见过有人写成system:user:add,但数据里是system:user:create,结果按钮死活不显示,排查半天。建议把权限name定义成常量,用的时候引用常量而不是手写字符串。
验证一下 Store 和权限判断是否正常:
npx ts-node -e " const permissions = ['dashboard', 'system:user', 'system:user:create', 'content:article']; const has = (p) => permissions.includes(p); console.log('有用户创建权限:', has('system:user:create')); console.log('有用户删除权限:', has('system:user:delete')); console.log('有角色管理权限:', has('system:role')); "预期输出true、false、false。如果结果不对,检查一下模拟数据里editor角色的permissionIds是否包含了正确的权限 ID。
5. 跑通登录与动态菜单,验证权限隔离效果
前面把类型、Store、守卫都配好了,这一步把它们串起来,跑通完整的登录流程和动态菜单渲染。登录页要做三件事:表单校验、调用登录接口、把返回的用户信息和权限写入 Store。
登录接口这里用模拟函数代替真实后端。核心逻辑是:根据用户名找到用户,拿到用户的roleIds,再根据角色拿到permissionIds,最后从权限表里查出完整的权限对象,返回给前端:
// src/pages/Login.tsx async function mockLogin(username: string, password: string) { await new Promise(resolve => setTimeout(resolve, 800)); const user = mockUsers.find(u => u.username === username); if (!user) throw new Error('用户不存在'); if (password !== '123456') throw new Error('密码错误'); if (user.status === 'disabled') throw new Error('账号已被禁用'); const userRoles = mockRoles.filter(r => user.roleIds.includes(r.id)); const permissionIds = new Set<string>(); userRoles.forEach(role => { role.permissionIds.forEach(pid => permissionIds.add(pid)); }); const userPermissions = mockPermissions.filter(p => permissionIds.has(p.id)); const permissionNames = userPermissions.map(p => p.name); return { token: `jwt_token_${Date.now()}`, refreshToken: `refresh_${Date.now()}`, user: { ...user, roles: userRoles }, permissions: permissionNames, menus: userPermissions, }; }登录成功后,login(data)会把数据写入 Store,buildPermissionTree自动构建菜单树,filterVisibleMenus过滤出可见菜单。侧边栏组件从 Store 读取menuTree,转换成 Ant Design 的 Menu items 渲染。
动态菜单的关键在于:不同角色登录后,menuTree的内容不同,侧边栏自然就不同。管理员能看到「系统管理」下的用户、角色、菜单三个子菜单,编辑者只能看到「用户管理」(且没有操作按钮),查看者只能看到「仪表盘」和「文章管理」。
用三个账号分别登录验证一下。启动项目:
npm run dev打开浏览器,用admin / 123456登录,侧边栏应该显示完整的菜单树。退出,用editor01 / 123456登录,侧边栏少了「角色管理」和「菜单管理」,进入用户管理页,「新增用户」按钮还在(因为 editor 有system:user:create权限),但「删除用户」按钮消失了(editor 没有system:user:delete权限)。再用viewer01 / 123456登录,侧边栏只剩「仪表盘」和「文章管理」,用户管理页直接进不去,手动输入/system/users会显示 403 页面。
这个验证过程可以用一段脚本模拟权限判断的结果:
npx ts-node -e " const adminPerms = ['system:user:create', 'system:user:update', 'system:user:delete']; const editorPerms = ['system:user:create']; const viewerPerms = []; const buttons = ['system:user:create', 'system:user:update', 'system:user:delete']; console.log('=== 管理员看到的按钮 ==='); buttons.forEach(b => console.log(b, adminPerms.includes(b) ? '显示' : '隐藏')); console.log('=== 编辑者看到的按钮 ==='); buttons.forEach(b => console.log(b, editorPerms.includes(b) ? '显示' : '隐藏')); console.log('=== 查看者看到的按钮 ==='); buttons.forEach(b => console.log(b, viewerPerms.includes(b) ? '显示' : '隐藏')); "预期输出:管理员三个按钮全显示,编辑者只显示「新增用户」,查看者全隐藏。如果结果不符,检查mockRoles里各角色的permissionIds是否配置正确。
这里有个实测经验:动态菜单渲染时,defaultOpenKeys要根据当前路径自动展开对应的父菜单。我一开始写死了['/system'],结果在内容管理页时系统管理菜单也展开着,很别扭。后来改成根据location.pathname动态计算:
defaultOpenKeys={[`/${location.pathname.split('/')[1]}`]}这样在/system/users时展开/system,在/content/articles时展开/content,符合直觉。
另外,菜单的selectedKeys也要和当前路径同步,否则点击菜单后高亮状态不对。用selectedKeys={[location.pathname]}即可,因为菜单项的key就是权限的path。
跑通这一步,一套带权限控制的后台骨架就成型了。你可以用三个账号反复切换,观察菜单、按钮、路由访问权限的变化,确认权限隔离生效。
6. 本篇常见报错与排查清单
即使代码是 Claude Code 生成的,实际跑起来还是会遇到各种报错。我把这一篇里最容易踩的坑列出来,对照着排查。
401 Unauthorized / invalid api key
这个报错出现在 Claude Code 调用模型时,说明 API Key 没配好。检查三件事:ANTHROPIC_API_KEY环境变量是否设置、Key 是否复制完整(有没有漏掉sk-前缀)、是否执行了source ~/.bashrc让配置生效。如果用的是 TaoToken 的 Key,确认 Base URL 填的是https://taotoken.net/api,不要加/v1。
local proxy failed / connection refused
这个报错说明 Claude Code 尝试连接的地址不对。检查ANTHROPIC_BASE_URL是否设置正确,有没有多余的空格或换行。在终端执行echo $ANTHROPIC_BASE_URL确认输出是https://taotoken.net/api。如果输出为空,说明环境变量没生效,重新 export 一次。
Cannot read properties of undefined (reading 'choices')
这个报错通常出现在 API 返回格式不符合预期时。Claude Code 期望的是 Anthropic 格式的响应,如果 Base URL 指向了一个不兼容的接口,就会解析失败。确认你用的是 TaoToken 的 API 地址,它兼容 Anthropic 协议。如果问题持续,检查 API Key 是否有余额或权限。
OAuth error / authentication failed
如果你之前用 Claude Code 登录过官方账号,可能会残留 OAuth 凭证,和 API Key 模式冲突。执行claude logout清除旧凭证,然后重新用 API Key 模式配置。确认ANTHROPIC_API_KEY已设置,且没有同时设置ANTHROPIC_AUTH_TOKEN之类的变量。
菜单不显示 / 侧边栏空白
检查filterVisibleMenus的过滤条件。菜单项必须同时满足type === 'menu'和visible === true。如果某个菜单的visible是false,它不会出现在侧边栏。另外,父菜单如果没有path但有children,也会被保留;如果既没有path也没有children,会被过滤掉。
按钮权限不生效 / 按钮一直显示
最常见的原因是permission参数和权限数据的name不一致。比如数据里是system:user:create,组件里写成了system:user:add。建议把权限name定义成常量文件,所有地方引用常量。另外检查hasPermission方法里超管判断逻辑,如果permissions包含*,所有按钮都会显示,这是预期行为。
路由守卫不拦截 / 直接输入 URL 能访问
检查AuthGuard是否正确包裹了路由元素。如果element直接是<UserListPage />而没有包<AuthGuard>,守卫不会生效。另外确认permission参数填的是权限name,不是路由路径。路由路径是/system/users,权限name是system:user,两者不要混淆。
TypeScript 类型报错 / 属性不存在
Claude Code 生成的代码引用了src/types/auth.ts里的类型。如果报错说某个属性不存在,检查类型定义文件是否完整,有没有被误删。另外确认tsconfig.json的strict模式是否开启,严格模式下children?: Permission[]这样的可选属性需要做空值判断。
登录后刷新页面登录态丢失
Zustand 的persist中间件默认存储到 localStorage,key 是auth-store。如果刷新后丢失,检查partialize函数是否包含了所有需要持久化的字段。另外,如果浏览器禁用了 localStorage,持久化会失败,需要降级到 sessionStorage 或内存存储。
菜单树构建顺序错乱
buildPermissionTree里有两轮遍历:第一轮创建所有节点并存入 Map,第二轮根据parentId建立父子关系。如果顺序反了,子节点找不到父节点,树就构建不出来。另外sortTree递归排序时,要确保sort字段是数字类型,字符串排序会得到错误结果。
排查的时候,善用浏览器控制台的console.log。在login方法里打印data.permissions和data.menus,确认后端返回的数据结构符合预期。在buildPermissionTree里打印tree,看看树形结构是否正确。大部分权限问题都是数据问题,代码逻辑本身很少出错。
7. 继续深入:从骨架到生产可用
到这里,一套带 RBAC 权限控制的 React Admin 后台骨架已经跑通了。你有了类型定义、状态管理、动态菜单、三层权限控制(路由级 + 按钮级 + 逻辑级),以及可复制的权限配置片段和角色-菜单映射表。这套骨架可以直接作为新项目的起点,把模拟数据换成真实后端接口即可。
但生产环境还有几件事要做。后端接口校验是必须的——前端的权限控制只是体验优化,真正的安全边界在后端。每个 API 请求都要带上 token,后端根据 token 解析用户角色,校验该用户是否有权限执行该操作。前端隐藏了删除按钮,不代表后端可以不校验删除接口的权限。
操作日志审计也是生产环境的刚需。谁在什么时候删了哪个用户、改了哪个角色,都要记录下来。这部分可以在后端做,也可以在前端埋点上报。日志管理页面的权限name是log:operation,只有管理员能看到。
如果你想让 Claude Code 继续帮你完善这套系统,可以给它更具体的任务,比如「给用户管理页加上导出 Excel 功能,导出按钮需要system:user:export权限」或者「给角色管理页加上权限变更预览,勾选权限树时实时显示变更前后的差异」。Claude Code 在已有代码基础上做增量开发的效果很好,因为它能读取现有文件,理解类型定义和组件结构。
对于需要长期做后台系统开发的团队,TaoToken 的 Coding Plan 在连续任务和长会话上更稳定,适合让 Claude Code 持续参与项目迭代。如果只是偶尔生成代码片段,基础的 API Key 就够了。
最后留一个实用技巧:把权限name定义成常量文件,比如src/constants/permissions.ts,导出PERM.USER_CREATE = 'system:user:create'这样的常量。所有组件引用常量而不是手写字符串,这样改权限命名时只需要改一个文件,不会漏掉某个组件。这个习惯能帮你省下大量排查「按钮为什么不显示」的时间。