Przeglądaj źródła

feat(menu): 侧栏菜单改两级可折叠(域分组 → 子项)

问题:mock 下发的 menuTree 全是平铺叶子,侧栏渲染成一级平铺菜单。

根因:渲染组件 side-menu.vue 本就支持递归(父节点 → el-sub-menu 可折叠,叶子 → el-menu-item 跳转),但 mock 菜单数据(auth.ts FULL_MENU/TEACHER_MENU)没有 children 层级,全渲染成一级。

修复:
- FULL_MENU 重排为 4 个域分组(机构管理/业务管理/教学管理/系统管理)+ 各自子项,父节点 code=域.group、无 path(仅展开),叶子 code 保持原域.资源.动作 三段式权限码不变。
- TEACHER_MENU 同步改两级。
- 新增 leafCodes() 递归收集叶子权限码,permsFor 改用它取 view 码(原来 tree.map(n.code) 在层级化后会误取父节点 group 码,破坏 v-perm 按钮显隐)。

渲染组件零改动。门禁全绿(vue-tsc/eslint/build)。
Q 1 godzinę temu
rodzic
commit
f99246ad86
1 zmienionych plików z 67 dodań i 18 usunięć
  1. 67 18
      src/api/mock/routes/auth.ts

+ 67 - 18
src/api/mock/routes/auth.ts

@@ -33,29 +33,78 @@ function hasValidToken(ctx: MockCtx): boolean {
 }
 
 /** 超管/教务/财务 全量菜单树(PC 端;与原型功能页一一对应)。 */
+/**
+ * 两级菜单(父 = 域分组节点,仅展开、无 path;子 = 叶子节点,带 path 与真实权限码)。
+ * 对齐后端 RBAC Resolver 下发的层级结构:父节点 code 为 `域.group`,子节点 code 为 `域.资源.动作`。
+ * side-menu.vue 递归渲染:父节点 → el-sub-menu(可折叠),叶子 → el-menu-item(router 点击跳转)。
+ */
 const FULL_MENU: MenuNode[] = [
-  { code: 'org.campus.view', label: '机构 / 校区', path: '/org/campuses' },
-  { code: 'org.member.view', label: '成员 / 班级', path: '/org/members' },
-  { code: 'academic.fee.view', label: '费用管理', path: '/academic/fee' },
-  { code: 'academic.schedule.view', label: '排班管理', path: '/academic/schedule' },
-  { code: 'academic.activity.view', label: '活动', path: '/academic/activity' },
-  { code: 'academic.divide.view', label: '分账', path: '/academic/divide' },
-  { code: 'academic.payroll.view', label: '课酬方案', path: '/academic/payroll' },
-  { code: 'academic.expenditure.view', label: '收支管理', path: '/academic/expenditure' },
-  { code: 'teaching.exam.view', label: '题库 / 组卷', path: '/teaching/exam' },
-  { code: 'teaching.analysis.view', label: '练习分析', path: '/teaching/analysis' },
-  { code: 'system.user.view', label: '用户 / 角色', path: '/system/users' },
-  { code: 'system.task.view', label: '任务监控', path: '/system/tasks' },
-  { code: 'system.log.view', label: '操作日志', path: '/system/logs' },
+  {
+    code: 'org.group',
+    label: '机构管理',
+    children: [
+      { code: 'org.campus.view', label: '机构 / 校区', path: '/org/campuses' },
+      { code: 'org.member.view', label: '成员 / 班级', path: '/org/members' },
+    ],
+  },
+  {
+    code: 'academic.group',
+    label: '业务管理',
+    children: [
+      { code: 'academic.fee.view', label: '费用管理', path: '/academic/fee' },
+      { code: 'academic.schedule.view', label: '排班管理', path: '/academic/schedule' },
+      { code: 'academic.activity.view', label: '活动', path: '/academic/activity' },
+      { code: 'academic.divide.view', label: '分账', path: '/academic/divide' },
+      { code: 'academic.payroll.view', label: '课酬方案', path: '/academic/payroll' },
+      { code: 'academic.expenditure.view', label: '收支管理', path: '/academic/expenditure' },
+    ],
+  },
+  {
+    code: 'teaching.group',
+    label: '教学管理',
+    children: [
+      { code: 'teaching.exam.view', label: '题库 / 组卷', path: '/teaching/exam' },
+      { code: 'teaching.analysis.view', label: '练习分析', path: '/teaching/analysis' },
+    ],
+  },
+  {
+    code: 'system.group',
+    label: '系统管理',
+    children: [
+      { code: 'system.user.view', label: '用户 / 角色', path: '/system/users' },
+      { code: 'system.task.view', label: '任务监控', path: '/system/tasks' },
+      { code: 'system.log.view', label: '操作日志', path: '/system/logs' },
+    ],
+  },
 ];
 
-/** 老师子集(仅排班/课程 + 教学)。 */
+/** 老师子集(仅排班/课程 + 教学;两级结构与 FULL_MENU 一致)。 */
 const TEACHER_MENU: MenuNode[] = [
-  { code: 'academic.schedule.view', label: '排班管理', path: '/academic/schedule' },
-  { code: 'teaching.exam.view', label: '题库 / 组卷', path: '/teaching/exam' },
-  { code: 'teaching.analysis.view', label: '练习分析', path: '/teaching/analysis' },
+  {
+    code: 'academic.group',
+    label: '业务管理',
+    children: [{ code: 'academic.schedule.view', label: '排班管理', path: '/academic/schedule' }],
+  },
+  {
+    code: 'teaching.group',
+    label: '教学管理',
+    children: [
+      { code: 'teaching.exam.view', label: '题库 / 组卷', path: '/teaching/exam' },
+      { code: 'teaching.analysis.view', label: '练习分析', path: '/teaching/analysis' },
+    ],
+  },
 ];
 
+/** 递归收集菜单树所有**叶子**节点的权限码(父节点 group 码不算;供 v-perm 菜单项显隐用)。 */
+function leafCodes(tree: MenuNode[]): string[] {
+  const out: string[] = [];
+  for (const n of tree) {
+    if (n.children && n.children.length > 0) out.push(...leafCodes(n.children));
+    else out.push(n.code);
+  }
+  return out;
+}
+
 /** 按 role 取菜单树。入参:role。出参:MenuNode[]。 */
 function menuFor(role: string): MenuNode[] {
   return role === 'teacher' ? TEACHER_MENU : FULL_MENU;
@@ -64,7 +113,7 @@ function menuFor(role: string): MenuNode[] {
 /** 按 role 取权限码集(含 view + 常见 button;mock 从宽,保证 v-perm 按钮可见)。 */
 function permsFor(role: string): string[] {
   const tree = menuFor(role);
-  const view = tree.map((n) => n.code);
+  const view = leafCodes(tree);
   // 按钮级:费用/活动/工单等常见动作
   const buttons = [
     'academic.fee.recharge', 'academic.fee.refund', 'academic.fee.clear',