index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  1. <!--
  2. Description: 课程详情页面
  3. Author: BaiQi
  4. Created Date: 2024-7-1
  5. -->
  6. <template>
  7. <div class="course-details">
  8. <div class="course-details-info">
  9. <div>
  10. <img class="course-details-image" :src="convertImageUrl(dataForm.courseImages[0], dataForm.courseName)" :alt="dataForm.courseName">
  11. </div>
  12. <div class="course-details-text">
  13. <div class="title">
  14. {{dataForm.courseName}}
  15. </div>
  16. <div class="item">
  17. <span class="item-title">任课老师:</span>
  18. {{dataForm.teacherNames?.filter(Boolean).join(",")}}
  19. </div>
  20. <div class="item">
  21. <span class="item-title">目标学生:</span>
  22. {{dataForm.targetGrade}}
  23. </div>
  24. <div class="item">
  25. <span class="item-title">起止时间:</span>
  26. {{new Date(Date.parse(dataForm.startTime)).toLocaleString()}} - {{new Date(Date.parse(dataForm.endTime)).toLocaleString()}}
  27. </div>
  28. <div>
  29. <template v-if="showCode && role == 'student'">
  30. <el-input v-model="enrollCode" placeholder="输入邀请码" type="text" style="width: 150px;margin-right: 20px"></el-input>
  31. <el-button color="#597d3b" @click="enroll">加入课程</el-button>
  32. </template>
  33. <template v-if="showEdit && role == 'teachers'">
  34. <el-button color="#597d3b" @click="visible = true; fileList[0] = {name: '壁纸', url: newCourseInfo.courseImages[0]}">编辑课程</el-button>
  35. </template>
  36. </div>
  37. </div>
  38. </div>
  39. <div class="course-tabs">
  40. <el-tabs v-model="activeTab" >
  41. <el-tab-pane v-for="item in items" :key="item.key" :name="item.key" :label="item.label">
  42. <template v-if="activeTab == '课程简介'">
  43. <div>
  44. {{dataForm.description}}
  45. </div>
  46. </template>
  47. <template v-else>
  48. <CourseHomeworkTable :disabled="isDisabled()" :columns="columns" :tableData="homeworkList" :role="role" :courseId="dataForm.courseId" :isShowNewHomework="showEdit && role == 'teachers'"/>
  49. </template>
  50. </el-tab-pane>
  51. </el-tabs>
  52. </div>
  53. </div>
  54. <el-dialog v-model="visible" title="编辑课程" >
  55. <el-form :model="newCourseInfo" label-width="auto" :rules="editCourseRules" ref="formRef">
  56. <!-- <el-form-item label="课程图片">-->
  57. <el-form-item label="课程名称" prop="courseName">
  58. <el-input v-model="newCourseInfo.courseName" placeholder="请输入名称"></el-input>
  59. </el-form-item>
  60. <el-form-item label="面向年级" prop="courseName">
  61. <el-input v-model="newCourseInfo.targetGrade" placeholder="请输入年级"></el-input>
  62. </el-form-item>
  63. <el-form-item label="开始时间" prop="startTime">
  64. <el-date-picker v-model="newCourseInfo.startTime" type="datetime" placeholder="截止时间" style="width: 180px"/>
  65. </el-form-item>
  66. <el-form-item label="截止时间" prop="endTime">
  67. <el-date-picker v-model="newCourseInfo.endTime" type="datetime" placeholder="截止时间" style="width: 180px"/>
  68. </el-form-item>
  69. <el-form-item label="选课码" prop="enrollCode">
  70. <el-input v-model="newCourseInfo.enrollCode" placeholder="请输入选课码"></el-input>
  71. </el-form-item>
  72. <!-- <el-form-item label="课程图片">-->
  73. <!-- <el-input v-model="newCourseInfo.courseImages[0]" placeholder="请输入图片链接"></el-input>-->
  74. <!-- </el-form-item>-->
  75. <el-form-item label="课程图片">
  76. <el-upload
  77. ref="uploadRef"
  78. drag
  79. action="/api/file"
  80. multiple
  81. v-model:file-list="fileList"
  82. :limit="1"
  83. :on-success="handleSuccess"
  84. :on-error="handleError"
  85. :on-exceed="handleExceed"
  86. :on-change="handleChange"
  87. list-type="picture-card"
  88. accept=".png, .jpg"
  89. :auto-upload="false"
  90. >
  91. <el-icon class="el-icon--upload"><upload-filled /></el-icon>
  92. <div class="el-upload__text">
  93. 拖拽上传或<em>点击此处上传</em>
  94. </div>
  95. <template #tip>
  96. <div class="el-upload__tip">
  97. jpg/png 文件不大于 500kb
  98. </div>
  99. </template>
  100. </el-upload>
  101. </el-form-item>
  102. <el-form-item label="描述" prop="description">
  103. <el-input v-model="newCourseInfo.description" placeholder="请输入描述" type="textarea" :rows="3"></el-input>
  104. </el-form-item>
  105. </el-form>
  106. <template #footer>
  107. <div class="dialog-footer">
  108. <el-button @click="visible = false">取消</el-button>
  109. <el-button type="primary" @click="submitForm(formRef)">确定</el-button>
  110. </div>
  111. </template>
  112. </el-dialog>
  113. </template>
  114. <script setup lang="ts">
  115. import {onMounted, reactive, ref} from 'vue';
  116. import {
  117. ElTabs,
  118. ElTabPane,
  119. ElMessage,
  120. ElNotification,
  121. type UploadFiles, type UploadFile, type FormRules, type FormInstance
  122. } from 'element-plus';
  123. import './index.scss'
  124. import type {CourseDetailVO, CourseVO, IAssignment, IAssignmentTableColumn, IAssignmentTableItem} from "@/types/vo";
  125. import {useRoute} from "vue-router";
  126. import useApis from "@/apis";
  127. import CourseHomeworkTable from "@/views/CourseSquare/component/CourseDetails/component/CourseHomeworkTable.vue";
  128. import type {AssignmentQuery} from "@/types/query";
  129. import {useStore} from "@/store";
  130. import type {CourseUpdateDTO, EnrollDTO} from "@/types/dto";
  131. import {UploadFilled} from "@element-plus/icons-vue"
  132. import type { UploadProps, UploadUserFile } from 'element-plus'
  133. import { convertImageUrl } from '@/utils/convertUtil';
  134. const apis = useApis()
  135. const store = useStore()
  136. const route = useRoute()
  137. const role:'teachers' | 'student' = store.user.role === 'STUDENT' ? 'student':'teachers';
  138. const courseId = +route.params.courseId
  139. let showCode = ref(true)
  140. let enrollCode = ref(null)
  141. let showEdit = ref(false)
  142. let visible = ref(false)
  143. let uploadRef = ref()
  144. // 用于展示课程信息
  145. let dataForm = reactive<CourseDetailVO>({
  146. courseId: null,
  147. courseName: '',
  148. teacherIds: [],
  149. teacherNames: [],
  150. courseImages: [''],
  151. targetGrade: '',
  152. startTime: '',
  153. endTime: '',
  154. createTime: '',
  155. description: '',
  156. })
  157. let newCourseInfo = reactive<CourseUpdateDTO>({
  158. courseName: '',
  159. teacherIds: [],
  160. courseImages: [''],
  161. targetGrade: '',
  162. startTime: '',
  163. endTime: '',
  164. description: '',
  165. enrollCode: '',
  166. })
  167. //用于查询课程包含的作业
  168. let query = reactive<AssignmentQuery>({
  169. assignmentId:null,
  170. teacherId:null,
  171. courseId:courseId,
  172. assignmentName:null,
  173. status:null,
  174. type:null
  175. })
  176. const editCourseRules = reactive<FormRules>({
  177. courseName: [
  178. {
  179. required: true,
  180. message: "课程名称不能为空",
  181. trigger: 'blur'
  182. }
  183. ],
  184. targetGrade: [
  185. {
  186. required: true,
  187. message: "面向年级不能为空",
  188. trigger: 'blur'
  189. }
  190. ],
  191. startTime: [
  192. {
  193. type: 'date',
  194. required: true,
  195. message: "开始时间不能为空",
  196. trigger: 'blur'
  197. }
  198. ],
  199. endTime: [
  200. {
  201. type: 'date',
  202. required: true,
  203. message: "截止时间不能为空",
  204. trigger: 'blur'
  205. }
  206. ],
  207. enrollCode: [
  208. {
  209. required: true,
  210. message: "选课码不能为空",
  211. trigger: 'blur'
  212. }
  213. ],
  214. description: [
  215. {
  216. required: true,
  217. message: "描述不能为空",
  218. trigger: 'blur'
  219. }
  220. ]
  221. })
  222. const activeTab = ref('课程简介');
  223. const items = ref([
  224. { label: '课程简介', key: '课程简介'},
  225. { label: '作业列表', key: '作业列表'},
  226. ]);
  227. const columns = ref<IAssignmentTableColumn[]>([
  228. {
  229. title: '作业名称',
  230. dataIndex: 'desc',
  231. key: 'desc',
  232. width: 200
  233. },
  234. {
  235. title: '类型',
  236. dataIndex: 'type',
  237. key: 'type',
  238. width: 80
  239. },
  240. {
  241. title: '截止日期',
  242. dataIndex: 'endDate',
  243. key: 'endDate',
  244. width: 170
  245. },
  246. {
  247. title: '发布人',
  248. dataIndex: 'publisher',
  249. key: 'publisher',
  250. width: 100
  251. },
  252. ]);
  253. // 用于展示课程所含的作业
  254. let homeworkList = ref<IAssignmentTableItem[]>([]);
  255. // 用于编辑课程展示预览图片
  256. const fileList = ref<UploadUserFile[]>([])
  257. const handleSuccess: UploadProps['onSuccess'] = (response: any, uploadFile: UploadFile, uploadFiles: UploadFiles) => {
  258. console.log("上传成功", response.data)
  259. newCourseInfo.courseImages[0] = response.data
  260. }
  261. const handleError: UploadProps['onError'] = (error: Error, uploadFile: UploadFile, uploadFiles: UploadFiles) => {
  262. console.log("上传失败", error)
  263. console.log(error, uploadFile, uploadFiles)
  264. }
  265. const handleExceed = (files, fileList) => {
  266. // console.log(files)
  267. uploadRef.value.clearFiles() //清空上传文件(限制一个,所以直接清空即可)
  268. const file = files[0]
  269. uploadRef.value.handleStart(file) //重新上传
  270. }
  271. //选择文件
  272. const handleChange = (file,fileList) => {
  273. // console.log(file)
  274. const fileImgType = file.name.match(/\.([^\.]+)$/)[1]; //匹配文件格式(最后一个'.'后的格式)或者匹配file.raw.type
  275. const isImageType = ['png','jpg'];
  276. const isLt5KB = file.size / 1024 < 500; //判断图片格式与大小
  277. if (isImageType.indexOf(fileImgType)==-1) {
  278. ElMessage.error('文件仅支持png、jpg格式') //限制文件类型
  279. uploadRef.value.clearFiles() //清空上传文件
  280. return false;
  281. }
  282. if (!isLt5KB) {
  283. ElMessage.error('文件不能超过500kb!') //限制文件大小
  284. uploadRef.value.clearFiles() //清空上传文件
  285. return false;
  286. }
  287. apis.uploadFile(file.raw)
  288. .then((res) => {
  289. newCourseInfo.courseImages[0] = res.data.data
  290. })
  291. .catch((err) => {
  292. console.log(err)
  293. })
  294. }
  295. /*
  296. 用于处理课程图片
  297. */
  298. const processCourses = () => {
  299. dataForm.courseImages[0] = dataForm.courseImages[0].split("?")[0]
  300. }
  301. const getCourse = () => {
  302. // 获取课程信息
  303. apis.getCourseByCourseId(courseId)
  304. .then((res: any) => {
  305. let data = res.data.data.records[0]
  306. Object.assign(dataForm, {...data})
  307. processCourses()
  308. getNewCourseInfo(dataForm)
  309. })
  310. .catch((err: any) => {
  311. console.error(err);
  312. })
  313. .finally(() => {});
  314. }
  315. const getNewCourseInfo = (dataForm: CourseVO) => {
  316. apis.getEnrollCode(store.user.id, courseId)
  317. .then((res: any) => {
  318. Object.assign(newCourseInfo, {
  319. courseName: dataForm.courseName,
  320. teacherIds: dataForm.teacherIds,
  321. courseImages: dataForm.courseImages,
  322. targetGrade: dataForm.targetGrade,
  323. startTime: dataForm.startTime,
  324. endTime: dataForm.endTime,
  325. description: dataForm.description,
  326. enrollCode: res.data.data
  327. })
  328. })
  329. .catch((err: any) => {
  330. Object.assign(newCourseInfo, {
  331. courseName: dataForm.courseName,
  332. teacherIds: dataForm.teacherIds,
  333. courseImages: dataForm.courseImages,
  334. targetGrade: dataForm.targetGrade,
  335. startTime: dataForm.startTime,
  336. endTime: dataForm.endTime,
  337. description: dataForm.description,
  338. enrollCode: ''
  339. })
  340. console.error(err);
  341. })
  342. }
  343. const getHomeworkList = () => {
  344. apis.getAssignmentByQuery(query)
  345. .then((res: any) => {
  346. let data = res.data.data.records
  347. homeworkList.value = convertHomeworkList(data)
  348. for(let i = 0; i < homeworkList.value.length; i++){
  349. apis.findUserById(+homeworkList.value[i].publisher)
  350. .then((res: any) => {
  351. homeworkList.value[i].publisher = res.data.data.records[0].name
  352. })
  353. }
  354. })
  355. .catch((err: any) => {
  356. console.error(err);
  357. })
  358. .finally(() => {
  359. });
  360. }
  361. /*
  362. 学生,是否显示选课码输入框选课
  363. */
  364. const isShowCode = () => {
  365. apis.getCourseByStudentId()
  366. .then((res: any) => {
  367. let data = res.data.data.records
  368. for(let i = 0 ; i < data.length; i++){
  369. if(data[i].courseId == courseId){
  370. showCode.value = false;
  371. break;
  372. }
  373. }
  374. })
  375. .catch((err: any) => {
  376. console.error(err);
  377. })
  378. .finally(() => {});
  379. }
  380. let enrollDTO = reactive<EnrollDTO>({
  381. studentId: null,
  382. courseId: courseId,
  383. enrollCode: enrollCode.value
  384. })
  385. /*
  386. 学生,加入课程
  387. */
  388. const enroll = () => {
  389. enrollDTO.enrollCode = enrollCode.value
  390. apis.enrollCourse(enrollDTO)
  391. .then((res:any) => {
  392. if(res.data.code == 400){
  393. ElMessage({
  394. message: '选课码错误',
  395. type: 'error',
  396. })
  397. }else {
  398. showCode.value = false
  399. ElMessage({
  400. message: '选课成功',
  401. type: 'success',
  402. })
  403. }
  404. })
  405. }
  406. /*
  407. 教师,是否显示编辑课程按钮
  408. */
  409. const isShowEdit = () => {
  410. apis.getCourseByTeacherId()
  411. .then((res: any) => {
  412. let total = res.data.data.total
  413. apis.getCourseByTeacherId(1, total)
  414. .then((res: any) => {
  415. let data = res.data.data.records
  416. for(let i = 0 ; i < data.length; i++){
  417. if(data[i].courseId == courseId){
  418. showEdit.value = true;
  419. break;
  420. }
  421. }
  422. })
  423. .catch((err: any) => {
  424. console.error(err);
  425. })
  426. .finally(() => {});
  427. })
  428. }
  429. /*
  430. 将后端传来的作业数据进行整形,便于前端展示
  431. */
  432. const convertHomeworkList = (data:IAssignment[]) => {
  433. let newHomeworkList: IAssignmentTableItem[] = ([])
  434. for(let i = 0; i < data.length; i++){
  435. let item:IAssignmentTableItem = {
  436. desc: data[i].assignmentName,
  437. type: data[i].type,
  438. publisher: data[i].teacherId.toString(),
  439. startDate: formatDate(data[i].startTime),
  440. endDate: formatDate(data[i].endTime),
  441. assignmentId: data[i].assignmentId,
  442. descriptionFile: data[i].descriptionFile != null ? data[i].descriptionFile: null,
  443. teacherId: data[i].teacherId.toString(),
  444. }
  445. newHomeworkList.push(item)
  446. }
  447. return newHomeworkList
  448. }
  449. /*
  450. 时间格式转换方法
  451. */
  452. function formatDate(date:string) {
  453. const d = new Date(date);
  454. const year = d.getFullYear();
  455. const month = String(d.getMonth() + 1).padStart(2, '0');
  456. const day = String(d.getDate()).padStart(2, '0');
  457. const hours = String(d.getHours()).padStart(2, '0');
  458. const minutes = String(d.getMinutes()).padStart(2, '0');
  459. const seconds = String(d.getSeconds()).padStart(2, '0');
  460. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  461. }
  462. let formRef = ref<FormInstance>()
  463. const submitForm = async (formEl: FormInstance | undefined) => {
  464. if (!formEl) return
  465. await formEl.validate((valid, fields) => {
  466. if (valid) {
  467. console.log('submit!')
  468. handleSubmit()
  469. } else {
  470. console.log('error submit!', fields)
  471. }
  472. })
  473. }
  474. const handleSubmit = () => {
  475. newCourseInfo.endTime = formatDate(newCourseInfo.endTime)
  476. newCourseInfo.startTime = formatDate(newCourseInfo.startTime)
  477. if(new Date(newCourseInfo.endTime) < new Date(newCourseInfo.startTime)){
  478. ElMessage.error('截止时间不能早于开始时间!');
  479. checkTimeAndAdjust();
  480. return;
  481. }
  482. // uploadRef.value!.submit()
  483. console.log(newCourseInfo)
  484. // TODO: 更新图片需要更新两次,不知道为什么
  485. apis.updateCourse(dataForm.courseId, newCourseInfo)
  486. .then((res:any) => {
  487. if(res.data.code===200) {
  488. ElNotification({
  489. title: '修改成功',
  490. type: 'success',
  491. })
  492. visible.value = false
  493. getCourse();
  494. // apis.getCourseByCourseId(dataForm.courseId)
  495. // .then((res:any) => {
  496. // console.log(res.data.data.records[0])
  497. // })
  498. // setTimeout(() => apis.updateCourse(dataForm.courseId, newCourseInfo), 500)
  499. }else {
  500. ElNotification({
  501. title: '修改失败',
  502. type: 'error',
  503. })
  504. visible.value = false
  505. }
  506. })
  507. .catch((err:any) => {
  508. console.log(err)
  509. })
  510. }
  511. const checkTimeAndAdjust = () => {
  512. const startTime = new Date(dataForm.startTime);
  513. let endTime = new Date(dataForm.endTime);
  514. // 检查截止时间是否早于开始时间
  515. if (endTime < startTime) {
  516. // 将截止时间设置为开始时间的下一天
  517. endTime.setDate(startTime.getDate() + 1);
  518. newCourseInfo.endTime = formatDate(endTime.toISOString()); // 更新格式
  519. ElMessage.warning('先前时间逻辑有误,截止时间已自动调整为开始时间的下一天!');
  520. }
  521. }
  522. function isDisabled(){
  523. if(role == "student") return showCode.value;
  524. if(role == "teachers") return !showEdit.value;
  525. }
  526. onMounted(() => {
  527. getCourse()
  528. getHomeworkList()
  529. if(role == 'student') isShowCode()
  530. if(role == 'teachers') isShowEdit()
  531. //检查并自动调整时间逻辑
  532. checkTimeAndAdjust();
  533. })
  534. </script>
  535. <style scoped>
  536. ::v-deep .el-tabs__item {
  537. color:#597D3B;
  538. opacity: 1;
  539. }
  540. ::v-deep .el-tabs__item:hover {
  541. cursor: pointer;
  542. opacity: 0.7;
  543. }
  544. /*去下划线 */
  545. ::v-deep .el-tabs__nav-wrap::after {
  546. position: static !important;
  547. }
  548. /* 下划线颜色 */
  549. ::v-deep .el-tabs__active-bar {
  550. background-color: #597D3B;
  551. }
  552. :deep(.el-upload-dragger) {
  553. padding: 0px 0px;
  554. }
  555. </style>