页面
章节列表
后端
数据库模拟数据
自己去你自己的 video_content
表中找一个 id
然后替换一下我下方的SQL脚本中的ID,然后执行即可
INSERT INTO `video_db`.`video_chapter` (`id`, `course_id`, `title`, `sort`, `gmt_create`, `gmt_modified`) VALUES ('1', '1380737860695597058', '第一章节', 0, '2021-04-10 14:31:58', '2021-04-10 14:32:01');
INSERT INTO `video_db`.`video_chapter` (`id`, `course_id`, `title`, `sort`, `gmt_create`, `gmt_modified`) VALUES ('2', '1380737860695597058', '第二章节', 0, '2021-04-10 14:31:58', '2021-04-10 14:32:01');
INSERT INTO `video_db`.`video_content_video` (`id`, `course_id`, `chapter_id`, `title`, `video_source_id`, `video_original_name`, `sort`, `play_count`, `is_free`, `duration`, `status`, `size`, `version`, `gmt_create`, `gmt_modified`) VALUES ('1', '1380737860695597058', '1', '第1小节', NULL, NULL, 0, 0, 0, 0, 'Empty', 0, 1, '2021-04-10 14:33:03', '2021-04-10 14:33:05');
INSERT INTO `video_db`.`video_content_video` (`id`, `course_id`, `chapter_id`, `title`, `video_source_id`, `video_original_name`, `sort`, `play_count`, `is_free`, `duration`, `status`, `size`, `version`, `gmt_create`, `gmt_modified`) VALUES ('2', '1380737860695597058', '1', '第2小节', NULL, NULL, 0, 0, 0, 0, 'Empty', 0, 1, '2021-04-10 14:33:03', '2021-04-10 14:33:05');
INSERT INTO `video_db`.`video_content_video` (`id`, `course_id`, `chapter_id`, `title`, `video_source_id`, `video_original_name`, `sort`, `play_count`, `is_free`, `duration`, `status`, `size`, `version`, `gmt_create`, `gmt_modified`) VALUES ('3', '1380737860695597058', '2', '第1小节', NULL, NULL, 0, 0, 0, 0, 'Empty', 0, 1, '2021-04-10 14:33:03', '2021-04-10 14:33:05');
定义对应的 VO 实体
ContentVideoVO
/**
* @author BNTang
* @version S2.3.2Dev
* @program video_parent
* @date Created in 2021/4/10 14:37
* @description 小节信息VO
**/
public class ContentVideoVO implements Serializable {
/**
* id
*/
private String id;
/**
* 小节标题
*/
private String title;
/**
* 小节是否免费
*/
private Boolean free;
}
ChapterVO
/**
* @author BNTang
* @version S2.3.2Dev
* @program video_parent
* @date Created in 2021/4/10 14:36
* @description 作品章节VO,作品章节信息
**/
public class ChapterVO {
private String id;
private String title;
/**
* 章节下的小节信息
*/
private List<ContentVideoVO> children;
}
定义查询嵌套章节与小节信息的数据结构接口
/**
* <b>
* 获取章节嵌套形式的数据结构列表
* </b>
*
* @param contentId 作品ID
* @return 章节小节嵌套形式的数据结构
*/
List<ChapterVO> getChapterSection(String contentId);
完善嵌套章节与小节信息数据结构业务
/**
* <p>
* 作品章节 服务实现类
* </p>
*
* @author BNTang
* @since 2021-04-08
*/
public class ChapterServiceImpl extends ServiceImpl<ChapterMapper, Chapter> implements ChapterService {
private ContentVideoService contentVideoService;
public List<ChapterVO> getChapterSection(String contentId) {
// 1.根据课程id查询课程里面所有的章节(章节)
QueryWrapper<Chapter> chapterQueryWrapper = new QueryWrapper<>();
chapterQueryWrapper.eq("content_id", contentId);
List<Chapter> chapterList = baseMapper.selectList(chapterQueryWrapper);
// 2.根据课程id查询课程里面所有的小节(小节)
QueryWrapper<ContentVideo> videoQueryWrapper = new QueryWrapper<>();
videoQueryWrapper.eq("content_id", contentId);
List<ContentVideo> contentVideoList = contentVideoService.list(videoQueryWrapper);
return chapterList.stream().map(m -> {
ChapterVO chapterVO = new ChapterVO();
// chapter对象值复制到ChapterVo里面
ChapterVO chapterVo = new ChapterVO();
BeanUtils.copyProperties(m, chapterVo);
// 创建集合,用于封装章节的小节
List<ContentVideoVO> subsectionList = new ArrayList<>();
// 4.遍历查询小节list集合,进行封装
contentVideoList.forEach(a1 -> {
// 判断:小节里面chapterId和章节里面id是否一样
if (a1.getChapterId().equals(m.getId())) {
// 进行封装
ContentVideoVO videoVo = new ContentVideoVO();
BeanUtils.copyProperties(a1, videoVo);
// 放到小节封装集合
subsectionList.add(videoVo);
}
});
// 把封装之后小节list集合,放到章节对象里面
chapterVo.setChildren(subsectionList);
return chapterVO;
}).collect(Collectors.toList());
}
}
Controller 层代码如下
/**
* <p>
* 作品章节 前端控制器
* </p>
*
* @author BNTang
* @since 2021-04-08
*/
("/service_video/chapter")
(tags = "作品章节组")
public class ChapterController {
private ChapterService chapterService;
/**
* <b>
* 获取章节嵌套形式的数据结构列表
* </b>
*/
(value = "获取章节嵌套形式的数据结构列表")
("getChapterContentVideo/{contentId}")
public ResponseResult getChapterSection( String contentId) {
return ResponseResult.ok().data("items", chapterService.getChapterSection(contentId));
}
}
前端
在 api 当中创建 chapter.js
import request from '@/utils/request'
const apiName = '/service_video/chapter'
export default {
getNestedTreeList(contentId) {
return request({
url: `${apiName}/getChapterContentVideo/${contentId}`,
method: 'get'
})
}
}
在 chapter.vue 页面当中加载章节小节列表信息
<script>
import chapter from '@/api/video/content/chapter';
export default {
data() {
return {
active: 0,
// 作品ID
contentId: '',
// 章节嵌套小节列表
chapterNestedList: []
};
},
created() {
// 获取路由当中的id
if (this.$route.params && this.$route.params.id) {
// 获取当前作品id
this.contentId = this.$route.params.id;
// 请求嵌套数据
this.getNestedTreeList();
}
},
methods: {
getNestedTreeList() {
chapter.getNestedTreeList(this.contentId).then(response => {
this.chapterNestedList = response.data.items;
})
},
pre() {
this.$router.push({path: '/content/info/' + this.contentId})
},
next() {
this.$router.push({path: '/content/send/' + this.contentId})
}
}
}
</script>
页面结构如下
<template>
<div class="app-container">
<h1>章节信息</h1>
<el-steps :active="2" finish-status="success">
<el-step title="填写作品基本信息"></el-step>
<el-step title="添加章节视频"></el-step>
<el-step title="完成"></el-step>
</el-steps>
<el-button type="text">添加章节</el-button>
<!--
章节
-->
<ul class="chapterList">
<li v-for="chapter in chapterList" :key="chapter.id">
<p>
{{ chapter.title }}
<span class="acts">
<el-button type="text">添加小节</el-button>
<el-button type="text">编辑</el-button>
<el-button type="text">删除</el-button>
</span>
</p>
<!-- 视频 -->
<ul class="chapterListList sectionList">
<li v-for="section in chapter.children" :key="section.id">
<p>
{{ section.title }}
<span class="acts">
<el-button type="text">编辑</el-button>
<el-button type="text">删除</el-button>
</span>
</p>
</li>
</ul>
</li>
</ul>
<el-button @click="pre">上一步</el-button>
<el-button @click="next">下一步</el-button>
</div>
</template>
样式代码如下
<style scoped>
ul {
list-style: none;
}
.chapterList {
position: relative;
list-style: none;
margin: 0;
padding: 0;
}
.chapterList li {
position: relative;
}
.chapterList p {
float: left;
font-size: 20px;
margin: 10px 0;
padding: 10px;
height: 70px;
line-height: 50px;
width: 100%;
border: 1px solid #DDD;
}
.chapterList .acts {
float: right;
font-size: 14px;
}
.sectionList {
padding-left: 50px;
}
.sectionList p {
float: left;
font-size: 14px;
margin: 10px 0;
padding: 10px;
height: 50px;
line-height: 30px;
width: 100%;
border: 1px dashed #DDD;
}
</style>
章节管理后端
在 common_base
工程当中添加自定义异常
/**
* @author BNTang
* @version S2.3.2Dev
* @program video_parent
* @date Created in 2021/4/10 17:27
* @description 自定义异常
**/
(callSuper = true)
public class BnTangException extends RuntimeException {
/**
* 状态码
*/
private Integer code;
/**
* 异常信息
*/
private String msg;
}
然后在全局异常处理器当中处理我们自己抛出的自定义异常
/**
* <b>
* 自定义异常
* </b>
*/
(BnTangException.class)
public ResponseResult error(BnTangException e) {
e.printStackTrace();
return ResponseResult.error().code(e.getCode()).message(e.getMsg());
}
接下来就都是修改 ChapterController
了如下
新增章节
/**
* <b>
* 新增章节
* </b>
*/
("/addChapter")
public ResponseResult addChapter( Chapter chapter) {
chapterService.save(chapter);
return ResponseResult.ok();
}
根据 ID 查询章节
/**
* <b>
* 根据 ID 查询章节
* </b>
*/
("/getChapterById/{id}")
(value = "根据 ID 查询章节")
public ResponseResult getChapterById( (name = "id", value = "章节ID", required = true)
String id) {
Chapter chapter = chapterService.getById(id);
return ResponseResult.ok().data("item", chapter);
}
根据 ID 修改章节
/**
* <b>
* 根据ID修改章节
* </b>
*/
(value = "根据ID修改章节")
("updateChapterById/{id}")
public ResponseResult updateChapterById( String id, Chapter chapter) {
chapter.setId(id);
chapterService.updateById(chapter);
return ResponseResult.ok();
}
根据 ID 删除章节
/**
* <b>
* 根据ID删除章节
* </b>
*/
(value = "根据ID删除章节")
("deleteChapterById/{id}")
public ResponseResult deleteChapterById( String id) {
chapterService.deleteChapterById(id);
return ResponseResult.ok();
}
业务类的实现和接口方法的定义都没有写的,先来看看最终 ChapterController 的代码如下
/**
* <p>
* 作品章节 前端控制器
* </p>
*
* @author BNTang
* @since 2021-04-08
*/
("/service_video/chapter")
(tags = "作品章节组")
public class ChapterController {
private ChapterService chapterService;
/**
* <b>
* 获取章节嵌套形式的数据结构列表
* </b>
*/
(value = "获取章节嵌套形式的数据结构列表")
("getChapterContentVideo/{contentId}")
public ResponseResult getChapterSection( String contentId) {
return ResponseResult.ok().data("items", chapterService.getChapterSection(contentId));
}
/**
* <b>
* 新增章节
* </b>
*/
("/addChapter")
(value = "新增章节")
public ResponseResult addChapter( Chapter chapter) {
chapterService.save(chapter);
return ResponseResult.ok();
}
/**
* <b>
* 根据 ID 查询章节
* </b>
*/
("/getChapterById/{id}")
(value = "根据 ID 查询章节")
public ResponseResult getChapterById( (name = "id", value = "章节ID", required = true)
String id) {
Chapter chapter = chapterService.getById(id);
return ResponseResult.ok().data("item", chapter);
}
/**
* <b>
* 根据ID修改章节
* </b>
*/
(value = "根据ID修改章节")
("updateChapterById/{id}")
public ResponseResult updateChapterById( String id, Chapter chapter) {
chapter.setId(id);
chapterService.updateById(chapter);
return ResponseResult.ok();
}
/**
* <b>
* 根据ID删除章节
* </b>
*/
(value = "根据ID删除章节")
("deleteChapterById/{id}")
public ResponseResult deleteChapterById( String id) {
chapterService.deleteChapterById(id);
return ResponseResult.ok();
}
}
紧接着就是来添加接口方法和完成实现类如下,在 ChapterService 中添加如下方法
/**
* <b>
* 根据ID删除章节
* </b>
*
* @param id 章节ID
*/
void deleteChapterById(String id);
实现类代码如下
public void deleteChapterById(String id) {
// 根据 id 查询是否存在小节,如果有则提示用户,有子节点
if (contentVideoService.isExistContentVideoWithChapterId(id)) {
throw new BnTangException(20001, "该分章节下存在小节,请先删除小节!");
}
baseMapper.deleteById(id);
}
紧接着在 contentVideoService
接口当中添加一个查询章节下是否存在小节的方法如下
/**
* 根据章节ID查询是否存在小节
*
* @param id 章节ID
* @return 是否存在小节
*/
boolean isExistContentVideoWithChapterId(String id);
实现类如下,后端完毕开始前端
/**
* <p>
* 作品视频 服务实现类
* </p>
*
* @author BNTang
* @since 2021-04-08
*/
public class ContentVideoServiceImpl extends ServiceImpl<ContentVideoMapper, ContentVideo> implements ContentVideoService {
public boolean isExistContentVideoWithChapterId(String id) {
QueryWrapper<ContentVideo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("chapter_id", id);
Integer count = baseMapper.selectCount(queryWrapper);
return null != count && count > 0;
}
}
章节管理前端
新增章节
在 api 的 chapter.js
当中编写前端调用请求
// 2.新增章节
saveChapter(chapter) {
return request({
url: `${apiName}/saveChapter`,
method: 'post',
data: chapter
})
},
定义 data 数据
// 是否显示章节表单
dialogChapterFormVisible: false,
// 章节对象
chapter: {
title: '',
sort: 0
},
章节表单 dialog
<!--
添加和修改章节表单
-->
<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>
定义表单提交方法
// 添加或者更新
saveOrUpdate() {
if (!this.chapter.id) {
this.saveData();
} else {
this.updateData();
}
},
saveData() {
this.chapter.contentId = this.contentId;
chapter.save(this.chapter).then(response => {
this.$message({
type: 'success',
message: response.message
})
}).catch((response) => {
this.$message({
type: 'error',
message: response.message
})
});
},
在测试添加功能之前先来修复一个 BUG,章节列表展示的问题,首先是前端
BUG解决,然后继续修改前端当点击 添加章节
的时候把表单对话框给显示出来如下
当然我这里是添加了一个方法,你可以直接把绑定的 data 值变为 true
即可显示, showDialog 方法内容如下
showDialog() {
this.dialogChapterFormVisible = true;
}
修改一下 api 请求地址之前写错了
处理添加之后的数据操作,如下
this.dialogChapterFormVisible = false;
// 请求嵌套数据
this.getNestedTreeList();
更新章节
添加注册事件
定义编辑方法
editChapter(chapterId) {
this.dialogChapterFormVisible = true;
chapter.getChapterById(chapterId).then(res => {
this.chapter = res.data.item;
}).catch(error => {
this.$message.error("系统繁忙!");
});
}
修改 chapter.js
添加对应的 api 方法
// 3.根据 ID 查询章节
getChapterById(chapterId) {
return request({
url: `${apiName}/getChapterById/${chapterId}`,
method: 'get',
});
}
更新按钮点击方法实现
// 添加或者更新
saveOrUpdate() {
if (!this.chapter.id) {
this.saveData();
} else {
this.updateData();
}
},
修改 chapter.js
添加对应的 api
// 4.根据 ID 查询章节
updateChapter(chapter) {
return request({
url: `${apiName}/updateChapterById`,
method: 'put',
data: chapter
});
}
// 更新章节
updateData() {
chapter.updateChapter(this.chapter).then(res => {
this.$message.success(res.message);
this.dialogChapterFormVisible = false;
this.getNestedTreeList();
}).catch(error => {
this.$message.error(error.message);
})
}
删除章节
定义删除方法
// 删除章节
deleteChapter(id) {
//提示
this.$confirm('真的要删除嘛?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消'
}).then(() => {
// 发送删除的请求
return chapter.deleteChapter(id);
}).then(() => {
// 重新加载数据
this.getNestedTreeList();
}).catch((error => {
// 取消
if (error === 'cancel') {
this.$message({
type: 'info',
message: '取消删除'
});
}
}));
},
编写对应的请求 api
// 删除章节
deleteChapter(id) {
//提示
this.$confirm('真的要删除嘛?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消'
}).then(() => {
// 发送删除的请求
return chapter.deleteChapter(id);
}).then(() => {
// 重新加载数据
this.getNestedTreeList();
}).catch((error => {
// 取消
if (error === 'cancel') {
this.$message({
type: 'info',
message: '取消删除'
});
}
}));
},