Kaynağa Gözat

新增课程权限

liyanbo 6 ay önce
ebeveyn
işleme
839ef4be79

+ 6 - 0
src/api/bjdx/course/index.ts

@@ -59,5 +59,11 @@ export const CourseApi = {
   // 根据类型id获取课程列表
   getCourseByTypeId: async (typeId: String) => {
     return await request.get({ url: `/bjdxWeb/course/getCourseByTypeId?typeId=` + typeId })
+  },
+
+
+  // 根据类型id获取课程列表
+  getCourseTypeTree: async () => {
+    return await request.get({ url: `/bjdxWeb/course/getCourseTypeTree`})
   }
 }

+ 10 - 0
src/api/system/permission/index.ts

@@ -16,6 +16,11 @@ export interface PermissionAssignRoleDataScopeReqVO {
   dataScopeDeptIds: number[]
 }
 
+export interface PermissionAssignRoleCourseScopeReqVO {
+  roleId: number
+  dataScopeCourseIds: number[]
+}
+
 // 查询角色拥有的菜单权限
 export const getRoleMenuList = async (roleId: number) => {
   return await request.get({ url: '/system/permission/list-role-menus?roleId=' + roleId })
@@ -31,6 +36,11 @@ export const assignRoleDataScope = async (data: PermissionAssignRoleDataScopeReq
   return await request.post({ url: '/system/permission/assign-role-data-scope', data })
 }
 
+// 赋予角色课程权限
+export const assignRoleCourseScope = async (data: PermissionAssignRoleCourseScopeReqVO) => {
+  return await request.post({ url: '/system/permission/assign-role-course-scope', data })
+}
+
 // 查询用户拥有的角色数组
 export const getUserRoleList = async (userId: number) => {
   return await request.get({ url: '/system/permission/list-user-roles?userId=' + userId })

+ 1 - 0
src/api/system/role/index.ts

@@ -9,6 +9,7 @@ export interface RoleVO {
   type: number
   dataScope: number
   dataScopeDeptIds: number[]
+  dataScopeCourseIds: number[]
   createTime: Date
 }
 

+ 1 - 0
src/types/auto-components.d.ts

@@ -144,6 +144,7 @@ declare module 'vue' {
     PropertiesPanel: typeof import('./../components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue')['default']
     Qrcode: typeof import('./../components/Qrcode/src/Qrcode.vue')['default']
     ReceiveTask: typeof import('./../components/bpmnProcessDesigner/package/penal/task/task-components/ReceiveTask.vue')['default']
+    RoleCoursePermissionForm: typeof import('./../views/system/role/RoleCoursePermissionForm.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterNode: typeof import('./../components/SimpleProcessDesignerV2/src/nodes/RouterNode.vue')['default']
     RouterNodeConfig: typeof import('./../components/SimpleProcessDesignerV2/src/nodes-config/RouterNodeConfig.vue')['default']

+ 0 - 2
src/types/auto-imports.d.ts

@@ -7,8 +7,6 @@ export {}
 declare global {
   const DICT_TYPE: typeof import('@/utils/dict')['DICT_TYPE']
   const EffectScope: typeof import('vue')['EffectScope']
-  const ElMessage: typeof import('element-plus/es')['ElMessage']
-  const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
   const computed: typeof import('vue')['computed']
   const createApp: typeof import('vue')['createApp']
   const customRef: typeof import('vue')['customRef']

+ 331 - 0
src/views/system/role/RoleCoursePermissionForm.vue

@@ -0,0 +1,331 @@
+<template>
+  <!-- 增加弹窗宽度从800px到1200px -->
+  <Dialog v-model="dialogVisible" title="课程数据权限" width="1200">
+    <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
+      <!-- 使用el-row和el-col让角色名称和角色标识在一排显示 -->
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="角色名称">
+            <el-tag>{{ formData.name }}</el-tag>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="角色标识">
+            <el-tag>{{ formData.code }}</el-tag>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-divider content-position="left">课程数据权限</el-divider>
+      <br/>
+
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <!-- 通识课权限 -->
+          <el-form-item label="通识课">
+            <el-card class="w-full h-300px !overflow-y-scroll" shadow="never">
+              <template #header>
+                全选/全不选:
+                <el-switch
+                  v-model="generalTreeNodeAll"
+                  active-text="是"
+                  inactive-text="否"
+                  inline-prompt
+                  @change="handleCheckedGeneralTreeNodeAll()"
+                />
+                全部展开/折叠:
+                <el-switch
+                  v-model="generalDeptExpand"
+                  active-text="展开"
+                  inactive-text="折叠"
+                  inline-prompt
+                  @change="handleCheckedGeneralTreeExpand"
+                />
+                父子联动:
+                <el-switch v-model="generalCheckStrictly" active-text="是" inactive-text="否" inline-prompt />
+              </template>
+              <el-tree
+                ref="generalTreeRef"
+                :check-strictly="!generalCheckStrictly"
+                :data="generalCourseOptions"
+                :props="defaultProps"
+                default-expand-all
+                empty-text="加载中,请稍后"
+                node-key="id"
+                show-checkbox
+              />
+            </el-card>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <!-- 实操课权限 -->
+          <el-form-item label="实操课">
+            <el-card class="w-full h-300px !overflow-y-scroll" shadow="never">
+              <template #header>
+                全选/全不选:
+                <el-switch
+                  v-model="practicalTreeNodeAll"
+                  active-text="是"
+                  inactive-text="否"
+                  inline-prompt
+                  @change="handleCheckedPracticalTreeNodeAll()"
+                />
+                全部展开/折叠:
+                <el-switch
+                  v-model="practicalDeptExpand"
+                  active-text="展开"
+                  inactive-text="折叠"
+                  inline-prompt
+                  @change="handleCheckedPracticalTreeExpand"
+                />
+                父子联动:
+                <el-switch v-model="practicalCheckStrictly" active-text="是" inactive-text="否" inline-prompt />
+              </template>
+              <el-tree
+                ref="practicalTreeRef"
+                :check-strictly="!practicalCheckStrictly"
+                :data="practicalCourseOptions"
+                :props="defaultProps"
+                default-expand-all
+                empty-text="加载中,请稍后"
+                node-key="id"
+                show-checkbox
+              />
+            </el-card>
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import { defaultProps } from '@/utils/tree'
+import * as RoleApi from '@/api/system/role'
+import { CourseApi } from '@/api/bjdx/course'
+import * as PermissionApi from '@/api/system/permission'
+
+defineOptions({ name: 'SystemRoleDataPermissionForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formData = reactive({
+  id: undefined,
+  name: '',
+  code: '',
+  dataScopeCourseIds: []
+})
+const formRef = ref() // 表单 Ref
+
+// 通识课相关变量
+const generalCourseOptions = ref<any[]>([]) // 通识课树形结构
+const generalDeptExpand = ref(true) // 通识课展开/折叠
+const generalTreeRef = ref() // 通识课菜单树组件 Ref
+const generalTreeNodeAll = ref(false) // 通识课全选/全不选
+const generalCheckStrictly = ref(true) // 通识课是否严格模式
+
+// 实操课相关变量
+const practicalCourseOptions = ref<any[]>([]) // 实操课树形结构
+const practicalDeptExpand = ref(true) // 实操课展开/折叠
+const practicalTreeRef = ref() // 实操课菜单树组件 Ref
+const practicalTreeNodeAll = ref(false) // 实操课全选/全不选
+const practicalCheckStrictly = ref(true) // 实操课是否严格模式
+
+/** 打开弹窗 */
+const open = async (row: RoleApi.RoleVO) => {
+  dialogVisible.value = true
+  resetForm()
+  // 加载课程列表
+  const allCourses = await CourseApi.getCourseTypeTree()
+  
+  // 修复筛选逻辑,确保通识课和实操课数据完全独立
+  generalCourseOptions.value = filterCoursesByType(allCourses, '通识课')
+  practicalCourseOptions.value = filterCoursesByType(allCourses, '实操课')
+  
+  // 设置数据
+  formData.id = row.id
+  formData.name = row.name
+  formData.code = row.code
+  await nextTick()
+  
+  // 需要在 DOM 渲染完成后,再设置选中状态
+  if (row.dataScopeCourseIds && row.dataScopeCourseIds.length > 0) {
+    row.dataScopeCourseIds.forEach((courseId: number): void => {
+      // 尝试在两个树中都设置选中状态,实际只会在对应树中找到匹配的节点
+      generalTreeRef.value?.setChecked(courseId * 100, true, false)
+      practicalTreeRef.value?.setChecked(courseId * 100, true, false)
+    })
+  }
+}
+
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/**
+ * 根据课程类型筛选课程,并保留完整的树结构
+ * @param courses 原始课程树数据
+ * @param type 课程类型
+ * @returns 筛选后的课程树数据
+ */
+function filterCoursesByType(courses: any[], type: string): any[] {
+  // 深拷贝原始数据,避免修改原始数组
+  const filteredCourses = JSON.parse(JSON.stringify(courses))
+  
+  // 递归处理树结构,只保留指定类型的节点及其父节点
+  function filterTreeNodes(nodes: any[]): any[] {
+    return nodes
+      .map(node => {
+        // 复制当前节点
+        const newNode = { ...node }
+        
+        // 递归处理子节点
+        if (node.children && node.children.length > 0) {
+          const filteredChildren = filterTreeNodes(node.children)
+          if (filteredChildren.length > 0) {
+            newNode.children = filteredChildren
+            return newNode
+          }
+        }
+        
+        // 如果当前节点是指定类型,或者有符合条件的子节点,则保留
+        if (node.type === type) {
+          return newNode
+        }
+        
+        return null
+      })
+      .filter(node => node !== null)
+  }
+  
+  return filterTreeNodes(filteredCourses)
+}
+
+/** 提交表单 *//** 提交表单 */
+const emit = defineEmits(['success'])
+const submitForm = async () => {
+  formLoading.value = true
+  try {
+    // 获取所有选中的节点
+    const generalCheckedKeys = generalTreeRef.value?.getCheckedKeys(false) || []
+    const practicalCheckedKeys = practicalTreeRef.value?.getCheckedKeys(false) || []
+    const allCheckedKeys = [...generalCheckedKeys, ...practicalCheckedKeys]
+    
+    // 过滤出只包含子节点的ID
+    // 由于我们已经分别筛选了数据,这里直接合并两个数组
+    const allCourseOptions = [...generalCourseOptions.value, ...practicalCourseOptions.value]
+    const childNodeKeys = filterChildNodeKeys(allCheckedKeys, allCourseOptions)
+    
+    const data = {
+      roleId: formData.id,
+      dataScopeCourseIds: childNodeKeys
+    }
+    
+    console.log('所有选中的节点:', allCheckedKeys)
+    console.log('只提交子节点:', childNodeKeys)
+    await PermissionApi.assignRoleCourseScope(data)
+    message.success(t('common.updateSuccess'))
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/**
+ * 过滤只包含子节点的ID
+ * @param checkedKeys 所有选中的节点ID
+ * @param treeData 树结构数据
+ * @returns 只包含子节点的ID数组
+ */
+function filterChildNodeKeys(checkedKeys: number[], treeData: any[]): number[] {
+  // 创建一个Map存储所有节点ID及其是否为叶子节点的状态
+  const nodeMap = new Map<number, boolean>()
+
+  // 递归遍历树结构,标记每个节点是否为叶子节点
+  function traverseTree(nodes: any[]) {
+    nodes.forEach(node => {
+      nodeMap.set(node.id, node.type === "course")
+      
+      // 递归处理子节点
+      if (node.children && node.children.length > 0) {
+        traverseTree(node.children)
+      }
+    })
+  }
+  
+  // 执行遍历
+  traverseTree(treeData)
+  
+  // 过滤出只包含叶子节点的ID
+  return checkedKeys.filter(key => {
+    // 如果节点存在于nodeMap中且是叶子节点,则保留
+    return nodeMap.has(key) && nodeMap.get(key)
+  })
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  // 重置通识课选项
+  generalTreeNodeAll.value = false
+  generalDeptExpand.value = true
+  generalCheckStrictly.value = true
+  generalTreeRef.value?.setCheckedNodes([])
+  
+  // 重置实操课选项
+  practicalTreeNodeAll.value = false
+  practicalDeptExpand.value = true
+  practicalCheckStrictly.value = true
+  practicalTreeRef.value?.setCheckedNodes([])
+  
+  // 重置表单
+  formData.id = undefined
+  formData.name = ''
+  formData.code = ''
+  formData.dataScopeCourseIds = []
+  
+  formRef.value?.resetFields()
+}
+
+/** 通识课全选/全不选 */
+const handleCheckedGeneralTreeNodeAll = () => {
+  generalTreeRef.value?.setCheckedNodes(generalTreeNodeAll.value ? generalCourseOptions.value : [])
+}
+
+/** 实操课全选/全不选 */
+const handleCheckedPracticalTreeNodeAll = () => {
+  practicalTreeRef.value?.setCheckedNodes(practicalTreeNodeAll.value ? practicalCourseOptions.value : [])
+}
+
+/** 通识课展开/折叠全部 */
+const handleCheckedGeneralTreeExpand = () => {
+  const nodes = generalTreeRef.value?.store.nodesMap
+  if (nodes) {
+    for (let node in nodes) {
+      if (nodes[node].expanded === generalDeptExpand.value) {
+        continue
+      }
+      nodes[node].expanded = generalDeptExpand.value
+    }
+  }
+}
+
+/** 实操课展开/折叠全部 */
+const handleCheckedPracticalTreeExpand = () => {
+  const nodes = practicalTreeRef.value?.store.nodesMap
+  if (nodes) {
+    for (let node in nodes) {
+      if (nodes[node].expanded === practicalDeptExpand.value) {
+        continue
+      }
+      nodes[node].expanded = practicalDeptExpand.value
+    }
+  }
+}
+</script>

+ 19 - 0
src/views/system/role/index.vue

@@ -135,6 +135,16 @@
           >
             数据权限
           </el-button>
+          <el-button
+            v-hasPermi="['system:permission:assign-role-data-scope']"
+            link
+            preIcon="ep:coin"
+            title="课程权限"
+            type="primary"
+            @click="openCoursePermissionForm(scope.row)"
+          >
+            课程权限
+          </el-button>
           <el-button
             v-hasPermi="['system:role:delete']"
             link
@@ -161,6 +171,8 @@
   <RoleAssignMenuForm ref="assignMenuFormRef" @success="getList" />
   <!-- 表单弹窗:数据权限 -->
   <RoleDataPermissionForm ref="dataPermissionFormRef" @success="getList" />
+  <!-- 表单弹窗:课程权限 -->
+  <RoleCoursePermissionForm ref="coursePermissionFormRef" @success="getList" />
 </template>
 <script lang="ts" setup>
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
@@ -170,6 +182,7 @@ import * as RoleApi from '@/api/system/role'
 import RoleForm from './RoleForm.vue'
 import RoleAssignMenuForm from './RoleAssignMenuForm.vue'
 import RoleDataPermissionForm from './RoleDataPermissionForm.vue'
+import RoleCoursePermissionForm from './RoleCoursePermissionForm.vue'
 
 defineOptions({ name: 'SystemRole' })
 
@@ -226,6 +239,12 @@ const openDataPermissionForm = async (row: RoleApi.RoleVO) => {
   dataPermissionFormRef.value.open(row)
 }
 
+/** 课程权限操作 */
+const coursePermissionFormRef = ref()
+const openCoursePermissionForm = async (row: RoleApi.RoleVO) => {
+  coursePermissionFormRef.value.open(row)
+}
+
 /** 菜单权限操作 */
 const assignMenuFormRef = ref()
 const openAssignMenuForm = async (row: RoleApi.RoleVO) => {