Selaa lähdekoodia

feat: 菜单权限控制

chenhao 2 vuotta sitten
vanhempi
commit
0d317b0d1d

+ 6 - 1
src/api/role.ts

@@ -6,7 +6,12 @@ const RoleApi: DefineApiModule<Role.ApiMap> = {
   /** 获取角色权限 */
   getRolePrivilege: '/api/role/privilege',
   /** 设置角色权限 */
-  setRolePrivilege: '/api/role/privilege/save',
+  setRolePrivilege: {
+    url: '/api/role/privilege/save',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+  },
 }
 
 export default RoleApi

+ 1 - 1
src/components/shared/ScoringPanelItem.vue

@@ -188,7 +188,7 @@ const joinStringChart = (str: string, index: number, chart: string) => {
 
 const onValidScore = (e: KeyboardEvent) => {
   const target = e.target as HTMLInputElement
-  const oldScore = `${currentScore.value || ''}`
+  const oldScore = `${currentScore.value ?? ''}`
   const start = target.selectionStart || 0
 
   if (!KEY_VALID_REG.test(e.key)) {

+ 33 - 0
src/hooks/useNavPermissions.ts

@@ -0,0 +1,33 @@
+import { reactive, ref, computed, watch } from 'vue'
+import useMainLayoutStore from '@/store/layout'
+
+const flatMenu = (menus: MainLayoutStore.MenuItem[]): MainLayoutStore.MenuItem[] => {
+  const list: MainLayoutStore.MenuItem[] = []
+  const stack = [...menus]
+  while (stack.length) {
+    const node = stack.shift()
+    if (node) {
+      list.push(node)
+      const children = node.children
+      if (children) {
+        list.push(...children)
+      }
+    }
+  }
+  return list
+}
+
+const useNavPermissions = () => {
+  const mainLayoutStore = useMainLayoutStore()
+
+  const menuList = computed(() => {
+    return flatMenu(mainLayoutStore?.renderMenus || [])
+  })
+
+  const hasPermissions = (code?: string) => {
+    return menuList.value?.some((m) => m.index === code || m.path === code)
+  }
+  return { hasPermissions }
+}
+
+export default useNavPermissions

+ 1 - 1
src/layout/main/LeftMenu.vue

@@ -7,7 +7,7 @@
     :collapse="mainLayoutStore.collapse"
   >
     <menu-item
-      v-for="menu in mainLayoutStore.menuList"
+      v-for="menu in mainLayoutStore.renderMenus"
       :key="menu.index"
       :current-index="defaultActive"
       :menu="menu"

+ 6 - 3
src/modules/admin-data/nav/index.vue

@@ -2,7 +2,7 @@
   <div class="full nav-page">
     <div class="flex flex-wrap">
       <div class="radius-base nav-card">数据管理</div>
-      <router-link to="/data/marking">
+      <router-link v-show="hasPermissions('/data/marking')" to="/data/marking">
         <div class="radius-base fill-blank m-l-base nav-item">
           <div class="full-w">
             <img class="nav-img" src="@img/评卷数据导入.png" alt="" />
@@ -10,7 +10,7 @@
           </div>
         </div>
       </router-link>
-      <router-link to="/data/paper">
+      <router-link v-show="hasPermissions('/data/paper')" to="/data/paper">
         <div class="radius-base fill-blank m-l-base nav-item">
           <div class="full-w">
             <img class="nav-img" src="@img/导入培训卷RF卷.png" alt="" />
@@ -18,7 +18,7 @@
           </div>
         </div>
       </router-link>
-      <router-link to="/data/export">
+      <router-link v-show="hasPermissions('/data/export')" to="/data/export">
         <div class="radius-base fill-blank m-l-base nav-item">
           <div class="full-w">
             <img class="nav-img" src="@img/CET成绩导出.png" alt="" />
@@ -32,6 +32,9 @@
 
 <script setup lang="ts" name="DataManageNav">
 /** 数据管理导航页 */
+import useNavPermissions from '@/hooks/useNavPermissions'
+
+const { hasPermissions } = useNavPermissions()
 </script>
 
 <style scoped lang="scss">

+ 5 - 2
src/modules/admin-exam/nav/index.vue

@@ -2,7 +2,7 @@
   <div class="full nav-page">
     <div class="flex flex-wrap">
       <div class="radius-base nav-card">考试管理</div>
-      <router-link to="/exam/manage">
+      <router-link v-show="hasPermissions('/exam/manage')" to="/exam/manage">
         <div class="radius-base fill-blank m-l-base nav-item">
           <div class="full-w">
             <img class="nav-img" src="@img/考试批次管理.png" alt="" />
@@ -10,7 +10,7 @@
           </div>
         </div>
       </router-link>
-      <router-link to="/subject/manage">
+      <router-link v-show="hasPermissions('/exam/manage')" to="/subject/manage">
         <div class="radius-base fill-blank m-l-base nav-item">
           <div class="full-w">
             <img class="nav-img" src="@img/科目管理.png" alt="" />
@@ -24,6 +24,9 @@
 
 <script setup lang="ts" name="ExamManageNav">
 /** 考试管理导航页 */
+import useNavPermissions from '@/hooks/useNavPermissions'
+
+const { hasPermissions } = useNavPermissions()
 </script>
 
 <style scoped lang="scss">

+ 65 - 8
src/modules/admin-role/setting/index.vue

@@ -4,10 +4,11 @@
       <div class="flex items-center">
         <span class="m-r-base">角色</span>
         <base-select v-model="role" :options="ROLE_OPTION"></base-select>
-        <el-button class="m-l-base" type="primary">保存</el-button>
+        <el-button class="m-l-base" type="primary" :loading="loading" @click="onSubmit">保存</el-button>
       </div>
       <div class="flex-1 p-base m-t-large fill-lighter full-scroll-y-auto privilege-tree">
-        <el-tree show-checkbox :data="[result]"> </el-tree>
+        <el-tree ref="treeRef" show-checkbox node-key="index" :data="menuTree || []" @check-change="onCheckChange">
+        </el-tree>
       </div>
     </div>
   </div>
@@ -15,22 +16,78 @@
 
 <script setup lang="ts" name="RoleSetting">
 /** 角色权限管理 */
-import { reactive, ref, watch } from 'vue'
+import { computed, nextTick, ref, watch } from 'vue'
 import { ElButton, ElTree } from 'element-plus'
-import BaseSelect from '@/components/element/BaseSelect.vue'
 import { ROLE_OPTION } from '@/constants/dicts'
 import useFetch from '@/hooks/useFetch'
+import useMainLayoutStore from '@/store/layout'
+import BaseSelect from '@/components/element/BaseSelect.vue'
+
+import type { ExtractApiResponse } from 'api-type'
+
+type MenuItem = MainLayoutStore.MenuItem
+
+type MenuItemWithId = MenuItem & { id: number }
 
 const props = defineProps<{ role: ROLE }>()
 
+const mainLayoutStore = useMainLayoutStore()
+
 const role = ref<ROLE>(props.role)
 
-const { fetch: getPrivilege, result } = useFetch('getRolePrivilege')
-const { fetch: setPrivilege } = useFetch('setRolePrivilege')
+const treeRef = ref<InstanceType<typeof ElTree>>()
+
+const checkedMenus = ref<MenuItemWithId[]>([])
+
+const { fetch: getPrivilege, result: privilege } = useFetch('getRolePrivilege')
+
+const { fetch: setPrivilege, loading } = useFetch('setRolePrivilege')
 
-watch(role, () => {
-  getPrivilege({ role: role.value })
+watch(
+  role,
+  () => {
+    role.value && getPrivilege({ role: role.value })
+  },
+  { immediate: true }
+)
+
+watch(privilege, () => {
+  nextTick(() => {
+    treeRef?.value?.setCheckedKeys(privilege?.value?.filter((d) => d.hasPrivilege)?.map((d) => d.code))
+  })
+})
+
+function filterPrivilege(
+  item: MenuItem,
+  privilege: ExtractApiResponse<'getRolePrivilege'>
+): MenuItemWithId | undefined {
+  const privilegeItem = privilege.find((d) => item.index === d.code)
+  if (privilegeItem) {
+    return {
+      ...item,
+      id: privilegeItem.id,
+      children: item.children?.map((child) => filterPrivilege(child, privilege)).filter((d) => !!d) as MenuItemWithId[],
+    }
+  }
+}
+
+const menuTree = computed(() => {
+  return mainLayoutStore.menuList?.reduce((menus, menu) => {
+    return menus.concat(filterPrivilege(menu, privilege.value || []) || [])
+  }, [] as MenuItem[])
 })
+
+const onCheckChange = () => {
+  checkedMenus.value = (treeRef?.value?.getCheckedNodes() as MenuItemWithId[]) || []
+}
+
+/** 保存 */
+const onSubmit = () => {
+  const checkedNodes = treeRef?.value?.getCheckedNodes() || []
+  const checkedHalfNodes = treeRef?.value?.getHalfCheckedNodes() || []
+  const privilegeIds = checkedNodes.concat(checkedHalfNodes).map((d) => d.id)
+  setPrivilege({ role: role.value, privilegeIds })
+}
 </script>
 
 <style scoped lang="scss">

+ 8 - 5
src/modules/expert/nav/index.vue

@@ -2,7 +2,7 @@
   <div class="full nav-page">
     <div class="flex flex-wrap">
       <div class="radius-base m-b-base m-r-base nav-card">专家卷浏览</div>
-      <router-link to="/expert/sample">
+      <router-link v-show="hasPermissions('/expert/sample')" to="/expert/sample">
         <div class="radius-base fill-blank m-b-base m-r-base nav-item">
           <div class="full-w">
             <img class="nav-img" src="@img/RF卷.png" alt="" />
@@ -10,7 +10,7 @@
           </div>
         </div>
       </router-link>
-      <router-link to="/expert/standard">
+      <router-link v-show="hasPermissions('/expert/standard')" to="/expert/standard">
         <div class="radius-base fill-blank m-b-base m-r-base nav-item">
           <div class="full-w">
             <img class="nav-img" src="@img/标准卷.png" alt="" />
@@ -18,7 +18,7 @@
           </div>
         </div>
       </router-link>
-      <router-link to="/expert/assess">
+      <router-link v-show="hasPermissions('/expert/assess')" to="/expert/assess">
         <div class="radius-base fill-blank m-b-base m-r-base nav-item">
           <div class="full-w">
             <img class="nav-img" src="@img/强制考核卷.png" alt="" />
@@ -26,7 +26,7 @@
           </div>
         </div>
       </router-link>
-      <router-link to="/expert/training">
+      <router-link v-show="hasPermissions('/expert/training')" to="/expert/training">
         <div class="radius-base fill-blank m-b-base m-r-base nav-item">
           <div class="full-w">
             <img class="nav-img" src="@img/AB培训卷.png" alt="" />
@@ -34,7 +34,7 @@
           </div>
         </div>
       </router-link>
-      <router-link to="/expert/expert">
+      <router-link v-show="hasPermissions('/expert/expert')" to="/expert/expert">
         <div class="radius-base fill-blank m-b-base nav-item">
           <div class="full-w">
             <img class="nav-img" src="@img/专家卷挑选.png" alt="" />
@@ -48,6 +48,9 @@
 
 <script setup lang="ts" name="ExpertNav">
 /** 专家卷浏览导航页 */
+import useNavPermissions from '@/hooks/useNavPermissions'
+
+const { hasPermissions } = useNavPermissions()
 </script>
 
 <style scoped lang="scss">

+ 24 - 18
src/modules/marking/inquiry-result/index.vue

@@ -20,7 +20,7 @@
       <div class="p-base radius-base fill-blank scroll-auto m-l-base table-view">
         <div class="flex items-center justify-between detail-info-table-header">
           <el-button custom-1 size="small" class="detail-info-label">
-            <span class="">自定抽查卷共:</span>
+            <span class="">自定抽查卷共:</span>
             <span class="m-l-extra-small detail-info-label-num">{{ pagination.total }}</span>
           </el-button>
           <el-pagination
@@ -56,7 +56,7 @@
     :toggle-modal="false"
     @submit="onSubmit"
   ></scoring-panel-with-confirm>
-  <base-dialog v-model="setExpertPaperVisible" title="设置专家卷" :footer="false">
+  <base-dialog v-model="setExpertPaperVisible" title="设置专家卷" :footer="false" :width="useVW(520)">
     <base-form size="small" :model="setExpertModel" :items="items" :label-width="useVW(100)">
       <template #form-item-confirm>
         <confirm-button
@@ -216,23 +216,23 @@ const setExpertModel = reactive<SetExpertModel>({
   forceGroupNumber: 'A',
 })
 
-const { fetch: getForceCheckGroupList, result: forceCheckGroupListResult } = useFetch('getForceCheckGroupList')
+// const { fetch: getForceCheckGroupList, result: forceCheckGroupListResult } = useFetch('getForceCheckGroupList')
 
-const forceCheckGroup = computed(() =>
-  forceCheckGroupListResult?.value?.map((group) => {
-    return { ...group, label: group.forceGroupNumber, labelSlot: `第${group.forceGroupNumber}组` }
-  })
-)
+// const forceCheckGroup = computed(() =>
+//   forceCheckGroupListResult?.value?.map((group) => {
+//     return { ...group, label: group.forceGroupNumber, labelSlot: `第${group.forceGroupNumber}组` }
+//   })
+// )
 
-watch(
-  query,
-  () => {
-    if (query.subjectCode && query.mainNumber) {
-      getForceCheckGroupList({ subjectCode: query.subjectCode as string, mainNumber: +query.mainNumber })
-    }
-  },
-  { deep: true, immediate: true }
-)
+// watch(
+//   query,
+//   () => {
+//     if (query.subjectCode && query.mainNumber) {
+//       getForceCheckGroupList({ subjectCode: query.subjectCode as string, mainNumber: +query.mainNumber })
+//     }
+//   },
+//   { deep: true, immediate: true }
+// )
 
 const items = computed<EpFormItem[]>(() => {
   let paperTypeItems: EpFormItem[] = [
@@ -254,7 +254,13 @@ const items = computed<EpFormItem[]>(() => {
       label: '考核卷组',
       prop: 'forceGroupNumber',
       slotType: 'radio',
-      slot: { options: [{ label: 'A' }, { label: 'B' }, ...forceCheckGroup.value] },
+      slot: {
+        options: [
+          { label: 'A' },
+          { label: 'B' },
+          ...Array.from({ length: 10 }).map((_, index) => ({ label: `${index + 1}` })),
+        ],
+      },
     },
   ]
   return paperTypeItems

+ 21 - 10
src/modules/marking/nav/index.vue

@@ -2,7 +2,7 @@
   <div class="full nav-page">
     <div class="flex flex-wrap">
       <div class="radius-base m-b-base m-r-base nav-card">评阅试卷</div>
-      <router-link to="/marking/mark">
+      <router-link v-show="hasPermissions('/marking/mark')" to="/marking/mark">
         <div class="radius-base fill-blank m-b-base m-r-base nav-item">
           <div class="full-w">
             <img class="nav-img" src="@img/正常评卷.png" alt="" />
@@ -10,7 +10,7 @@
           </div>
         </div>
       </router-link>
-      <router-link to="/marking/training-record">
+      <router-link v-show="hasPermissions('/marking/training-record')" to="/marking/training-record">
         <div class="radius-base fill-blank m-b-base m-r-base nav-item">
           <div class="full-w">
             <img class="nav-img" src="@img/查看培训记录.png " alt="" />
@@ -18,7 +18,7 @@
           </div>
         </div>
       </router-link>
-      <router-link to="/marking/statistics">
+      <router-link v-show="hasPermissions('/marking/statistics')" to="/marking/statistics">
         <div class="radius-base fill-blank m-b-base m-r-base nav-item">
           <div class="full-w">
             <img class="nav-img" src="@img/个人统计.png" alt="" />
@@ -26,7 +26,7 @@
           </div>
         </div>
       </router-link>
-      <router-link to="/marking/arbitration">
+      <router-link v-show="hasPermissions('/marking/arbitration')" to="/marking/arbitration">
         <div class="radius-base fill-blank m-b-base m-r-base nav-item">
           <div class="full-w">
             <img class="nav-img" src="@img/仲裁卷.png" alt="" />
@@ -34,7 +34,7 @@
           </div>
         </div>
       </router-link>
-      <router-link to="/marking/problem">
+      <router-link v-show="hasPermissions('/marking/problem')" to="/marking/problem">
         <div class="radius-base fill-blank m-b-base m-r-base nav-item">
           <div class="full-w">
             <img class="nav-img" src="@img/问题卷.png" alt="" />
@@ -42,7 +42,7 @@
           </div>
         </div>
       </router-link>
-      <router-link to="/marking/similar">
+      <router-link v-show="hasPermissions('/marking/similar')" to="/marking/similar">
         <div class="radius-base fill-blank m-b-base m-r-base nav-item">
           <div class="full-w">
             <img class="nav-img" src="@img/雷同卷.png" alt="" />
@@ -50,7 +50,7 @@
           </div>
         </div>
       </router-link>
-      <router-link to="/marking/repeat">
+      <router-link v-show="hasPermissions('/marking/repeat')" to="/marking/repeat">
         <div class="radius-base fill-blank m-b-base m-r-base nav-item">
           <div class="full-w">
             <img class="nav-img" src="@img/重评卷.png" alt="" />
@@ -58,7 +58,7 @@
           </div>
         </div>
       </router-link>
-      <router-link to="/marking/assess">
+      <router-link v-show="hasPermissions('/marking/assess')" to="/marking/assess">
         <div class="radius-base fill-blank m-b-base m-r-base nav-item">
           <div class="full-w">
             <img class="nav-img" src="@img/强制考核分发.png" alt="" />
@@ -66,20 +66,31 @@
           </div>
         </div>
       </router-link>
-      <router-link to="/marking/inquiry">
-        <div class="radius-base fill-blank m-b-base nav-item">
+      <router-link v-show="hasPermissions('/marking/inquiry')" to="/marking/inquiry">
+        <div class="radius-base fill-blank m-b-base m-r-base nav-item">
           <div class="full-w">
             <img class="nav-img" src="@img/自定义查询.png" alt="" />
             <div class="m-t-extra-base nav-title">自定义查询</div>
           </div>
         </div>
       </router-link>
+      <router-link v-show="hasPermissions('/system-check')" to="/system-check">
+        <div class="radius-base fill-blank m-b-base nav-item">
+          <div class="full-w">
+            <img class="nav-img" src="@img/自定义查询.png" alt="" />
+            <div class="m-t-extra-base nav-title">系统抽查卷</div>
+          </div>
+        </div>
+      </router-link>
     </div>
   </div>
 </template>
 
 <script setup lang="ts" name="MarkingNav">
 /** 评阅试卷导航页 */
+import useNavPermissions from '@/hooks/useNavPermissions'
+
+const { hasPermissions } = useNavPermissions()
 </script>
 
 <style scoped lang="scss">

+ 7 - 4
src/modules/quality/nav/index.vue

@@ -2,7 +2,7 @@
   <div class="full nav-page">
     <div class="flex flex-wrap">
       <div class="radius-base m-b-base m-r-base nav-card">质量统计</div>
-      <router-link to="/quality/self-check">
+      <router-link v-show="hasPermissions('/quality/self-check')" to="/quality/self-check">
         <div class="radius-base fill-blank m-b-base m-r-base nav-item">
           <div class="full-w">
             <img class="nav-img" src="@img/自查一致性分析.png" alt="" />
@@ -10,7 +10,7 @@
           </div>
         </div>
       </router-link>
-      <router-link to="/quality/subjective-check">
+      <router-link v-show="hasPermissions('/quality/subjective-check')" to="/quality/subjective-check">
         <div class="radius-base fill-blank m-b-base m-r-base nav-item">
           <div class="full-w">
             <img class="nav-img" src="@img/主观题校验.png" alt="" />
@@ -18,7 +18,7 @@
           </div>
         </div>
       </router-link>
-      <router-link to="/quality/ending-check">
+      <router-link v-show="hasPermissions('/quality/ending-check')" to="/quality/ending-check">
         <div class="radius-base fill-blank m-b-base m-r-base nav-item">
           <div class="full-w">
             <img class="nav-img" src="@img/科目进度收尾.png" alt="" />
@@ -26,7 +26,7 @@
           </div>
         </div>
       </router-link>
-      <router-link to="/quality/spot-check">
+      <router-link v-show="hasPermissions('/quality/spot-check')" to="/quality/spot-check">
         <div class="radius-base fill-blank m-b-base nav-item">
           <div class="full-w">
             <img class="nav-img" src="@img/抽查情况统计.png" alt="" />
@@ -40,6 +40,9 @@
 
 <script setup lang="ts" name="QualityNav">
 /** 质量统计导航页 */
+import useNavPermissions from '@/hooks/useNavPermissions'
+
+const { hasPermissions } = useNavPermissions()
 </script>
 
 <style scoped lang="scss">

+ 11 - 13
src/store/layout.ts

@@ -26,16 +26,16 @@ export function getMenuRotes() {
   const tempRoutes = router.getRoutes()
 
   /** 给后端配置权限的菜单路由 */
-  console.log(
-    tempRoutes
-      .filter((route) => !route.path.startsWith('/example') && route.meta.menu)
-      .map((_) => ({
-        menuId: _.meta.menuId,
-        name: _.name,
-        path: _.path,
-        label: _.meta.label,
-      }))
-  )
+  // console.log(
+  //   tempRoutes
+  //     .filter((route) => !route.path.startsWith('/example') && route.meta.menu)
+  //     .map((_) => ({
+  //       menuId: _.meta.menuId,
+  //       name: _.name,
+  //       path: _.path,
+  //       label: _.meta.label,
+  //     }))
+  // )
 
   const routesMap = tempRoutes
     .map((_, i) => _.meta?.menuId + '-' + i)
@@ -86,7 +86,7 @@ function filterPrivilege(
   item: MainLayoutStore.MenuItem,
   privilege: ExtractApiResponse<'getUserPrivilege'>
 ): MainLayoutStore.MenuItem | undefined {
-  if (privilege.some((d) => item.path && d.privilegeUri.startsWith(item.path))) {
+  if (privilege.some((d) => item.index === d.code)) {
     return {
       ...item,
       children: item.children?.filter((child) => !!filterPrivilege(child, privilege)),
@@ -117,8 +117,6 @@ const useMainLayoutStore = defineStore<
         this.renderMenus = this.menuList.reduce((menus, menu) => {
           return menus.concat(filterPrivilege(menu, privilege) || [])
         }, [] as MainLayoutStore.MenuItem[])
-
-        console.log(this.renderMenus)
       } catch (error) {
         console.error(error)
       }

+ 8 - 8
types/api.d.ts

@@ -1240,11 +1240,11 @@ declare module 'api-type' {
       updaterName: string
     }
 
-    type getRoleList = BaseDefine<{ role?: ROLE }, RawRole[]>
-    type getRolePrivilege = BaseDefine<
+    type GetRoleList = BaseDefine<{ role?: ROLE }, RawRole[]>
+    type GetRolePrivilege = BaseDefine<
       { role: ROLE },
       {
-        code: ROLE
+        code: string
         hasPrivilege: boolean
         id: number
         name: string
@@ -1253,17 +1253,17 @@ declare module 'api-type' {
         nodeName: string
         parentId: number
         parentNodeId: number
-      }
+      }[]
     >
 
-    type setRolePrivilege = BaseDefine<{ role: ROLE; privilegeIds: number[] }>
+    type SetRolePrivilege = BaseDefine<{ role: ROLE; privilegeIds: number[] }>
 
     /** role api end */
 
     export interface ApiMap {
-      getRoleList: getRoleList
-      getRolePrivilege: getRolePrivilege
-      setRolePrivilege: setRolePrivilege
+      getRoleList: GetRoleList
+      getRolePrivilege: GetRolePrivilege
+      setRolePrivilege: SetRolePrivilege
     }
   }