在线教育(七)
一、课程发布表单-步骤导航
一、需求
二、配置路由
1、添加路由
// 课程管理
{
path: '/edu/course',
component: Layout,
redirect: '/edu/course/list',
name: 'Course',
meta: { title: '课程管理', icon: 'form' },
children: [
{
path: 'list',
name: 'EduCourseList',
component: () => import('@/views/edu/course/list'),
meta: { title: '课程列表' }
},
{
path: 'info',
name: 'EduCourseInfo',
component: () => import('@/views/edu/course/info'),
meta: { title: '发布课程' }
},
{
path: 'info/:id',
name: 'EduCourseInfoEdit',
component: () => import('@/views/edu/course/info'),
meta: { title: '编辑课程基本信息', noCache: true },
hidden: true
},
{
path: 'chapter/:id',
name: 'EduCourseChapterEdit',
component: () => import('@/views/edu/course/chapter'),
meta: { title: '编辑课程大纲', noCache: true },
hidden: true
},
{
path: 'publish/:id',
name: 'EduCoursePublishEdit',
component: () => import('@/views/edu/course/publish'),
meta: { title: '发布课程', noCache: true },
hidden: true
}
]
},
2、添加vue组件
三、整合步骤条组件
参考 http://element-cn.eleme.io/#/zh-CN/component/steps
1、课程信息页面
info.vue
<template>
<div class="app-container">
<h2 style="text-align: center;">发布新课程</h2>
<el-steps :active="1" process-status="wait" align-center style="margin-bottom: 40px;">
<el-step title="填写课程基本信息"/>
<el-step title="创建课程大纲"/>
<el-step title="提交审核"/>
</el-steps>
<el-form label-width="120px">
<el-form-item>
<el-button :disabled="saveBtnDisabled" type="primary" @click="next">保存并下一步</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
data() {
return {
saveBtnDisabled: false // 保存按钮是否禁用
}
},
created() {
console.log('info created')
},
methods: {
next() {
console.log('next')
this.$router.push({ path: '/edu/course/chapter/1' })
}
}
}
</script>
2、课程大纲页面
chapter.vue
<template>
<div class="app-container">
<h2 style="text-align: center;">发布新课程</h2>
<el-steps :active="2" process-status="wait" align-center style="margin-bottom: 40px;">
<el-step title="填写课程基本信息"/>
<el-step title="创建课程大纲"/>
<el-step title="提交审核"/>
</el-steps>
<el-form label-width="120px">
<el-form-item>
<el-button @click="previous">上一步</el-button>
<el-button :disabled="saveBtnDisabled" type="primary" @click="next">下一步</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
data() {
return {
saveBtnDisabled: false // 保存按钮是否禁用
}
},
created() {
console.log('chapter created')
},
methods: {
previous() {
console.log('previous')
this.$router.push({ path: '/edu/course/info/1' })
},
next() {
console.log('next')
this.$router.push({ path: '/edu/course/publish/1' })
}
}
}
</script>
3、课程发布页面
publish.vue
<template>
<div class="app-container">
<h2 style="text-align: center;">发布新课程</h2>
<el-steps :active="3" process-status="wait" align-center style="margin-bottom: 40px;">
<el-step title="填写课程基本信息"/>
<el-step title="创建课程大纲"/>
<el-step title="提交审核"/>
</el-steps>
<el-form label-width="120px">
<el-form-item>
<el-button @click="previous">返回修改</el-button>
<el-button :disabled="saveBtnDisabled" type="primary" @click="publish">发布课程</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
data() {
return {
saveBtnDisabled: false // 保存按钮是否禁用
}
},
created() {
console.log('publish created')
},
methods: {
previous() {
console.log('previous')
this.$router.push({ path: '/edu/course/chapter/1' })
},
publish() {
console.log('publish')
this.$router.push({ path: '/edu/course/list' })
}
}
}
</script>
二、编辑课程基本信息
一、后台api
1、定义form表单对象
CourseInfoForm.java
package com.guli.edu.form;
@ApiModel(value = "课程基本信息", description = "编辑课程基本信息的表单对象")
@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 = "课程标题")
private String title;
@ApiModelProperty(value = "课程销售价格,设置为0则可免费观看")
private BigDecimal price;
@ApiModelProperty(value = "总课时")
private Integer lessonNum;
@ApiModelProperty(value = "课程封面图片路径")
private String cover;
@ApiModelProperty(value = "课程简介")
private String description;
}
2、修改CourseDescription主键生成策略
@ApiModelProperty(value = "课程ID")
@TableId(value = "id", type = IdType.INPUT)
private String id;
3、定义常量
实体类Course.Java中定义
public static final String COURSE_DRAFT = "Draft";
public static final String COURSE_NORMAL = "Normal";
4、定义控制层接口
CourseAdminController.java
package com.guli.edu.controller.admin;
@Api(description="课程管理")
@CrossOrigin //跨域
@RestController
@RequestMapping("/admin/edu/course")
public class CourseAdminController {
@Autowired
private CourseService courseService;
@ApiOperation(value = "新增课程")
@PostMapping("save-course-info")
public R saveCourseInfo(
@ApiParam(name = "CourseInfoForm", value = "课程基本信息", required = true)
@RequestBody CourseInfoForm courseInfoForm){
String courseId = courseService.saveCourseInfo(courseInfoForm);
if(!StringUtils.isEmpty(courseId)){
return R.ok().data("courseId", courseId);
}else{
return R.error().message("保存失败");
}
}
}
5、定义业务层方法
接口:CourseService.java
/**
* 保存课程和课程详情信息
* @param courseInfoForm
* @return 新生成的课程id
*/
String saveCourseInfo(CourseInfoForm courseInfoForm);
实现:CourseServiceImpl.java
@Autowired
private CourseDescriptionService courseDescriptionService;
@Override
public String saveCourseInfo(CourseInfoForm courseInfoForm) {
//保存课程基本信息
Course course = new Course();
course.setStatus(Course.COURSE_DRAFT);
BeanUtils.copyProperties(courseInfoForm, course);
boolean resultCourseInfo = this.save(course);
if(!resultCourseInfo){
throw new GuliException(20001, "课程信息保存失败");
}
//保存课程详情信息
CourseDescription courseDescription = new CourseDescription();
courseDescription.setDescription(courseInfoForm.getDescription());
courseDescription.setId(course.getId());
boolean resultDescription = courseDescriptionService.save(courseDescription);
if(!resultDescription){
throw new GuliException(20001, "课程详情信息保存失败");
}
return course.getId();
}
6、Swagger测试
二、前端实现
1、定义api
import request from '@/utils/request'
const api_name = '/admin/edu/course'
export default {
saveCourseInfo(courseInfo) {
return request({
url: `${api_name}/save-course-info`,
method: 'post',
data: courseInfo
})
}
}
2、组件模板
<el-form label-width="120px">
<el-form-item label="课程标题">
<el-input v-model="courseInfo.title" placeholder=" 示例:机器学习项目课:从基础到搭建项目视频课程。专业名称注意大小写"/>
</el-form-item>
<!-- 所属分类 TODO -->
<!-- 课程讲师 TODO -->
<el-form-item label="总课时">
<el-input-number :min="0" v-model="courseInfo.lessonNum" controls-position="right" placeholder="请填写课程的总课时数"/>
</el-form-item>
<!-- 课程简介 TODO -->
<!-- 课程封面 TODO -->
<el-form-item label="课程价格">
<el-input-number :min="0" v-model="courseInfo.price" controls-position="right" placeholder="免费课程请设置为0元"/> 元
</el-form-item>
<el-form-item>
<el-button :disabled="saveBtnDisabled" type="primary" @click="next">保存并下一步</el-button>
</el-form-item>
</el-form>
3、组件js
<script>
import course from '@/api/edu/course'
const defaultForm = {
title: '',
subjectId: '',
teacherId: '',
lessonNum: 0,
description: '',
cover: '',
price: 0
}
export default {
data() {
return {
courseInfo: defaultForm,
saveBtnDisabled: false // 保存按钮是否禁用
}
},
watch: {
$route(to, from) {
console.log('watch $route')
this.init()
}
},
created() {
console.log('info created')
this.init()
},
methods: {
init() {
if (this.$route.params && this.$route.params.id) {
const id = this.$route.params.id
console.log(id)
} else {
this.courseInfo = { ...defaultForm }
}
},
next() {
console.log('next')
this.saveBtnDisabled = true
if (!this.courseInfo.id) {
this.saveData()
} else {
this.updateData()
}
},
// 保存
saveData() {
course.saveCourseInfo(this.courseInfo).then(response => {
this.$message({
type: 'success',
message: '保存成功!'
})
return response// 将响应结果传递给then
}).then(response => {
this.$router.push({ path: '/edu/course/chapter/' + response.data.courseId })
}).catch((response) => {
this.$message({
type: 'error',
message: response.message
})
})
},
updateData() {
this.$router.push({ path: '/edu/course/chapter/1' })
}
}
}
</script>
三、课程分类多级联动的实现
一、需求
二、获取一级分类
1、组件数据定义
定义在data中
subjectNestedList: [],//一级分类列表
subSubjectList: []//二级分类列表
2、组件模板
<!-- 所属分类:级联下拉列表 -->
<!-- 一级分类 -->
<el-form-item label="课程类别">
<el-select
v-model="courseInfo.subjectParentId"
placeholder="请选择">
<el-option
v-for="subject in subjectNestedList"
:key="subject.id"
:label="subject.title"
:value="subject.id"/>
</el-select>
</el-form-item>
3、组件脚本
表单初始化时获取一级分类嵌套列表,引入subject api
import subject from '@/api/edu/subject'
定义方法
init() {
......
// 初始化分类列表
this.initSubjectList()
},
initSubjectList() {
subject.getNestedTreeList().then(response => {
this.subjectNestedList = response.data.items
})
},
三、级联显示二级分类
1、组件模板
<!-- 二级分类 -->
<el-select v-model="courseInfo.subjectId" placeholder="请选择">
<el-option
v-for="subject in subSubjectList"
:key="subject.value"
:label="subject.title"
:value="subject.id"/>
</el-select>
2、注册change事件
在一级分类的组件中注册change事件
<el-select @change="subjectLevelOneChanged" ......
3、定义change事件方法
subjectLevelOneChanged(value) {
console.log(value)
for (let i = 0; i < this.subjectNestedList.length; i++) {
if (this.subjectNestedList[i].id === value) {
this.subSubjectList = this.subjectNestedList[i].children
this.courseInfo.subjectId = ''
}
}
},
四、讲师下拉列表
一、前端实现
1、组件模板
<!-- 课程讲师 -->
<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、定义api
api/edu/teacher.js
getList() {
return request({
url: api_name,
method: 'get'
})
},
组件中引入teacher api
import teacher from '@/api/edu/teacher'
3、组件脚本
定义data
teacherList: [] // 讲师列表
表单初始化时获取讲师列表
init() {
......
// 获取讲师列表
this.initTeacherList()
},
initTeacherList() {
teacher.getList().then(response => {
this.teacherList = response.data.items
})
},
五、富文本编辑器Tinymce
一、Tinymce可视化编辑器
参考
https://panjiachen.gitee.io/vue-element-admin/#/components/tinymce
https://panjiachen.gitee.io/vue-element-admin/#/example/create
二、组件初始化
Tinymce是一个传统javascript插件,默认不能用于Vue.js因此需要做一些特殊的整合步骤
1、复制脚本库
将脚本库复制到项目的static目录下(在vue-element-admin-master的static路径下)
2、配置html变量
在 guli-admin/build/webpack.dev.conf.js 中添加配置
使在html页面中可是使用这里定义的BASE_URL变量
new HtmlWebpackPlugin({
......,
templateParameters: {
BASE_URL: config.dev.assetsPublicPath + config.dev.assetsSubDirectory
}
})
3、引入js脚本
在guli-admin/index.html 中引入js脚本
<script src=<%= BASE_URL %>/tinymce4.7.5/tinymce.min.js></script>
<script src=<%= BASE_URL %>/tinymce4.7.5/langs/zh_CN.js></script>
三、组件引入
为了让Tinymce能用于Vue.js项目,vue-element-admin-master对Tinymce进行了封装,下面我们将它引入到我们的课程信息页面
1、复制组件
src/components/Tinymce
2、引入组件
课程信息组件中引入 Tinymce
import Tinymce from '@/components/Tinymce'
export default {
components: { Tinymce },
......
}
3、组件模板
<!-- 课程简介-->
<el-form-item label="课程简介">
<tinymce :height="300" v-model="courseInfo.description"/>
</el-form-item>
4、组件样式
在info.vue文件的最后添加如下代码,调整上传图片按钮的高度
<style scoped>
.tinymce-container {
line-height: 29px;
}
</style>
5、图片的base64编码
Tinymce中的图片上传功能直接存储的是图片的base64编码,因此无需图片服务器
六、课程封面
一、整合上传组件
参考 http://element-cn.eleme.io/#/zh-CN/component/upload 用户头像上传
1、上传默认封面
创建文件夹cover,上传默认的课程封面
2、定义默认封面
const defaultForm = {
......,
cover: process.env.OSS_PATH + '/cover/default.gif',
......
}
3、定义data数据
BASE_API: process.env.BASE_API // 接口API地址
4、组件模板
在info.vue中添加上传组件模板
<!-- 课程封面-->
<el-form-item label="课程封面">
<el-upload
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
:action="BASE_API+'/admin/oss/file/upload?host=cover'"
class="avatar-uploader">
<img :src="courseInfo.cover">
</el-upload>
</el-form-item>
5、结果回调
handleAvatarSuccess(res, file) {
console.log(res)// 上传响应
console.log(URL.createObjectURL(file.raw))// base64编码
this.courseInfo.cover = res.data.url
},
beforeAvatarUpload(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
}
二、修改后端api
1、修改上传controller
添加host可选参数
/**
* 文件上传
*
* @param file
*/
@ApiOperation(value = "文件上传")
@PostMapping("upload")
public R upload(
@ApiParam(name = "file", value = "文件", required = true)
@RequestParam("file") MultipartFile file,
@ApiParam(name = "host", value = "文件上传路径", required = false)) {
String uploadUrl = fileService.upload(file);
//返回r对象
return R.ok().message("文件上传成功").data("url", uploadUrl);
}
2、综合测试
七、课程信息回显
一、后端实现
1、业务层
接口:CourseService.java
CourseInfoForm getCourseInfoFormById(String id);
实现:CourseServiceImpl.java
@Override
public CourseInfoForm getCourseInfoFormById(String id) {
Course course = this.getById(id);
if(course == null){
throw new GuliException(20001, "数据不存在");
}
CourseInfoForm courseInfoForm = new CourseInfoForm();
BeanUtils.copyProperties(course, courseInfoForm);
CourseDescription courseDescription = courseDescriptionService.getById(id);
if(course != null){
courseInfoForm.setDescription(courseDescription.getDescription());
}
return courseInfoForm;
}
2、web层
@ApiOperation(value = "根据ID查询课程")
@GetMapping("course-info/{id}")
public R getById(
@ApiParam(name = "id", value = "课程ID", required = true)
@PathVariable String id){
CourseInfoForm courseInfoForm = courseService.getCourseInfoFormById(id);
return R.ok().data("item", courseInfoForm);
}
3、Swagger中测试
二、前端实现
1、定义api
api/edu/course.js
getCourseInfoById(id) {
return request({
url: `${api_name}/course-info/${id}`,
method: 'get'
})
}
2、组件js
init() {
if (this.$route.params && this.$route.params.id) {
const id = this.$route.params.id
//根据id获取课程基本信息
this.fetchCourseInfoById(id)
}
......
},
fetchCourseInfoById(id) {
course.getCourseInfoById(id).then(response => {
this.courseInfo = response.data.item
}).catch((response) => {
this.$message({
type: 'error',
message: response.message
})
})
},
三、解决级联下拉菜单回显问题
1、数据库中增加冗余列
subject_parent_id 课程专业父级ID
2、pojo中增加属性
entity.Course.java
form.CourseInfo.java
@ApiModelProperty(value = "课程专业父级ID")
private String subjectParentId;
3、vue组件中绑定数据
edu/course/infoinfo.vue
<el-select v-model="courseInfo.subjectParentId" ......
4、修改init方法
将 this.initSubjectList() 和 this.initTeacherList()移至else
init() {
if (this.$route.params && this.$route.params.id) {
const id = this.$route.params.id
// 根据id获取课程基本信息
this.fetchCourseInfoById(id)
} else {
this.courseInfo = { ...defaultForm }
// 初始化分类列表
this.initSubjectList()
// 获取讲师列表
this.initTeacherList()
}
},
5、修改fetchCourseInfoById方法
fetchCourseInfoById(id) {
course.getCourseInfoById(id).then(responseCourse => {
this.courseInfo = responseCourse.data.item
// 初始化分类列表
subject.getNestedTreeList().then(responseSubject => {
this.subjectNestedList = responseSubject.data.items
for (let i = 0; i < this.subjectNestedList.length; i++) {
if (this.subjectNestedList[i].id === this.courseInfo.subjectParentId) {
this.subSubjectList = this.subjectNestedList[i].children
}
}
})
// 获取讲师列表
this.initTeacherList()
}).catch((response) => {
this.$message({
type: 'error',
message: response.message
})
})
},
八、更新课程信息
一、后端实现
1、业务层
接口:CourseService.java
void updateCourseInfoById(CourseInfoForm courseInfoForm);
实现:CourseServiceImpl.java
@Override
public void updateCourseInfoById(CourseInfoForm courseInfoForm) {
//保存课程基本信息
Course course = new Course();
BeanUtils.copyProperties(courseInfoForm, course);
boolean resultCourseInfo = this.updateById(course);
if(!resultCourseInfo){
throw new GuliException(20001, "课程信息保存失败");
}
//保存课程详情信息
CourseDescription courseDescription = new CourseDescription();
courseDescription.setDescription(courseInfoForm.getDescription());
courseDescription.setId(course.getId());
boolean resultDescription = courseDescriptionService.updateById(courseDescription);
if(!resultDescription){
throw new GuliException(20001, "课程详情信息保存失败");
}
}
2、web层
@ApiOperation(value = "更新课程")
@PutMapping("update-course-info/{id}")
public R updateCourseInfoById(
@ApiParam(name = "CourseInfoForm", value = "课程基本信息", required = true)
@RequestBody CourseInfoForm courseInfoForm,
@ApiParam(name = "id", value = "课程ID", required = true)
@PathVariable String id){
courseService.updateCourseInfoById(courseInfoForm);
return R.ok();
}
二、前端实现
1、定义api
course.js
updateCourseInfoById(courseInfo) {
return request({
url: `${api_name}/update-course-info/${courseInfo.id}`,
method: 'put',
data: courseInfo
})
}
2、组件js
info.vue
updateData() {
this.saveBtnDisabled = true
course.updateCourseInfoById(this.courseInfo).then(response => {
this.$message({
type: 'success',
message: '修改成功!'
})
return response// 将响应结果传递给then
}).then(response => {
this.$router.push({ path: '/edu/course/chapter/' + response.data.courseId })
}).catch((response) => {
// console.log(response)
this.$message({
type: 'error',
message: '保存失败'
})
})
},