Ver Fonte

1、配置blockly课程、类型(主题、类型)、配置等所有管理
2、管理列表页优化查询
3、融合blockly数据和课程数据

liyanbo há 5 meses atrás
pai
commit
b3821bc2be

+ 60 - 0
src/api/blockly/blockly/index.ts

@@ -0,0 +1,60 @@
+import request from '@/config/axios'
+
+// 课程 VO
+export interface BlocklyVO {
+  id: number // 课程d
+  bcName: string // 课程名称
+  bcContentType: "all", // 课程内容类型
+  bcContent: string, // 课程内容
+
+  blocklyInfo: string // 简介
+  blocklyUserImage: string // 人物图标
+  blocklyUserDirection: number // 人物朝向
+  blocklyTileSize: string // 地图方格尺寸
+  blocklyStartPoint: string // 地图开始坐标
+  blocklyBackground: string // 地图背景图
+  blocklyEndPoint: string // 地图结束坐标
+  blocklyWalkablePoints: string // 地图可行走坐标
+
+  bcIsInspect: false // 课程是否有检查
+  bcType: number // 课程类型
+  bcTypeName: string // 课程类型
+  bcLabel: string // 课程标签
+  bcOrder: number // 课程排序
+  bcStatus: "0" // 课程状态
+  tenantId: Number // 租户id
+}
+
+// 课程 API
+export const BlocklyApi = {
+  // 查询课程分页
+  getBlocklyPage: async (params: any) => {
+    return await request.get({ url: `/blockly/blockly/page`, params })
+  },
+
+  // 查询课程详情
+  getBlockly: async (id: number) => {
+    return await request.get({ url: `/blockly/blockly/get?id=` + id })
+  },
+
+  // 新增课程
+  createBlockly: async (data: BlocklyVO) => {
+    return await request.post({ url: `/blockly/blockly/create`, data })
+  },
+
+  // 修改课程
+  updateBlockly: async (data: BlocklyVO) => {
+    return await request.put({ url: `/blockly/blockly/update`, data })
+  },
+
+  // 删除课程
+  deleteBlockly: async (id: number) => {
+    return await request.delete({ url: `/blockly/blockly/delete?id=` + id })
+  },
+
+  // 导出课程 Excel
+  exportBlockly: async (params) => {
+    return await request.download({ url: `/blockly/blockly/export-excel`, params })
+  },
+
+}

+ 56 - 0
src/api/blockly/blocklyConfig/index.ts

@@ -0,0 +1,56 @@
+import request from '@/config/axios'
+
+// 课程配置 VO
+export interface CourseConfigVO {
+  id: number // 课程配置id
+  ccCourseId: number // 课程id
+  courseName: string // 课程名称
+  ccTime: number // 课程暂停时长
+  ccQuestId: number // 试题id
+  questContent: string // 试题内容
+  ccAnswerJudge: string // 是否显示答案, // 选择的试题id
+  ccQuestSource: undefined // 问题呈现类型
+  ccQuestContent: undefined  // 问题内容
+  ccQuestOption: undefined  // 问题选项
+  ccAiAnswer: undefined  // AI答案
+  ccAnswer: undefined  // 答案
+  tenantId: Number // 租户id
+}
+
+// 课程配置 API
+export const BlocklyConfigApi = {
+  // 查询课程配置分页
+  getCourseConfigPage: async (params: any) => {
+    return await request.get({ url: `/blockly/blockly-config/page`, params })
+  },
+
+  // 查询课程配置详情
+  getCourseConfig: async (id: number) => {
+    return await request.get({ url: `/blockly/blockly-config/get?id=` + id })
+  },
+
+  // 查询课程配置试题详情
+  getCourseConfigQuestion: async (ccCourseId: number) => {
+    return await request.get({ url: `/blockly/blockly-config/getConfigQuest?ccCourseId=` + ccCourseId })
+  },
+
+  // 新增课程配置
+  createCourseConfig: async (data: CourseConfigVO) => {
+    return await request.post({ url: `/blockly/blockly-config/create`, data })
+  },
+
+  // 修改课程配置
+  updateCourseConfig: async (data: CourseConfigVO) => {
+    return await request.put({ url: `/blockly/blockly-config/update`, data })
+  },
+
+  // 删除课程配置
+  deleteCourseConfig: async (id: number) => {
+    return await request.delete({ url: `/blockly/blockly-config/delete?id=` + id })
+  },
+
+  // 导出课程配置 Excel
+  exportCourseConfig: async (params) => {
+    return await request.download({ url: `/blockly/blockly-config/export-excel`, params })
+  }
+}

+ 50 - 0
src/api/blockly/blocklyType/index.ts

@@ -0,0 +1,50 @@
+import request from '@/config/axios'
+
+// 课程-类型 VO
+export interface BlocklyTypeVO {
+  id: number // 课程类型id
+  ctType: string, // 课程类型名称
+  ctTypeNode: undefined, // 课程类型节点
+  ctTypeSort: undefined, // 课程类型排序
+  ctParentId: number, // 课程类型父级id
+  ctTypeDescribe: string, // 课程类型描述
+  tenantId: Number // 租户id
+}
+
+// 课程-类型 API
+export const BlocklyTypeApi = {
+  // 查询课程-类型列表
+  getBlocklyTypeList: async (params) => {
+    return await request.get({ url: `/blockly/blockly-type/list`, params })
+  },
+  // 查询课程-类型列表
+  getBlocklyTypeSimpleList: async (params) => {
+    return await request.get({ url: `/blockly/blockly-type/simple-list`, params })
+  },
+
+  // 查询课程-类型详情
+  getBlocklyType: async (id: number) => {
+    return await request.get({ url: `/blockly/blockly-type/get?id=` + id })
+  },
+
+  // 新增课程-类型
+  createBlocklyType: async (data: BlocklyTypeVO) => {
+    return await request.post({ url: `/blockly/blockly-type/create`, data })
+  },
+
+  // 修改课程-类型
+  updateBlocklyType: async (data: BlocklyTypeVO) => {
+    return await request.put({ url: `/blockly/blockly-type/update`, data })
+  },
+
+  // 删除课程-类型
+  deleteBlocklyType: async (id: number) => {
+    return await request.delete({ url: `/blockly/blockly-type/delete?id=` + id })
+  },
+
+  // 导出课程-类型 Excel
+  exportBlocklyType: async (params) => {
+    return await request.download({ url: `/blockly/blockly-type/export-excel`, params })
+  },
+
+}

+ 20 - 0
src/router/modules/remaining.ts

@@ -387,6 +387,26 @@ const remainingRouter: AppRouteRecordRaw[] = [
       }
     ]
   },
+  {
+    path: '/blockly',
+    component: Layout,
+    name: 'Blockly',
+    meta: {
+      hidden: true
+    },
+    children: [
+      {
+        path: 'blockly/config',
+        component: () => import('@/views/blockly/blocklyConfig/index.vue'),
+        name: 'blocklyConfig',
+        meta: {
+          title: 'Blockly 配置',
+          icon: 'ep:setting',
+          noCache: false
+        }
+      }
+    ]
+  },
 ]
 
 export default remainingRouter

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

