谷粒学院项目
项目后台设计---课程管理
本模块涉及6张数据库表:
edu_course: 课程表:存储课程基本信息
edu_course_description: 课程简介表:存储课程简介信息
edu_chapter: 课程章节表:存储课程章节信息
edu_video: 课程小节表:存储章节里面小节信息
edu_teacher: 讲师表
edu_subject: 分类表
课程相关表的关系:
一.添加课程基本信息--后端实现
1.使用代码生成器生成课程相关代码
2.细节问题
3.创建vo类,用于封装表单提交数据
package com.atguigu.eduservice.entity.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class CourseInfoVo {
@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;
}
4.编写controller和service部分
controller:
package com.atguigu.eduservice.controller;
import com.atguigu.commonutils.R;
import com.atguigu.eduservice.entity.vo.CourseInfoVo;
import com.atguigu.eduservice.service.EduCourseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* <p>
* 课程 前端控制器
* </p>
*
* @author testjava
* @since 2022-10-05
*/
@RestController
@RequestMapping("/eduservice/edu-course")
@CrossOrigin
public class EduCourseController {
@Autowired
private EduCourseService courseService;
// 添加课程基本信息的方法
@PostMapping("addCourseInfo")
public R addCourseInfo(@RequestBody CourseInfoVo courseInfoVo) {
// 返回添加之后课程id,为了后面添加大纲使用
String id = courseService.saveCourseInfo(courseInfoVo);
return R.ok().data("CourseId", id);
}
}
Service:
package com.atguigu.eduservice.service.impl;
import com.atguigu.eduservice.entity.EduCourse;
import com.atguigu.eduservice.entity.EduCourseDescription;
import com.atguigu.eduservice.entity.vo.CourseInfoVo;
import com.atguigu.eduservice.mapper.EduCourseMapper;
import com.atguigu.eduservice.service.EduCourseDescriptionService;
import com.atguigu.eduservice.service.EduCourseService;
import com.atguigu.servicebase.exceptionhandler.GuliException;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* <p>
* 课程 服务实现类
* </p>
*
* @author testjava
* @since 2022-10-05
*/
@Service
public class EduCourseServiceImpl extends ServiceImpl<EduCourseMapper, EduCourse> implements EduCourseService {
// 课程简介表的service对象
@Autowired
private EduCourseDescriptionService eduCourseDescriptionService;
// 添加课程基本信息的方法
@Override
public String saveCourseInfo(CourseInfoVo courseInfoVo) {
// 1.向课程表添加课程基本信息
// CourseInfoVo转换成eduCourse对象
EduCourse eduCourse = new EduCourse();
BeanUtils.copyProperties(courseInfoVo, eduCourse);
int insert = baseMapper.insert(eduCourse);
if (insert <= 0) {
// 添加失败
throw new GuliException(20001, "添加课程信息失败");
}
// 添加成功
// 得到添加课程成功以后的课程id
String cid = eduCourse.getId();
// 2.向课程简介表添加课程简介
// 创建课程描述对象
EduCourseDescription courseDescription = new EduCourseDescription();
// 手动设置描述id就是课程id
courseDescription.setId(cid);
// 往对象中赋值
courseDescription.setDescription(courseInfoVo.getDescription());
// 往描述表中添加数据
eduCourseDescriptionService.save(courseDescription);
return cid;
}
}
二.添加课程基本信息--前端实现
1.添加课程管理路由
2.创建调用后端的api接口
3.info.vue编写
(1)把讲师用下拉列表显示
(2)把课程分类用下拉列表显示,分类做成二级联动效果
(3)课程封面上传
(4)优化简介
1.整合tiny-mce组件
2.配置html变量
3.引入js脚本
4.页面中使用文本编辑器组件
(5)最终代码
<template>
<div class="app-container">
<h2 style="text-align: center;">发布新课程</h2>
<el-steps :active="1" process-status="wait" align-center style="marginbottom: 40px;">
<el-step title="填写课程基本信息"/>
<el-step title="创建课程大纲"/>
<el-step title="最终发布"/>
</el-steps>
<el-form label-width="120px">
<el-form-item label="课程标题">
<el-input v-model="courseInfo.title" placeholder=" 示例:机器学习项目课:从基础到搭建项目视频课程."></el-input>
</el-form-item>
<!-- 所属分类 TODO -->
<el-form-item label="课程分类">
<el-select v-model="courseInfo.subjectParentId" placeholder="一级分类" @change="subjectLevelOneChanged">
<el-option v-for="subject in subjectOneList" :key="subject.id" :label="subject.title" :value="subject.id"/>
</el-select>
<el-select v-model="courseInfo.subjectId" placeholder="二级分类">
<el-option v-for="subject in subjectTwoList" :key="subject.id" :label="subject.title" :value="subject.id"/>
</el-select>
</el-form-item>
<!-- 课程讲师 TODO -->
<!-- 课程讲师 -->
<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>
<el-form-item label="总课时">
<el-input-number :min="0" v-model="courseInfo.lessonNum" controls-position="right" placeholder="请填写课程总课时数"/>
</el-form-item>
<!-- 课程简介-->
<el-form-item label="课程简介">
<tinymce :height="300" v-model="courseInfo.description"/>
</el-form-item>
<!-- 课程封面 TODO -->
<!-- 课程封面-->
<el-form-item label="课程封面">
<el-upload
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
:action="BASE_API+'/eduoss/fileoss'"
class="avatar-uploader">
<img :src="courseInfo.cover">
</el-upload>
</el-form-item>
<el-form-item label="课程价格">
<el-input-number :min="0" v-model="courseInfo.price" controls-position="right" placeholder="请填写课程价格"/>
</el-form-item>
<el-form-item>
<el-button :disabled="saveBtnDisabled" type="primary" @click="saveOrUpdate">保存并下一步</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import course from '@/api/edu/course'
import subject from '@/api/edu/subject'
import Tinymce from '@/components/Tinymce' // 引入组件
export default{
// 声明组件
components: { Tinymce },
data(){
return{
saveBtnDisabled:false,
courseInfo:{ //前端向后端提交的表单对象
title: '',
subjectId: '', // 二级分类id
subjectParentId:'', // 一级分类id
teacherId: '',
lessonNum: 0,
description: '',
cover: '/static/01.jpg',
price: 0
},
BASE_API: process.env.BASE_API, // 接口API地址
teacherList:[], // 封装所有讲师的数据
subjectOneList:[], // 封装所有一级分类
subjectTwoList:[] // 封装所有二级分类
}
},
created(){
// 初始化所有讲师
this.getListTeacher()
// 初始化一级分类
this.getListOnesubject()
},
methods:{
// 上传封面成功调用的方法
handleAvatarSuccess(response, file){
this.$message({
type: 'success',
message: '上传成功'
})
// 给往后端传的对象赋值
this.courseInfo.cover = response.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
},
// 当点击某个一级分类时,调用这个方法
subjectLevelOneChanged(value){
// value就是一级分类的id值,框架帮我们做了封装
// 遍历所有一级分类
for(let i = 0; i < this.subjectOneList.length; i++){
if(this.subjectOneList[i].id === value){
// 如果当前遍历到的一级分类的id值与点击的一级分类的id值相同
// 添加二级分类
this.subjectTwoList = this.subjectOneList[i].children
// 把二级分类的id值清空
this.courseInfo.subjectId = ''
}
}
},
// 查询所有讲师
getListTeacher(){
course.getListTeacher()
.then(response => {
this.teacherList = response.data.items
})
},
// 查询所有一级分类
getListOnesubject(){
subject.getSubjectList()
.then(response =>{
this.subjectOneList = response.data.list
})
},
saveOrUpdate() {
// 调用方法,往后端发送数据
course.addCourseInfo(this.courseInfo)
.then(response => {
// 提示信息
this.$message({
type: 'success',
message: '添加课程信息成功'
});
// 跳转到第二步
this.$router.push({path:'/course/chapter/'+response.data.CourseId})
})
}
}
}
</script>
<style scoped>
.tinymce-container {
line-height: 29px;
}
</style>
三:课程大纲列表--后端实现
1.创建两个实体类,章节和小节,在章节实体类使用list表示小节
2.编写controller和service
package com.atguigu.eduservice.controller;
import com.atguigu.commonutils.R;
import com.atguigu.eduservice.entity.chapter.ChapterVo;
import com.atguigu.eduservice.service.EduChapterService;
import com.atguigu.eduservice.service.EduCourseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* <p>
* 课程 前端控制器
* </p>
*
* @author testjava
* @since 2022-10-05
*/
@RestController
@RequestMapping("/eduservice/chapter")
public class EduChapterController {
@Autowired
private EduChapterService eduChapterService;
// 课程大纲列表,根据课程id进行查询
@GetMapping("getChapterVide/{courseId}")
public R getChapterVide(@PathVariable String courseId) {
List<ChapterVo> list = eduChapterService.getChapterVideoByCourseId(courseId);
return R.ok().data("allChapterVideo", list);
}
}
package com.atguigu.eduservice.service.impl;
import com.atguigu.eduservice.entity.EduChapter;
import com.atguigu.eduservice.entity.EduVideo;
import com.atguigu.eduservice.entity.chapter.ChapterVo;
import com.atguigu.eduservice.entity.chapter.VideoVo;
import com.atguigu.eduservice.mapper.EduChapterMapper;
import com.atguigu.eduservice.service.EduChapterService;
import com.atguigu.eduservice.service.EduVideoService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* <p>
* 课程 服务实现类
* </p>
*
* @author testjava
* @since 2022-10-05
*/
@Service
public class EduChapterServiceImpl extends ServiceImpl<EduChapterMapper, EduChapter> implements EduChapterService {
@Autowired
private EduVideoService eduVideoService;
@Override
public List<ChapterVo> getChapterVideoByCourseId(String courseId) {
// 1.根据课程id查询出所有章节数据
// Select * from edu_chapter Where course_id = courseId;
QueryWrapper<EduChapter> wrapperChapter = new QueryWrapper<>();
wrapperChapter.eq("course_id", courseId);
// 得到所有章节数据
List<EduChapter> chapterList = baseMapper.selectList(wrapperChapter);
// 2.根据课程id查询出所有小节数据
// Select * from edu_video Where course_id = courseId;
QueryWrapper<EduVideo> wrapperVideo = new QueryWrapper<>();
wrapperVideo.eq("course_id", courseId);
// 得到所有小节数据
List<EduVideo> videoList = eduVideoService.list(wrapperVideo);
// 3.遍历查询章节list集合进行封装
List<ChapterVo> list = new ArrayList<>();
for (EduChapter eduChapter : chapterList) {
// 将eduChapter转化为ChapterVo
ChapterVo chapterVo = new ChapterVo();
BeanUtils.copyProperties(eduChapter, chapterVo);
list.add(chapterVo);
// 4.封装小节
for (EduVideo eduVideo : videoList) {
if (eduVideo.getCourseId().equals(eduChapter.getCourseId())) {
// 可以封装
VideoVo videoVo = new VideoVo();
BeanUtils.copyProperties(eduVideo, videoVo);
chapterVo.getChildren().add(videoVo);
}
}
}
return list;
}
}
四:课程大纲列表--前端实现
(1)定义调用后端api的方法
import request from '@/utils/request'
export default{
// 前端调用后端api的方法
// 1.根据课程id过去章节和小节数据列表
getAllChapterVideo(courseId){
return request({
url: `/eduservice/chapter/getChapterVide/${courseId}`,
method: 'get'
})
}
}
(2)前端chapter.vue页面初步实现
<template>
<div class="app-container">
<h2 style="text-align: center;">发布新课程</h2>
<el-steps :active="2" process-status="wait" align-center style="marginbottom: 40px;">
<el-step title="填写课程基本信息"/>
<el-step title="创建课程大纲"/>
<el-step title="提交审核"/>
</el-steps>
<!-- 自己编写的列表显示功能
<ul>
<li v-for="chapter in chapterVideoList" :key="chapter.id">
{{chapter.title}}
<ul>>
<li v-for="video in chapter.children" :key="video.id">
{{video.title}}
</li>
</ul>
</li>
</ul> -->
<el-button type="text">添加章节</el-button>
<!-- 章节 -->
<ul class="chanpterList">
<li v-for="chapter in chapterVideoList" :key="chapter.id">
<p>
{{ chapter.title }}
</p>
<!-- 视频 -->
<ul class="chanpterList videoList">
<li v-for="video in chapter.children" :key="video.id">
<p>
{{ video.title }}
</p>
</li>
</ul>
</li>
</ul>
<div>
<el-button @click="previous">上一步</el-button>
<el-button :disabled="saveBtnDisabled" type="primary" @click="next">下一步</el-button>
</div>
</div>
</template>
<script>
import chapter from '@/api/edu/chapter'
export default{
data(){
return{
saveBtnDisabled:false,
courseId:'', // 课程id
chapterVideoList:[]
}
},
created(){
// 获取到路由里面的课程id值
if(this.$route.params && this.$route.params.id){
this.courseId = this.$route.params.id
this.getChapterVideo()
}
},
methods:{
//根据课程id过去章节和小节数据列表
getChapterVideo(){
chapter.getAllChapterVideo(this.courseId)
.then(response =>{
this.chapterVideoList = response.data.allChapterVideo
})
},
previous(){
// 跳转到第一步
this.$router.push({path:'/course/info/'+this.courseId})
},
next(){
// 跳转到第二步
this.$router.push({path:'/course/publish/'+this.courseId})
}
}
}
</script>
<style scoped>
.chanpterList{
position: relative;
list-style: none;
margin: 0;
padding: 0;
}
.chanpterList li{
position: relative;
}
.chanpterList p{
float: left;
font-size: 20px;
margin: 10px 0;
padding: 10px;
height: 70px;
line-height: 50px;
width: 100%;
border: 1px solid #DDD;
}
.chanpterList .acts {
float: right;
font-size: 14px;
}
.videoList{
padding-left: 50px;
}
.videoList p{
float: left;
font-size: 14px;
margin: 10px 0;
padding: 10px;
height: 50px;
line-height: 30px;
width: 100%;
border: 1px dotted #DDD;
}
</style>
五:修改课程信息--后端实现
(1)定义controller
package com.atguigu.eduservice.controller;
import com.atguigu.commonutils.R;
import com.atguigu.eduservice.entity.vo.CourseInfoVo;
import com.atguigu.eduservice.service.EduCourseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* <p>
* 课程 前端控制器
* </p>
*
* @author testjava
* @since 2022-10-05
*/
@RestController
@RequestMapping("/eduservice/edu-course")
@CrossOrigin
public class EduCourseController {
@Autowired
private EduCourseService courseService;
// 添加课程基本信息的方法
@PostMapping("addCourseInfo")
public R addCourseInfo(@RequestBody CourseInfoVo courseInfoVo) {
// 返回添加之后课程id,为了后面添加大纲使用
String id = courseService.saveCourseInfo(courseInfoVo);
return R.ok().data("CourseId", id);
}
// 根据课程id查询课程基本信息
@GetMapping("getCourseInfo/{courseId}")
public R getCourseInfo(@PathVariable String courseId) {
CourseInfoVo courseInfoVo = courseService.getCourseInfo(courseId);
return R.ok().data("courseInfoVo", courseInfoVo);
}
// 修改课程信息
@PostMapping("updateCourseInfo")
public R updateCourseInfo(@RequestBody CourseInfoVo courseInfoVo) {
courseService.updateCourseInfo(courseInfoVo);
return R.ok();
}
}
(2)定义service
package com.atguigu.eduservice.service.impl;
import com.atguigu.eduservice.entity.EduCourse;
import com.atguigu.eduservice.entity.EduCourseDescription;
import com.atguigu.eduservice.entity.vo.CourseInfoVo;
import com.atguigu.eduservice.mapper.EduCourseMapper;
import com.atguigu.eduservice.service.EduCourseDescriptionService;
import com.atguigu.eduservice.service.EduCourseService;
import com.atguigu.servicebase.exceptionhandler.GuliException;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* <p>
* 课程 服务实现类
* </p>
*
* @author testjava
* @since 2022-10-05
*/
@Service
public class EduCourseServiceImpl extends ServiceImpl<EduCourseMapper, EduCourse> implements EduCourseService {
// 课程简介表的service对象
@Autowired
private EduCourseDescriptionService eduCourseDescriptionService;
// 添加课程基本信息的方法
@Override
public String saveCourseInfo(CourseInfoVo courseInfoVo) {
// 1.向课程表添加课程基本信息
// CourseInfoVo转换成eduCourse对象
EduCourse eduCourse = new EduCourse();
BeanUtils.copyProperties(courseInfoVo, eduCourse);
int insert = baseMapper.insert(eduCourse);
if (insert <= 0) {
// 添加失败
throw new GuliException(20001, "添加课程信息失败");
}
// 添加成功
// 得到添加课程成功以后的课程id
String cid = eduCourse.getId();
// 2.向课程简介表添加课程简介
// 创建课程描述对象
EduCourseDescription courseDescription = new EduCourseDescription();
// 手动设置描述id就是课程id
courseDescription.setId(cid);
// 往对象中赋值
courseDescription.setDescription(courseInfoVo.getDescription());
// 往描述表中添加数据
eduCourseDescriptionService.save(courseDescription);
return cid;
}
@Override
public CourseInfoVo getCourseInfo(String courseId) {
// 1.先查询课程表
EduCourse eduCourse = baseMapper.selectById(courseId);
// 2.查询描述表
EduCourseDescription eduCourseDescription = eduCourseDescriptionService.getById(courseId);
// 3.拼接数据
CourseInfoVo courseInfoVo = new CourseInfoVo();
BeanUtils.copyProperties(eduCourse, courseInfoVo);
BeanUtils.copyProperties(eduCourseDescription, courseInfoVo);
return courseInfoVo;
}
@Override
public void updateCourseInfo(CourseInfoVo courseInfoVo) {
// 1.修改课程表
EduCourse eduCourse = new EduCourse();
BeanUtils.copyProperties(courseInfoVo, eduCourse);
int update = baseMapper.updateById(eduCourse);
if(update == 0){
throw new GuliException(20001,"修改课程信息失败");
}
// 2.修改描述表
EduCourseDescription eduCourseDescription = new EduCourseDescription();
BeanUtils.copyProperties(courseInfoVo, eduCourseDescription);
eduCourseDescriptionService.updateById(eduCourseDescription);
}
}
六:修改课程信息--前端实现
(1)定义调用后端api的接口
(2)修改chapter.vue页面跳转路径
(3)在info.vue页面实现数据回显
403: 1.跨域 2.路径写错了
(4)最终实现
<template>
<div class="app-container">
<h2 style="text-align: center;">发布新课程</h2>
<el-steps :active="1" process-status="wait" align-center style="marginbottom: 40px;">
<el-step title="填写课程基本信息"/>
<el-step title="创建课程大纲"/>
<el-step title="最终发布"/>
</el-steps>
<el-form label-width="120px">
<el-form-item label="课程标题">
<el-input v-model="courseInfo.title" placeholder=" 示例:机器学习项目课:从基础到搭建项目视频课程."></el-input>
</el-form-item>
<!-- 所属分类 TODO -->
<el-form-item label="课程分类">
<el-select v-model="courseInfo.subjectParentId" placeholder="一级分类" @change="subjectLevelOneChanged">
<el-option v-for="subject in subjectOneList" :key="subject.id" :label="subject.title" :value="subject.id"/>
</el-select>
<el-select v-model="courseInfo.subjectId" placeholder="二级分类">
<el-option v-for="subject in subjectTwoList" :key="subject.id" :label="subject.title" :value="subject.id"/>
</el-select>
</el-form-item>
<!-- 课程讲师 TODO -->
<!-- 课程讲师 -->
<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>
<el-form-item label="总课时">
<el-input-number :min="0" v-model="courseInfo.lessonNum" controls-position="right" placeholder="请填写课程总课时数"/>
</el-form-item>
<!-- 课程简介-->
<el-form-item label="课程简介">
<tinymce :height="300" v-model="courseInfo.description"/>
</el-form-item>
<!-- 课程封面 TODO -->
<!-- 课程封面-->
<el-form-item label="课程封面">
<el-upload
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
:action="BASE_API+'/eduoss/fileoss'"
class="avatar-uploader">
<img :src="courseInfo.cover">
</el-upload>
</el-form-item>
<el-form-item label="课程价格">
<el-input-number :min="0" v-model="courseInfo.price" controls-position="right" placeholder="请填写课程价格"/>
</el-form-item>
<el-form-item>
<el-button :disabled="saveBtnDisabled" type="primary" @click="saveOrUpdate">保存并下一步</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import course from '@/api/edu/course'
import subject from '@/api/edu/subject'
import Tinymce from '@/components/Tinymce' // 引入组件
export default{
// 声明组件
components: { Tinymce },
data(){
return{
saveBtnDisabled:false,
courseInfo:{ //前端向后端提交的表单对象
title: '',
subjectId: '', // 二级分类id
subjectParentId:'', // 一级分类id
teacherId: '',
lessonNum: 0,
description: '',
cover: '/static/01.jpg',
price: 0
},
courseId:'',
BASE_API: process.env.BASE_API, // 接口API地址
teacherList:[], // 封装所有讲师的数据
subjectOneList:[], // 封装所有一级分类
subjectTwoList:[] // 封装所有二级分类
}
},
created(){
// 获取路由中的课程id值
if(this.$route.params && this.$route.params.id){
// 说明路径中有id值,我们做修改操作
this.courseId = this.$route.params.id
// 调用根据id查询的方法
this.getInfo()
} else {
// 说明路径中没有id值,我们做添加操作
// 初始化所有讲师
this.getListTeacher()
// 初始化一级分类
this.getListOnesubject()
}
},
watch: { // 监听
$route(to, from) { // 路由变化的方式,当路由发生变化,这个方法就会执行
this.init()
}
},
methods:{
// 清空相关数据
init(){
this.courseInfo = {}
this.courseInfo.cover = '/static/01.jpg'
},
// 根据课程id查询
getInfo(){
course.getCourseInfoId(this.courseId)
.then(response =>{
this.courseInfo = response.data.courseInfoVo
// 初始化分类列表
// 1.查询出所有分类
subject.getSubjectList()
.then(response =>{
// 2.获取所有一级分类
this.subjectOneList = response.data.list
for(let i = 0; i < this.subjectOneList.length; i++){
// 3.遍历所有一级分类
if(this.subjectOneList[i].id === this.courseInfo.subjectParentId){
// 当前遍历到的一级分类等于当前课程所属的一级分类
this.subjectTwoList = this.subjectOneList[i].children
}
}
})
// 初始化所有讲师
this.getListTeacher()
})
},
// 上传封面成功调用的方法
handleAvatarSuccess(response, file){
this.$message({
type: 'success',
message: '上传成功'
})
// 给往后端传的对象赋值
this.courseInfo.cover = response.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
},
// 当点击某个一级分类时,调用这个方法
subjectLevelOneChanged(value){
// value就是一级分类的id值,框架帮我们做了封装
// 遍历所有一级分类
for(let i = 0; i < this.subjectOneList.length; i++){
if(this.subjectOneList[i].id === value){
// 如果当前遍历到的一级分类的id值与点击的一级分类的id值相同
// 添加二级分类
this.subjectTwoList = this.subjectOneList[i].children
// 把二级分类的id值清空
this.courseInfo.subjectId = ''
}
}
},
// 查询所有讲师
getListTeacher(){
course.getListTeacher()
.then(response => {
this.teacherList = response.data.items
})
},
// 查询所有一级分类
getListOnesubject(){
subject.getSubjectList()
.then(response =>{
this.subjectOneList = response.data.list
})
},
// 添加课程
addCourse(){
// 调用方法,往后端发送数据
course.addCourseInfo(this.courseInfo)
.then(response => {
// 提示信息
this.$message({
type: 'success',
message: '添加课程信息成功'
});
// 跳转到第二步
this.$router.push({path:'/course/chapter/'+response.data.CourseId})
})
},
// 修改课程
updateCourse(){
course.updateCourseInfo(this.courseInfo)
.then(response =>{
// 提示信息
this.$message({
type: 'success',
message: '修改课程信息成功'
});
// 跳转到第二步
this.$router.push({path:'/course/chapter/'+this.courseId})
})
},
saveOrUpdate() {
// 判断是添加还是修改
if(!this.courseInfo.id){
// 添加
this.addCourse()
} else {
// 更新
this.updateCourse()
}
}
}
}
</script>
<style scoped>
.tinymce-container {
line-height: 29px;
}
</style>
七: 章节/小节--添加,修改,删除
1:增加添加章节按钮
<el-button type="text">添加章节</el-button>
2:点击添加章节按钮,弹出提示框
3:开发章节后端接口--添加、修改、删除
4:修改前端页面(chapter.vue)
(1)添加调用后端接口的api
(2)实现添加章节功能
出现这个,说明后端代码出现问题了
(3)实现章节修改功能
<template>
<div class="app-container">
<h2 style="text-align: center;">发布新课程</h2>
<el-steps :active="2" process-status="wait" align-center style="marginbottom: 40px;">
<el-step title="填写课程基本信息"/>
<el-step title="创建课程大纲"/>
<el-step title="提交审核"/>
</el-steps>
<!-- 自己编写的列表显示功能
<ul>
<li v-for="chapter in chapterVideoList" :key="chapter.id">
{{chapter.title}}
<ul>>
<li v-for="video in chapter.children" :key="video.id">
{{video.title}}
</li>
</ul>
</li>
</ul> -->
<el-button type="text" @click="dialogChapter">添加章节</el-button>
<!-- 章节 -->
<ul class="chanpterList">
<li v-for="chapter in chapterVideoList" :key="chapter.id">
<p>
{{ chapter.title }}
<span class="acts">
<el-button style="" type="text" @click="openEditChapter(chapter.id)">编辑</el-button>
<el-button type="text">删除</el-button>
</span>
</p>
<!-- 视频 -->
<ul class="chanpterList videoList">
<li v-for="video in chapter.children" :key="video.id">
<p>
{{ video.title }}
</p>
</li>
</ul>
</li>
</ul>
<div>
<el-button @click="previous">上一步</el-button>
<el-button :disabled="saveBtnDisabled" type="primary" @click="next">下一步</el-button>
</div>
<!-- 添加和修改章节表单 -->
<el-dialog :visible.sync="dialogChapterFormVisible" title="添加章节">
<el-form :model="chapter" label-width="120px">
<el-form-item label="章节标题">
<el-input v-model="chapter.title"/>
</el-form-item>
<el-form-item label="章节排序">
<el-input-number v-model="chapter.sort" :min="0" controls-position="right"/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogChapterFormVisible = false">取 消</el-button>
<el-button type="primary" @click="saveOrUpdate">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import chapter from '@/api/edu/chapter'
export default{
data(){
return{
saveBtnDisabled:false,
courseId:'', // 课程id
chapterVideoList:[],
dialogChapterFormVisible:false, // 表示章节弹框的值
chapter:{ // 封装章节数据
courseId:'',
title:'',
sort:0
},
}
},
created(){
// 获取到路由里面的课程id值
if(this.$route.params && this.$route.params.id){
this.courseId = this.$route.params.id
this.getChapterVideo()
}
},
methods:{
// 点击修改按钮后,调用的方法
openEditChapter(chapterId){
// 弹出修改框---和保存框是一个
this.dialogChapterFormVisible = true
// 根据chapterid查询出数据,进行回显
chapter.getChapterById(chapterId)
.then(response => {
this.chapter = response.data.eduChapter
})
},
// 点击添加章节按钮后,执行的方法
dialogChapter(){
this.dialogChapterFormVisible = true
// 章节数据清空
this.chapter = {}
},
// 添加章节
addChapter(){
// 设置课程id到chapter对象当中去
this.chapter.courseId = this.courseId
chapter.addChapter(this.chapter)
.then(response =>{
// 关闭弹窗
this.dialogChapterFormVisible = false
// 提示成功信息
this.$message({
type: 'success',
message: '操作成功!'
});
// 刷新页面
this.getChapterVideo()
})
},
// 修改章节的方法
updateChapter(){
chapter.updateChapter(this.chapter)
.then(response =>{
// 关闭弹窗
this.dialogChapterFormVisible = false
// 提示成功信息
this.$message({
type: 'success',
message: '修改成功!'
});
// 刷新页面
this.getChapterVideo()
})
},
// 添加/修改 章节
saveOrUpdate(){
// 根据章节里面是否有id来进行判断是修改还是添加
if(this.chapter.courseId == ''){
// 没有课程id
// 添加
this.addChapter()
} else {
// 修改
this.updateChapter()
}
},
//根据课程id获取章节和小节数据列表
getChapterVideo(){
chapter.getAllChapterVideo(this.courseId)
.then(response =>{
this.chapterVideoList = response.data.allChapterVideo
})
},
previous(){
// 跳转到第一步
this.$router.push({path:'/course/info/'+this.courseId})
},
next(){
// 跳转到第二步
this.$router.push({path:'/course/publish/'+this.courseId})
}
}
}
</script>
<style scoped>
.chanpterList{
position: relative;
list-style: none;
margin: 0;
padding: 0;
}
.chanpterList li{
position: relative;
}
.chanpterList p{
float: left;
font-size: 20px;
margin: 10px 0;
padding: 10px;
height: 70px;
line-height: 50px;
width: 100%;
border: 1px solid #DDD;
}
.chanpterList .acts {
float: right;
font-size: 14px;
}
.videoList{
padding-left: 50px;
}
.videoList p{
float: left;
font-size: 14px;
margin: 10px 0;
padding: 10px;
height: 50px;
line-height: 30px;
width: 100%;
border: 1px dotted #DDD;
}
</style>
(4)实现章节删除功能
TODO 需要判断是否有小节
(5)实现小节添加功能--后端接口实现
```
package com.atguigu.eduservice.controller;
import com.atguigu.commonutils.R;
import com.atguigu.eduservice.entity.EduVideo;
import com.atguigu.eduservice.service.EduVideoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* <p>
* 课程视频 前端控制器
* </p>
*
* @author testjava
* @since 2022-10-05
*/
@RestController
@RequestMapping("/eduservice/video")
@CrossOrigin
public class EduVideoController {
@Autowired
EduVideoService eduVideoService;
// 添加小节
@PostMapping("addVideo")
public R addVideo(@RequestBody EduVideo eduVideo) {
eduVideoService.save(eduVideo);
return R.ok();
}
// 根据小节id查询小节
@GetMapping("getVideoById/{videoId}")
public R getVideoById(@PathVariable String videoId) {
EduVideo eduVideo = eduVideoService.getById(videoId);
return R.ok().data("eduVideo", eduVideo);
}
// 根据小节id删除小节
// TODO 后面这个方法需要完善:在删除小节时,同时要把里面的视频也删除
@DeleteMapping("deleteVideo/{videoId}")
public R deleteVideo(@PathVariable String videoId) {
eduVideoService.removeById(videoId);
return R.ok();
}
// 修改小节
@PostMapping("updateVideo")
public R updateVideo(@RequestBody EduVideo eduVideo) {
eduVideoService.updateById(eduVideo);
return R.ok();
}
}
```
(5)实现小节添加功能--前端页面实现
定义api接口
import request from '@/utils/request'
export default{
// 前端调用后端api的方法
// 添加小节的方法
addVideo(video){
return request({
url: `/eduservice/video/addVideo`,
method: 'post',
data: video
})
},
// 根据小节id删除小节
deleteVideo(videoId){
return request({
url: `/eduservice/video/deleteVideo/${videoId}`,
method: 'delete',
})
},
// 修改小节
updateVideo(video){
return request({
url: `/eduservice/video/updateVideo`,
method: 'post',
data: video
})
},
// 根据小节id查询小节
getVideoById(videoId){
return request({
url: `/eduservice/video/getVideoById/${videoId}`,
method: 'get',
})
}
}
chapter.vue页面的完善
<template>
<div class="app-container">
<h2 style="text-align: center;">发布新课程</h2>
<el-steps :active="2" process-status="wait" align-center style="marginbottom: 40px;">
<el-step title="填写课程基本信息"/>
<el-step title="创建课程大纲"/>
<el-step title="提交审核"/>
</el-steps>
<!-- 自己编写的列表显示功能
<ul>
<li v-for="chapter in chapterVideoList" :key="chapter.id">
{{chapter.title}}
<ul>>
<li v-for="video in chapter.children" :key="video.id">
{{video.title}}
</li>
</ul>
</li>
</ul> -->
<el-button type="text" @click="dialogChapter">添加章节</el-button>
<!-- 章节 -->
<ul class="chanpterList">
<li v-for="chapter in chapterVideoList" :key="chapter.id">
<p>
{{ chapter.title }}
<span class="acts">
<el-button type="text" @click="openVideo(chapter.id)">添加小节</el-button>
<el-button style="" type="text" @click="openEditChapter(chapter.id)">编辑</el-button>
<el-button type="text" @click="removeChapter(chapter.id)">删除</el-button>
</span>
</p>
<!-- 视频 -->
<ul class="chanpterList videoList">
<li v-for="video in chapter.children" :key="video.id">
<p>
{{ video.title }}
</p>
</li>
</ul>
</li>
</ul>
<div>
<el-button @click="previous">上一步</el-button>
<el-button :disabled="saveBtnDisabled" type="primary" @click="next">下一步</el-button>
</div>
<!-- 添加和修改章节表单 -->
<el-dialog :visible.sync="dialogChapterFormVisible" title="添加章节">
<el-form :model="chapter" label-width="120px">
<el-form-item label="章节标题">
<el-input v-model="chapter.title"/>
</el-form-item>
<el-form-item label="章节排序">
<el-input-number v-model="chapter.sort" :min="0" controls-position="right"/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogChapterFormVisible = false">取 消</el-button>
<el-button type="primary" @click="saveOrUpdate">确 定</el-button>
</div>
</el-dialog>
<!-- 添加和修改课时表单 -->
<el-dialog :visible.sync="dialogVideoFormVisible" title="添加课时">
<el-form :model="video" label-width="120px">
<el-form-item label="课时标题">
<el-input v-model="video.title"/>
</el-form-item>
<el-form-item label="课时排序">
<el-input-number v-model="video.sort" :min="0" controls-position="right"/>
</el-form-item>
<el-form-item label="是否免费">
<el-radio-group v-model="video.free">
<el-radio :label="true">免费</el-radio>
<el-radio :label="false">默认</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="上传视频">
<!-- TODO -->
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVideoFormVisible = false">取 消</el-button>
<el-button :disabled="saveVideoBtnDisabled" type="primary" @click="saveOrUpdateVideo">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import chapter from '@/api/edu/chapter'
import video from '@/api/edu/video'
export default{
data(){
return{
saveBtnDisabled:false,
courseId:'', // 课程id
chapterVideoList:[],
dialogChapterFormVisible:false, // 表示章节弹框的值
dialogVideoFormVisible:false, // 表示小节弹框的值
chapter:{ // 封装章节数据
courseId:'',
title:'',
sort:0
},
video:{ // 封装小节数据
chapterId:'',
courseId:'',
title:'',
sort:0,
free:0,
videoSourceId:''
}
}
},
created(){
// 获取到路由里面的课程id值
if(this.$route.params && this.$route.params.id){
this.courseId = this.$route.params.id
this.getChapterVideo()
}
},
methods:{
//**************************************针对小节的操作**********************************/
// 点击添加小节,调用这个方法
openVideo(chapterId){
// 弹出框
this.dialogVideoFormVisible = true
// 给小节数据添加上章节id
this.video.chapterId = chapterId
// 小节数据清空
this.video.title = ''
this.video.sort = 0
this.free = 0
},
// 添加小节的方法
addVideo(){
// 设置课程id到video对象当中去
this.video.courseId = this.courseId
// 调用api接口,往数据库中添加小节
video.addVideo(this.video)
.then(response => {
// 关闭弹窗
this.dialogVideoFormVisible = false
// 提示成功信息
this.$message({
type: 'success',
message: '操作成功!'
});
// 刷新页面
this.getChapterVideo()
})
},
// 点击小节弹框的确定以后,要执行的方法
saveOrUpdateVideo(){
this.addVideo()
},
//**************************************针对章节的操作**********************************/
// 点击删除按钮以后,调用的方法
removeChapter(chapterId){
// 删除提示框
this.$confirm('此操作将永久删除讲师记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() =>{
// 点击确定以后要执行的操作
// 调用后端api接口,进行删除操作
chapter.deleteChapter(chapterId)
.then(response => {
// 提示删除成功信息
this.$message({
type: 'success',
message: '删除成功!'
});
// 返回页面
// 根据课程id获取章节和小节数据列表
// 这里我们已经有了课程id了,所以不需要在重新赋值了
this.getChapterVideo()
})
})
},
// 点击修改按钮后,调用的方法
openEditChapter(chapterId){
// 弹出修改框---和保存框是一个
this.dialogChapterFormVisible = true
// 根据chapterid查询出数据,进行回显
chapter.getChapterById(chapterId)
.then(response => {
this.chapter = response.data.eduChapter
})
},
// 点击添加章节按钮后,执行的方法
dialogChapter(){
this.dialogChapterFormVisible = true
// 章节数据清空
this.chapter = {}
},
// 添加章节
addChapter(){
// 设置课程id到chapter对象当中去
this.chapter.courseId = this.courseId
chapter.addChapter(this.chapter)
.then(response =>{
// 关闭弹窗
this.dialogChapterFormVisible = false
// 提示成功信息
this.$message({
type: 'success',
message: '操作成功!'
});
// 刷新页面
this.getChapterVideo()
})
},
// 修改章节的方法
updateChapter(){
chapter.updateChapter(this.chapter)
.then(response =>{
// 关闭弹窗
this.dialogChapterFormVisible = false
// 提示成功信息
this.$message({
type: 'success',
message: '修改成功!'
});
// 刷新页面
this.getChapterVideo()
})
},
// 添加/修改 章节
saveOrUpdate(){
// 根据章节里面是否有id来进行判断是修改还是添加
if(this.chapter.courseId){
// 没有课程id
// 添加
this.updateChapter()
} else {
// 修改
this.addChapter()
}
},
//根据课程id获取章节和小节数据列表
getChapterVideo(){
chapter.getAllChapterVideo(this.courseId)
.then(response =>{
this.chapterVideoList = response.data.allChapterVideo
})
},
previous(){
// 跳转到第一步
this.$router.push({path:'/course/info/'+this.courseId})
},
next(){
// 跳转到第二步
this.$router.push({path:'/course/publish/'+this.courseId})
}
}
}
</script>
<style scoped>
.chanpterList{
position: relative;
list-style: none;
margin: 0;
padding: 0;
}
.chanpterList li{
position: relative;
}
.chanpterList p{
float: left;
font-size: 20px;
margin: 10px 0;
padding: 10px;
height: 70px;
line-height: 50px;
width: 100%;
border: 1px solid #DDD;
}
.chanpterList .acts {
float: right;
font-size: 14px;
}
.videoList{
padding-left: 50px;
}
.videoList p{
float: left;
font-size: 14px;
margin: 10px 0;
padding: 10px;
height: 50px;
line-height: 30px;
width: 100%;
border: 1px dotted #DDD;
}
</style>
(6)实现小节修改功能--前端页面实现
chapter.vue
<template>
<div class="app-container">
<h2 style="text-align: center;">发布新课程</h2>
<el-steps :active="2" process-status="wait" align-center style="marginbottom: 40px;">
<el-step title="填写课程基本信息"/>
<el-step title="创建课程大纲"/>
<el-step title="提交审核"/>
</el-steps>
<!-- 自己编写的列表显示功能
<ul>
<li v-for="chapter in chapterVideoList" :key="chapter.id">
{{chapter.title}}
<ul>>
<li v-for="video in chapter.children" :key="video.id">
{{video.title}}
</li>
</ul>
</li>
</ul> -->
<el-button type="text" @click="dialogChapter">添加章节</el-button>
<!-- 章节 -->
<ul class="chanpterList">
<li v-for="chapter in chapterVideoList" :key="chapter.id">
<p>
{{ chapter.title }}
<span class="acts">
<el-button type="text" @click="openVideo(chapter.id)">添加小节</el-button>
<el-button style="" type="text" @click="openEditChapter(chapter.id)">编辑</el-button>
<el-button type="text" @click="removeChapter(chapter.id)">删除</el-button>
</span>
</p>
<!-- 视频 -->
<ul class="chanpterList videoList">
<li v-for="video in chapter.children" :key="video.id">
<p>
{{ video.title }}
<span class="acts">
<el-button style="" type="text" @click="openEditVideo(video.id)">编辑</el-button>
<el-button type="text" @click="removeChapter(chapter.id)">删除</el-button>
</span>
</p>
</li>
</ul>
</li>
</ul>
<div>
<el-button @click="previous">上一步</el-button>
<el-button :disabled="saveBtnDisabled" type="primary" @click="next">下一步</el-button>
</div>
<!-- 添加和修改章节表单 -->
<el-dialog :visible.sync="dialogChapterFormVisible" title="添加章节">
<el-form :model="chapter" label-width="120px">
<el-form-item label="章节标题">
<el-input v-model="chapter.title"/>
</el-form-item>
<el-form-item label="章节排序">
<el-input-number v-model="chapter.sort" :min="0" controls-position="right"/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogChapterFormVisible = false">取 消</el-button>
<el-button type="primary" @click="saveOrUpdate">确 定</el-button>
</div>
</el-dialog>
<!-- 添加和修改课时表单 -->
<el-dialog :visible.sync="dialogVideoFormVisible" title="添加课时">
<el-form :model="video" label-width="120px">
<el-form-item label="课时标题">
<el-input v-model="video.title"/>
</el-form-item>
<el-form-item label="课时排序">
<el-input-number v-model="video.sort" :min="0" controls-position="right"/>
</el-form-item>
<el-form-item label="是否免费">
<el-radio-group v-model="video.free">
<el-radio :label="true">免费</el-radio>
<el-radio :label="false">默认</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="上传视频">
<!-- TODO -->
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVideoFormVisible = false">取 消</el-button>
<el-button :disabled="saveVideoBtnDisabled" type="primary" @click="saveOrUpdateVideo">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import chapter from '@/api/edu/chapter'
import video from '@/api/edu/video'
export default{
data(){
return{
saveBtnDisabled:false,
courseId:'', // 课程id
chapterVideoList:[],
dialogChapterFormVisible:false, // 表示章节弹框的值
dialogVideoFormVisible:false, // 表示小节弹框的值
chapter:{ // 封装章节数据
courseId:'',
title:'',
sort:0
},
video:{ // 封装小节数据
chapterId:'',
courseId:'',
title:'',
sort:0,
free:0,
videoSourceId:''
}
}
},
created(){
// 获取到路由里面的课程id值
if(this.$route.params && this.$route.params.id){
this.courseId = this.$route.params.id
this.getChapterVideo()
}
},
methods:{
//**************************************针对小节的操作**********************************/
// 点击修改按钮后,调用的方法
openEditVideo(videoId){
// 弹出修改框 --- 和添加框是一个
this.dialogVideoFormVisible = true
// 根据小节id查询出数据,进行回显
video.getVideoById(videoId)
.then(response => {
this.video = response.data.eduVideo
})
},
// 点击添加小节,调用这个方法
openVideo(chapterId){
// 弹出框
this.dialogVideoFormVisible = true
// 给小节数据添加上章节id
this.video.chapterId = chapterId
// 小节数据清空
this.video.title = ''
this.video.sort = 0
this.free = 0
},
// 添加小节的方法
addVideo(){
alert("debug")
// 设置课程id到video对象当中去
this.video.courseId = this.courseId
// 调用api接口,往数据库中添加小节
video.addVideo(this.video)
.then(response => {
// 关闭弹窗
this.dialogVideoFormVisible = false
// 提示成功信息
this.$message({
type: 'success',
message: '操作成功!'
});
// 刷新页面
this.getChapterVideo()
})
},
// 修改小节的方法
updateVideo(){
video.updateVideo(this.video)
.then(response => {
// 关闭弹窗
this.dialogVideoFormVisible = false
// 提示成功信息
this.$message({
type: 'success',
message: '修改成功!'
});
// 刷新页面
this.getChapterVideo()
})
},
// 点击小节弹框的确定以后,要执行的方法
saveOrUpdateVideo(){
// 根据章节id来判断是添加还是修改小节
if(this.video.chapterId){
// 有章节id,说明是修改小节
this.updateVideo()
} else {
// 添加小节
this.addVideo()
}
},
//**************************************针对章节的操作**********************************/
// 点击删除按钮以后,调用的方法
removeChapter(chapterId){
// 删除提示框
this.$confirm('此操作将永久删除讲师记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() =>{
// 点击确定以后要执行的操作
// 调用后端api接口,进行删除操作
chapter.deleteChapter(chapterId)
.then(response => {
// 提示删除成功信息
this.$message({
type: 'success',
message: '删除成功!'
});
// 返回页面
// 根据课程id获取章节和小节数据列表
// 这里我们已经有了课程id了,所以不需要在重新赋值了
this.getChapterVideo()
})
})
},
// 点击修改按钮后,调用的方法
openEditChapter(chapterId){
// 弹出修改框---和保存框是一个
this.dialogChapterFormVisible = true
// 根据chapterid查询出数据,进行回显
chapter.getChapterById(chapterId)
.then(response => {
this.chapter = response.data.eduChapter
})
},
// 点击添加章节按钮后,执行的方法
dialogChapter(){
this.dialogChapterFormVisible = true
// 章节数据清空
this.chapter = {}
},
// 添加章节
addChapter(){
// 设置课程id到chapter对象当中去
this.chapter.courseId = this.courseId
chapter.addChapter(this.chapter)
.then(response =>{
// 关闭弹窗
this.dialogChapterFormVisible = false
// 提示成功信息
this.$message({
type: 'success',
message: '操作成功!'
});
// 刷新页面
this.getChapterVideo()
})
},
// 修改章节的方法
updateChapter(){
chapter.updateChapter(this.chapter)
.then(response =>{
// 关闭弹窗
this.dialogChapterFormVisible = false
// 提示成功信息
this.$message({
type: 'success',
message: '修改成功!'
});
// 刷新页面
this.getChapterVideo()
})
},
// 添加/修改 章节
saveOrUpdate(){
// 根据章节里面是否有课程id来进行判断是修改还是添加
if(this.chapter.courseId){
// 有课程id
// 修改
this.updateChapter()
} else {
// 添加
this.addChapter()
}
},
//根据课程id获取章节和小节数据列表
getChapterVideo(){
chapter.getAllChapterVideo(this.courseId)
.then(response =>{
this.chapterVideoList = response.data.allChapterVideo
})
},
previous(){
// 跳转到第一步
this.$router.push({path:'/course/info/'+this.courseId})
},
next(){
// 跳转到第二步
this.$router.push({path:'/course/publish/'+this.courseId})
}
}
}
</script>
<style scoped>
.chanpterList{
position: relative;
list-style: none;
margin: 0;
padding: 0;
}
.chanpterList li{
position: relative;
}
.chanpterList p{
float: left;
font-size: 20px;
margin: 10px 0;
padding: 10px;
height: 70px;
line-height: 50px;
width: 100%;
border: 1px solid #DDD;
}
.chanpterList .acts {
float: right;
font-size: 14px;
}
.videoList{
padding-left: 50px;
}
.videoList p{
float: left;
font-size: 14px;
margin: 10px 0;
padding: 10px;
height: 50px;
line-height: 30px;
width: 100%;
border: 1px dotted #DDD;
}
</style>
(7)实现小节删除功能--前端页面实现
chapter.vue
<template>
<div class="app-container">
<h2 style="text-align: center;">发布新课程</h2>
<el-steps :active="2" process-status="wait" align-center style="marginbottom: 40px;">
<el-step title="填写课程基本信息"/>
<el-step title="创建课程大纲"/>
<el-step title="提交审核"/>
</el-steps>
<!-- 自己编写的列表显示功能
<ul>
<li v-for="chapter in chapterVideoList" :key="chapter.id">
{{chapter.title}}
<ul>>
<li v-for="video in chapter.children" :key="video.id">
{{video.title}}
</li>
</ul>
</li>
</ul> -->
<el-button type="text" @click="dialogChapter">添加章节</el-button>
<!-- 章节 -->
<ul class="chanpterList">
<li v-for="chapter in chapterVideoList" :key="chapter.id">
<p>
{{ chapter.title }}
<span class="acts">
<el-button type="text" @click="openVideo(chapter.id)">添加小节</el-button>
<el-button style="" type="text" @click="openEditChapter(chapter.id)">编辑</el-button>
<el-button type="text" @click="removeChapter(chapter.id)">删除</el-button>
</span>
</p>
<!-- 视频 -->
<ul class="chanpterList videoList">
<li v-for="video in chapter.children" :key="video.id">
<p>
{{ video.title }}
<span class="acts">
<el-button style="" type="text" @click="openEditVideo(video.id)">编辑</el-button>
<el-button type="text" @click="removeVideo(video.id)">删除</el-button>
</span>
</p>
</li>
</ul>
</li>
</ul>
<div>
<el-button @click="previous">上一步</el-button>
<el-button :disabled="saveBtnDisabled" type="primary" @click="next">下一步</el-button>
</div>
<!-- 添加和修改章节表单 -->
<el-dialog :visible.sync="dialogChapterFormVisible" title="添加章节">
<el-form :model="chapter" label-width="120px">
<el-form-item label="章节标题">
<el-input v-model="chapter.title"/>
</el-form-item>
<el-form-item label="章节排序">
<el-input-number v-model="chapter.sort" :min="0" controls-position="right"/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogChapterFormVisible = false">取 消</el-button>
<el-button type="primary" @click="saveOrUpdate">确 定</el-button>
</div>
</el-dialog>
<!-- 添加和修改课时表单 -->
<el-dialog :visible.sync="dialogVideoFormVisible" title="添加课时">
<el-form :model="video" label-width="120px">
<el-form-item label="课时标题">
<el-input v-model="video.title"/>
</el-form-item>
<el-form-item label="课时排序">
<el-input-number v-model="video.sort" :min="0" controls-position="right"/>
</el-form-item>
<el-form-item label="是否免费">
<el-radio-group v-model="video.free">
<el-radio :label="true">免费</el-radio>
<el-radio :label="false">默认</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="上传视频">
<!-- TODO -->
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVideoFormVisible = false">取 消</el-button>
<el-button :disabled="saveVideoBtnDisabled" type="primary" @click="saveOrUpdateVideo">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import chapter from '@/api/edu/chapter'
import video from '@/api/edu/video'
export default{
data(){
return{
saveBtnDisabled:false,
courseId:'', // 课程id
chapterVideoList:[],
dialogChapterFormVisible:false, // 表示章节弹框的值
dialogVideoFormVisible:false, // 表示小节弹框的值
chapter:{ // 封装章节数据
courseId:'',
title:'',
sort:0
},
video:{ // 封装小节数据
chapterId:'',
courseId:'',
title:'',
sort:0,
free:0,
videoSourceId:''
}
}
},
created(){
// 获取到路由里面的课程id值
if(this.$route.params && this.$route.params.id){
this.courseId = this.$route.params.id
this.getChapterVideo()
}
},
methods:{
//**************************************针对小节的操作**********************************/
// 点击删除按钮后,调用的方法
removeVideo(videoId){
// 删除提示框
this.$confirm('此操作将永久删除小节记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 点确定以后执行的操作
video.deleteVideo(videoId)
.then(response => {
// 提示删除成功信息
this.$message({
type: 'success',
message: '删除成功!'
});
// 返回页面
// 根据课程id获取章节和小节数据列表
// 这里我们已经有了课程id了,所以不需要在重新赋值了
this.getChapterVideo()
})
})
},
// 点击修改按钮后,调用的方法
openEditVideo(videoId){
// 弹出修改框 --- 和添加框是一个
this.dialogVideoFormVisible = true
// 根据小节id查询出数据,进行回显
video.getVideoById(videoId)
.then(response => {
this.video = response.data.eduVideo
})
},
// 点击添加小节,调用这个方法
openVideo(chapterId){
// 弹出框
this.dialogVideoFormVisible = true
// 给小节数据添加上章节id
this.video.chapterId = chapterId
// 小节数据清空
this.video.title = ''
this.video.sort = 0
this.free = 0
},
// 添加小节的方法
addVideo(){
alert("debug")
// 设置课程id到video对象当中去
this.video.courseId = this.courseId
// 调用api接口,往数据库中添加小节
video.addVideo(this.video)
.then(response => {
// 关闭弹窗
this.dialogVideoFormVisible = false
// 提示成功信息
this.$message({
type: 'success',
message: '操作成功!'
});
// 刷新页面
this.getChapterVideo()
})
},
// 修改小节的方法
updateVideo(){
video.updateVideo(this.video)
.then(response => {
// 关闭弹窗
this.dialogVideoFormVisible = false
// 提示成功信息
this.$message({
type: 'success',
message: '修改成功!'
});
// 刷新页面
this.getChapterVideo()
})
},
// 点击小节弹框的确定以后,要执行的方法
saveOrUpdateVideo(){
// 根据章节id来判断是添加还是修改小节
if(this.video.chapterId){
// 有章节id,说明是修改小节
this.updateVideo()
} else {
// 添加小节
this.addVideo()
}
},
//**************************************针对章节的操作**********************************/
// 点击删除按钮以后,调用的方法
removeChapter(chapterId){
// 删除提示框
this.$confirm('此操作将永久删除章节记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() =>{
// 点击确定以后要执行的操作
// 调用后端api接口,进行删除操作
chapter.deleteChapter(chapterId)
.then(response => {
// 提示删除成功信息
this.$message({
type: 'success',
message: '删除成功!'
});
// 返回页面
// 根据课程id获取章节和小节数据列表
// 这里我们已经有了课程id了,所以不需要在重新赋值了
this.getChapterVideo()
})
})
},
// 点击修改按钮后,调用的方法
openEditChapter(chapterId){
// 弹出修改框---和保存框是一个
this.dialogChapterFormVisible = true
// 根据chapterid查询出数据,进行回显
chapter.getChapterById(chapterId)
.then(response => {
this.chapter = response.data.eduChapter
})
},
// 点击添加章节按钮后,执行的方法
dialogChapter(){
this.dialogChapterFormVisible = true
// 章节数据清空
this.chapter = {}
},
// 添加章节
addChapter(){
// 设置课程id到chapter对象当中去
this.chapter.courseId = this.courseId
chapter.addChapter(this.chapter)
.then(response =>{
// 关闭弹窗
this.dialogChapterFormVisible = false
// 提示成功信息
this.$message({
type: 'success',
message: '操作成功!'
});
// 刷新页面
this.getChapterVideo()
})
},
// 修改章节的方法
updateChapter(){
chapter.updateChapter(this.chapter)
.then(response =>{
// 关闭弹窗
this.dialogChapterFormVisible = false
// 提示成功信息
this.$message({
type: 'success',
message: '修改成功!'
});
// 刷新页面
this.getChapterVideo()
})
},
// 添加/修改 章节
saveOrUpdate(){
// 根据章节里面是否有课程id来进行判断是修改还是添加
if(this.chapter.courseId){
// 有课程id
// 修改
this.updateChapter()
} else {
// 添加
this.addChapter()
}
},
//根据课程id获取章节和小节数据列表
getChapterVideo(){
chapter.getAllChapterVideo(this.courseId)
.then(response =>{
this.chapterVideoList = response.data.allChapterVideo
})
},
previous(){
// 跳转到第一步
this.$router.push({path:'/course/info/'+this.courseId})
},
next(){
// 跳转到第二步
this.$router.push({path:'/course/publish/'+this.courseId})
}
}
}
</script>
<style scoped>
.chanpterList{
position: relative;
list-style: none;
margin: 0;
padding: 0;
}
.chanpterList li{
position: relative;
}
.chanpterList p{
float: left;
font-size: 20px;
margin: 10px 0;
padding: 10px;
height: 70px;
line-height: 50px;
width: 100%;
border: 1px solid #DDD;
}
.chanpterList .acts {
float: right;
font-size: 14px;
}
.videoList{
padding-left: 50px;
}
.videoList p{
float: left;
font-size: 14px;
margin: 10px 0;
padding: 10px;
height: 50px;
line-height: 30px;
width: 100%;
border: 1px dotted #DDD;
}
</style>
(8)chapter.vue页面最终实现
<template>
<div class="app-container">
<h2 style="text-align: center;">发布新课程</h2>
<el-steps :active="2" process-status="wait" align-center style="marginbottom: 40px;">
<el-step title="填写课程基本信息"/>
<el-step title="创建课程大纲"/>
<el-step title="提交审核"/>
</el-steps>
<!-- 自己编写的列表显示功能
<ul>
<li v-for="chapter in chapterVideoList" :key="chapter.id">
{{chapter.title}}
<ul>>
<li v-for="video in chapter.children" :key="video.id">
{{video.title}}
</li>
</ul>
</li>
</ul> -->
<el-button type="text" @click="dialogChapter">添加章节</el-button>
<!-- 章节 -->
<ul class="chanpterList">
<li v-for="chapter in chapterVideoList" :key="chapter.id">
<p>
{{ chapter.title }}
<span class="acts">
<el-button type="text" @click="openVideo(chapter.id)">添加小节</el-button>
<el-button style="" type="text" @click="openEditChapter(chapter.id)">编辑</el-button>
<el-button type="text" @click="removeChapter(chapter.id)">删除</el-button>
</span>
</p>
<!-- 视频 -->
<ul class="chanpterList videoList">
<li v-for="video in chapter.children" :key="video.id">
<p>
{{ video.title }}
<span class="acts">
<el-button style="" type="text" @click="openEditVideo(video.id)">编辑</el-button>
<el-button type="text" @click="removeVideo(video.id)">删除</el-button>
</span>
</p>
</li>
</ul>
</li>
</ul>
<div>
<el-button @click="previous">上一步</el-button>
<el-button :disabled="saveBtnDisabled" type="primary" @click="next">下一步</el-button>
</div>
<!-- 添加和修改章节表单 -->
<el-dialog :visible.sync="dialogChapterFormVisible" title="添加章节">
<el-form :model="chapter" label-width="120px">
<el-form-item label="章节标题">
<el-input v-model="chapter.title"/>
</el-form-item>
<el-form-item label="章节排序">
<el-input-number v-model="chapter.sort" :min="0" controls-position="right"/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogChapterFormVisible = false">取 消</el-button>
<el-button type="primary" @click="saveOrUpdate">确 定</el-button>
</div>
</el-dialog>
<!-- 添加和修改课时表单 -->
<el-dialog :visible.sync="dialogVideoFormVisible" title="添加课时">
<el-form :model="video" label-width="120px">
<el-form-item label="课时标题">
<el-input v-model="video.title"/>
</el-form-item>
<el-form-item label="课时排序">
<el-input-number v-model="video.sort" :min="0" controls-position="right"/>
</el-form-item>
<el-form-item label="是否免费">
<el-radio-group v-model="video.free">
<el-radio :label="true">免费</el-radio>
<el-radio :label="false">默认</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="上传视频">
<!-- TODO -->
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVideoFormVisible = false">取 消</el-button>
<el-button :disabled="saveVideoBtnDisabled" type="primary" @click="saveOrUpdateVideo">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import chapter from '@/api/edu/chapter'
import video from '@/api/edu/video'
export default{
data(){
return{
saveBtnDisabled:false,
courseId:'', // 课程id
chapterVideoList:[],
dialogChapterFormVisible:false, // 表示章节弹框的值
dialogVideoFormVisible:false, // 表示小节弹框的值
chapter:{ // 封装章节数据
courseId:'',
title:'',
sort:0
},
video:{ // 封装小节数据
chapterId:'',
courseId:'',
title:'',
sort:0,
free:0,
videoSourceId:''
}
}
},
created(){
// 获取到路由里面的课程id值
if(this.$route.params && this.$route.params.id){
this.courseId = this.$route.params.id
this.getChapterVideo()
}
},
methods:{
//**************************************针对小节的操作**********************************/
// 点击删除按钮后,调用的方法
removeVideo(videoId){
// 删除提示框
this.$confirm('此操作将永久删除小节记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 点确定以后执行的操作
video.deleteVideo(videoId)
.then(response => {
// 提示删除成功信息
this.$message({
type: 'success',
message: '删除成功!'
});
// 返回页面
// 根据课程id获取章节和小节数据列表
// 这里我们已经有了课程id了,所以不需要在重新赋值了
this.getChapterVideo()
})
})
},
// 点击编辑按钮后,调用的方法
openEditVideo(videoId){
// 弹出修改框 --- 和添加框是一个
this.dialogVideoFormVisible = true
// 根据小节id查询出数据,进行回显
video.getVideoById(videoId)
.then(response => {
this.video = response.data.eduVideo
})
},
// 点击添加小节,调用这个方法
openVideo(chapterId){
// 弹出框
this.dialogVideoFormVisible = true
// 给小节数据添加上章节id
this.video.chapterId = chapterId
// 小节数据清空
this.video.courseId = ''
this.video.title = ''
this.video.sort = 0
this.free = 0
},
// 添加小节的方法
addVideo(){
// 设置课程id到video对象当中去
this.video.courseId = this.courseId
// 调用api接口,往数据库中添加小节
video.addVideo(this.video)
.then(response => {
// 关闭弹窗
this.dialogVideoFormVisible = false
// 提示成功信息
this.$message({
type: 'success',
message: '添加成功!'
});
// 刷新页面
this.getChapterVideo()
})
},
// 修改小节的方法
updateVideo(){
video.updateVideo(this.video)
.then(response => {
// 关闭弹窗
this.dialogVideoFormVisible = false
// 提示成功信息
this.$message({
type: 'success',
message: '修改成功!'
});
// 刷新页面
this.getChapterVideo()
})
},
// 点击小节弹框的确定以后,要执行的方法
saveOrUpdateVideo(){
// 根据小节里面是否有课程id来进行判断是修改还是添加
if(this.video.courseId){
// 有课程id 说明是修改小节
this.updateVideo()
} else {
// 添加小节
this.addVideo()
}
},
//**************************************针对章节的操作**********************************/
// 点击删除按钮以后,调用的方法
removeChapter(chapterId){
// 删除提示框
this.$confirm('此操作将永久删除章节记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() =>{
// 点击确定以后要执行的操作
// 调用后端api接口,进行删除操作
chapter.deleteChapter(chapterId)
.then(response => {
// 提示删除成功信息
this.$message({
type: 'success',
message: '删除成功!'
});
// 返回页面
// 根据课程id获取章节和小节数据列表
// 这里我们已经有了课程id了,所以不需要在重新赋值了
this.getChapterVideo()
})
})
},
// 点击修改按钮后,调用的方法
openEditChapter(chapterId){
// 弹出修改框---和保存框是一个
this.dialogChapterFormVisible = true
// 根据chapterid查询出数据,进行回显
chapter.getChapterById(chapterId)
.then(response => {
this.chapter = response.data.eduChapter
})
},
// 点击添加章节按钮后,执行的方法
dialogChapter(){
this.dialogChapterFormVisible = true
// 章节数据清空
this.chapter = {}
},
// 添加章节
addChapter(){
// 设置课程id到chapter对象当中去
this.chapter.courseId = this.courseId
chapter.addChapter(this.chapter)
.then(response =>{
// 关闭弹窗
this.dialogChapterFormVisible = false
// 提示成功信息
this.$message({
type: 'success',
message: '操作成功!'
});
// 刷新页面
this.getChapterVideo()
})
},
// 修改章节的方法
updateChapter(){
chapter.updateChapter(this.chapter)
.then(response =>{
// 关闭弹窗
this.dialogChapterFormVisible = false
// 提示成功信息
this.$message({
type: 'success',
message: '修改成功!'
});
// 刷新页面
this.getChapterVideo()
})
},
// 添加/修改 章节
saveOrUpdate(){
// 根据章节里面是否有课程id来进行判断是修改还是添加
if(this.chapter.courseId){
// 有课程id
// 修改
this.updateChapter()
} else {
// 添加
this.addChapter()
}
},
//根据课程id获取章节和小节数据列表
getChapterVideo(){
chapter.getAllChapterVideo(this.courseId)
.then(response =>{
this.chapterVideoList = response.data.allChapterVideo
})
},
previous(){
// 跳转到第一步
this.$router.push({path:'/course/info/'+this.courseId})
},
next(){
// 跳转到第二步()
this.$router.push({path:'/course/publish/'+this.courseId})
}
}
}
</script>
<style scoped>
.chanpterList{
position: relative;
list-style: none;
margin: 0;
padding: 0;
}
.chanpterList li{
position: relative;
}
.chanpterList p{
float: left;
font-size: 20px;
margin: 10px 0;
padding: 10px;
height: 70px;
line-height: 50px;
width: 100%;
border: 1px solid #DDD;
}
.chanpterList .acts {
float: right;
font-size: 14px;
}
.videoList{
padding-left: 50px;
}
.videoList p{
float: left;
font-size: 14px;
margin: 10px 0;
padding: 10px;
height: 50px;
line-height: 30px;
width: 100%;
border: 1px dotted #DDD;
}
</style>
八: 课程信息确认
1.后端接口实现
我们要在课程信息确认页面显示的数据包括:课程名称、课程价格(edu_course)、课程分类(edu_subject)、课程简介(edu_course_description)、课程讲师(edu_teacher)。总共包含4张表,我们需要自己写sql语句实现
(1)自己编写sql语句
select ec.id, ec.price, ec.lesson_num as lessonNum, ec.title, ec.cover,
et.name as teacherName, //注意,这里变量别名必须与返回的实体类中属性名一致
es1.title as subjectLevelOne,
es2.title as subjectLevelTwo
from edu_course ec left outer join edu_course_description ecd on ec.id = ecd.id
left outer join edu_teacher et on ec.teacher_id = et.id
left outer join edu_subject es1 on ec.subject_parent_id = es1.id
left outer join edu_subject es2 on ec.subject_id = es2.id
where ec.id = '?';
(2)整合后端接口
1.编写返回给前端的实体类
2.在mapper接口中定义实现复杂查询的方法
3.实现controller、service方法
4.但是项目一启动,报错
原因:未编译.xml文件(maven加载的时候,把java文件夹里面.java类型文件进行编译,其他类型文件不会加载)
解决方案:
1.复制xml到target中去
2.把xml文件放到resource中去
3.通过配置来实现(推荐)
(1)pom.xml
(2)项目application中进行配置
(3)编写前端页面
<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>
<div class="ccInfo">
<img :src="publishCourse.cover">
<div class="main">
<h2>{{ publishCourse.title }}</h2>
<p class="gray"><span>共{{ publishCourse.lessonNum }}课时</span></p>
<p><span>所属分类:{{ publishCourse.subjectLevelOne }} — {{publishCourse.subjectLevelTwo }}</span></p>
<p>课程讲师:{{ publishCourse.teacherName }}</p>
<h3 class="red">¥{{ publishCourse.price }}</h3>
</div>
</div>
<div>
<el-button @click="previous">返回修改</el-button>
<el-button :disabled="saveBtnDisabled" type="primary" @click="publish">发布课程</el-button>
</div>
</div>
</template>
<script>
import course from "@/api/edu/course"
export default {
data() {
return {
saveBtnDisabled: false, // 保存按钮是否禁用
courseId:'', // 课程id
publishCourse: {},
}
},
created() {
// 获取路由中课程的id值
if(this.$route.params && this.$route.params.id){
this.courseId = this.$route.params.id
// 根据课程id值来查询显示数据
this.getPublishCourse()
}
},
methods: {
// 根据课程id值来查询显示数据
getPublishCourse(){
course.getPublishCourseInfo(this.courseId)
.then(response => {
this.publishCourse = response.data.publishCourse
})
},
previous() {
console.log('previous')
this.$router.push({ path: '/course/chapter/1' })
},
publish() {
console.log('publish')
this.$router.push({ path: '/course/list' })
}
}
}
</script>
<style scoped>
.ccInfo {
background: #f5f5f5;
padding: 20px;
overflow: hidden;
border: 1px dashed #DDD;
margin-bottom: 40px;
position: relative;
}
.ccInfo img {
background: #d6d6d6;
width: 500px;
height: 278px;
display: block;
float: left;
border: none;
}
.ccInfo .main {
margin-left: 520px;
}
.ccInfo .main h2 {
font-size: 28px;
margin-bottom: 30px;
line-height: 1;
font-weight: normal;
}
.ccInfo .main p {
margin-bottom: 10px;
word-wrap: break-word;
line-height: 24px;
max-height: 48px;
overflow: hidden;
}
.ccInfo .main p {
margin-bottom: 10px;
word-wrap: break-word;
line-height: 24px;
max-height: 48px;
overflow: hidden;
}
.ccInfo .main h3 {
left: 540px;
bottom: 20px;
line-height: 1;
font-size: 28px;
color: #d32f24;
font-weight: normal;
position: absolute;
}
</style>