zt-admin-template 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -0
- package/template/.env.development +2 -0
- package/template/.env.production +2 -0
- package/template/.env.test +2 -0
- package/template/.kiro/specs/course-backend-integration/.config.kiro +1 -0
- package/template/.kiro/specs/course-backend-integration/design.md +234 -0
- package/template/.kiro/specs/course-backend-integration/requirements.md +116 -0
- package/template/.kiro/specs/course-backend-integration/tasks.md +0 -0
- package/template/COMPLETION_CHECKLIST.md +305 -0
- package/template/DEPLOYMENT_GUIDE.md +391 -0
- package/template/FINAL_SUMMARY.md +428 -0
- package/template/IMPLEMENTATION_SUMMARY.md +382 -0
- package/template/INTEGRATION_GUIDE.md +458 -0
- package/template/PROJECT_OVERVIEW.md +343 -0
- package/template/QUICK_START.md +273 -0
- package/template/RBAC_Tutorial.md +424 -0
- package/template/README.md +16 -0
- package/template/React_Antd_TS_Tutorial.md +279 -0
- package/template/START_ALL.md +163 -0
- package/template/SYSTEM_MANAGEMENT.md +247 -0
- package/template/eslint.config.js +29 -0
- package/template/index.html +13 -0
- package/template/koa-server/README.md +65 -0
- package/template/koa-server/app.js +625 -0
- package/template/koa-server/package-lock.json +1547 -0
- package/template/koa-server/package.json +26 -0
- package/template/koa-server/public/assets/index-B1Cj4mG9.css +1 -0
- package/template/koa-server/public/assets/index-Mgxg-xqT.js +503 -0
- package/template/koa-server/public/favicon.svg +1 -0
- package/template/koa-server/public/icons.svg +24 -0
- package/template/koa-server/public/index.html +14 -0
- package/template/koa-server/uploads/1774265088480-962006467.png +0 -0
- package/template/koa-server/uploads/file-1774346891704-610962013.png +0 -0
- package/template/koa-server/uploads/file-1774346898887-58636533.png +0 -0
- package/template/koa-server/uploads/file-1774346912676-771862547.png +0 -0
- package/template/koa-server/uploads/file-1774347025308-130037894.png +0 -0
- package/template/koa-server/uploads/file-1774347031104-766499773.png +0 -0
- package/template/koa-server/uploads/file-1774347094969-731402203.png +0 -0
- package/template/koa-server/uploads/file-1774347101948-330296656.png +0 -0
- package/template/koa-server/uploads/file-1774351682377-932868720.png +0 -0
- package/template/koa-server/uploads/file-1774352037654-877426905.png +0 -0
- package/template/koa-server/uploads/file-1774352175463-386248997.png +0 -0
- package/template/koa-server/uploads/file-1774361446433-405859961.png +0 -0
- package/template/koa-server/uploads/file-1774361512207-465806267.png +0 -0
- package/template/lianxi.html +15 -0
- package/template/package-lock.json +6307 -0
- package/template/package.json +36 -0
- package/template/public/favicon.svg +1 -0
- package/template/public/icons.svg +24 -0
- package/template/src/App.css +184 -0
- package/template/src/App.tsx +44 -0
- package/template/src/api/course.ts +86 -0
- package/template/src/api/menu.ts +55 -0
- package/template/src/api/role.ts +58 -0
- package/template/src/api/user.ts +58 -0
- package/template/src/assets/hero.png +0 -0
- package/template/src/assets/react.svg +1 -0
- package/template/src/assets/vite.svg +1 -0
- package/template/src/components/Child.tsx +10 -0
- package/template/src/components/MainLayout.tsx +169 -0
- package/template/src/components/SunZi.tsx +13 -0
- package/template/src/contexts/ThemeContext.tsx +33 -0
- package/template/src/hooks/usePermission.tsx +62 -0
- package/template/src/index.css +111 -0
- package/template/src/main.tsx +13 -0
- package/template/src/pages/Dashboard.tsx +39 -0
- package/template/src/pages/Users.tsx +95 -0
- package/template/src/pages/banner/BannerList.tsx +182 -0
- package/template/src/pages/course/Course.tsx +586 -0
- package/template/src/pages/course/CourseList.tsx +168 -0
- package/template/src/pages/system/menu/Menu.tsx +501 -0
- package/template/src/pages/system/role/Role.tsx +458 -0
- package/template/src/pages/system/user/User.tsx +364 -0
- package/template/src/types/permission.ts +21 -0
- package/template/src/utils/request.tsx +94 -0
- package/template/src/vite-env.d.ts +1 -0
- package/template/tsconfig.app.json +32 -0
- package/template/tsconfig.json +7 -0
- package/template/tsconfig.node.json +13 -0
- package/template/vite.config.ts +30 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
# React + Ant Design + TypeScript 实现 RBAC 权限控制教程
|
|
2
|
+
|
|
3
|
+
本文档详细介绍了如何在 React 项目中实现基于角色的访问控制(RBAC),涵盖权限定义、Context 状态管理、Hook 封装、菜单过滤、路由守卫以及按钮级权限控制。
|
|
4
|
+
|
|
5
|
+
## 0. 场景举例:什么是 RBAC 权限控制?
|
|
6
|
+
|
|
7
|
+
为了更好地理解接下来的代码实现,我们先通过一个具体的场景来看看三种不同粒度的权限控制。
|
|
8
|
+
|
|
9
|
+
假设系统中有两个角色:**超级管理员 (Admin)** 和 **普通员工 (Staff)**。
|
|
10
|
+
|
|
11
|
+
### 1. 菜单权限 (Menu Permission)
|
|
12
|
+
**目标**:不同角色看到的侧边栏菜单是不一样的。
|
|
13
|
+
|
|
14
|
+
* **Admin**:可以看到 "仪表盘"、"用户管理"、"系统设置"。
|
|
15
|
+
* **Staff**:只能看到 "仪表盘"。
|
|
16
|
+
* **实现效果**:Staff 登录后,侧边栏根本不会渲染 "用户管理" 和 "系统设置" 的菜单项。
|
|
17
|
+
|
|
18
|
+
**代码场景示例**:
|
|
19
|
+
```tsx
|
|
20
|
+
// 菜单配置:通过 permission 字段标记该菜单需要的权限
|
|
21
|
+
const menuItems = [
|
|
22
|
+
{
|
|
23
|
+
label: '仪表盘',
|
|
24
|
+
key: '/dashboard',
|
|
25
|
+
// 无 permission 字段,表示所有人可见
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
label: '用户管理',
|
|
29
|
+
key: '/users',
|
|
30
|
+
permission: 'menu:users' // 只有拥有 'menu:users' 权限的用户才能看到
|
|
31
|
+
}
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// 使用自定义 Hook 过滤菜单
|
|
35
|
+
const { filterMenu } = usePermission();
|
|
36
|
+
const visibleMenus = filterMenu(menuItems);
|
|
37
|
+
// Staff 用户(无 'menu:users' 权限)得到的 visibleMenus 将不包含用户管理
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 2. 路由权限 (Route Permission)
|
|
41
|
+
**目标**:防止用户通过直接在浏览器地址栏输入 URL 访问未授权页面。
|
|
42
|
+
* **场景**:虽然 Staff 看不到 "用户管理" 菜单,但他如果知道 URL 是 `/users` 并直接在地址栏输入访问。
|
|
43
|
+
* **实现效果**:路由守卫(Guard)会拦截该请求,检测到 Staff 没有 `page:users` 权限,自动跳转到 403 无权限页面。
|
|
44
|
+
|
|
45
|
+
**代码场景示例**:
|
|
46
|
+
```tsx
|
|
47
|
+
// 路由配置:使用 AuthRoute 包裹需要保护的组件
|
|
48
|
+
<Routes>
|
|
49
|
+
{/* 公开页面 */}
|
|
50
|
+
<Route path="/login" element={<Login />} />
|
|
51
|
+
|
|
52
|
+
{/* 受保护页面:需要 'page:users' 权限 */}
|
|
53
|
+
<Route
|
|
54
|
+
path="/users"
|
|
55
|
+
element={
|
|
56
|
+
<AuthRoute requiredPermission="page:users">
|
|
57
|
+
<UsersPage />
|
|
58
|
+
</AuthRoute>
|
|
59
|
+
}
|
|
60
|
+
/>
|
|
61
|
+
</Routes>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 3. 按钮权限 (Button Permission)
|
|
65
|
+
**目标**:在同一个页面中,不同角色看到的操作按钮不同。
|
|
66
|
+
* **场景**:在 "用户列表" 页面。
|
|
67
|
+
* **Admin**:可以看到 "新增用户"、"编辑"、"删除" 按钮。
|
|
68
|
+
* **Staff**:只能查看列表数据,**看不到**(或禁用)所有操作按钮。
|
|
69
|
+
* **实现效果**:通过 `AuthButton` 组件包裹按钮,判断当前用户是否有 `button:add` 或 `button:delete` 权限,从而决定是否渲染该按钮。
|
|
70
|
+
|
|
71
|
+
**代码场景示例**:
|
|
72
|
+
```tsx
|
|
73
|
+
// 在页面组件中
|
|
74
|
+
const UsersPage = () => {
|
|
75
|
+
return (
|
|
76
|
+
<div>
|
|
77
|
+
<h1>用户列表</h1>
|
|
78
|
+
|
|
79
|
+
{/* 只有拥有 'button:add' 权限才会渲染这个按钮 */}
|
|
80
|
+
<AuthButton requiredPermission="button:add">
|
|
81
|
+
<Button type="primary">新增用户</Button>
|
|
82
|
+
</AuthButton>
|
|
83
|
+
|
|
84
|
+
<Table ... />
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 1. 定义权限类型
|
|
93
|
+
|
|
94
|
+
### 模拟后端返回的数据结构
|
|
95
|
+
在实际开发中,用户登录成功后,后端接口通常会返回如下结构的数据。我们在前端定义类型时需要与之匹配。
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// 模拟 API 响应:登录成功后返回的用户信息
|
|
99
|
+
const mockLoginResponse = {
|
|
100
|
+
code: 200,
|
|
101
|
+
data: {
|
|
102
|
+
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
|
103
|
+
userInfo: {
|
|
104
|
+
id: "1001",
|
|
105
|
+
name: "Super Admin",
|
|
106
|
+
avatar: "https://example.com/avatar.png",
|
|
107
|
+
// 关键:后端返回的权限列表
|
|
108
|
+
permissions: [
|
|
109
|
+
"dashboard:view",
|
|
110
|
+
"user:view",
|
|
111
|
+
"user:add",
|
|
112
|
+
"user:edit",
|
|
113
|
+
"user:delete",
|
|
114
|
+
"system:settings"
|
|
115
|
+
],
|
|
116
|
+
// 可选:后端返回的角色列表
|
|
117
|
+
roles: ["admin"]
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 前端类型定义
|
|
124
|
+
首先,我们需要定义权限相关的类型,确保代码的类型安全。
|
|
125
|
+
|
|
126
|
+
创建文件:`src/types/permission.ts`
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
/** 权限类型 */
|
|
130
|
+
export type PermissionCode = string; // 如 "user:list"、"button:add"、"menu:dashboard"
|
|
131
|
+
|
|
132
|
+
/** 用户权限信息 */
|
|
133
|
+
export interface UserPermission {
|
|
134
|
+
permissions: PermissionCode[]; // 用户拥有的权限码列表
|
|
135
|
+
roles: string[]; // 角色(可选,用于角色级权限)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** 路由配置(含权限) */
|
|
139
|
+
export interface AuthRouteConfig {
|
|
140
|
+
path: string;
|
|
141
|
+
element: React.ReactNode;
|
|
142
|
+
|
|
143
|
+
// 用于生成菜单的 UI 配置
|
|
144
|
+
title?: string; // 菜单显示的文字,如 "用户管理"
|
|
145
|
+
icon?: string; // 菜单显示的图标,如 "UserOutlined"
|
|
146
|
+
hidden?: boolean; // 如果为 true,则该路由不生成在菜单中(如 "登录页"、"404页")
|
|
147
|
+
|
|
148
|
+
// 权限控制配置
|
|
149
|
+
permission?: PermissionCode; // 访问该路由/看到该菜单需要的权限码
|
|
150
|
+
|
|
151
|
+
children?: AuthRouteConfig[]; // 子路由/子菜单
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**为什么这样定义?**
|
|
156
|
+
在后台管理系统中,我们通常采用 **"路由即菜单"** 的设计模式。一份配置同时用于生成:
|
|
157
|
+
1. **路由表 (`<Routes>`)**:根据 `path` 和 `element` 渲染组件。
|
|
158
|
+
2. **侧边栏菜单 (`<Menu>`)**:根据 `title` 和 `icon` 渲染菜单项。
|
|
159
|
+
* **title**: 决定了菜单上显示的文字(例如 "用户管理")。
|
|
160
|
+
* **icon**: 决定了菜单项前面的小图标。
|
|
161
|
+
* **permission**: 同时控制了**路由能否访问**以及**菜单是否显示**。
|
|
162
|
+
|
|
163
|
+
## 2. 创建权限 Context 和 Hook
|
|
164
|
+
|
|
165
|
+
我们需要一个全局的 Context 来存储当前用户的权限信息,并提供一个 Hook 供组件使用。
|
|
166
|
+
|
|
167
|
+
创建文件:`src/hooks/usePermission.tsx`
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import React, { createContext, useContext, useMemo } from 'react';
|
|
171
|
+
import { UserPermission, PermissionCode } from '../types/permission';
|
|
172
|
+
|
|
173
|
+
// 权限上下文(存储当前用户的权限信息)
|
|
174
|
+
const PermissionContext = createContext<UserPermission | null>(null);
|
|
175
|
+
|
|
176
|
+
// 权限Provider(全局包裹App,注入用户权限)
|
|
177
|
+
export const PermissionProvider: React.FC<{
|
|
178
|
+
children: React.ReactNode;
|
|
179
|
+
permission: UserPermission;
|
|
180
|
+
}> = ({ children, permission }) => {
|
|
181
|
+
return (
|
|
182
|
+
<PermissionContext.Provider value={permission}>
|
|
183
|
+
{children}
|
|
184
|
+
</PermissionContext.Provider>
|
|
185
|
+
);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// 核心权限Hook
|
|
189
|
+
export const usePermission = () => {
|
|
190
|
+
const permission = useContext(PermissionContext);
|
|
191
|
+
|
|
192
|
+
// 判断是否拥有指定权限码
|
|
193
|
+
const hasPermission = useMemo(
|
|
194
|
+
() => (code: PermissionCode) => {
|
|
195
|
+
if (!permission) return false;
|
|
196
|
+
if (!code) return true; // 无权限码则默认可见
|
|
197
|
+
return permission.permissions.includes(code);
|
|
198
|
+
},
|
|
199
|
+
[permission]
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// 过滤带权限的菜单列表
|
|
203
|
+
const filterMenu = useMemo(
|
|
204
|
+
() => {
|
|
205
|
+
const filter = (menus: any[]) => {
|
|
206
|
+
return menus.filter(menu => {
|
|
207
|
+
// 1. 隐藏的菜单直接过滤
|
|
208
|
+
if (menu.hidden) return false;
|
|
209
|
+
// 2. 无权限码则保留
|
|
210
|
+
if (!menu.permission) return true;
|
|
211
|
+
// 3. 有权限码则判断是否拥有
|
|
212
|
+
const hasMenuPermission = hasPermission(menu.permission);
|
|
213
|
+
// 4. 递归处理子菜单
|
|
214
|
+
if (menu.children && menu.children.length > 0) {
|
|
215
|
+
menu.children = filter(menu.children);
|
|
216
|
+
}
|
|
217
|
+
return hasMenuPermission;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return filter
|
|
221
|
+
},
|
|
222
|
+
[hasPermission]
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
hasPermission,
|
|
227
|
+
filterMenu,
|
|
228
|
+
currentPermission: permission,
|
|
229
|
+
};
|
|
230
|
+
};
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## 3. 实现路由权限守卫 (AuthRoute)
|
|
234
|
+
|
|
235
|
+
我们需要一个高阶组件(HOC)或封装组件来拦截路由访问。
|
|
236
|
+
|
|
237
|
+
创建文件:`src/components/AuthRoute.tsx`
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
import React from 'react';
|
|
241
|
+
import { Navigate } from 'react-router-dom';
|
|
242
|
+
import { usePermission } from '../hooks/usePermission';
|
|
243
|
+
import { PermissionCode } from '../types/permission';
|
|
244
|
+
|
|
245
|
+
interface AuthRouteProps {
|
|
246
|
+
children: React.ReactNode;
|
|
247
|
+
requiredPermission?: PermissionCode;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const AuthRoute: React.FC<AuthRouteProps> = ({ children, requiredPermission }) => {
|
|
251
|
+
const { hasPermission } = usePermission();
|
|
252
|
+
|
|
253
|
+
// 如果没有传入权限码,直接放行
|
|
254
|
+
if (!requiredPermission) {
|
|
255
|
+
return <>{children}</>;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 检查是否有权限
|
|
259
|
+
if (!hasPermission(requiredPermission)) {
|
|
260
|
+
// 无权限,跳转到 403 页面或登录页
|
|
261
|
+
return <Navigate to="/403" replace />;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 有权限,渲染子组件
|
|
265
|
+
return <>{children}</>;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
export default AuthRoute;
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## 4. 实现按钮级权限组件 (AuthButton)
|
|
272
|
+
|
|
273
|
+
对于细粒度的按钮控制,我们可以封装一个 `AuthButton` 组件。
|
|
274
|
+
|
|
275
|
+
创建文件:`src/components/AuthButton.tsx`
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
import React from 'react';
|
|
279
|
+
import { usePermission } from '../hooks/usePermission';
|
|
280
|
+
import { PermissionCode } from '../types/permission';
|
|
281
|
+
|
|
282
|
+
interface AuthButtonProps {
|
|
283
|
+
children: React.ReactNode;
|
|
284
|
+
requiredPermission: PermissionCode;
|
|
285
|
+
fallback?: React.ReactNode; // 无权限时的替代展示(如禁用按钮或空)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const AuthButton: React.FC<AuthButtonProps> = ({
|
|
289
|
+
children,
|
|
290
|
+
requiredPermission,
|
|
291
|
+
fallback = null
|
|
292
|
+
}) => {
|
|
293
|
+
const { hasPermission } = usePermission();
|
|
294
|
+
|
|
295
|
+
if (hasPermission(requiredPermission)) {
|
|
296
|
+
return <>{children}</>;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return <>{fallback}</>;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
export default AuthButton;
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## 5. 实现动态菜单渲染 (SidebarMenu)
|
|
306
|
+
|
|
307
|
+
根据用户的权限,动态生成侧边栏菜单。
|
|
308
|
+
|
|
309
|
+
创建文件:`src/components/SidebarMenu.tsx`
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
import React from 'react';
|
|
313
|
+
import { Menu } from 'antd';
|
|
314
|
+
import { useNavigate } from 'react-router-dom';
|
|
315
|
+
import { usePermission } from '../hooks/usePermission';
|
|
316
|
+
import { AuthRouteConfig } from '../types/permission';
|
|
317
|
+
|
|
318
|
+
interface SidebarMenuProps {
|
|
319
|
+
routes: AuthRouteConfig[];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const SidebarMenu: React.FC<SidebarMenuProps> = ({ routes }) => {
|
|
323
|
+
const navigate = useNavigate();
|
|
324
|
+
const { filterMenu } = usePermission();
|
|
325
|
+
|
|
326
|
+
// 1. 过滤出用户可见的菜单项
|
|
327
|
+
const visibleRoutes = filterMenu(routes);
|
|
328
|
+
|
|
329
|
+
// 2. 将路由配置转换为 Ant Design Menu 的 items 格式
|
|
330
|
+
const menuItems = visibleRoutes.map(route => {
|
|
331
|
+
// 简化处理:这里假设只有一级或二级菜单,实际可递归处理
|
|
332
|
+
if (route.children) {
|
|
333
|
+
return {
|
|
334
|
+
key: route.path,
|
|
335
|
+
icon: route.icon, // 注意:实际项目中需要将字符串图标转换为组件
|
|
336
|
+
label: route.title,
|
|
337
|
+
children: route.children.map(child => ({
|
|
338
|
+
key: child.path,
|
|
339
|
+
label: child.title,
|
|
340
|
+
})),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
key: route.path,
|
|
345
|
+
icon: route.icon,
|
|
346
|
+
label: route.title,
|
|
347
|
+
};
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
return (
|
|
351
|
+
<Menu
|
|
352
|
+
mode="inline"
|
|
353
|
+
theme="dark"
|
|
354
|
+
items={menuItems}
|
|
355
|
+
onClick={({ key }) => navigate(key)}
|
|
356
|
+
/>
|
|
357
|
+
);
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
export default SidebarMenu;
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## 6. 综合使用示例
|
|
364
|
+
|
|
365
|
+
最后,我们在 `App.tsx` 中将所有内容串联起来。
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
import React, { useState } from 'react';
|
|
370
|
+
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
|
371
|
+
import { PermissionProvider } from './hooks/usePermission';
|
|
372
|
+
import AuthRoute from './components/AuthRoute';
|
|
373
|
+
import AuthButton from './components/AuthButton';
|
|
374
|
+
import { Button } from 'antd';
|
|
375
|
+
|
|
376
|
+
// 模拟从后端获取的用户权限
|
|
377
|
+
const mockUserPermission = {
|
|
378
|
+
permissions: ['dashboard:view', 'user:view', 'button:add'],
|
|
379
|
+
roles: ['admin']
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const Dashboard = () => <h1>仪表盘 (Public)</h1>;
|
|
383
|
+
const Users = () => (
|
|
384
|
+
<div>
|
|
385
|
+
<h1>用户管理 (Protected)</h1>
|
|
386
|
+
<AuthButton requiredPermission="button:add" fallback={<Button disabled>无权限新增</Button>}>
|
|
387
|
+
<Button type="primary">新增用户</Button>
|
|
388
|
+
</AuthButton>
|
|
389
|
+
<AuthButton requiredPermission="button:delete">
|
|
390
|
+
<Button danger>删除用户</Button>
|
|
391
|
+
</AuthButton>
|
|
392
|
+
</div>
|
|
393
|
+
);
|
|
394
|
+
const Forbidden = () => <h1>403 无权限</h1>;
|
|
395
|
+
|
|
396
|
+
const App: React.FC = () => {
|
|
397
|
+
// 实际项目中,这里应该是在 useEffect 中请求 API 获取权限
|
|
398
|
+
const [permission] = useState(mockUserPermission);
|
|
399
|
+
|
|
400
|
+
return (
|
|
401
|
+
<PermissionProvider permission={permission}>
|
|
402
|
+
<BrowserRouter>
|
|
403
|
+
<Routes>
|
|
404
|
+
<Route path="/" element={<Dashboard />} />
|
|
405
|
+
|
|
406
|
+
{/* 路由权限保护 */}
|
|
407
|
+
<Route
|
|
408
|
+
path="/users"
|
|
409
|
+
element={
|
|
410
|
+
<AuthRoute requiredPermission="user:view">
|
|
411
|
+
<Users />
|
|
412
|
+
</AuthRoute>
|
|
413
|
+
}
|
|
414
|
+
/>
|
|
415
|
+
|
|
416
|
+
<Route path="/403" element={<Forbidden />} />
|
|
417
|
+
</Routes>
|
|
418
|
+
</BrowserRouter>
|
|
419
|
+
</PermissionProvider>
|
|
420
|
+
);
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
export default App;
|
|
424
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# React + Vite
|
|
2
|
+
|
|
3
|
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
|
4
|
+
|
|
5
|
+
Currently, two official plugins are available:
|
|
6
|
+
|
|
7
|
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
|
8
|
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
|
9
|
+
|
|
10
|
+
## React Compiler
|
|
11
|
+
|
|
12
|
+
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
|
13
|
+
|
|
14
|
+
## Expanding the ESLint configuration
|
|
15
|
+
|
|
16
|
+
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|