@@ -10,6 +10,8 @@ declare module 'vue' {
     AppLinkInput: typeof import('./../components/AppLinkInput/index.vue')['default']
     AppLinkSelectDialog: typeof import('./../components/AppLinkInput/AppLinkSelectDialog.vue')['default']
     Backtop: typeof import('./../components/Backtop/src/Backtop.vue')['default']
+    BlocklyConfig: typeof import('./../views/blockly/blocklyConfig/index.vue')['default']
+    BlocklyConfigForm: typeof import('./../views/blockly/blocklyConfig/BlocklyConfigForm.vue')['default']
     BoundaryEventTimer: typeof import('./../components/bpmnProcessDesigner/package/penal/custom-config/components/BoundaryEventTimer.vue')['default']
     CallActivity: typeof import('./../components/bpmnProcessDesigner/package/penal/task/task-components/CallActivity.vue')['default']
     CardTitle: typeof import('./../components/Card/src/CardTitle.vue')['default']
@@ -45,7 +47,6 @@ declare module 'vue' {
     Draggable: typeof import('./../components/Draggable/index.vue')['default']
     Echart: typeof import('./../components/Echart/src/Echart.vue')['default']
     Editor: typeof import('./../components/Editor/src/Editor.vue')['default']
-    ElAside: typeof import('element-plus/es')['ElAside']
     ElAutoResizer: typeof import('element-plus/es')['ElAutoResizer']
     ElAvatar: typeof import('element-plus/es')['ElAvatar']
     ElBadge: typeof import('element-plus/es')['ElBadge']
@@ -54,9 +55,6 @@ declare module 'vue' {
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
     ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
     ElCol: typeof import('element-plus/es')['ElCol']
-    ElCollapseTransition: typeof import('element-plus/es')['ElCollapseTransition']
-    ElContainer: typeof import('element-plus/es')['ElContainer']
-    ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
     ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
     ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
     ElDialog: typeof import('element-plus/es')['ElDialog']
@@ -74,17 +72,14 @@ declare module 'vue' {
     ElementProperties: typeof import('./../components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue')['default']
     ElementTask: typeof import('./../components/bpmnProcessDesigner/package/penal/task/ElementTask.vue')['default']
     ElEmpty: typeof import('element-plus/es')['ElEmpty']
-    ElFooter: typeof import('element-plus/es')['ElFooter']
     ElForm: typeof import('element-plus/es')['ElForm']
     ElFormItem: typeof import('element-plus/es')['ElFormItem']
-    ElHeader: typeof import('element-plus/es')['ElHeader']
     ElIcon: typeof import('element-plus/es')['ElIcon']
     ElImage: typeof import('element-plus/es')['ElImage']
     ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
     ElInput: typeof import('element-plus/es')['ElInput']
     ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
     ElLink: typeof import('element-plus/es')['ElLink']
-    ElMain: typeof import('element-plus/es')['ElMain']
     ElOption: typeof import('element-plus/es')['ElOption']
     ElPagination: typeof import('element-plus/es')['ElPagination']
     ElPopover: typeof import('element-plus/es')['ElPopover']
@@ -97,8 +92,6 @@ declare module 'vue' {
     ElSegmented: typeof import('element-plus/es')['ElSegmented']
     ElSelect: typeof import('element-plus/es')['ElSelect']
     ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
-    ElSlider: typeof import('element-plus/es')['ElSlider']
-    ElSpace: typeof import('element-plus/es')['ElSpace']
     ElSwitch: typeof import('element-plus/es')['ElSwitch']
     ElTable: typeof import('element-plus/es')['ElTable']
     ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
@@ -106,11 +99,7 @@ declare module 'vue' {
     ElTabPane: typeof import('element-plus/es')['ElTabPane']
     ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTag: typeof import('element-plus/es')['ElTag']
-    ElText: typeof import('element-plus/es')['ElText']
-    ElTimeline: typeof import('element-plus/es')['ElTimeline']
-    ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
     ElTooltip: typeof import('element-plus/es')['ElTooltip']
-    ElTree: typeof import('element-plus/es')['ElTree']
     ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
     ElUpload: typeof import('element-plus/es')['ElUpload']
     EndEventNode: typeof import('./../components/SimpleProcessDesignerV2/src/nodes/EndEventNode.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']

+ 2 - 1
src/utils/dict.ts

@@ -260,5 +260,6 @@ export enum DICT_TYPE {
 
   // ========== Blockly - 地图编程游戏  ==========
   AI_BLOCKLY_MAP_TYPE = 'ai_blockly_map_type', // 地图类型
-
+  BLOCKLY_COURSE_LABEL = 'blockly_course_label', // 课程标签
+  BLOCKLY_COURSE_COUTNET_TYPE = 'blockly_course_content_type' // 课程内容类型
 }

+ 5 - 0
src/views/bjdx/course/index.vue

@@ -29,6 +29,7 @@
           placeholder="请选择课程类型"
           :default-expand-all="true"
           class="!w-240px"
+          @change="handleQuery"
         />
       </el-form-item>
       <el-form-item label="内容类型" prop="courseContentType">
@@ -37,6 +38,7 @@
           placeholder="请选择内容类型"
           clearable
           class="!w-240px"
+          @change="handleQuery"
         >
           <el-option
             v-for="dict in getStrDictOptions(DICT_TYPE.COURSE_COUTNET_TYPE)"
@@ -79,6 +81,7 @@
           placeholder="请选择是否有检查"
           clearable
           class="!w-240px"
+          @change="handleQuery"
         >
           <el-option
             v-for="dict in getStrDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
@@ -94,6 +97,7 @@
           placeholder="请选择课程标签"
           clearable
           class="!w-240px"
+          @change="handleQuery"
         >
           <el-option
             v-for="dict in getStrDictOptions(DICT_TYPE.COURSE_LABEL)"
@@ -109,6 +113,7 @@
           placeholder="请选择课程状态"
           clearable
           class="!w-240px"
+          @change="handleQuery"
         >
           <el-option
             v-for="dict in getStrDictOptions(DICT_TYPE.COMMON_STATUS)"

+ 1 - 0
src/views/bjdx/coursetype/index.vue

@@ -23,6 +23,7 @@
           placeholder="请选择类型节点类型"
           clearable
           class="!w-240px"
+          @change="handleQuery"
         >
           <el-option label="年级" value="0" />
           <el-option label="ai通识课" value="1" />

+ 1 - 0
src/views/bjdx/questionnaire/ConfigQuestion.vue

@@ -24,6 +24,7 @@
           placeholder="请选择试题类型"
           clearable
           class="!w-240px"
+          @change="handleQuery"
         >
           <el-option
             v-for="dict in getStrDictOptions(DICT_TYPE.COURSE_QUESTION_TYPE)"

+ 1007 - 0
src/views/blockly/blockly/BlocklyForm.vue

@@ -0,0 +1,1007 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible" width="1000px">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="大纲课程" prop="bcType">
+        <el-tree-select
+          v-model="formData.bcType"
+          :data="bcTypeTree"
+          :props="{...defaultProps,
+            label: (node) => `${node.ctTypeNode === undefined ? node.ctType : node.ctTypeSort + '、' + node.ctType}`,
+            }"
+          placeholder="请选择课程类型"
+          :default-expand-all="true"
+        />
+      </el-form-item>
+      <el-form-item label="课程名称" prop="bcName">
+        <el-input v-model="formData.bcName" placeholder="请输入课程名称" />
+      </el-form-item>
+
+      <el-form-item label="课程标签" prop="bcLabel">
+        <el-select
+          v-model="formData.bcLabel"
+          placeholder="请选择课程标签"
+          clearable
+        >
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.BLOCKLY_COURSE_LABEL)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="内容类型" prop="bcContentType">
+        <el-segmented v-model="formData.bcContentType" :options="getStrDictOptions(DICT_TYPE.BLOCKLY_COURSE_COUTNET_TYPE)" />
+      </el-form-item>
+
+      <el-form-item v-if="formData.bcContentType === 'all'" label="课程内容" prop="bcContent">
+        <Editor v-model="formData.bcContent" height="150px" />
+      </el-form-item>
+
+      <el-form-item v-if="formData.bcContentType === 'text'" label="纯文本" prop="bcContent">
+        <Editor v-model="formData.bcContent" height="150px"  />
+      </el-form-item>
+
+      <el-form-item v-if="formData.bcContentType === 'image'" label="课程图片集" prop="bcContent">
+        <UploadImgs v-model="formData.bcContent"
+                    @upload-progress="handleUploadProgress"
+                    @upload-start="handleUploadStart"
+                    @upload-complete="handleUploadComplete"/>
+      </el-form-item>
+
+      <el-form-item v-if="formData.bcContentType === 'music'" label="课程音频" prop="bcContent">
+        <UploadMusic v-model="formData.bcContent"
+                     @upload-progress="handleUploadProgress"
+                     @upload-start="handleUploadStart"
+                     @upload-complete="handleUploadComplete"/>
+      </el-form-item>
+
+      <el-form-item v-if="formData.bcContentType === 'video'" label="课程视频" prop="bcContent">
+        <UploadVideo
+          v-model="formData.bcContent"
+          @upload-progress="handleUploadProgress"
+          @upload-start="handleUploadStart"
+          @upload-complete="handleUploadComplete"
+        />
+      </el-form-item>
+
+      <el-form-item v-if="formData.bcContentType === 'ppt'" label="课程PPT" prop="bcContent">
+        <UploadFile v-model="formData.bcContent" :fileType="['ppt','pptx']" :fileSize="50"
+                    @upload-progress="handleUploadProgress"
+                    @upload-start="handleUploadStart"
+                    @upload-complete="handleUploadComplete"/>
+      </el-form-item>
+
+      <!-- 上传进度条 -->
+      <el-form-item>
+        <div v-if="isUploading" class="uploadProgress">
+          <el-progress :percentage="uploadProgress" />
+          <div class="text-xs text-gray-500 text-right mt-1">{{ uploadProgress }}% 已上传</div>
+        </div>
+      </el-form-item>
+
+      <template v-if="formData.bcContentType === 'blockly'">
+        <el-form-item label="Blockly配置" prop="blocklyConfig">
+          <el-button type="primary" @click="openBlocklyConfigDialog">配置Blockly数据</el-button>
+          <div v-if="hasBlocklyConfig" class="mt-2 text-sm text-green-600">
+            ✓ Blockly数据已配置
+          </div>
+          <div v-else class="mt-2 text-sm text-red-600">
+            ⚠ Blockly数据未配置
+          </div>
+        </el-form-item>
+      </template>
+
+      <template v-else>
+        <el-form-item label="课程是否有检查" prop="bcIsInspect">
+          <el-radio-group v-model="formData.bcIsInspect">
+            <el-radio
+              v-for="dict in getStrDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+              :key="dict.value"
+              :label="dict.value"
+            >
+              {{ dict.label }}
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </template>
+
+      <el-form-item label="课程排序" prop="bcOrder">
+        <el-input-number v-model="formData.bcOrder" placeholder="请输入课程排序" class="!w-1/1" />
+      </el-form-item>
+
+      <el-form-item label="课程状态" prop="bcStatus">
+        <el-radio-group v-model="formData.bcStatus">
+          <el-radio
+            v-for="dict in getStrDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading || isUploading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+
+  <!-- Blockly配置弹框 -->
+  <Dialog :title="blocklyConfigTitle" v-model="blocklyConfigVisible" width="1250px" >
+    <el-form
+      ref="blocklyConfigFormRef"
+      :model="formData"
+      :rules="blocklyConfigRules"
+      label-width="130px"
+    >
+
+      <el-form-item label="Blockly信息" prop="blocklyInfo">
+        <Editor v-model="formData.blocklyInfo" height="150px" />
+      </el-form-item>
+
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="人物图标" prop="blocklyUserImage">
+            <UploadImg v-model="formData.blocklyUserImage" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="地图背景图" prop="blocklyBackground">
+            <UploadImg v-model="formData.blocklyBackground" />
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="12">
+          <el-form-item label="人物朝向" prop="blocklyUserDirection">
+            <el-radio-group v-model="formData.blocklyUserDirection" size="large">
+              <el-radio-button label="上" :value="0" />
+              <el-radio-button label="右" :value="1" />
+              <el-radio-button label="下" :value="2" />
+              <el-radio-button label="左" :value="3" />
+            </el-radio-group>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <!-- 地图开始坐标 - 拆分为X轴和Y轴 -->
+          <el-form-item label="地图开始坐标" required>
+            <div class="coordinate-group">
+              X:
+              <el-input-number
+                v-model="blocklyStartPointX"
+                placeholder="X轴"
+                :min="1"
+                :max="blocklyTileX"
+                :step="1"
+                style="width: 120px"
+              />
+              Y:
+              <el-input-number
+                v-model="blocklyStartPointY"
+                placeholder="Y轴"
+                :min="1"
+                :max="blocklyTileY"
+                :step="1"
+                style="width: 120px"
+              />
+            </div>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="地图方格尺寸" required>
+            <div class="coordinate-group">
+              X:
+              <el-input-number
+                v-model="blocklyTileX"
+                placeholder="横向数量"
+                :min="1"
+                :step="1"
+                style="width: 120px"
+              />
+              Y:
+              <el-input-number
+                v-model="blocklyTileY"
+                placeholder="纵向数量"
+                :min="1"
+                :step="1"
+                style="width: 120px"
+              />
+            </div>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <!-- 地图结束坐标 - 拆分为X轴和Y轴 -->
+          <el-form-item label="地图结束坐标" required>
+            <div class="coordinate-group">
+              X:
+              <el-input-number
+                v-model="blocklyEndPointX"
+                placeholder="X轴"
+                :min="1"
+                :max="blocklyTileX"
+                :step="1"
+                style="width: 120px"
+              />
+              Y:
+              <el-input-number
+                v-model="blocklyEndPointY"
+                placeholder="Y轴"
+                :min="1"
+                :max="blocklyTileY"
+                :step="1"
+                style="width: 120px"
+              />
+            </div>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <!-- 地图可行走坐标 - 动态矩阵显示 -->
+      <el-form-item label="地图方格配置" required>
+        <el-row type="flex" style="width: 100%;">
+          <!-- 左侧方格矩阵 -->
+          <el-col :span="10">
+            <div class="map-grid-container">
+              <div
+                class="map-grid"
+                :style="{ gridTemplateColumns: `repeat(${blocklyTileX || 5}, 1fr)`,
+                   gridTemplateRows: `repeat(${blocklyTileY || 5}, 1fr)` ,
+                   backgroundImage: formData.blocklyBackground ? `url(${formData.blocklyBackground})` : 'none', backgroundSize: 'cover',
+                   backgroundPosition: 'center' }"
+              >
+                <div
+                  v-for="(cell, index) in blocklyGridCells"
+                  :key="index"
+                  class="grid-cell"
+                  :class="{
+                      'walkable': isBlocklyWalkable(cell.x, cell.y),
+                      'selected': selectedBlocklyPoint && selectedBlocklyPoint.x === cell.x && selectedBlocklyPoint.y === cell.y,
+                      'has-icon': getBlocklyPointByXY(cell.x, cell.y)?.img
+                    }"
+                  @click="selectBlocklyCell(cell)"
+                >
+                  <div class="cell-coordinate">{{ cell.x }},{{ cell.y }}</div>
+                  <div v-if="getBlocklyPointByXY(cell.x, cell.y)?.img" class="cell-icon">
+                    <img :src="getBlocklyPointByXY(cell.x, cell.y).img" alt="图标" />
+                  </div>
+                </div>
+              </div>
+            </div>
+          </el-col>
+
+          <!-- 右侧配置项 -->
+          <el-col :span="14">
+            <div class="point-config-container" v-if="selectedBlocklyPoint">
+              <el-form
+                label-width="100px"
+                :model="selectedBlocklyPoint"
+                class="config-form"
+                size="large"
+              >
+                <el-card class="config-card" shadow="hover">
+                  <template #header>
+                    <div class="card-header">
+                      <span>坐标配置</span>
+                      <el-tag size="small" type="info">{{ selectedBlocklyPoint.x }},{{ selectedBlocklyPoint.y }}</el-tag>
+                    </div>
+                  </template>
+
+                  <!-- 第一行:是否可行走 -->
+                  <div class="config-row">
+                    <el-form-item label="是否可行走" class="config-item">
+                      <el-switch
+                        v-model="selectedBlocklyPoint.walkable"
+                        active-text="是"
+                        inactive-text="否"
+                        size="large"
+                      />
+                    </el-form-item>
+                  </div>
+
+                  <template v-if="selectedBlocklyPoint.walkable">
+                    <!-- 第二行:地图类型 -->
+                    <div class="config-row">
+                      <el-form-item label="地图类型" class="config-item">
+                        <el-select
+                          v-model="selectedBlocklyPoint.type"
+                          placeholder="选择地图类型"
+                          style="width: 100%"
+                          clearable
+                          size="large"
+                        >
+                          <el-option
+                            v-for="dict in getStrDictOptions(DICT_TYPE.AI_BLOCKLY_MAP_TYPE)"
+                            :key="dict.value"
+                            :label="dict.label"
+                            :value="dict.value"
+                          />
+                        </el-select>
+                      </el-form-item>
+
+                      <el-form-item
+                        label="是否必须完成"
+                        class="config-item"
+                        v-if="selectedBlocklyPoint.type === 'task'"
+                      >
+                        <el-switch
+                          v-model="selectedBlocklyPoint.must"
+                          active-text="是"
+                          inactive-text="否"
+                          size="large"
+                        />
+                      </el-form-item>
+                    </div>
+
+                    <!-- 第三行:图标上传 -->
+                    <div class="config-row">
+                      <el-form-item label="初始图标" class="config-item" v-if="selectedBlocklyPoint.type !== ''">
+                        <div class="icon-upload-wrapper">
+                          <UploadImg
+                            v-model="selectedBlocklyPoint.img"
+                            title="点击上传初始图标"
+                            class="custom-upload"
+                          />
+                          <div v-if="selectedBlocklyPoint.img" class="upload-tip">已上传</div>
+                        </div>
+                      </el-form-item>
+
+                      <el-form-item
+                        label="完成图标"
+                        class="config-item"
+                        v-if="selectedBlocklyPoint.type !== '' && selectedBlocklyPoint.type === 'task'"
+                      >
+                        <div class="icon-upload-wrapper">
+                          <UploadImg
+                            v-model="selectedBlocklyPoint.endImg"
+                            title="点击上传完成图标"
+                            class="custom-upload"
+                          />
+                          <div v-if="selectedBlocklyPoint.endImg" class="upload-tip">已上传</div>
+                        </div>
+                      </el-form-item>
+                    </div>
+
+                    <el-form-item label="提示语" class="form-item-full" v-if="selectedBlocklyPoint.type !== '' && selectedBlocklyPoint.type !== 'task'">
+                      <el-input
+                        v-model="selectedBlocklyPoint.tip"
+                        placeholder="输入初始提示语"
+                        style="width: 100%"
+                        show-word-limit
+                        maxlength="100"
+                        type="textarea"
+                        :rows="2"
+                      />
+                    </el-form-item>
+
+                    <el-form-item label="未完成提示语" class="form-item-full" v-if="selectedBlocklyPoint.type !== '' && selectedBlocklyPoint.type === 'task'">
+                      <el-input
+                        v-model="selectedBlocklyPoint.unfinishedTip"
+                        placeholder="输入未完成提示语"
+                        style="width: 100%"
+                        show-word-limit
+                        maxlength="100"
+                        type="textarea"
+                        :rows="2"
+                      />
+                    </el-form-item>
+                    <el-form-item label="完成提示语" class="form-item-full" v-if="selectedBlocklyPoint.type !== '' && selectedBlocklyPoint.type === 'task'">
+                      <el-input
+                        v-model="selectedBlocklyPoint.finishedTip"
+                        placeholder="输入完成提示语"
+                        style="width: 100%"
+                        show-word-limit
+                        maxlength="100"
+                        type="textarea"
+                        :rows="2"
+                      />
+                    </el-form-item>
+                  </template>
+                </el-card>
+              </el-form>
+            </div>
+
+            <div v-else class="no-selection">
+              <el-empty description="请点击左侧方格进行配置" />
+            </div>
+          </el-col>
+        </el-row>
+      </el-form-item>
+
+    </el-form>
+    <template #footer>
+      <el-button @click="saveBlocklyConfig" type="primary">确 定</el-button>
+      <el-button @click="blocklyConfigVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getStrDictOptions, DICT_TYPE } from '@/utils/dict'
+import { BlocklyApi, BlocklyVO } from '@/api/blockly/blockly'
+import { defaultProps, handleTree } from '@/utils/tree'
+import { BlocklyTypeApi } from '@/api/blockly/blocklyType'
+
+/** 课程 表单 */
+defineOptions({ name: 'BlocklyForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+
+// Blockly配置弹框相关状态
+const blocklyConfigVisible = ref(false)
+const blocklyConfigTitle = ref('配置Blockly数据')
+const blocklyConfigFormRef = ref() // Blockly配置表单Ref
+
+// Blockly配置相关响应式数据
+const blocklyStartPointX = ref<number>()
+const blocklyStartPointY = ref<number>()
+const blocklyEndPointX = ref<number>()
+const blocklyEndPointY = ref<number>()
+const blocklyTileX = ref<number>(5) // 横向数量,默认5
+const blocklyTileY = ref<number>(5) // 纵向数量,默认5
+
+// 选中的点
+const selectedBlocklyPoint = ref<BlocklyWalkablePoint | null>(null)
+
+// 可行走点接口定义
+interface BlocklyWalkablePoint {
+  x: number
+  y: number
+  walkable: boolean
+  type: string
+  must: boolean
+  img: string
+  endImg: string
+  tip: string
+  unfinishedTip: string
+  finishedTip: string
+}
+
+// 可行走点数组
+const blocklyWalkablePoints = ref<BlocklyWalkablePoint[]>([])
+
+const formData = ref({
+  id: undefined,
+  bcName: undefined,
+  bcContentType: undefined,
+  bcContent: undefined,
+
+  blocklyInfo: undefined,
+  blocklyUserImage: 'https://learn-ai.com.cn/admin-api/infra/file/29/get/20251107/user_1762504554550.png',
+  blocklyUserDirection: 0,
+  blocklyTileSize: undefined, // 保持兼容性
+  blocklyStartPoint: undefined,
+  blocklyBackground: undefined,
+  blocklyEndPoint: undefined,
+  blocklyWalkablePoints: undefined,
+
+  bcIsInspect: "false",
+  bcType: undefined,
+  bcLabel: undefined,
+  bcOrder: undefined,
+  bcStatus: "0",
+  tenantId: undefined,
+})
+
+// 添加上传进度相关的状态
+const uploadProgress = ref(0)
+const isUploading = ref(false)
+
+// 所有格子数据
+const blocklyGridCells = computed(() => {
+  const cells = []
+  for (let y = 1; y <= (blocklyTileY.value || 5); y++) {
+    for (let x = 1; x <= (blocklyTileX.value || 5); x++) {
+      cells.push({ x, y })
+    }
+  }
+  return cells
+})
+
+// 计算属性:检查是否已配置Blockly数据
+const hasBlocklyConfig = computed(() => {
+  return formData.value.blocklyUserImage &&
+    formData.value.blocklyBackground &&
+    blocklyStartPointX.value !== undefined &&
+    blocklyStartPointY.value !== undefined &&
+    blocklyEndPointX.value !== undefined &&
+    blocklyEndPointY.value !== undefined &&
+    blocklyTileX.value !== undefined &&
+    blocklyTileY.value !== undefined
+})
+
+const formRules = reactive({
+  bcType: [{ required: true, message: '课程类型不能为空', trigger: 'blur' }],
+  bcName: [{ required: true, message: '课程名称不能为空', trigger: 'blur' }],
+  bcLabel: [{ required: true, message: '课程标签不能为空', trigger: 'blur' }],
+  bcOrder: [{ required: true, message: '课程排序不能为空', trigger: 'blur' }],
+  // 当内容类型为blockly时,需要验证blockly配置
+  blocklyConfig: [{
+    validator: (rule: any, value: any, callback: any) => {
+      if (formData.value.bcContentType === 'blockly' && !hasBlocklyConfig.value) {
+        callback(new Error('请配置Blockly数据'))
+      } else {
+        callback()
+      }
+    },
+    trigger: 'blur'
+  }]
+})
+
+// Blockly配置表单验证规则
+const blocklyConfigRules = reactive({
+  blocklyUserImage: [{ required: true, message: '人物图标不能为空', trigger: 'blur' }],
+  blocklyBackground: [{ required: true, message: '地图背景图不能为空', trigger: 'blur' }],
+  blocklyUserDirection: [{ required: true, message: '人物朝向不能为空', trigger: 'blur' }],
+  // blocklyInfo: [{ required: true, message: 'Blockly信息不能为空', trigger: 'blur' }]
+})
+
+const formRef = ref() // 表单 Ref
+const bcTypeTree = ref() // 树形结构
+
+/** 获取指定坐标的点配置 */
+const getBlocklyPointByXY = (x: number, y: number): BlocklyWalkablePoint | undefined => {
+  return blocklyWalkablePoints.value.find(point => point.x === x && point.y === y)
+}
+
+/** 判断指定坐标是否可行走 */
+const isBlocklyWalkable = (x: number, y: number): boolean => {
+  const point = getBlocklyPointByXY(x, y)
+  return point ? point.walkable : false
+}
+
+/** 选择格子 */
+const selectBlocklyCell = (cell: { x: number, y: number }) => {
+  // 查找是否已有该点的配置
+  let point = getBlocklyPointByXY(cell.x, cell.y)
+
+  // 如果没有,创建新的配置
+  if (!point) {
+    point = {
+      x: cell.x,
+      y: cell.y,
+      walkable: false,
+      type: '',
+      must: false,
+      img: '',
+      endImg: '',
+      tip: '',
+      unfinishedTip: '',
+      finishedTip: ''
+    }
+    blocklyWalkablePoints.value.push(point)
+  }
+
+  selectedBlocklyPoint.value = point
+}
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      const blocklyData = await BlocklyApi.getBlockly(id)
+      formData.value = {
+        ...formData.value,
+        ...blocklyData,
+        bcContent: blocklyData.bcType === "image" ? blocklyData.bcContent?.split(',') : blocklyData.bcContent
+      }
+      // 确保 bcType 为正确的 id 类型
+      formData.value.bcType = blocklyData.bcType ? Number(blocklyData.bcType) : undefined
+
+      // 处理回显数据 - 解析坐标JSON
+      if (blocklyData.blocklyStartPoint) {
+        try {
+          const point = JSON.parse(blocklyData.blocklyStartPoint)
+          blocklyStartPointX.value = point.x
+          blocklyStartPointY.value = point.y
+        } catch (e) {
+          console.error('解析地图开始坐标失败:', e)
+        }
+      }
+
+      if (blocklyData.blocklyEndPoint) {
+        try {
+          const point = JSON.parse(blocklyData.blocklyEndPoint)
+          blocklyEndPointX.value = point.x
+          blocklyEndPointY.value = point.y
+        } catch (e) {
+          console.error('解析地图结束坐标失败:', e)
+        }
+      }
+
+      // 解析地图尺寸
+      if (blocklyData.blocklyTileSize) {
+        try {
+          const point = JSON.parse(blocklyData.blocklyTileSize)
+          blocklyTileX.value = point.x
+          blocklyTileY.value = point.y
+        } catch (e) {
+          console.error('解析地图尺寸失败:', e)
+        }
+      }
+
+      // 解析地图可行走点
+      if (blocklyData.blocklyWalkablePoints) {
+        try {
+          const points = JSON.parse(blocklyData.blocklyWalkablePoints)
+          blocklyWalkablePoints.value = points.map((p: any) => ({
+            x: p.x,
+            y: p.y,
+            walkable: true,
+            type: p.type || '',
+            must: p.must || false,
+            img: p.img || '',
+            endImg: p.endImg || '',
+            tip: p.tip || '',
+            unfinishedTip: p.unfinishedTip || '',
+            finishedTip: p.finishedTip || ''
+          }))
+        } catch (e) {
+          console.error('解析地图可行走坐标失败:', e)
+          blocklyWalkablePoints.value = []
+        }
+      }
+    } finally {
+      formLoading.value = false
+    }
+  }
+
+  await getbcTypeTree()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 打开Blockly配置弹框 */
+const openBlocklyConfigDialog = () => {
+  blocklyConfigVisible.value = true
+}
+
+/** 保存Blockly配置 */
+const saveBlocklyConfig = async () => {
+  // 坐标数据验证
+  if (blocklyStartPointX.value === undefined || blocklyStartPointY.value === undefined) {
+    message.error('地图开始坐标不能为空')
+    return
+  }
+
+  if (blocklyEndPointX.value === undefined || blocklyEndPointY.value === undefined) {
+    message.error('地图结束坐标不能为空')
+    return
+  }
+
+  if (blocklyTileX.value === undefined || blocklyTileY.value === undefined) {
+    message.error('地图方格尺寸不能为空')
+    return
+  }
+
+  // 校验Blockly配置表单
+  await blocklyConfigFormRef.value.validate()
+
+  // 设置坐标JSON
+  formData.value.blocklyTileSize = JSON.stringify({ x: blocklyTileX.value, y: blocklyTileY.value })
+  formData.value.blocklyStartPoint = JSON.stringify({ x: blocklyStartPointX.value, y: blocklyStartPointY.value })
+  formData.value.blocklyEndPoint = JSON.stringify({ x: blocklyEndPointX.value, y: blocklyEndPointY.value })
+
+  // 只保存可行走的点
+  const walkableOnlyPoints = blocklyWalkablePoints.value.filter(point => point.walkable)
+  formData.value.blocklyWalkablePoints = JSON.stringify(walkableOnlyPoints)
+
+  blocklyConfigVisible.value = false
+  message.success('Blockly配置已保存')
+}
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+
+  // 特殊校验:当内容类型为blockly时,需要验证blockly配置
+  if (formData.value.bcContentType === 'blockly' && !hasBlocklyConfig.value) {
+    message.error('请先配置Blockly数据')
+    return
+  }
+
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = { ...formData.value } as unknown as BlocklyVO
+    if (data.bcContentType === 'image') {
+      data.bcContent = data.bcContent?.join(',')
+    }
+    if (formType.value === 'create') {
+      await BlocklyApi.createBlockly(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await BlocklyApi.updateBlockly(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+// 添加处理上传进度的方法
+const handleUploadProgress = (progress: number) => {
+  uploadProgress.value = progress
+}
+
+const handleUploadStart = () => {
+  isUploading.value = true
+  uploadProgress.value = 0
+}
+
+const handleUploadComplete = () => {
+  isUploading.value = false
+  uploadProgress.value = 100
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    bcName: undefined,
+    bcContentType: undefined,
+    bcContent: undefined,
+
+    blocklyInfo: undefined,
+    blocklyUserImage: 'https://learn-ai.com.cn/admin-api/infra/file/29/get/20251107/user_1762504554550.png',
+    blocklyUserDirection: 0,
+    blocklyTileSize: undefined, // 保持兼容性
+    blocklyStartPoint: undefined,
+    blocklyBackground: undefined,
+    blocklyEndPoint: undefined,
+    blocklyWalkablePoints: undefined,
+
+    bcIsInspect: "false",
+    bcType: undefined,
+    bcLabel: undefined,
+    bcOrder: undefined,
+    bcStatus: "0",
+    tenantId: undefined,
+  }
+
+  // 重置Blockly配置相关数据
+  blocklyStartPointX.value = undefined
+  blocklyStartPointY.value = undefined
+  blocklyEndPointX.value = undefined
+  blocklyEndPointY.value = undefined
+  blocklyTileX.value = 5
+  blocklyTileY.value = 5
+  blocklyWalkablePoints.value = []
+  selectedBlocklyPoint.value = null
+
+  formRef.value?.resetFields()
+}
+/** 获得课程-类型树 */
+const getbcTypeTree = async () => {
+  bcTypeTree.value = []
+  const data = await BlocklyTypeApi.getBlocklyTypeSimpleList()
+  const root: Tree = { id: 0, ctType: '课程类型', children: [] }
+  root.children = handleTree(data, 'id', 'ctParentId')
+  bcTypeTree.value.push(root)
+}
+</script>
+
+<style>
+.demo-tabs > .el-tabs__content {
+  padding: 32px;
+  color: #6b778c;
+  font-size: 32px;
+  font-weight: 600;
+}
+.demo-tabs .custom-tabs-label .el-icon {
+  vertical-align: middle;
+}
+.demo-tabs .custom-tabs-label span {
+  vertical-align: middle;
+  margin-left: 4px;
+}
+.uploadProgress{
+  width: 50%;
+}
+
+/* 与MapGameForm.vue保持一致的样式 */
+.coordinate-group {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.map-grid-container {
+  overflow: auto;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  padding: 10px;
+  background-color: #f9f9f9;
+}
+
+.map-grid {
+  display: grid;
+  gap: 2px;
+  width: 100%;
+}
+
+.grid-cell {
+  aspect-ratio: 1;
+  border: 1px solid #dcdfe6;
+  background-color: rgba(255, 255, 255, 0.9);
+  cursor: pointer;
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: all 0.3s;
+  min-height: 40px;
+}
+
+.grid-cell:hover {
+  border-color: #409eff;
+}
+
+.grid-cell.selected {
+  border: 3px solid #409eff;
+  background-color: rgba(236, 245, 255, 0.8);
+  box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.3);
+  transform: scale(1.05);
+  z-index: 10;
+}
+
+.grid-cell.selected.walkable {
+  border: 3px solid #409eff;
+  background-color: rgba(230, 247, 233, 0.8);
+  box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.3);
+}
+
+.grid-cell.selected.has-icon {
+  border: 3px solid #409eff;
+  background-color: rgba(255, 243, 230, 0.8);
+  box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.3);
+}
+
+.grid-cell.walkable {
+  background-color: rgba(240, 249, 235, 0.8);
+  border: 3px solid rgb(103, 194, 58);
+}
+
+.grid-cell.has-icon {
+  background-color: rgba(253, 246, 236, 0.8);
+}
+
+.cell-coordinate {
+  position: absolute;
+  top: 2px;
+  left: 4px;
+  font-size: 12px;
+  color: #909399;
+}
+
+.cell-icon {
+  max-width: 80%;
+  max-height: 80%;
+}
+
+.cell-icon img {
+  max-width: 100%;
+  max-height: 100%;
+  object-fit: contain;
+}
+
+/* 右侧配置区域样式 - 修复高度显示问题 */
+.point-config-container {
+  overflow-y: auto;
+  padding: 10px;
+}
+
+.config-form {
+  width: 100%;
+}
+
+.config-card {
+  margin-bottom: 0;
+  border-radius: 8px;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  font-weight: bold;
+  color: #303133;
+}
+
+/* 配置项行布局 */
+.config-row {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20px;
+  margin-bottom: 20px;
+  align-items: flex-start;
+}
+
+.config-item {
+  flex: 1;
+  min-width: 200px;
+  margin-bottom: 0 !important;
+}
+
+.form-item-full {
+  margin-bottom: 20px;
+}
+
+/* 图标上传包装器 */
+.icon-upload-wrapper {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.upload-tip {
+  font-size: 12px;
+  color: #67c23a;
+  margin-top: 4px;
+}
+
+.no-selection {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 300px;
+  background-color: #f5f7fa;
+  border-radius: 8px;
+  border: 1px dashed #dcdfe6;
+}
+
+/* 优化滚动条样式 */
+.point-config-container::-webkit-scrollbar {
+  width: 8px;
+  height: 8px;
+}
+
+.point-config-container::-webkit-scrollbar-thumb {
+  border-radius: 4px;
+  background: #c0c4cc;
+}
+
+.point-config-container::-webkit-scrollbar-track {
+  background: #f0f0f0;
+  border-radius: 4px;
+}
+
+/* 调整标签宽度以适应行布局 */
+:deep(.el-form-item__label) {
+  white-space: nowrap;
+}
+
+/* 响应式调整 */
+@media screen and (max-width: 768px) {
+  .config-row {
+    flex-direction: column;
+  }
+
+  .config-item {
+    width: 100%;
+  }
+}
+</style>

+ 329 - 0
src/views/blockly/blockly/index.vue

@@ -0,0 +1,329 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="90px"
+    >
+      <el-form-item label="大纲课程" prop="bcType">
+        <el-tree-select
+          v-model="queryParams.bcType"
+          :data="bcTypeTree"
+          :props="{...defaultProps,
+            label: (node) => `${node.ctTypeNode === undefined ? node.ctType : node.ctTypeSort + '、' + node.ctType}`,
+            }"
+          placeholder="请选择课程类型"
+          :default-expand-all="true"
+          class="!w-240px"
+          @change="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="内容类型" prop="bcContentType">
+        <el-select
+          v-model="queryParams.bcContentType"
+          placeholder="请选择内容类型"
+          clearable
+          class="!w-240px"
+          @change="handleQuery"
+        >
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.BLOCKLY_COURSE_COUTNET_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="课程名称" prop="bcName">
+        <el-input
+          v-model="queryParams.bcName"
+          placeholder="请输入课程名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="是否有检查" prop="bcIsInspect">
+        <el-select
+          v-model="queryParams.bcIsInspect"
+          placeholder="请选择是否有检查"
+          clearable
+          class="!w-240px"
+          @change="handleQuery"
+        >
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="课程标签" prop="bcLabel">
+        <el-select
+          v-model="queryParams.bcLabel"
+          placeholder="请选择课程标签"
+          clearable
+          class="!w-240px"
+          @change="handleQuery"
+        >
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.BLOCKLY_COURSE_LABEL)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="课程状态" prop="bcStatus">
+        <el-select
+          v-model="queryParams.bcStatus"
+          placeholder="请选择课程状态"
+          clearable
+          class="!w-240px"
+          @change="handleQuery"
+        >
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['blockly:blockly:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['blockly:blockly:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="课程类型" align="center" prop="bcTypeName" />
+      <el-table-column label="课程名称" align="center" prop="bcName" />
+      <el-table-column label="内容类型" align="center" prop="bcContentType" >
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.BLOCKLY_COURSE_COUTNET_TYPE" :value="scope.row.bcContentType" />
+        </template>
+      </el-table-column>
+      <el-table-column label="课程内容" align="center" prop="bcContent" width="120px">
+          【预览查看】
+      </el-table-column>
+      <el-table-column label="是否有检查" align="center" prop="bcIsInspect">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.bcIsInspect" />
+        </template>
+      </el-table-column>
+      <el-table-column label="课程标签" align="center" prop="bcLabel">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.BLOCKLY_COURSE_LABEL" :value="scope.row.bcLabel" />
+        </template>
+      </el-table-column>
+      <el-table-column label="课程排序" align="center" prop="bcOrder"/>
+      <el-table-column label="课程状态" align="center" prop="bcStatus">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.bcStatus" />
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" min-width="180px"> <!-- 调整列宽适配新按钮 -->
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            v-if="scope.row.tenantId == getTenantId()"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['blockly:blockly:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="success"
+            @click="handleConfig(scope.row.id,scope.row.bcName,scope.row.tenantId)"
+          >
+            配置
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            v-if="scope.row.tenantId == getTenantId()"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['blockly:blockly:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <BlocklyForm ref="formRef" @success="getList" />
+
+</template>
+
+<script setup lang="ts">
+import { getStrDictOptions, DICT_TYPE } from '@/utils/dict'
+import download from '@/utils/download'
+import { BlocklyApi, BlocklyVO } from '@/api/blockly/blockly'
+import BlocklyForm from './BlocklyForm.vue'
+import { useRouter } from 'vue-router'
+import { defaultProps, handleTree } from '@/utils/tree'
+import { BlocklyTypeApi } from '@/api/blockly/blocklyType'
+import { getTenantId } from '@/utils/auth'
+import { ElButton } from 'element-plus'
+
+/** 课程 列表 */
+defineOptions({ name: 'Blockly' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<BlocklyVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  bcName: undefined,
+  bcContentType: undefined,
+  bcContent: undefined,
+
+  blocklyInfo: undefined,
+  blocklyUserImage: undefined,
+  blocklyUserDirection: undefined,
+  blocklyStartPoint: undefined,
+  blocklyBackground: undefined,
+  blocklyEndPoint: undefined,
+  blocklyWalkablePoints: undefined,
+
+  bcIsInspect: undefined,
+  bcType: undefined,
+  bcTypeName: undefined,
+  bcLabel: undefined,
+  bcOrder: undefined,
+  bcStatus: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+const bcTypeTree = ref() // 树形结构
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await BlocklyApi.getBlocklyPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = async () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  getbcTypeTree()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await BlocklyApi.deleteBlockly(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 配置按钮操作 */
+const router = useRouter()
+const handleConfig = (id: number,bcName: string,tenantId: number) => {
+  // router.push({
+  //   name: 'blocklyConfig',
+  //   query: { bcId: id , bcName: bcName,tenantId: tenantId}
+  // })
+
+  router.push({
+    name: 'courseConfig',
+    query: { courseId: id , courseName: bcName,tenantId: tenantId}
+  })
+}
+
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await BlocklyApi.exportBlockly(queryParams)
+    download.excel(data, '课程.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 获得课程-类型树 */
+const getbcTypeTree = async () => {
+  bcTypeTree.value = []
+  const data = await BlocklyTypeApi.getBlocklyTypeSimpleList()
+
+  const root: Tree = { id: 0, ctType: '课程类型', children: [] }
+  root.children = handleTree(data, 'id', 'ctParentId')
+  bcTypeTree.value.push(root)
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+  getbcTypeTree()
+})
+</script>

+ 319 - 0
src/views/blockly/blocklyConfig/BlocklyConfigForm.vue

@@ -0,0 +1,319 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible" width="1000px">
+    <!-- 顶部:课程配置表单 -->
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="课程名称" prop="ccCourseId">
+        <el-input v-model="formData.courseName" placeholder="请输入课程名称" :disabled="true" />
+        <el-input v-model="formData.ccCourseId" type="hidden" />
+        <!-- 隐藏真实课程id -->
+      </el-form-item>
+
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="课程暂停时长" prop="ccTime" required>
+            <el-input-number v-model="formData.ccTime" :min="1" :step="1" step-strictly>
+              <template #suffix><span>秒</span></template>
+            </el-input-number>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="是否显示答案" prop="ccAnswerJudge" >
+            <el-radio-group v-model="formData.ccAnswerJudge">
+              <el-radio
+                v-for="dict in getStrDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+                :key="dict.value"
+                :label="dict.value"
+              >{{ dict.label }}
+              </el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <br/>
+          <el-form-item label="问题呈现类型" prop="ccQuestSource" required>
+            <el-segmented v-model="formData.ccQuestSource" :options="getStrDictOptions(DICT_TYPE.COURSE_QUEST_SHOW_TYPE)" />
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="24" v-if="formData.ccQuestSource === '1'" required>
+          <el-form-item label="试题内容" prop="ccQuestContent">
+            <Editor v-model="formData.ccQuestContent" height="150px" />
+          </el-form-item>
+
+          <el-form-item label="试题选项" prop="ccQuestOption">
+            <el-input v-model="formData.ccQuestOption" placeholder="请输入试题选项( 示例:多个选项用英文,分割 )" />
+          </el-form-item>
+
+          <el-form-item label="答  案" prop="ccAnswer">
+            <el-input v-model="formData.ccAnswer" placeholder="请输入答案" />
+          </el-form-item>
+
+          <el-form-item label="Ai问题提示" prop="ccAiQuestTip">
+            <el-input v-model="formData.ccAiQuestTip" placeholder="请输入Ai问题提示(多条请使用英文,豆包分割)" />
+          </el-form-item>
+
+          <el-form-item label="Ai答案" prop="ccAiAnswer">
+            <el-input v-model="formData.ccAiAnswer" placeholder="请输入Ai答案" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <div v-if="formData.ccQuestSource === '2'">
+        <ContentWrap>
+          <!-- 搜索工作栏 -->
+          <el-form
+            class="-mb-15px"
+            :model="questQueryParams"
+            ref="queryFormRef"
+            :inline="true"
+            label-width="68px"
+          >
+            <el-form-item label="试题类型" prop="cqQuestType">
+              <el-select
+                v-model="questQueryParams.cqQuestType"
+                placeholder="请选择试题类型"
+                clearable
+                class="!w-240px"
+              >
+                <el-option
+                  v-for="dict in getStrDictOptions(DICT_TYPE.COURSE_QUESTION_TYPE)"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="试题内容" prop="cqQuestion">
+              <el-input
+                v-model="questQueryParams.cqQuestion"
+                placeholder="请输入试题内容"
+                clearable
+                @keyup.enter="loadQuestionList"
+                class="!w-240px"
+              />
+            </el-form-item>
+            <el-form-item>
+              <el-button @click="loadQuestionList"
+              ><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button
+              >
+              <el-button @click="resetQuestionQuery"
+              ><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button
+              >
+            </el-form-item>
+          </el-form>
+        </ContentWrap>
+
+        <!-- 列表 -->
+        <ContentWrap>
+          <el-table
+            v-loading="questionLoading"
+            :data="questionList"
+            :stripe="true"
+            :show-overflow-tooltip="true"
+          >
+            <el-table-column label="选择" width="80">
+              <template #default="scope">
+                <el-radio
+                  v-model="formData.ccQuestId"
+                  :label="scope.row.id"
+                  @change="handleQuestionSelect(scope.row)"
+                />
+              </template>
+            </el-table-column>
+            <el-table-column label="试题内容" align="center" prop="cqQuestion" >
+              <template #default="scope">
+                <div v-html="scope.row.cqQuestion"></div>
+              </template>
+            </el-table-column>
+            <el-table-column label="试题解析" align="center" prop="cqQuestAnalysis" >
+              <template #default="scope">
+                <div v-html="scope.row.cqQuestAnalysis"></div>
+              </template>
+            </el-table-column>
+            <el-table-column label="试题类型" align="center" prop="cqQuestType" >
+              <template #default="scope">
+                <dict-tag :type="DICT_TYPE.COURSE_QUESTION_TYPE" :value="scope.row.cqQuestType" />
+              </template>
+            </el-table-column>
+          </el-table>
+          <!-- 分页 -->
+          <Pagination
+            :total="questionTotal"
+            v-model:page="questQueryParams.pageNo"
+            v-model:limit="questQueryParams.pageSize"
+            @pagination="loadQuestionList"
+          />
+        </ContentWrap>
+      </div>
+    </el-form>
+
+    <!-- 底部按钮 -->
+    <template #footer>
+<!--      <el-button @click="submitForm" type="primary" :disabled="formLoading">同步内容</el-button>-->
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script setup lang="ts">
+import { getStrDictOptions, DICT_TYPE } from '@/utils/dict'
+import { CourseQuestionApi} from '@/api/bjdx/coursequestion' // 新增试题API引入
+
+/** 合并后的课程配置与试题选择表单 */
+defineOptions({ name: 'CourseConfigWithQuestionForm' })
+
+const { t } = useI18n()
+const message = useMessage()
+
+// 原课程配置相关状态
+const dialogVisible = ref(false)
+const dialogTitle = ref('')
+const formLoading = ref(false)
+const formType = ref('')
+const formData = ref({
+  id: undefined,
+  ccCourseId: undefined, // 课程id(隐藏字段)
+  courseName: undefined, // 课程名称(显示用)
+  ccTime: undefined, // 暂停时长(必填)
+  ccAnswerJudge: undefined, // 是否显示答案(必填)
+  ccQuestId: undefined, // 选择的试题id
+  ccQuestSource: '0', // 问题呈现类型
+  ccQuestContent: undefined,  // 问题内容
+  ccQuestOption: undefined,  // 问题选项
+  ccAiQuestTip: undefined,  // AI问题提示
+  ccAiAnswer: undefined,  // AI答案
+  ccAnswer: undefined,  // 答案
+  tenantId: undefined,
+})
+const formRules = reactive({
+  ccTime: [{ required: true, message: t('common.required'), trigger: 'blur' }],
+  ccQuestContent: [{ required: true, message: t('common.required'), trigger: 'change' }]
+})
+const formRef = ref()
+
+// 新增:试题列表相关状态
+const questionLoading = ref(false)
+const questionList = ref<any[]>([])
+const queryParams = reactive({
+  cqQuestType: undefined,
+  cqQuestion: undefined
+})
+const selectedQuestIds = ref<number[]>([]) // 记录选中的试题id
+
+/** 打开弹窗(新增参数:courseId, courseName) */
+const open = async (type: string, id?: number, courseId?: number, courseName?: string) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await CourseConfigApi.getCourseConfig(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+
+  // 初始化课程信息
+  formData.value.ccCourseId = courseId
+  formData.value.courseName = courseName
+
+  // 加载试题列表
+  await loadQuestionList()
+}
+
+/** 新增试题分页相关状态 */
+const questionTotal = ref(0) // 总条数
+
+const questQueryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  cqQuestion: undefined,
+  cqQuestAnalysis: undefined,
+  cqQuestType: undefined,
+  cqQuestAnswerId: undefined
+})
+
+/** 加载试题列表(修改分页参数传递) */
+const loadQuestionList = async () => {
+  questionLoading.value = true
+  try {
+    const res = await CourseQuestionApi.getCourseQuestionPage(questQueryParams)
+    questionList.value = res.list
+    questionTotal.value = res.total // 更新总条数
+  } finally {
+    questionLoading.value = false
+  }
+}
+
+/** 重置试题筛选 */
+const resetQuestionQuery = () => {
+  questQueryParams.cqQuestType = undefined
+  questQueryParams.cqQuestion = undefined
+  loadQuestionList()
+}
+
+/** 处理试题选择 */
+const handleQuestionSelect = (row: any) => {
+  formData.value.ccQuestId = row.id // 同步到表单数据
+}
+
+
+/** 提交表单(修改:包含ccQuestId) */
+const emit = defineEmits(['success'])
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as CourseConfigVO
+    if (formType.value === 'create') {
+      await CourseConfigApi.createCourseConfig(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await CourseConfigApi.updateCourseConfig(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单(新增:重置试题相关状态) */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    ccCourseId: undefined,
+    courseName: undefined,
+    ccTime: undefined,
+    ccAnswerJudge: undefined,
+    ccQuestId: undefined, // 选择的试题id
+    ccQuestSource: '0', // 问题呈现类型
+    ccQuestContent: undefined,  // 问题内容
+    ccQuestOption: undefined,  // 问题选项
+    ccAiQuestTip: undefined,  // AI问题提示
+    ccAiAnswer: undefined,  // AI答案
+    ccAnswer: undefined,  // 答案,
+    tenantId: undefined,
+  }
+  formRef.value?.resetFields()
+  selectedQuestIds.value = []
+  questionList.value = []
+}
+
+defineExpose({ open })
+</script>

+ 240 - 0
src/views/blockly/blocklyConfig/index.vue

@@ -0,0 +1,240 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="100px"
+    >
+      <el-form-item label="课程名称" prop="ccCourseId">
+        <el-input
+          v-model="queryParams.courseName"
+          placeholder="请输入课程id"
+          clearable
+          disabled
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="试题名称" prop="ccQuest">
+        <el-input
+          v-model="queryParams.questContent"
+          placeholder="请输入试题名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="课程暂停时长" prop="ccTime">
+        <el-input
+          v-model="queryParams.ccTime"
+          placeholder="请输入课程暂停时长"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="是否显示答案" prop="ccAnswerJudge">
+        <el-select
+          v-model="queryParams.ccAnswerJudge"
+          placeholder="请选择是否显示答案"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          v-if="tenantId == getTenantId()"
+          @click="openForm('create', null, queryParams.ccCourseId, queryParams.courseName)"
+          v-hasPermi="['bjdx:course-config:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['bjdx:course-config:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+<!--      <el-table-column label="课程配置id" align="center" prop="id" />-->
+      <el-table-column label="课程名称" align="center" prop="courseName" />
+      <el-table-column label="课程暂停时长" align="center" prop="ccTime" />
+      <el-table-column label="暂停类型" align="center" prop="ccQuestSource" >
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COURSE_QUEST_SHOW_TYPE" :value="scope.row.ccQuestSource" />
+        </template>
+      </el-table-column>
+      <el-table-column label="试题内容" align="center" prop="ccQuestContent">
+        <template #default="scope">
+          <div v-html="scope.row.ccQuestContent"></div>
+        </template>
+      </el-table-column>
+      <el-table-column label="试题选项" align="center" prop="ccQuestOption" />
+      <el-table-column label="试题答案" align="center" prop="ccAnswer" />
+      <el-table-column label="Ai答案" align="center" prop="ccAiAnswer" />
+      <el-table-column label="是否显示答案" align="center" prop="ccAnswerJudge">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.ccAnswerJudge" />
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" min-width="120px">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            v-if="scope.row.tenantId == getTenantId()"
+            @click="openForm('update', scope.row.id, queryParams.ccCourseId, queryParams.courseName)"
+            v-hasPermi="['bjdx:course-config:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            v-if="scope.row.tenantId == getTenantId()"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['bjdx:course-config:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <BlocklyConfigForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getStrDictOptions, DICT_TYPE } from '@/utils/dict'
+import download from '@/utils/download'
+import { useRoute } from 'vue-router'
+import { BlocklyConfigApi, CourseConfigVO } from '@/api/blockly/blocklyConfig'
+import BlocklyConfigForm from './BlocklyConfigForm.vue'
+import { getTenantId } from '@/utils/auth'
+import { ElButton } from 'element-plus'
+
+/** 课程配置 列表 */
+defineOptions({ name: 'BlocklyConfig' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<CourseConfigVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  ccCourseId: undefined,
+  courseName: undefined,
+  ccTime: undefined,
+  ccQuestId: undefined,
+  questContent: undefined,
+  ccAnswerJudge: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+const route = useRoute()
+const tenantId = ref()
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    queryParams.ccCourseId = route.query.bcId
+    queryParams.courseName = route.query.bcName
+    tenantId.value = route.query.tenantId
+
+    console.log(queryParams)
+    const data = await BlocklyConfigApi.getCourseConfigPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number, courseId?: number, courseName?: string) => {
+  console.log(type, id, courseId, courseName)
+  formRef.value.open(type, id, courseId, courseName)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await BlocklyConfigApi.deleteCourseConfig(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await BlocklyConfigApi.exportCourseConfig(queryParams)
+    download.excel(data, '课程配置.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 157 - 0
src/views/blockly/blocklyType/BlocklyTypeForm.vue

@@ -0,0 +1,157 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="父级类型" prop="ctParentId">
+        <el-tree-select
+          v-model="formData.ctParentId"
+          :data="blocklyTypeTree"
+          :props="{...defaultProps,
+            label: (node) => `${node.ctType}${node.ctTypeNode === '0' ? '(主题)' : '(类型)'}`,
+            disabled: (node) => node.id !== 0 && node.ctTypeNode !== '0'
+            }"
+          :default-expanded-keys="[0]"
+          check-strictly
+          default-expand-all
+          placeholder="请选择课程父级类型"
+        />
+      </el-form-item>
+      <el-form-item label="课程类型节点" prop="ctTypeNode">
+        <el-segmented v-model="formData.ctTypeNode" :options="[
+          { label: '主题', value: '0' },
+          { label: '类型', value: '1' },
+        ]" />
+      </el-form-item>
+      <el-form-item label="课程类型名称" prop="ctType">
+        <el-input v-model="formData.ctType" placeholder="请输入课程类型名称" />
+      </el-form-item>
+      <el-form-item label="课程封面" prop="ctTypeImage">
+        <UploadImg v-model="formData.ctTypeImage" />
+      </el-form-item>
+      <el-form-item label="课程类型描述" prop="ctTypeDescribe">
+        <el-input
+          type="textarea"
+          v-model="formData.ctTypeDescribe"
+          placeholder="请输入课程类型描述"
+        />
+      </el-form-item>
+      <el-form-item label="课程类型排序" prop="ctTypeSort">
+        <el-input-number v-model="formData.ctTypeSort" :step="1" step-strictly placeholder="请输入课程类型排序"
+                 class="!w-100%"/>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { BlocklyTypeApi, BlocklyTypeVO } from '@/api/blockly/blocklyType'
+import { defaultProps, handleTree } from '@/utils/tree'
+
+/** 课程-类型 表单 */
+defineOptions({ name: 'BlocklyTypeForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  ctType: undefined,
+  ctTypeImage: undefined,
+  ctTypeNode: 0,
+  ctTypeSort: undefined,
+  ctParentId: 0,
+  ctTypeDescribe: undefined,
+  tenantId: undefined,
+})
+const formRules = reactive({
+  ctParentId: [{ required: true, message: '父级节点不能为空', trigger: 'blur' }],
+  ctTypeNode: [{ required: true, message: '节点类型不能为空', trigger: 'blur' }],
+  ctType: [{ required: true, message: '类型名称不能为空', trigger: 'blur' }],
+  ctTypeSort: [{ required: true, message: '排序不能为空', trigger: 'blur' }],
+})
+const formRef = ref() // 表单 Ref
+const blocklyTypeTree = ref() // 树形结构
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number, parentId?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  if (parentId) {
+    formData.value.ctParentId = parentId
+  }
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await BlocklyTypeApi.getBlocklyType(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+  await getBlocklyTypeTree()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as BlocklyTypeVO
+    if (formType.value === 'create') {
+      await BlocklyTypeApi.createBlocklyType(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await BlocklyTypeApi.updateBlocklyType(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    ctType: undefined,
+    ctTypeNode: 0,
+    ctTypeSort: undefined,
+    ctParentId: 0,
+    ctTypeDescribe: undefined,
+    tenantId: undefined,
+  }
+  formRef.value?.resetFields()
+}
+
+/** 获得课程-类型树 */
+const getBlocklyTypeTree = async () => {
+  blocklyTypeTree.value = []
+  const data = await BlocklyTypeApi.getBlocklyTypeSimpleList()
+
+  const filteredData = data.filter(item => item.ctTypeNode === '0')
+  const root: Tree = { id: 0, ctType: '课程类型', children: [] }
+  root.children = handleTree(filteredData, 'id', 'ctParentId')
+  blocklyTypeTree.value.push(root)
+}
+</script>

+ 234 - 0
src/views/blockly/blocklyType/index.vue

@@ -0,0 +1,234 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="100px"
+    >
+      <el-form-item label="课程类型名称" prop="ctType">
+        <el-input
+          v-model="queryParams.ctType"
+          placeholder="请输入课程类型名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="课程类型节点" prop="ctTypeNode">
+        <el-select
+          v-model="queryParams.ctTypeNode"
+          placeholder="请选择类型节点类型"
+          clearable
+          class="!w-240px"
+          @change="handleQuery"
+        >
+          <el-option label="主题" value="0" />
+          <el-option label="类型" value="1" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="课程类型描述" prop="ctTypeDescribe">
+        <el-input
+          v-model="queryParams.ctTypeDescribe"
+          placeholder="请输入课程类型描述"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['blockly:blockly-type:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['blockly:blockly-type:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+        <el-button type="danger" plain @click="toggleExpandAll">
+          <Icon icon="ep:sort" class="mr-5px" /> 展开/折叠
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table
+      v-loading="loading"
+      :data="list"
+      :stripe="true"
+      :show-overflow-tooltip="true"
+      row-key="id"
+      :default-expand-all="isExpandAll"
+      v-if="refreshTable"
+    >
+      <el-table-column label="课程类型名称" align="center" prop="ctType" />
+      <el-table-column label="节点类型" align="center" prop="ctTypeNode" >
+        <template #default="scope">
+          <el-tag type="info" v-if="scope.row.ctTypeNode === '0'">主题</el-tag>
+          <el-tag type="warning" v-else>类型</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="封面" align="center" prop="ctTypeImage" >
+        <template #default="scope">
+          <img :src="scope.row.ctTypeImage" class="w-150px"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="课程类型描述" align="center" prop="ctTypeDescribe" />
+      <el-table-column label="排序" align="center" prop="ctTypeSort" />
+      <el-table-column label="操作" align="center" min-width="120px">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            v-if="scope.row.tenantId == getTenantId()"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['blockly:blockly-type:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="primary"
+            v-if="scope.row.tenantId == getTenantId()"
+            @click="openForm('create', undefined, scope.row.id)"
+            v-hasPermi="['blockly:blockly-type:create']"
+          >
+            新增
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            v-if="scope.row.tenantId == getTenantId()"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['blockly:blockly-type:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <BlocklyTypeForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { handleTree } from '@/utils/tree'
+import download from '@/utils/download'
+import { BlocklyTypeApi, BlocklyTypeVO } from '@/api/blockly/blocklyType'
+import BlocklyTypeForm from './BlocklyTypeForm.vue'
+import { ElButton } from 'element-plus'
+import { getTenantId } from '@/utils/auth'
+
+/** 课程-类型 列表 */
+defineOptions({ name: 'BlocklyType' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<BlocklyTypeVO[]>([]) // 列表的数据
+const queryParams = reactive({
+  ctType: undefined,
+  ctTypeNode: undefined,
+  ctParentId: undefined,
+  ctTypeDescribe: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await BlocklyTypeApi.getBlocklyTypeList(queryParams)
+    list.value = handleTree(data, 'id', 'ctParentId')
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number, parentId?: number) => {
+  formRef.value.open(type, id, parentId)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await BlocklyTypeApi.deleteBlocklyType(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await BlocklyTypeApi.exportBlocklyType(queryParams)
+    download.excel(data, '课程-类型.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 展开/折叠操作 */
+const isExpandAll = ref(true) // 是否展开,默认全部展开
+const refreshTable = ref(true) // 重新渲染表格状态
+const toggleExpandAll = async () => {
+  refreshTable.value = false
+  isExpandAll.value = !isExpandAll.value
+  await nextTick()
+  refreshTable.value = true
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>