CourseForm.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. <template>
  2. <Dialog :title="dialogTitle" v-model="dialogVisible" width="1000px">
  3. <el-form
  4. ref="formRef"
  5. :model="formData"
  6. :rules="formRules"
  7. label-width="120px"
  8. v-loading="formLoading"
  9. >
  10. <el-form-item label="课程类型" prop="courseType">
  11. <el-tree-select
  12. v-model="formData.courseType"
  13. :data="courseTypeTree"
  14. :props="{
  15. ...defaultProps,
  16. label: (node) => `${node.ctType}${node.ctTypeNode === '0' ? '(年级)' : node.ctTypeNode === '1' ? '(ai通识课)' : '(ai实操课)'}`,
  17. // 根据 ctTypeNode 字段判断是否禁用选项
  18. disabled: (node) => node.ctTypeNode === '0',
  19. // 明确指定 value 字段为 id
  20. value: 'id'
  21. }"
  22. placeholder="请选择课程类型"
  23. :default-expand-all="true"
  24. />
  25. </el-form-item>
  26. <el-form-item label="课程名称" prop="courseName">
  27. <el-input v-model="formData.courseName" placeholder="请输入课程名称" />
  28. </el-form-item>
  29. <el-form-item label="内容类型" prop="courseName">
  30. <el-segmented v-model="formData.courseContentType" :options="getStrDictOptions(DICT_TYPE.COURSE_COUTNET_TYPE)" />
  31. </el-form-item>
  32. <el-form-item v-if="formData.courseContentType === 'all'" label="课程内容" prop="courseContent">
  33. <Editor v-model="formData.courseContent" height="150px" />
  34. </el-form-item>
  35. <el-form-item v-if="formData.courseContentType === 'text'" label="纯文本" prop="coursePath">
  36. <Editor v-model="formData.courseContent" height="150px" />
  37. </el-form-item>
  38. <el-form-item v-if="formData.courseContentType === 'image'" label="课程图片集" prop="coursePath">
  39. <UploadImgs v-model="formData.courseImagePath" />
  40. </el-form-item>
  41. <el-form-item v-if="formData.courseContentType === 'music'" label="课程音频" prop="coursePath">
  42. <UploadMusic v-model="formData.courseMusicPath" />
  43. </el-form-item>
  44. <el-form-item v-if="formData.courseContentType === 'video'" label="课程视频" prop="coursePath">
  45. <UploadVideo v-model="formData.courseVideoPath" />
  46. </el-form-item>
  47. <el-form-item v-if="formData.courseContentType === 'ppt'" label="课程PPT" prop="courseFilePath">
  48. <UploadFile v-model="formData.courseFilePath" :fileType="['ppt','pptx']" :fileSize="50"/>
  49. </el-form-item>
  50. <!-- <el-form-item label="课程大小" prop="courseSize">-->
  51. <!-- <el-input-number v-model="formData.courseSize" :min="1" :step="1" step-strictly>-->
  52. <!-- <template #suffix><span>MB</span></template>-->
  53. <!-- </el-input-number>-->
  54. <!-- </el-form-item>-->
  55. <template v-if="formData.courseContentType === 'ailab'">
  56. <el-form-item >
  57. <br/><h2>AI实验室</h2><br/>
  58. </el-form-item>
  59. </template>
  60. <template v-else-if="formData.courseContentType === 'quest'">
  61. <el-form-item >
  62. <br/><h2>问题</h2><br/>
  63. </el-form-item>
  64. <div style="width: 80%;margin: 0px auto">
  65. <!-- 基础信息 -->
  66. <div class="mb-4">
  67. <label class="block text-gray-700 font-medium mb-1">题目标题</label>
  68. <input
  69. v-model="formData.courseBlocklyJson.title"
  70. class="w-full border border-gray-300 rounded p-2"
  71. placeholder="输入题目标题"
  72. />
  73. </div>
  74. <div class="mb-4">
  75. <label class="block text-gray-700 font-medium mb-1">题目描述</label>
  76. <textarea
  77. v-model="formData.courseBlocklyJson.description"
  78. class="w-full border border-gray-300 rounded p-2"
  79. rows="3"
  80. placeholder="输入题目描述"
  81. ></textarea>
  82. </div>
  83. <!-- 迷宫地图配置 -->
  84. <div class="mb-4">
  85. <label class="block text-gray-700 font-medium mb-1">迷宫尺寸</label>
  86. <div class="flex gap-2">
  87. <input
  88. v-model.number="formData.courseBlocklyJson.mazeConfig.width"
  89. class="w-1/2 border border-gray-300 rounded p-2"
  90. type="number"
  91. min="5"
  92. max="20"
  93. placeholder="宽度"
  94. />
  95. <input
  96. v-model.number="formData.courseBlocklyJson.mazeConfig.height"
  97. class="w-1/2 border border-gray-300 rounded p-2"
  98. type="number"
  99. min="5"
  100. max="20"
  101. placeholder="高度"
  102. />
  103. </div>
  104. </div>
  105. <div class="mb-4">
  106. <label class="block text-gray-700 font-medium mb-1">墙坐标(JSON 数组,如 [[0,1]])</label>
  107. <textarea
  108. v-model="formData.courseBlocklyJson.mazeConfig.walls"
  109. class="w-full border border-gray-300 rounded p-2"
  110. rows="3"
  111. placeholder="输入墙坐标"
  112. ></textarea>
  113. </div>
  114. <div class="mb-4">
  115. <label class="block text-gray-700 font-medium mb-1">起点坐标</label>
  116. <div class="flex gap-2">
  117. <input
  118. v-model.number="formData.courseBlocklyJson.mazeConfig.start[0]"
  119. class="w-1/2 border border-gray-300 rounded p-2"
  120. type="number"
  121. placeholder="X"
  122. />
  123. <input
  124. v-model.number="formData.courseBlocklyJson.mazeConfig.start[1]"
  125. class="w-1/2 border border-gray-300 rounded p-2"
  126. type="number"
  127. placeholder="Y"
  128. />
  129. </div>
  130. </div>
  131. <div class="mb-4">
  132. <label class="block text-gray-700 font-medium mb-1">终点坐标</label>
  133. <div class="flex gap-2">
  134. <input
  135. v-model.number="formData.courseBlocklyJson.mazeConfig.end[0]"
  136. class="w-1/2 border border-gray-300 rounded p-2"
  137. type="number"
  138. placeholder="X"
  139. />
  140. <input
  141. v-model.number="formData.courseBlocklyJson.mazeConfig.end[1]"
  142. class="w-1/2 border border-gray-300 rounded p-2"
  143. type="number"
  144. placeholder="Y"
  145. />
  146. </div>
  147. </div>
  148. <!-- 初始积木配置 -->
  149. <div class="mb-4">
  150. <label class="block text-gray-700 font-medium mb-1">初始积木(JSON 数组,如 {"type":"move_forward","count":3} )</label>
  151. <textarea
  152. v-model="formData.courseBlocklyJson.initialBlocks"
  153. class="w-full border border-gray-300 rounded p-2"
  154. rows="3"
  155. placeholder="输入初始积木配置"
  156. ></textarea>
  157. </div>
  158. <!-- 答案配置 -->
  159. <div class="mb-4">
  160. <label class="block text-gray-700 font-medium mb-1">参考解法代码</label>
  161. <textarea
  162. v-model="formData.courseBlocklyJson.solutionCode"
  163. class="w-full border border-gray-300 rounded p-2"
  164. rows="3"
  165. placeholder="输入参考代码(如 moveForward(3); turnRight(); )"
  166. ></textarea>
  167. </div>
  168. </div>
  169. </template>
  170. <template v-else>
  171. <el-form-item label="课程是否有检查" prop="courseIsInspect">
  172. <el-radio-group v-model="formData.courseIsInspect">
  173. <el-radio
  174. v-for="dict in getStrDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
  175. :key="dict.value"
  176. :label="dict.value"
  177. >
  178. {{ dict.label }}
  179. </el-radio>
  180. </el-radio-group>
  181. </el-form-item>
  182. <el-form-item label="课程耗时" prop="courseTime">
  183. <el-input-number v-model="formData.courseTime" :min="1" :step="1" step-strictly>
  184. <template #suffix><span>分</span></template>
  185. </el-input-number>
  186. </el-form-item>
  187. <!-- <el-form-item label="课程作者" prop="courseAuthor">-->
  188. <!-- <el-input v-model="formData.courseAuthor" placeholder="请输入课程作者" />-->
  189. <!-- </el-form-item>-->
  190. <!-- <el-form-item label="课程老师" prop="courseTeacher">-->
  191. <!-- <el-input v-model="formData.courseTeacher" placeholder="请输入课程老师" />-->
  192. <!-- </el-form-item>-->
  193. <el-form-item label="课程标签" prop="courseLabel">
  194. <el-select
  195. v-model="formData.courseLabel"
  196. placeholder="请选择课程标签"
  197. clearable
  198. >
  199. <el-option
  200. v-for="dict in getStrDictOptions(DICT_TYPE.COURSE_LABEL)"
  201. :key="dict.value"
  202. :label="dict.label"
  203. :value="dict.value"
  204. />
  205. </el-select>
  206. </el-form-item>
  207. <el-form-item label="课程排序" prop="courseOrder">
  208. <el-input-number v-model="formData.courseOrder" placeholder="请输入课程排序" class="!w-1/1" />
  209. </el-form-item>
  210. </template>
  211. <el-form-item label="课程状态" prop="courseStatus">
  212. <el-radio-group v-model="formData.courseStatus">
  213. <el-radio
  214. v-for="dict in getStrDictOptions(DICT_TYPE.COMMON_STATUS)"
  215. :key="dict.value"
  216. :label="dict.value"
  217. >
  218. {{ dict.label }}
  219. </el-radio>
  220. </el-radio-group>
  221. </el-form-item>
  222. </el-form>
  223. <template #footer>
  224. <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
  225. <el-button @click="dialogVisible = false">取 消</el-button>
  226. </template>
  227. </Dialog>
  228. </template>
  229. <script setup lang="ts">
  230. import { getStrDictOptions, DICT_TYPE } from '@/utils/dict'
  231. import { CourseApi, CourseVO } from '@/api/bjdx/course'
  232. import { defaultProps, handleTree } from '@/utils/tree'
  233. import { CourseTypeApi } from '@/api/bjdx/coursetype'
  234. /** 课程 表单 */
  235. defineOptions({ name: 'CourseForm' })
  236. const { t } = useI18n() // 国际化
  237. const message = useMessage() // 消息弹窗
  238. const dialogVisible = ref(false) // 弹窗的是否展示
  239. const dialogTitle = ref('') // 弹窗的标题
  240. const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
  241. const formType = ref('') // 表单的类型:create - 新增;update - 修改
  242. const formData = ref({
  243. id: undefined,
  244. courseName: undefined,
  245. courseContentType: undefined,
  246. courseImagePath: undefined,
  247. courseVideoPath: undefined,
  248. courseMusicPath: undefined,
  249. courseFilePath: undefined,
  250. courseContent: undefined,
  251. courseBlocklyJson:{
  252. title: '',
  253. description: '',
  254. mazeConfig: {
  255. width: 10,
  256. height: 10,
  257. walls: [],
  258. start: [0, 0],
  259. end: [9, 9]
  260. },
  261. initialBlocks: [],
  262. solutionCode: ''
  263. },
  264. courseAuthor: undefined,
  265. courseTeacher: undefined,
  266. courseSize: undefined,
  267. courseTime: undefined,
  268. courseIsInspect: "false",
  269. courseType: undefined,
  270. courseLabel: undefined,
  271. courseOrder: undefined,
  272. courseStatus: "0",
  273. tenantId: undefined,
  274. })
  275. const formRules = reactive({
  276. courseType: [{ required: true, message: '课程类型不能为空', trigger: 'blur' }],
  277. courseName: [{ required: true, message: '课程名称不能为空', trigger: 'blur' }],
  278. courseContent: [{ required: true, message: '课程内容不能为空', trigger: 'blur' }],
  279. courseLabel: [{ required: true, message: '课程标签不能为空', trigger: 'blur' }],
  280. courseOrder: [{ required: true, message: '课程排序不能为空', trigger: 'blur' }]
  281. })
  282. const formRef = ref() // 表单 Ref
  283. const courseTypeTree = ref() // 树形结构
  284. /** 打开弹窗 */
  285. const open = async (type: string, id?: number) => {
  286. dialogVisible.value = true
  287. dialogTitle.value = t('action.' + type)
  288. formType.value = type
  289. resetForm()
  290. // 修改时,设置数据
  291. if (id) {
  292. formLoading.value = true
  293. try {
  294. const courseData = await CourseApi.getCourse(id)
  295. formData.value = {
  296. ...formData.value,
  297. ...courseData,
  298. // courseLabel: courseData.courseLabel?.split(','),
  299. courseImagePath: courseData.courseImagePath?.split(',')
  300. }
  301. // 确保 courseType 为正确的 id 类型
  302. formData.value.courseType = courseData.courseType ? Number(courseData.courseType) : undefined
  303. } finally {
  304. formLoading.value = false
  305. }
  306. }
  307. await getCourseTypeTree()
  308. }
  309. defineExpose({ open }) // 提供 open 方法,用于打开弹窗
  310. /** 提交表单 */
  311. const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
  312. const submitForm = async () => {
  313. // 校验表单
  314. await formRef.value.validate()
  315. // 提交请求
  316. formLoading.value = true
  317. try {
  318. const data = { ...formData.value } as unknown as CourseVO
  319. if (data.courseContentType === 'image') {
  320. data.courseImagePath = data.courseImagePath?.join(',')
  321. }
  322. // data.courseLabel = data.courseLabel?.join(',')
  323. if (formType.value === 'create') {
  324. await CourseApi.createCourse(data)
  325. message.success(t('common.createSuccess'))
  326. } else {
  327. await CourseApi.updateCourse(data)
  328. message.success(t('common.updateSuccess'))
  329. }
  330. dialogVisible.value = false
  331. // 发送操作成功的事件
  332. emit('success')
  333. } finally {
  334. formLoading.value = false
  335. }
  336. }
  337. /** 重置表单 */
  338. const resetForm = () => {
  339. formData.value = {
  340. id: undefined,
  341. courseName: undefined,
  342. courseContentType: undefined,
  343. courseImagePath: undefined,
  344. courseVideoPath: undefined,
  345. courseFilePath: undefined,
  346. courseContent: undefined,
  347. courseBlocklyJson:{
  348. title: '',
  349. description: '',
  350. mazeConfig: {
  351. width: 10,
  352. height: 10,
  353. walls: [],
  354. start: [0, 0],
  355. end: [9, 9]
  356. },
  357. initialBlocks: [],
  358. solutionCode: ''
  359. },
  360. courseAuthor: undefined,
  361. courseTeacher: undefined,
  362. courseSize: undefined,
  363. courseTime: undefined,
  364. courseIsInspect: "false",
  365. courseType: undefined,
  366. courseLabel: undefined,
  367. courseOrder: undefined,
  368. courseStatus: "0",
  369. tenantId: undefined,
  370. }
  371. formRef.value?.resetFields()
  372. }
  373. /** 获得课程-类型树 */
  374. const getCourseTypeTree = async () => {
  375. courseTypeTree.value = []
  376. const data = await CourseTypeApi.getCourseTypeList()
  377. const root: Tree = { id: 0, ctType: '课程类型', ctTypeNode: '0', children: [] }
  378. root.children = handleTree(data, 'id', 'ctParentId')
  379. courseTypeTree.value.push(root)
  380. }
  381. </script>
  382. <style>
  383. .demo-tabs > .el-tabs__content {
  384. padding: 32px;
  385. color: #6b778c;
  386. font-size: 32px;
  387. font-weight: 600;
  388. }
  389. .demo-tabs .custom-tabs-label .el-icon {
  390. vertical-align: middle;
  391. }
  392. .demo-tabs .custom-tabs-label span {
  393. vertical-align: middle;
  394. margin-left: 4px;
  395. }
  396. </style>