Prechádzať zdrojové kódy

更改课程类型选择标签样式组件
新增角色web路由权限配置,将课程权限整合在一起更改

liyanbo 3 mesiacov pred
rodič
commit
c8e536e2f3

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

@@ -31,6 +31,14 @@ export interface PermissionAssignRoleAiCourseScopeReqVO {
   dataScopeAiCourseIds: number[]
 }
 
+export interface PermissionAssignRoleWebScopeReqVO {
+  roleId: number
+  dataScopeWebRoute: String[]
+  dataScopeCourseIds: number[]
+  dataScopeBlocklyIds: number[]
+  dataScopeAiCourseIds: number[]
+}
+
 // 查询角色拥有的菜单权限
 export const getRoleMenuList = async (roleId: number) => {
   return await request.get({ url: '/system/permission/list-role-menus?roleId=' + roleId })
@@ -61,6 +69,11 @@ export const assignRoleAiCourseScope = async (data: PermissionAssignRoleAiCourse
   return await request.post({ url: '/system/permission/assign-role-aiCourse-scope', data })
 }
 
+// 赋予角色Web路由权限
+export const assignRoleWebScope = async (data: PermissionAssignRoleWebScopeReqVO) => {
+  return await request.post({ url: '/system/permission/assign-role-web-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

@@ -12,6 +12,7 @@ export interface RoleVO {
   dataScopeCourseIds: number[]
   dataScopeBlocklyIds: number[]
   dataScopeAiCourseIds: number[]
+  dataScopeWebRoute: String[]
   createTime: Date
 }
 

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

@@ -52,6 +52,7 @@ declare module 'vue' {
     ElButton: typeof import('element-plus/es')['ElButton']
     ElCard: typeof import('element-plus/es')['ElCard']
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
+    ElCheckboxButton: typeof import('element-plus/es')['ElCheckboxButton']
     ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
     ElCol: typeof import('element-plus/es')['ElCol']
     ElCollapseTransition: typeof import('element-plus/es')['ElCollapseTransition']
@@ -145,7 +146,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']
-    RoleAiCoursePermissionForm: typeof import('./../views/system/role/RoleAiCoursePermissionForm.vue')['default']
+    RoleWebPermissionForm: typeof import('./../views/system/role/RoleWebPermissionForm.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']

+ 1 - 0
src/utils/dict.ts

@@ -264,4 +264,5 @@ export enum DICT_TYPE {
   BLOCKLY_COURSE_COUTNET_TYPE = 'blockly_course_content_type', // 课程内容类型
   BLOCKLY_MAP_SPECIAL = 'blockly_map_special', // 地图特殊方块类型
   BLOCKLY_MAP_MARK = 'blockly_map_mark', // 地图标记标签
+  WEB_ROLE_ROUTE = 'web_role_route', // 角色Web路由权限
 }

+ 5 - 1
src/views/aiCourse/aiCourse/AiCourseForm.vue

@@ -49,7 +49,11 @@
       </el-form-item>
 
       <el-form-item label="内容类型" prop="bcContentType">
-        <el-segmented v-model="formData.bcContentType" :options="getStrDictOptions(DICT_TYPE.COURSE_COUTNET_TYPE)" />
+        <el-radio-group v-model="formData.bcContentType">
+          <el-radio-button v-for="dict in getStrDictOptions(DICT_TYPE.COURSE_COUTNET_TYPE)" :key="dict.value" :label="dict.value">
+            {{ dict.label }}
+          </el-radio-button>
+        </el-radio-group>
       </el-form-item>
 
       <el-form-item v-if="formData.bcContentType === 'all'" label="课程内容" prop="bcContent">

+ 5 - 1
src/views/bjdx/course/CourseForm.vue

@@ -38,7 +38,11 @@
 
 
       <el-form-item label="内容类型" prop="courseContentType">
-        <el-segmented v-model="formData.courseContentType" :options="getStrDictOptions(DICT_TYPE.COURSE_COUTNET_TYPE)" />
+        <el-radio-group v-model="formData.courseContentType">
+          <el-radio-button v-for="dict in getStrDictOptions(DICT_TYPE.COURSE_COUTNET_TYPE)" :key="dict.value" :label="dict.value">
+            {{ dict.label }}
+          </el-radio-button>
+        </el-radio-group>
       </el-form-item>
 
       <el-form-item v-if="formData.courseContentType === 'all'" label="课程内容" prop="courseContent">

+ 5 - 1
src/views/blockly/blockly/BlocklyForm.vue

@@ -49,7 +49,11 @@
       </el-form-item>
 
       <el-form-item label="内容类型" prop="bcContentType">
-        <el-segmented v-model="formData.bcContentType" :options="getStrDictOptions(DICT_TYPE.BLOCKLY_COURSE_COUTNET_TYPE)" />
+        <el-radio-group v-model="formData.bcContentType">
+          <el-radio-button v-for="dict in getStrDictOptions(DICT_TYPE.BLOCKLY_COURSE_COUTNET_TYPE)" :key="dict.value" :label="dict.value">
+            {{ dict.label }}
+          </el-radio-button>
+        </el-radio-group>
       </el-form-item>
 
       <el-form-item v-if="formData.bcContentType === 'all'" label="课程内容" prop="bcContent">

+ 552 - 0
src/views/system/role/RoleWebPermissionForm.vue

@@ -0,0 +1,552 @@
+<template>
+  <Dialog v-model="dialogVisible" title="web端角色路由配置" width="1200">
+    <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="120px">
+      <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">web路由数据权限</el-divider>
+      <br/>
+
+      <!-- 字典数据选择 -->
+      <el-form-item label="路由类型">
+        <el-checkbox-group v-model="formData.dataScopeWebRoute" size="large">
+          <el-checkbox-button v-for="dict in routeTypeOptions" :key="dict.value" :label="dict.value">
+            {{ dict.label }}
+          </el-checkbox-button>
+        </el-checkbox-group>
+      </el-form-item>
+
+      <!-- 标签页 -->
+      <el-tabs v-model="activeTab" type="border-card">
+        <!-- 课程权限 -->
+        <el-tab-pane label="课程权限" name="course">
+          <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="courseTabs.generalTreeNodeAll"
+                      active-text="是"
+                      inactive-text="否"
+                      inline-prompt
+                      @change="handleCheckedTreeNodeAll('course', 'general')"
+                    />
+                    全部展开/折叠:
+                    <el-switch
+                      v-model="courseTabs.generalDeptExpand"
+                      active-text="展开"
+                      inactive-text="折叠"
+                      inline-prompt
+                      @change="handleCheckedTreeExpand('course', 'general')"
+                    />
+                    父子联动:
+                    <el-switch v-model="courseTabs.generalCheckStrictly" active-text="是" inactive-text="否" inline-prompt />
+                  </template>
+                  <el-tree
+                    ref="generalTreeRef"
+                    :check-strictly="!courseTabs.generalCheckStrictly"
+                    :data="courseTabs.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="courseTabs.practicalTreeNodeAll"
+                      active-text="是"
+                      inactive-text="否"
+                      inline-prompt
+                      @change="handleCheckedTreeNodeAll('course', 'practical')"
+                    />
+                    全部展开/折叠:
+                    <el-switch
+                      v-model="courseTabs.practicalDeptExpand"
+                      active-text="展开"
+                      inactive-text="折叠"
+                      inline-prompt
+                      @change="handleCheckedTreeExpand('course', 'practical')"
+                    />
+                    父子联动:
+                    <el-switch v-model="courseTabs.practicalCheckStrictly" active-text="是" inactive-text="否" inline-prompt />
+                  </template>
+                  <el-tree
+                    ref="practicalTreeRef"
+                    :check-strictly="!courseTabs.practicalCheckStrictly"
+                    :data="courseTabs.practicalCourseOptions"
+                    :props="defaultProps"
+                    default-expand-all
+                    empty-text="加载中,请稍后"
+                    node-key="id"
+                    show-checkbox
+                  />
+                </el-card>
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-tab-pane>
+
+        <!-- blockly课程权限 -->
+        <el-tab-pane label="blockly课程权限" name="blockly">
+          <el-form-item label="blockly课程权限">
+            <el-card class="w-full h-300px !overflow-y-scroll" shadow="never">
+              <template #header>
+                全选/全不选:
+                <el-switch
+                  v-model="blocklyTabs.generalTreeNodeAll"
+                  active-text="是"
+                  inactive-text="否"
+                  inline-prompt
+                  @change="handleCheckedTreeNodeAll('blockly', 'general')"
+                />
+                全部展开/折叠:
+                <el-switch
+                  v-model="blocklyTabs.generalDeptExpand"
+                  active-text="展开"
+                  inactive-text="折叠"
+                  inline-prompt
+                  @change="handleCheckedTreeExpand('blockly', 'general')"
+                />
+                父子联动:
+                <el-switch v-model="blocklyTabs.generalCheckStrictly" active-text="是" inactive-text="否" inline-prompt />
+              </template>
+              <el-tree
+                ref="blocklyTreeRef"
+                :check-strictly="!blocklyTabs.generalCheckStrictly"
+                :data="blocklyTabs.generalBlocklyOptions"
+                :props="defaultProps"
+                default-expand-all
+                empty-text="加载中,请稍后"
+                node-key="id"
+                show-checkbox
+              />
+            </el-card>
+          </el-form-item>
+        </el-tab-pane>
+
+        <!-- AI实验课权限 -->
+        <el-tab-pane label="AI实验课权限" name="aiCourse">
+          <el-form-item label="AI实验课权限">
+            <el-card class="w-full h-300px !overflow-y-scroll" shadow="never">
+              <template #header>
+                全选/全不选:
+                <el-switch
+                  v-model="aiCourseTabs.generalTreeNodeAll"
+                  active-text="是"
+                  inactive-text="否"
+                  inline-prompt
+                  @change="handleCheckedTreeNodeAll('aiCourse', 'general')"
+                />
+                全部展开/折叠:
+                <el-switch
+                  v-model="aiCourseTabs.generalDeptExpand"
+                  active-text="展开"
+                  inactive-text="折叠"
+                  inline-prompt
+                  @change="handleCheckedTreeExpand('aiCourse', 'general')"
+                />
+                父子联动:
+                <el-switch v-model="aiCourseTabs.generalCheckStrictly" active-text="是" inactive-text="否" inline-prompt />
+              </template>
+              <el-tree
+                ref="aiCourseTreeRef"
+                :check-strictly="!aiCourseTabs.generalCheckStrictly"
+                :data="aiCourseTabs.generalAiCourseOptions"
+                :props="defaultProps"
+                default-expand-all
+                empty-text="加载中,请稍后"
+                node-key="id"
+                show-checkbox
+              />
+            </el-card>
+          </el-form-item>
+        </el-tab-pane>
+      </el-tabs>
+    </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 { DICT_TYPE, getDictOptions } from '@/utils/dict'
+import * as RoleApi from '@/api/system/role'
+import { CourseApi } from '@/api/bjdx/course'
+import { AiCourseTypeApi } from '@/api/aiCourse/aiCourseType'
+import { BlocklyTypeApi } from '@/api/blockly/blocklyType'
+import * as PermissionApi from '@/api/system/permission'
+
+defineOptions({ name: 'SystemRoleWebPermissionForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const activeTab = ref('course') // 当前激活的标签页
+const routeTypeOptions = ref(getDictOptions(DICT_TYPE.WEB_ROLE_ROUTE)) // 路由类型字典选项
+console.log('routeTypeOptions', routeTypeOptions)
+const formData = reactive({
+  id: undefined,
+  name: '',
+  code: '',
+  dataScopeCourseIds: [],
+  dataScopeAiCourseIds: [],
+  dataScopeBlocklyIds: [],
+  dataScopeWebRoute: []
+})
+const formRef = ref() // 表单 Ref
+
+// 课程权限相关变量
+const generalTreeRef = ref() // 通识课菜单树组件 Ref
+const practicalTreeRef = ref() // 实操课菜单树组件 Ref
+const courseTabs = reactive({
+  generalCourseOptions: [] as any[], // 通识课树形结构
+  generalDeptExpand: true, // 通识课展开/折叠
+  generalTreeNodeAll: false, // 通识课全选/全不选
+  generalCheckStrictly: true, // 通识课是否严格模式
+  practicalCourseOptions: [] as any[], // 实操课树形结构
+  practicalDeptExpand: true, // 实操课展开/折叠
+  practicalTreeNodeAll: false, // 实操课全选/全不选
+  practicalCheckStrictly: true, // 实操课是否严格模式
+})
+
+// AI实验课权限相关变量
+const aiCourseTreeRef = ref() // AI实验课菜单树组件 Ref
+const aiCourseTabs = reactive({
+  generalAiCourseOptions: [] as any[], // AI实验课树形结构
+  generalDeptExpand: true, // AI实验课展开/折叠
+  generalTreeNodeAll: false, // AI实验课全选/全不选
+  generalCheckStrictly: true, // AI实验课是否严格模式
+})
+
+// blockly课程权限相关变量
+const blocklyTreeRef = ref() // blockly课程菜单树组件 Ref
+const blocklyTabs = reactive({
+  generalBlocklyOptions: [] as any[], // blockly课程树形结构
+  generalDeptExpand: true, // blockly课程展开/折叠
+  generalTreeNodeAll: false, // blockly课程全选/全不选
+  generalCheckStrictly: true, // blockly课程是否严格模式
+})
+
+/** 打开弹窗 */
+const open = async (row: RoleApi.RoleVO) => {
+  dialogVisible.value = true
+  resetForm()
+
+  // 设置数据
+  formData.id = row.id
+  formData.name = row.name
+  formData.code = row.code
+  formData.dataScopeWebRoute = row.dataScopeWebRoute || []
+  // formData.dataScopeWebRoute = ["course","aiCourse","blockly"]
+
+  // 加载各类型数据
+  await Promise.all([
+    loadCourseData(row),
+    loadAiCourseData(row),
+    loadBlocklyData(row)
+  ])
+  
+  await nextTick()
+  
+    // 兼容旧数据格式
+    // if (formData.dataScopeWebRoute.length === 0) {
+    //   if (row.dataScopeCourseIds && row.dataScopeCourseIds.length > 0) {
+    //     formData.dataScopeWebRoute.push('course')
+    //   }
+    //   if (row.dataScopeAiCourseIds && row.dataScopeAiCourseIds.length > 0) {
+    //     formData.dataScopeWebRoute.push('aiCourse')
+    //   }
+    //   if (row.dataScopeBlocklyIds && row.dataScopeBlocklyIds.length > 0) {
+    //     formData.dataScopeWebRoute.push('blockly')
+    //   }
+    // }
+  
+  // 默认选择第一个可用的标签页
+  if (formData.dataScopeWebRoute.length > 0) {
+    activeTab.value = formData.dataScopeWebRoute[0]
+  }
+}
+
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 加载课程数据 */
+async function loadCourseData(row: RoleApi.RoleVO) {
+  const allCourses = await CourseApi.getCourseTypeTree()
+
+  courseTabs.generalCourseOptions = filterCoursesByType(allCourses, '通识课')
+  courseTabs.practicalCourseOptions = filterCoursesByType(allCourses, '实操课')
+
+  await nextTick()
+
+  // 设置选中状态
+  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)
+    })
+  }
+
+}
+
+/** 加载AI实验课数据 */
+async function loadAiCourseData(row: RoleApi.RoleVO) {
+  const allCourses = await AiCourseTypeApi.getAiCourseTypeTree()
+  aiCourseTabs.generalAiCourseOptions = filterCoursesByType(allCourses, "aiCourseType")
+
+  await nextTick()
+
+  // 设置选中状态
+  if (row.dataScopeAiCourseIds && row.dataScopeAiCourseIds.length > 0) {
+    row.dataScopeAiCourseIds.forEach((courseId: number): void => {
+      aiCourseTreeRef.value?.setChecked(courseId, true, false)
+    })
+  }
+}
+
+/** 加载blockly课程数据 */
+async function loadBlocklyData(row: RoleApi.RoleVO) {
+  const allCourses = await BlocklyTypeApi.getBlocklyTypeTree()
+  blocklyTabs.generalBlocklyOptions = filterCoursesByType(allCourses, "blocklyType")
+
+  await nextTick()
+
+  // 设置选中状态
+  if (row.dataScopeBlocklyIds && row.dataScopeBlocklyIds.length > 0) {
+    row.dataScopeBlocklyIds.forEach((courseId: number): void => {
+      blocklyTreeRef.value?.setChecked(courseId, true, false)
+    })
+  }
+}
+
+/**
+ * 根据课程类型筛选课程,并保留完整的树结构
+ * @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 data: any = {
+      roleId: formData.id,
+      dataScopeWebRoute: formData.dataScopeWebRoute
+    }
+
+    // 处理课程权限
+    const generalCheckedKeys = generalTreeRef.value?.getCheckedKeys(false) || []
+    const practicalCheckedKeys = practicalTreeRef.value?.getCheckedKeys(false) || []
+    const allCheckedKeys = [...generalCheckedKeys, ...practicalCheckedKeys]
+    const allCourseOptions = [...courseTabs.generalCourseOptions, ...courseTabs.practicalCourseOptions]
+    // 课程类型的叶子节点类型是'通识课'或'实操课',所以不需要过滤叶子节点类型
+    data.dataScopeCourseIds = allCheckedKeys.filter(key => {
+      // 只保留实际存在于树结构中的节点ID
+      const nodeMap = new Map<number, boolean>()
+      function traverseTree(nodes: any[]) {
+        nodes.forEach(node => {
+          nodeMap.set(node.id, true)
+          if (node.children && node.children.length > 0) {
+            traverseTree(node.children)
+          }
+        })
+      }
+      traverseTree(allCourseOptions)
+      return nodeMap.has(key)
+    })
+
+    // 处理AI实验课权限
+    const aiCourseCheckedKeys = aiCourseTreeRef.value?.getCheckedKeys(false) || []
+    data.dataScopeAiCourseIds = filterChildNodeKeys(aiCourseCheckedKeys, aiCourseTabs.generalAiCourseOptions, "aiCourseType")
+
+    // 处理blockly课程权限
+    const blocklyCheckedKeys = blocklyTreeRef.value?.getCheckedKeys(false) || []
+    data.dataScopeBlocklyIds = filterChildNodeKeys(blocklyCheckedKeys, blocklyTabs.generalBlocklyOptions, "blocklyType")
+
+    console.log('提交的数据:', data)
+    await PermissionApi.assignRoleWebScope(data)
+    message.success(t('common.updateSuccess'))
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/**
+ * 过滤只包含子节点的ID
+ * @param checkedKeys 所有选中的节点ID
+ * @param treeData 树结构数据
+ * @param leafType 叶子节点类型
+ * @returns 只包含子节点的ID数组
+ */
+function filterChildNodeKeys(checkedKeys: number[], treeData: any[], leafType: string): number[] {
+  // 创建一个Map存储所有节点ID及其是否为叶子节点的状态
+  const nodeMap = new Map<number, boolean>()
+
+  // 递归遍历树结构,标记每个节点是否为叶子节点
+  function traverseTree(nodes: any[]) {
+    nodes.forEach(node => {
+      nodeMap.set(node.id, node.type === leafType)
+
+      // 递归处理子节点
+      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 = () => {
+
+  // 重置课程权限选项
+  courseTabs.generalCourseOptions = []
+  courseTabs.generalDeptExpand = true
+  courseTabs.generalTreeNodeAll = false
+  courseTabs.generalCheckStrictly = true
+  generalTreeRef.value?.setCheckedNodes([])
+
+  courseTabs.practicalCourseOptions = []
+  courseTabs.practicalDeptExpand = true
+  courseTabs.practicalTreeNodeAll = false
+  courseTabs.practicalCheckStrictly = true
+  practicalTreeRef.value?.setCheckedNodes([])
+
+  // 重置AI实验课选项
+  aiCourseTabs.generalAiCourseOptions = []
+  aiCourseTabs.generalDeptExpand = true
+  aiCourseTabs.generalTreeNodeAll = false
+  aiCourseTabs.generalCheckStrictly = true
+  aiCourseTreeRef.value?.setCheckedNodes([])
+
+  // 重置blockly课程选项
+  blocklyTabs.generalBlocklyOptions = []
+  blocklyTabs.generalDeptExpand = true
+  blocklyTabs.generalTreeNodeAll = false
+  blocklyTabs.generalCheckStrictly = true
+  blocklyTreeRef.value?.setCheckedNodes([])
+
+  // 重置表单数据
+    formData.id = undefined
+    formData.name = ''
+    formData.code = ''
+    formData.dataScopeCourseIds = []
+    formData.dataScopeAiCourseIds = []
+    formData.dataScopeBlocklyIds = []
+    formData.dataScopeWebRoute = []
+
+    formRef.value?.resetFields()
+}
+
+/** 处理全选/全不选 */
+function handleCheckedTreeNodeAll(tabType: string, treeType: string) {
+  if (tabType === 'course') {
+    if (treeType === 'general') {
+      generalTreeRef.value?.setCheckedNodes(courseTabs.generalTreeNodeAll ? courseTabs.generalCourseOptions : [])
+    } else if (treeType === 'practical') {
+      practicalTreeRef.value?.setCheckedNodes(courseTabs.practicalTreeNodeAll ? courseTabs.practicalCourseOptions : [])
+    }
+  } else if (tabType === 'aiCourse') {
+    aiCourseTreeRef.value?.setCheckedNodes(aiCourseTabs.generalTreeNodeAll ? aiCourseTabs.generalAiCourseOptions : [])
+  } else if (tabType === 'blockly') {
+    blocklyTreeRef.value?.setCheckedNodes(blocklyTabs.generalTreeNodeAll ? blocklyTabs.generalBlocklyOptions : [])
+  }
+}
+
+/** 处理展开/折叠全部 */
+function handleCheckedTreeExpand(tabType: string, treeType: string) {
+  if (tabType === 'course') {
+    const treeRef = treeType === 'general' ? generalTreeRef.value : practicalTreeRef.value
+    const expandValue = treeType === 'general' ? courseTabs.generalDeptExpand : courseTabs.practicalDeptExpand
+    expandTreeNodes(treeRef, expandValue)
+  } else if (tabType === 'aiCourse') {
+    expandTreeNodes(aiCourseTreeRef.value, aiCourseTabs.generalDeptExpand)
+  } else if (tabType === 'blockly') {
+    expandTreeNodes(blocklyTreeRef.value, blocklyTabs.generalDeptExpand)
+  }
+}
+
+/** 展开/折叠树节点 */
+function expandTreeNodes(treeRef: any, expand: boolean) {
+  const nodes = treeRef?.store.nodesMap
+  if (nodes) {
+    for (let node in nodes) {
+      if (nodes[node].expanded === expand) {
+        continue
+      }
+      nodes[node].expanded = expand
+    }
+  }
+}
+</script>

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

@@ -145,6 +145,16 @@
           >
             课程权限
           </el-button>
+          <el-button
+            v-hasPermi="['system:permission:assign-role-data-scope']"
+            link
+            preIcon="ep:coin"
+            title="Web权限"
+            type="primary"
+            @click="openWebPermissionForm(scope.row)"
+          >
+            Web权限
+          </el-button>
           <el-button
             v-hasPermi="['system:permission:assign-role-data-scope']"
             link
@@ -191,6 +201,8 @@
   <RoleAssignMenuForm ref="assignMenuFormRef" @success="getList" />
   <!-- 表单弹窗:数据权限 -->
   <RoleDataPermissionForm ref="dataPermissionFormRef" @success="getList" />
+  <!-- 表单弹窗:Web权限 -->
+  <RoleWebPermissionForm ref="webPermissionFormRef" @success="getList" />
   <!-- 表单弹窗:课程权限 -->
   <RoleCoursePermissionForm ref="coursePermissionFormRef" @success="getList" />
   <!-- 表单弹窗:blockly课程权限 -->
@@ -209,6 +221,7 @@ import RoleDataPermissionForm from './RoleDataPermissionForm.vue'
 import RoleCoursePermissionForm from './RoleCoursePermissionForm.vue'
 import RoleBlocklyPermissionForm from './RoleBlocklyPermissionForm.vue'
 import RoleAiCoursePermissionForm from './RoleAiCoursePermissionForm.vue'
+import RoleWebPermissionForm from './RoleWebPermissionForm.vue'
 
 
 defineOptions({ name: 'SystemRole' })
@@ -266,6 +279,12 @@ const openDataPermissionForm = async (row: RoleApi.RoleVO) => {
   dataPermissionFormRef.value.open(row)
 }
 
+/** Web权限操作 */
+const webPermissionFormRef = ref()
+const openWebPermissionForm = async (row: RoleApi.RoleVO) => {
+  webPermissionFormRef.value.open(row)
+}
+
 /** 课程权限操作 */
 const coursePermissionFormRef = ref()
 const openCoursePermissionForm = async (row: RoleApi.RoleVO) => {