个人笔记,仅供参考
一、总览 1.课程管理目标:
2.配置路由 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 { path : '/course' , component : Layout , redirect : '/course/list' , name : 'Course' , meta : { title : '课程管理' }, children : [ { path : 'list' , name : 'CourseList' , component : () => import ('@/views/course/list' ), meta : { title : '课程列表' } }, { path : 'info' , name : 'CourseInfo' , component : () => import ('@/views/course/form' ), meta : { title : '发布课程' } }, { path : 'info/:id' , name : 'CourseInfoEdit' , component : () => import ('@/views/course/form' ), meta : { title : '编辑课程' }, hidden : true }, { path : 'chapter/:id' , name : 'CourseChapterEdit' , component : () => import ('@/views/course/form' ), meta : { title : '编辑大纲' }, hidden : true } ] },
3.发布课程结构
el-steps
三个步骤的主体页面
创建子组件,然后根据v-if控制是否显示。
这里主体页面跟随步骤导航条的变化而变化,所以v-if判断active的值决定渲染哪个页面。
4.新建Vue文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <template> <div class="app-container"> <h2 style="text-align: center;">发布新课程</h2> <el-steps :active="active" finish-status="success" simple style="margin-bottom: 40px"> <el-step title="填写课程基本信息" /> <el-step title="创建课程大纲" /> <el-step title="发布课程" /> </el-steps> <!-- 填写课程基本信息 --> <info v-if="active === 0" /> <!-- 创建课程大纲 --> <chapter v-if="active === 1" /> <!-- 发布课程 --> <Publish v-if="active === 2 || active === 3" /> </div> </template> <script> // 引入子组件 import Info from '@/views/course/components/Info' import Chapter from '@/views/course/components/Chapter' import Publish from '@/views/course/components/Publish' export default { components: { Info, Chapter, Publish }, // 注册子组件 data() { return { active: 0, courseId: null } } } </script>
对于import Chapter from ‘@/views/course/components/Chapter’
先找…/components/Chapter.vue
再找…/components/Chapter/Index.vue 或 Chapter.vue(与文件夹同名)
courseId是否有值决定了是新增还是修改
3.1.2components/Info.vue
规范:子组件文件名首字母大写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <template> <div class="app-container"> <!-- 课程信息表单 TODO --> <div style="text-align:center"> <el-button :disabled="saveBtnDisabled" type="primary" @click="saveAndNext()">保存并下一步</el-button> </div> </div> </template> <script> export default { data() { return { saveBtnDisabled: false // 按钮是否禁用 } }, methods: { // 保存并下一步 saveAndNext() { this.saveBtnDisabled = true this.$parent.active = 1 } } } </script>
3.1.3components/Chapter/Index.vue 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <template> <div class="app-container"> <!-- 章节列表 TODO --> <div style="text-align:center"> <el-button type="primary" @click="prev()">上一步</el-button> <el-button type="primary" @click="next()">下一步</el-button> </div> </div> </template> <script> export default { methods: { // 上一步 prev() { this.$parent.active = 0 }, // 下一步 next() { this.$parent.active = 2 } } } </script>
3.1.4components/Publish.vue 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <template> <div class="app-container"> <!--课程预览 TODO--> <div style="text-align:center"> <el-button type="primary" @click="prev()">上一步</el-button> <el-button :disabled="publishBtnDisabled" type="primary" @click="publish()">发布课程</el-button> </div> </div> </template> <script> export default { data() { return { publishBtnDisabled: false // 按钮是否禁用 } }, methods: { // 上一步 prev() { this.$parent.active = 1 }, // 下一步 publish() { this.publishBtnDisabled = true this.$parent.active = 3 } } } </script>
3.1.5list.vue
二、发布课程-课程基本信息 0.说明 先写发布课程中第一个步骤,编辑课程基本信息的页面。
1.后端 1.1思路
1.2实体类 根据表单数据创建实体类对象
…/edu/entity/vo/CourseInfoForm.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package com.atguigu.guli.service.edu.entity.form;@ApiModel("课程基本信息") @Data public class CourseInfoForm implements Serializable { private static final long serialVersionUID = 1L ; @ApiModelProperty(value = "课程ID") private String id; @ApiModelProperty(value = "课程讲师ID") private String teacherId; @ApiModelProperty(value = "课程专业ID") private String subjectId; @ApiModelProperty(value = "课程专业父级ID") private String subjectParentId; @ApiModelProperty(value = "课程标题") private String title; @ApiModelProperty(value = "课程销售价格,设置为0则可免费观看") private BigDecimal price; @ApiModelProperty(value = "总课时") private Integer lessonNum; @ApiModelProperty(value = "课程封面图片路径") private String cover; @ApiModelProperty(value = "课程简介") private String description; }
1.3修改 编写接口之前需要修改两个地方,一个是修改CourseDescription主键策略,另一个是为课程状态添加两个常量。
说明
保存课程基本信息需要保存到课程表和课程简介表,对应实体类Course和CourseDescription。
而两张表是一对一的关系,所以我们这里采取主键跟随的策略。
也就是,保存完Course表返回courseId后,再保存CourseDescription,并设置与课程相同的主键(courseId)。
所以这里要将CourseDescription主键策略设置为空。
1.3.1CourseDescription主键策略
修改CourseDescription.java
1 2 3 @ApiModelProperty(value = "ID") @TableId(value = "id", type = IdType.NONE) private String id;
1.3.2课程状态常量
Course.java
1 2 public static final String COURSE_DRAFT = "Draft" ;public static final String COURSE_NORMAL = "Normal" ;
1.4接口开发 1.4.1Controller
CourseController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package com.atguigu.guli.service.edu.controller;import com.atguigu.guli.common.base.result.R;import com.atguigu.guli.service.edu.entity.vo.CourseInfoForm;import com.atguigu.guli.service.edu.service.CourseService;import io.swagger.annotations.Api;import io.swagger.annotations.ApiOperation;import io.swagger.annotations.ApiParam;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;@Api(description="课程管理") @CrossOrigin @RestController @RequestMapping("/admin/edu/course") public class CourseController { @Autowired private CourseService courseService; @ApiOperation("新增课程") @PostMapping("save-course-info") public R saveCourseInfo ( @ApiParam(value = "课程基本信息", required = true) @RequestBody CourseInfoForm courseInfoForm) { String courseId = courseService.saveCourseInfo(courseInfoForm); return R.ok().data("courseId" , courseId).message("保存成功" ); } }
1.4.2Service
CourseService.java
1 String saveCourseInfo (CourseInfoForm courseInfoForm) ;
CourseServiceImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Service public class CourseServiceImpl extends ServiceImpl <CourseMapper, Course> implements CourseService { @Autowired private CourseDescriptionMapper courseDescriptionMapper; @Transactional(rollbackFor = Exception.class) @Override public String saveCourseInfo (CourseInfoForm courseInfoForm) { Course course = new Course (); course.setStatus(Course.COURSE_DRAFT); BeanUtils.copyProperties(courseInfoForm, course); baseMapper.insert(course); CourseDescription courseDescription = new CourseDescription (); courseDescription.setDescription(courseInfoForm.getDescription()); courseDescription.setId(course.getId()); courseDescriptionMapper.insert(courseDescription); return course.getId(); } }
2.前端 2.1API 1 2 3 4 5 6 7 8 9 10 11 import request from '@/utils/request' export default { saveCourseInfo (courseInfo ) { return request ({ url : '/admin/edu/course/save-course-info' , method : 'post' , data : courseInfo }) } }
2.2简单字段保存
Info.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <el-form label-width ="120px" > <el-form-item label ="课程标题" > <el-input v-model ="courseInfo.title" placeholder =" 示例:机器学习项目课:从基础到搭建项目视频课程。专业名称注意大小写" /> </el-form-item > <el-form-item label ="总课时" > <el-input-number :min ="0" v-model ="courseInfo.lessonNum" controls-position ="right" placeholder ="请填写课程的总课时数" /> </el-form-item > <el-form-item label ="课程价格" > <el-input-number :min ="0" v-model ="courseInfo.price" controls-position ="right" placeholder ="免费课程请设置为0元" /> 元 </el-form-item > </el-form >
Info.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <script> import courseApi from '@/api/course' export default { data ( ) { return { saveBtnDisabled : false , courseInfo : { price : 0 , lessonNum : 0 , teacherId : '' , subjectId : '' , subjectParentId : '' , cover : '' , description : '' } } }, methods : { saveAndNext ( ) { this .saveBtnDisabled = true this .saveData () }, saveData ( ) { courseApi.saveCourseInfo (this .courseInfo ).then (response => { this .$message .success (response.message ) this .$parent .courseId = response.data .courseId this .$parent .active = 1 }) } } } </script>
注:调用父组件:this.$parent
2.3其他字段保存 2.3.1讲师下拉框 查询讲师列表并渲染到下拉框组件
Info.vue
1 import teacherApi from '@/api/teacher'
1 2 3 4 5 initTeacherList ( ) { teacherApi.list ().then (response => { this .teacherList = response.data .items }) },
1 2 3 4 created ( ) { this .initTeacherList () },
1 2 3 4 5 6 7 8 9 10 11 12 <el-form-item label ="课程讲师" > <el-select v-model ="courseInfo.teacherId" placeholder ="请选择" > <el-option v-for ="teacher in teacherList" :key ="teacher.id" :label ="teacher.name" :value ="teacher.id" /> </el-select > </el-form-item >
2.3.2课程分类 课程分类涉及到二级连调,而对于二级连调,有两种解决方案。
方案一
方案二
这里我们采用方案一来实现二级连调。
<1>获取嵌套数据
Info.vue
1 import subjectApi from '@/api/subject'
提前定义好data接收数据
1 2 subjectList : [],subjectLevelTwoList : []
Info.vue的methods中
1 2 3 4 5 initSubjectList ( ) { subjectApi.getNestedTreeList ().then (response => { this .subjectList = response.data .items }) },
Info.vue的created中
1 2 3 4 5 6 created ( ) { this .initTeacherList () this .initSubjectList () },
<2>展示一级分类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <el-form-item label ="课程类别" > <el-select v-model ="courseInfo.subjectParentId" placeholder ="请选择" > <el-option v-for ="subject in subjectList" :key ="subject.id" :label ="subject.title" :value ="subject.id" /> </el-select > </el-form-item >
<3>展示二级分类 一级分类修改时,将对应数据绑定到subjectLevelTwoList,然后在组件模板中渲染。
1 <el-select @change ="subjectChanged" ...
1 2 3 4 5 6 7 8 subjectChanged (value ) { this .subjectList .forEach (subject => { if (subject.id === value) { this .courseInfo .subjectId = '' this .subjectLevelTwoList = subject.children } }) },
1 2 3 4 5 6 7 8 <el-select v-model ="courseInfo.subjectId" placeholder ="请选择" > <el-option v-for ="subject in subjectLevelTwoList" :key ="subject.id" :label ="subject.title" :value ="subject.id" /> </el-select >
2.3.3课程简介 <1>引入Tinymce
课程简介需要先引入富文本编辑器Tinymce。Tinymce是一个传统javascript插件,默认不能用于Vue.js因此需要做一些特殊的整合步骤。
复制脚本库
vue-element-admin-master/static/tinymce4.7.5
–复制–>
guli-admin/static/tinymce4.7.5
guli-admin/index.html
1 2 <script src ="/static/tinymce4.7.5/tinymce.min.js" > </script > <script src ="/static/tinymce4.7.5/langs/zh_CN.js" > </script >
复制组件
vue-element-admin-master/src/components/Tinymce
–复制–>
guli-admin/src/components/Tinymce
引入组件并注册
Info.vue
1 2 3 4 5 6 import Tinymce from '@/components/Tinymce' export default { components : { Tinymce }, ...... }
<2>使用
1 2 3 4 <el-form-item label ="课程简介" > <tinymce :height ="300" v-model ="courseInfo.description" /> </el-form-item >
<3>样式微调
样式有一点瑕疵,需要略微调整。
Info.vue
1 2 3 4 5 6 <style > .tinymce-container { position : relative; line-height : normal; } </style >
2.3.4课程封面 课程封面与讲师头像 部分流程一样。
后端采取相同接口,不过module参数为cover。
前端:
Info.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 <el-form-item label ="课程封面" > <el-upload :show-file-list ="false" :on-success ="handleCoverSuccess" :before-upload ="beforeCoverUpload" :on-error ="handleCoverError" class ="cover-uploader" action ="http://localhost:8120/admin/oss/file/upload?module=cover" > <img v-if ="courseInfo.cover" :src ="courseInfo.cover" > <i v-else class ="el-icon-plus avatar-uploader-icon" /> </el-upload > </el-form-item >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <style > .cover-uploader .el-upload { border : 1px dashed #d9d9d9 ; border-radius : 6px ; cursor : pointer; position : relative; overflow : hidden; } .cover-uploader .el-upload :hover { border-color : #409EFF ; } .cover-uploader .avatar-uploader-icon { font-size : 28px ; color : #8c939d ; width : 640px ; height : 357px ; line-height : 357px ; text-align : center; } .cover-uploader img { width : 640px ; height : 357px ; display : block; } </style >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 handleCoverSuccess (res, file ) { if (res.success ) { this .courseInfo .cover = res.data .url this .$forceUpdate() } else { this .$message .error ('上传失败1' ) } }, beforeCoverUpload (file ) { const isJPG = file.type === 'image/jpeg' const isLt2M = file.size / 1024 / 1024 < 2 if (!isJPG) { this .$message .error ('上传头像图片只能是 JPG 格式!' ) } if (!isLt2M) { this .$message .error ('上传头像图片大小不能超过 2MB!' ) } return isJPG && isLt2M }, handleCoverError ( ) { console .log ('error' ) this .$message .error ('上传失败2' ) },
三、课程信息回显 编辑讲师、创建课程大纲页上一步按钮都需要进行回显。这里先对点击上一步按钮跳转讲师信息页进行回显表单。
1.后端 通过保存在父组件form.vue中的courseId查询出CourseInfoForm对象,然后封装到R对象返回。
1.1Controller
CourseController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 @ApiOperation("根据ID查询课程") @GetMapping("course-info/{id}") public R getById ( @ApiParam(value = "课程ID", required = true) @PathVariable String id) { CourseInfoForm courseInfoForm = courseService.getCourseInfoById(id); if (courseInfoForm != null ) { return R.ok().data("item" , courseInfoForm); } else { return R.error().message("数据不存在" ); } }
1.2Service
CourseService.java
1 CourseInfoForm getCourseInfoById (String id) ;
CourseServiceImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Override public CourseInfoForm getCourseInfoById (String id) { Course course = baseMapper.selectById(id); if (course == null ){ return null ; } CourseDescription courseDescription = courseDescriptionMapper.selectById(id); CourseInfoForm courseInfoForm = new CourseInfoForm (); BeanUtils.copyProperties(course, courseInfoForm); courseInfoForm.setDescription(courseDescription.getDescription()); return courseInfoForm; }
2.前端 前端将查询的数据绑定到courseInfo,页面表单与数据双向绑定,所以会直接回显出来。
2.1API
api/course.js
1 2 3 4 5 6 getCourseInfoById (id ) { return request ({ url : `/admin/edu/course/course-info/${id} ` , method : 'get' }) }
2.2组件js
Info.vue
1 2 3 4 5 fetchCourseInfoById (id ) { courseApi.getCourseInfoById (id).then (response => { this .courseInfo = response.data .item }) },
页面渲染前判断父组件courseId,如果存在courseId,则回显
1 2 3 4 5 6 7 8 9 created ( ) { if (this .$parent .courseId ) { this .fetchCourseInfoById (this .$parent .courseId ) } this .initTeacherList () this .initSubjectList () },
2.3二级分类渲染问题
问题说明
因为initSubjectList()方法只将一级分类列表(subjectList)进行绑定,二级分类列表(subjectLevelTwoList)没有绑定,二级列表没有数据。
所以二级分类列表回填时,根据id找不到对应title值,然后值能显示出id值。
解决:填充二级分类列表
遍历一级分类列表,取出与当前一级分类对应的嵌套数据。(subject>children)
将children赋值给subjectLevelTwoList
Info.vue
填充二级分类列表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 fetchCourseInfoById (id ) { courseApi.getCourseInfoById (id).then (response => { this .courseInfo = response.data .item subjectApi.getNestedTreeList ().then (response => { this .subjectList = response.data .items this .subjectList .forEach (subject => { if (subject.id === this .courseInfo .subjectParentId ) { this .subjectLevelTwoList = subject.children } }) }) }) },
回显状态填充,新增状态不填充
1 2 3 4 5 6 7 8 9 10 created ( ) { if (this .$parent .courseId ) { this .fetchCourseInfoById (this .$parent .courseId ) } else { this .initSubjectList () } this .initTeacherList () },
四、更新
1.后端
CourseController.java
1 2 3 4 5 6 7 8 9 @ApiOperation("更新课程") @PutMapping("update-course-info") public R updateCourseInfoById ( @ApiParam(value = "课程基本信息", required = true) @RequestBody CourseInfoForm courseInfoForm) { courseService.updateCourseInfoById(courseInfoForm); return R.ok().message("修改成功" ); }
CourseService.java
1 void updateCourseInfoById (CourseInfoForm courseInfoForm) ;
CourseServiceImpl.java
多表修改操作,涉及事务,添加注解@Transactional(rollbackFor = Exception.class)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Transactional(rollbackFor = Exception.class) @Override public void updateCourseInfoById (CourseInfoForm courseInfoForm) { Course course = new Course (); BeanUtils.copyProperties(courseInfoForm, course); baseMapper.updateById(course); CourseDescription courseDescription = new CourseDescription (); courseDescription.setDescription(courseInfoForm.getDescription()); courseDescription.setId(course.getId()); courseDescriptionMapper.updateById(courseDescription); }
2.前端 2.1API
src/course.js
1 2 3 4 5 6 7 updateCourseInfoById(courseInfo) { return request({ url: '/admin/edu/course/update-course-info' , method: 'put' , data: courseInfo }) }
Info.vue
1 2 3 4 5 6 7 updateData() { courseApi.updateCourseInfoById(this .courseInfo).then(response => { this .$message.success(response.message) this .$parent.active = 1 }) }
1 2 3 4 5 6 7 8 9 saveAndNext() { this .saveBtnDisabled = true if (!this .$parent.courseId) { this .saveData() } else { this .updateData() } }