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.
Files changed (80) hide show
  1. package/package.json +11 -0
  2. package/template/.env.development +2 -0
  3. package/template/.env.production +2 -0
  4. package/template/.env.test +2 -0
  5. package/template/.kiro/specs/course-backend-integration/.config.kiro +1 -0
  6. package/template/.kiro/specs/course-backend-integration/design.md +234 -0
  7. package/template/.kiro/specs/course-backend-integration/requirements.md +116 -0
  8. package/template/.kiro/specs/course-backend-integration/tasks.md +0 -0
  9. package/template/COMPLETION_CHECKLIST.md +305 -0
  10. package/template/DEPLOYMENT_GUIDE.md +391 -0
  11. package/template/FINAL_SUMMARY.md +428 -0
  12. package/template/IMPLEMENTATION_SUMMARY.md +382 -0
  13. package/template/INTEGRATION_GUIDE.md +458 -0
  14. package/template/PROJECT_OVERVIEW.md +343 -0
  15. package/template/QUICK_START.md +273 -0
  16. package/template/RBAC_Tutorial.md +424 -0
  17. package/template/README.md +16 -0
  18. package/template/React_Antd_TS_Tutorial.md +279 -0
  19. package/template/START_ALL.md +163 -0
  20. package/template/SYSTEM_MANAGEMENT.md +247 -0
  21. package/template/eslint.config.js +29 -0
  22. package/template/index.html +13 -0
  23. package/template/koa-server/README.md +65 -0
  24. package/template/koa-server/app.js +625 -0
  25. package/template/koa-server/package-lock.json +1547 -0
  26. package/template/koa-server/package.json +26 -0
  27. package/template/koa-server/public/assets/index-B1Cj4mG9.css +1 -0
  28. package/template/koa-server/public/assets/index-Mgxg-xqT.js +503 -0
  29. package/template/koa-server/public/favicon.svg +1 -0
  30. package/template/koa-server/public/icons.svg +24 -0
  31. package/template/koa-server/public/index.html +14 -0
  32. package/template/koa-server/uploads/1774265088480-962006467.png +0 -0
  33. package/template/koa-server/uploads/file-1774346891704-610962013.png +0 -0
  34. package/template/koa-server/uploads/file-1774346898887-58636533.png +0 -0
  35. package/template/koa-server/uploads/file-1774346912676-771862547.png +0 -0
  36. package/template/koa-server/uploads/file-1774347025308-130037894.png +0 -0
  37. package/template/koa-server/uploads/file-1774347031104-766499773.png +0 -0
  38. package/template/koa-server/uploads/file-1774347094969-731402203.png +0 -0
  39. package/template/koa-server/uploads/file-1774347101948-330296656.png +0 -0
  40. package/template/koa-server/uploads/file-1774351682377-932868720.png +0 -0
  41. package/template/koa-server/uploads/file-1774352037654-877426905.png +0 -0
  42. package/template/koa-server/uploads/file-1774352175463-386248997.png +0 -0
  43. package/template/koa-server/uploads/file-1774361446433-405859961.png +0 -0
  44. package/template/koa-server/uploads/file-1774361512207-465806267.png +0 -0
  45. package/template/lianxi.html +15 -0
  46. package/template/package-lock.json +6307 -0
  47. package/template/package.json +36 -0
  48. package/template/public/favicon.svg +1 -0
  49. package/template/public/icons.svg +24 -0
  50. package/template/src/App.css +184 -0
  51. package/template/src/App.tsx +44 -0
  52. package/template/src/api/course.ts +86 -0
  53. package/template/src/api/menu.ts +55 -0
  54. package/template/src/api/role.ts +58 -0
  55. package/template/src/api/user.ts +58 -0
  56. package/template/src/assets/hero.png +0 -0
  57. package/template/src/assets/react.svg +1 -0
  58. package/template/src/assets/vite.svg +1 -0
  59. package/template/src/components/Child.tsx +10 -0
  60. package/template/src/components/MainLayout.tsx +169 -0
  61. package/template/src/components/SunZi.tsx +13 -0
  62. package/template/src/contexts/ThemeContext.tsx +33 -0
  63. package/template/src/hooks/usePermission.tsx +62 -0
  64. package/template/src/index.css +111 -0
  65. package/template/src/main.tsx +13 -0
  66. package/template/src/pages/Dashboard.tsx +39 -0
  67. package/template/src/pages/Users.tsx +95 -0
  68. package/template/src/pages/banner/BannerList.tsx +182 -0
  69. package/template/src/pages/course/Course.tsx +586 -0
  70. package/template/src/pages/course/CourseList.tsx +168 -0
  71. package/template/src/pages/system/menu/Menu.tsx +501 -0
  72. package/template/src/pages/system/role/Role.tsx +458 -0
  73. package/template/src/pages/system/user/User.tsx +364 -0
  74. package/template/src/types/permission.ts +21 -0
  75. package/template/src/utils/request.tsx +94 -0
  76. package/template/src/vite-env.d.ts +1 -0
  77. package/template/tsconfig.app.json +32 -0
  78. package/template/tsconfig.json +7 -0
  79. package/template/tsconfig.node.json +13 -0
  80. package/template/vite.config.ts +30 -0
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>admin-system</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,65 @@
1
+ # Admin System Backend Server
2
+
3
+ 基于 Koa 的后端服务器
4
+
5
+ ## 安装依赖
6
+
7
+ ```bash
8
+ cd koa-server
9
+ npm install
10
+ ```
11
+
12
+ ## 启动服务器
13
+
14
+ ### 开发模式(自动重启)
15
+ ```bash
16
+ npm run dev
17
+ ```
18
+
19
+ ### 生产模式
20
+ ```bash
21
+ npm start
22
+ ```
23
+
24
+ 服务器将在 `http://localhost:8080` 启动
25
+
26
+ ## API 端点
27
+
28
+ ### 用户管理
29
+ - `GET /system/user/list` - 获取用户列表
30
+ - `GET /system/user/:id` - 获取用户详情
31
+ - `POST /system/user` - 新增用户
32
+ - `PUT /system/user` - 编辑用户
33
+ - `DELETE /system/user/:id` - 删除用户
34
+
35
+ ### 角色管理
36
+ - `GET /system/role/list` - 获取角色列表
37
+ - `GET /system/role/:id` - 获取角色详情
38
+ - `POST /system/role` - 新增角色
39
+ - `PUT /system/role` - 编辑角色
40
+ - `DELETE /system/role/:id` - 删除角色
41
+
42
+ ### 菜单管理
43
+ - `GET /system/menu/list` - 获取菜单列表
44
+ - `GET /system/menu/:id` - 获取菜单详情
45
+ - `POST /system/menu` - 新增菜单
46
+ - `PUT /system/menu` - 编辑菜单
47
+ - `DELETE /system/menu/:id` - 删除菜单
48
+
49
+ ## 响应格式
50
+
51
+ 所有 API 响应格式如下:
52
+
53
+ ```json
54
+ {
55
+ "code": 200,
56
+ "message": "成功",
57
+ "data": {}
58
+ }
59
+ ```
60
+
61
+ ## 错误处理
62
+
63
+ - `200` - 成功
64
+ - `404` - 资源不存在
65
+ - `500` - 服务器错误
@@ -0,0 +1,625 @@
1
+ const Koa = require('koa');
2
+ const Router = require('koa-router');
3
+ const cors = require('koa-cors');
4
+ const bodyParser = require('koa-bodyparser');
5
+ const path = require('path');
6
+ const static = require('koa-static');
7
+ const multer = require('@koa/multer');
8
+ const fs = require('fs');
9
+
10
+ // 配置 multer
11
+ const storage = multer.diskStorage({
12
+ destination: function (req, file, cb) {
13
+ const uploadPath = path.join(__dirname, 'uploads');
14
+ // 确保上传目录存在
15
+ if (!fs.existsSync(uploadPath)) {
16
+ fs.mkdirSync(uploadPath, { recursive: true });
17
+ }
18
+ cb(null, uploadPath);
19
+ },
20
+ filename: function (req, file, cb) {
21
+ const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
22
+ cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
23
+ }
24
+ });
25
+
26
+ const upload = multer({ storage: storage });
27
+
28
+ // 创建 Koa 实例
29
+ const app = new Koa();
30
+ const router = new Router();
31
+
32
+ // 中间件
33
+ app.use(cors());
34
+ app.use(bodyParser());
35
+
36
+ // 静态文件服务
37
+ const publicPath = path.join(__dirname, 'public');
38
+ const uploadsPath = path.join(__dirname, 'uploads');
39
+ console.log('Static paths:', publicPath, uploadsPath);
40
+ app.use(static(publicPath));
41
+ app.use(static(uploadsPath));
42
+
43
+ // 模拟数据
44
+ const users = [
45
+ {
46
+ id: '1',
47
+ username: 'admin',
48
+ nickName: '管理员',
49
+ userType: '管理员',
50
+ email: 'admin@qq.com',
51
+ phonenumber: '15888888888',
52
+ sex: '0',
53
+ status: 0,
54
+ delFlag: 0,
55
+ createTime: '2026-01-18 10:58:15',
56
+ dept: '研发部门',
57
+ roles: ['admin'],
58
+ },
59
+ {
60
+ id: '2',
61
+ username: 'test',
62
+ nickName: '测试员',
63
+ userType: '普通角色',
64
+ email: 'test@qq.com',
65
+ phonenumber: '15666666666',
66
+ sex: '1',
67
+ status: 0,
68
+ delFlag: 0,
69
+ createTime: '2026-01-18 10:58:15',
70
+ dept: '测试部门',
71
+ roles: ['common'],
72
+ },
73
+ {
74
+ id: '3',
75
+ username: 'dev',
76
+ nickName: '开发者',
77
+ userType: '普通角色',
78
+ email: 'dev@qq.com',
79
+ phonenumber: '15777777777',
80
+ sex: '0',
81
+ status: 0,
82
+ delFlag: 0,
83
+ createTime: '2026-01-17 14:30:00',
84
+ dept: '研发部门',
85
+ roles: ['common'],
86
+ },
87
+ ];
88
+
89
+ const roles = [
90
+ {
91
+ id: '1',
92
+ roleName: '超级管理员',
93
+ roleKey: 'admin',
94
+ roleSort: 1,
95
+ status: 0,
96
+ delFlag: 0,
97
+ createTime: '2026-01-18 10:58:15',
98
+ remark: '超级管理员',
99
+ permissions: ['*:*:*'],
100
+ },
101
+ {
102
+ id: '2',
103
+ roleName: '普通角色',
104
+ roleKey: 'common',
105
+ roleSort: 2,
106
+ status: 0,
107
+ delFlag: 0,
108
+ createTime: '2026-01-18 10:58:15',
109
+ remark: '普通角色',
110
+ permissions: ['system:user:query', 'system:role:query'],
111
+ },
112
+ {
113
+ id: '3',
114
+ roleName: '测试角色',
115
+ roleKey: 'test',
116
+ roleSort: 3,
117
+ status: 0,
118
+ delFlag: 0,
119
+ createTime: '2026-01-17 09:30:00',
120
+ remark: '测试人员角色',
121
+ permissions: ['system:user:query', 'system:menu:query'],
122
+ },
123
+ ];
124
+
125
+ const menus = [
126
+ {
127
+ id: '1',
128
+ menuName: '系统管理',
129
+ parentId: '0',
130
+ orderNum: 1,
131
+ path: 'system',
132
+ menuType: 'M',
133
+ visible: 0,
134
+ status: 0,
135
+ icon: 'system',
136
+ createTime: '2026-01-18 10:58:15',
137
+ isFrame: 1,
138
+ isCache: 0,
139
+ children: [
140
+ {
141
+ id: '100',
142
+ menuName: '用户管理',
143
+ parentId: '1',
144
+ orderNum: 1,
145
+ path: 'user',
146
+ component: 'system/user/index',
147
+ menuType: 'C',
148
+ visible: 0,
149
+ status: 0,
150
+ perms: 'system:user:list',
151
+ icon: 'user',
152
+ createTime: '2026-01-18 10:58:15',
153
+ isFrame: 0,
154
+ isCache: 0,
155
+ },
156
+ {
157
+ id: '101',
158
+ menuName: '角色管理',
159
+ parentId: '1',
160
+ orderNum: 2,
161
+ path: 'role',
162
+ component: 'system/role/index',
163
+ menuType: 'C',
164
+ visible: 0,
165
+ status: 0,
166
+ perms: 'system:role:list',
167
+ icon: 'peoples',
168
+ createTime: '2026-01-18 10:58:15',
169
+ isFrame: 0,
170
+ isCache: 0,
171
+ },
172
+ {
173
+ id: '102',
174
+ menuName: '菜单管理',
175
+ parentId: '1',
176
+ orderNum: 3,
177
+ path: 'menu',
178
+ component: 'system/menu/index',
179
+ menuType: 'C',
180
+ visible: 0,
181
+ status: 0,
182
+ perms: 'system:menu:list',
183
+ icon: 'tree-table',
184
+ createTime: '2026-01-18 10:58:15',
185
+ isFrame: 0,
186
+ isCache: 0,
187
+ },
188
+ ],
189
+ },
190
+ {
191
+ id: '2',
192
+ menuName: '业务管理',
193
+ parentId: '0',
194
+ orderNum: 2,
195
+ path: 'business',
196
+ menuType: 'M',
197
+ visible: 0,
198
+ status: 0,
199
+ icon: 'shopping',
200
+ createTime: '2026-01-17 15:20:00',
201
+ isFrame: 1,
202
+ isCache: 0,
203
+ children: [
204
+ {
205
+ id: '200',
206
+ menuName: '订单管理',
207
+ parentId: '2',
208
+ orderNum: 1,
209
+ path: 'order',
210
+ component: 'business/order/index',
211
+ menuType: 'C',
212
+ visible: 0,
213
+ status: 0,
214
+ perms: 'business:order:list',
215
+ icon: 'shopping-cart',
216
+ createTime: '2026-01-17 15:20:00',
217
+ isFrame: 0,
218
+ isCache: 0,
219
+ },
220
+ ],
221
+ },
222
+ ];
223
+
224
+ // 响应格式化
225
+ const formatResponse = (code, message, data = null) => ({
226
+ code,
227
+ message,
228
+ data,
229
+ });
230
+
231
+ // ==================== 用户管理 ====================
232
+
233
+ // 获取用户列表
234
+ router.get('/system/user/list', (ctx) => {
235
+ const { pageNum = 1, pageSize = 10, username, phonenumber, status } = ctx.query;
236
+
237
+ let filteredUsers = [...users];
238
+
239
+ if (username) {
240
+ filteredUsers = filteredUsers.filter(u =>
241
+ u.username.toLowerCase().includes(username.toLowerCase())
242
+ );
243
+ }
244
+
245
+ if (phonenumber) {
246
+ filteredUsers = filteredUsers.filter(u =>
247
+ u.phonenumber.includes(phonenumber)
248
+ );
249
+ }
250
+
251
+ if (status !== undefined && status !== '') {
252
+ const statusNum = parseInt(status);
253
+ filteredUsers = filteredUsers.filter(u => u.status === statusNum);
254
+ }
255
+
256
+ const start = (pageNum - 1) * pageSize;
257
+ const end = start + parseInt(pageSize);
258
+
259
+ ctx.body = formatResponse(200, '获取成功', {
260
+ rows: filteredUsers.slice(start, end),
261
+ total: filteredUsers.length,
262
+ });
263
+ });
264
+
265
+ // 获取用户详情
266
+ router.get('/system/user/:id', (ctx) => {
267
+ const { id } = ctx.params;
268
+ const user = users.find((u) => u.id === id);
269
+
270
+ if (!user) {
271
+ ctx.status = 404;
272
+ ctx.body = formatResponse(404, '用户不存在');
273
+ return;
274
+ }
275
+
276
+ ctx.body = formatResponse(200, '获取成功', user);
277
+ });
278
+
279
+ // 新增用户
280
+ router.post('/system/user', (ctx) => {
281
+ const { username, nickName, email, phonenumber, sex, dept, status, remark } = ctx.request.body;
282
+
283
+ const newUser = {
284
+ id: String(users.length + 1),
285
+ username,
286
+ nickName,
287
+ userType: '普通角色',
288
+ email,
289
+ phonenumber,
290
+ sex,
291
+ status: status ? 0 : 1,
292
+ delFlag: 0,
293
+ createTime: new Date().toLocaleString(),
294
+ dept,
295
+ remark,
296
+ roles: ['common'],
297
+ };
298
+
299
+ users.push(newUser);
300
+ ctx.body = formatResponse(200, '添加成功', newUser);
301
+ });
302
+
303
+ // 编辑用户
304
+ router.put('/system/user', (ctx) => {
305
+ const { id, username, nickName, email, phonenumber, sex, dept, status, remark } = ctx.request.body;
306
+
307
+ const userIndex = users.findIndex((u) => u.id === id);
308
+ if (userIndex === -1) {
309
+ ctx.status = 404;
310
+ ctx.body = formatResponse(404, '用户不存在');
311
+ return;
312
+ }
313
+
314
+ users[userIndex] = {
315
+ ...users[userIndex],
316
+ username,
317
+ nickName,
318
+ email,
319
+ phonenumber,
320
+ sex,
321
+ dept,
322
+ status: status ? 0 : 1,
323
+ remark,
324
+ };
325
+
326
+ ctx.body = formatResponse(200, '编辑成功', users[userIndex]);
327
+ });
328
+
329
+ // 删除用户
330
+ router.delete('/system/user/:id', (ctx) => {
331
+ const { id } = ctx.params;
332
+ const userIndex = users.findIndex((u) => u.id === id);
333
+
334
+ if (userIndex === -1) {
335
+ ctx.status = 404;
336
+ ctx.body = formatResponse(404, '用户不存在');
337
+ return;
338
+ }
339
+
340
+ users.splice(userIndex, 1);
341
+ ctx.body = formatResponse(200, '删除成功');
342
+ });
343
+
344
+ // ==================== 角色管理 ====================
345
+
346
+ // 获取角色列表
347
+ router.get('/system/role/list', (ctx) => {
348
+ const { pageNum = 1, pageSize = 10 } = ctx.query;
349
+ const start = (pageNum - 1) * pageSize;
350
+ const end = start + pageSize;
351
+
352
+ ctx.body = formatResponse(200, '获取成功', {
353
+ rows: roles.slice(start, end),
354
+ total: roles.length,
355
+ });
356
+ });
357
+
358
+ // 获取角色详情
359
+ router.get('/system/role/:id', (ctx) => {
360
+ const { id } = ctx.params;
361
+ const role = roles.find((r) => r.id === id);
362
+
363
+ if (!role) {
364
+ ctx.status = 404;
365
+ ctx.body = formatResponse(404, '角色不存在');
366
+ return;
367
+ }
368
+
369
+ ctx.body = formatResponse(200, '获取成功', role);
370
+ });
371
+
372
+ // 新增角色
373
+ router.post('/system/role', (ctx) => {
374
+ const { roleName, roleKey, roleSort, status, remark, permissions } = ctx.request.body;
375
+
376
+ const newRole = {
377
+ id: String(roles.length + 1),
378
+ roleName,
379
+ roleKey,
380
+ roleSort,
381
+ status: status ? 0 : 1,
382
+ delFlag: 0,
383
+ createTime: new Date().toLocaleString(),
384
+ remark,
385
+ permissions,
386
+ };
387
+
388
+ roles.push(newRole);
389
+ ctx.body = formatResponse(200, '添加成功', newRole);
390
+ });
391
+
392
+ // 编辑角色
393
+ router.put('/system/role', (ctx) => {
394
+ const { id, roleName, roleKey, roleSort, status, remark, permissions } = ctx.request.body;
395
+
396
+ const roleIndex = roles.findIndex((r) => r.id === id);
397
+ if (roleIndex === -1) {
398
+ ctx.status = 404;
399
+ ctx.body = formatResponse(404, '角色不存在');
400
+ return;
401
+ }
402
+
403
+ roles[roleIndex] = {
404
+ ...roles[roleIndex],
405
+ roleName,
406
+ roleKey,
407
+ roleSort,
408
+ status: status ? 0 : 1,
409
+ remark,
410
+ permissions,
411
+ };
412
+
413
+ ctx.body = formatResponse(200, '编辑成功', roles[roleIndex]);
414
+ });
415
+
416
+ // 删除角色
417
+ router.delete('/system/role/:id', (ctx) => {
418
+ const { id } = ctx.params;
419
+ const roleIndex = roles.findIndex((r) => r.id === id);
420
+
421
+ if (roleIndex === -1) {
422
+ ctx.status = 404;
423
+ ctx.body = formatResponse(404, '角色不存在');
424
+ return;
425
+ }
426
+
427
+ roles.splice(roleIndex, 1);
428
+ ctx.body = formatResponse(200, '删除成功');
429
+ });
430
+
431
+ // ==================== 菜单管理 ====================
432
+
433
+ // 获取菜单列表
434
+ router.get('/system/menu/list', (ctx) => {
435
+ ctx.body = formatResponse(200, '获取成功', menus);
436
+ });
437
+
438
+ // 获取菜单详情
439
+ router.get('/system/menu/:id', (ctx) => {
440
+ const { id } = ctx.params;
441
+
442
+ const findMenu = (items) => {
443
+ for (const item of items) {
444
+ if (item.id === id) return item;
445
+ if (item.children) {
446
+ const found = findMenu(item.children);
447
+ if (found) return found;
448
+ }
449
+ }
450
+ return null;
451
+ };
452
+
453
+ const menu = findMenu(menus);
454
+ if (!menu) {
455
+ ctx.status = 404;
456
+ ctx.body = formatResponse(404, '菜单不存在');
457
+ return;
458
+ }
459
+
460
+ ctx.body = formatResponse(200, '获取成功', menu);
461
+ });
462
+
463
+ // 新增菜单
464
+ router.post('/system/menu', (ctx) => {
465
+ const { menuName, parentId, orderNum, path, component, menuType, status, perms, icon, remark } = ctx.request.body;
466
+
467
+ const newMenu = {
468
+ id: String(Date.now()),
469
+ menuName,
470
+ parentId,
471
+ orderNum,
472
+ path,
473
+ component,
474
+ menuType,
475
+ visible: 0,
476
+ status: status ? 0 : 1,
477
+ perms,
478
+ icon,
479
+ createTime: new Date().toLocaleString(),
480
+ isFrame: 0,
481
+ isCache: 0,
482
+ remark,
483
+ };
484
+
485
+ if (parentId === '0') {
486
+ menus.push(newMenu);
487
+ } else {
488
+ const addToParent = (items) => {
489
+ for (const item of items) {
490
+ if (item.id === parentId) {
491
+ if (!item.children) item.children = [];
492
+ item.children.push(newMenu);
493
+ return true;
494
+ }
495
+ if (item.children && addToParent(item.children)) return true;
496
+ }
497
+ return false;
498
+ };
499
+ addToParent(menus);
500
+ }
501
+
502
+ ctx.body = formatResponse(200, '添加成功', newMenu);
503
+ });
504
+
505
+ // 编辑菜单
506
+ router.put('/system/menu', (ctx) => {
507
+ const { id, menuName, parentId, orderNum, path, component, menuType, status, perms, icon, remark } = ctx.request.body;
508
+
509
+ const updateMenu = (items) => {
510
+ for (const item of items) {
511
+ if (item.id === id) {
512
+ Object.assign(item, {
513
+ menuName,
514
+ parentId,
515
+ orderNum,
516
+ path,
517
+ component,
518
+ menuType,
519
+ status: status ? 0 : 1,
520
+ perms,
521
+ icon,
522
+ remark,
523
+ });
524
+ return true;
525
+ }
526
+ if (item.children && updateMenu(item.children)) return true;
527
+ }
528
+ return false;
529
+ };
530
+
531
+ if (updateMenu(menus)) {
532
+ ctx.body = formatResponse(200, '编辑成功');
533
+ } else {
534
+ ctx.status = 404;
535
+ ctx.body = formatResponse(404, '菜单不存在');
536
+ }
537
+ });
538
+
539
+ // 删除菜单
540
+ router.delete('/system/menu/:id', (ctx) => {
541
+ const { id } = ctx.params;
542
+
543
+ const deleteMenu = (items) => {
544
+ for (let i = 0; i < items.length; i++) {
545
+ if (items[i].id === id) {
546
+ items.splice(i, 1);
547
+ return true;
548
+ }
549
+ if (items[i].children && deleteMenu(items[i].children)) return true;
550
+ }
551
+ return false;
552
+ };
553
+
554
+ if (deleteMenu(menus)) {
555
+ ctx.body = formatResponse(200, '删除成功');
556
+ } else {
557
+ ctx.status = 404;
558
+ ctx.body = formatResponse(404, '菜单不存在');
559
+ }
560
+ });
561
+
562
+ // 文件上传 - 支持任意字段名
563
+ router.post('/upload.do', upload.any(), (ctx) => {
564
+ const file = ctx.files?.[0];
565
+ if (!file) {
566
+ ctx.status = 400;
567
+ ctx.body = formatResponse(400, '请选择文件');
568
+ return;
569
+ }
570
+ const fileUrl = `http://localhost:8085/${file.filename}`;
571
+ ctx.body = formatResponse(200, '上传成功', {
572
+ url: fileUrl,
573
+ filename: file.filename,
574
+ originalname: file.originalname
575
+ });
576
+ });
577
+
578
+ // 课程文件上传接口(兼容真实后端路径)
579
+ router.post('/api/courses/upload', upload.any(), (ctx) => {
580
+ const file = ctx.files?.[0];
581
+ if (!file) {
582
+ ctx.status = 400;
583
+ ctx.body = formatResponse(400, '请选择文件');
584
+ return;
585
+ }
586
+ const fileUrl = `http://localhost:8085/${file.filename}`;
587
+ // 真实后端直接返回 URL 字符串作为 data
588
+ ctx.body = formatResponse(200, '上传成功', fileUrl);
589
+ });
590
+
591
+ // 健康检查
592
+ router.get('/health', (ctx) => {
593
+ ctx.body = { status: 'ok' };
594
+ });
595
+
596
+ // SPA 兜底路由:所有非 API 请求都返回 index.html,支持前端路由刷新
597
+ router.get('/(.*)', (ctx, next) => {
598
+ const reqPath = ctx.path;
599
+ // API 和上传接口不走兜底
600
+ if (
601
+ reqPath.startsWith('/system') ||
602
+ reqPath.startsWith('/api') ||
603
+ reqPath.startsWith('/upload') ||
604
+ reqPath.startsWith('/health')
605
+ ) {
606
+ return next();
607
+ }
608
+ const indexPath = path.join(__dirname, 'public/index.html');
609
+ if (fs.existsSync(indexPath)) {
610
+ ctx.type = 'html';
611
+ ctx.body = fs.createReadStream(indexPath);
612
+ } else {
613
+ ctx.status = 404;
614
+ ctx.body = 'index.html not found, please build the frontend first.';
615
+ }
616
+ });
617
+
618
+ // 注册路由
619
+ app.use(router.routes()).use(router.allowedMethods());
620
+
621
+ // 启动服务器
622
+ const port = 8085;
623
+ app.listen(port, () => {
624
+ console.log(`Koa 服务器已启动: http://localhost:${port}`);
625
+ });