资料 |
---|
资料地址 |
后台管理系统目录 | 前台展示系统目录 |
---|---|
1 - 构建工程篇 | 7 - 渲染前台篇 |
2 - 前后交互篇 | 8 - 前台登录篇 |
3 - 文件上传篇 | 9 - 前台课程篇 |
4 - 课程管理篇 | 10 - 前台支付篇 |
5 - 章节管理篇 | 11 - 统计分析篇 |
6 - 微服务治理 | 12 - 项目完结篇 |
目录
一、EasyExcel 基本使用
1.1、Excel导入导出的应用场景
-
数据导入:减轻录入工作量
-
数据导出:统计信息归档
-
数据传输:异构系统之间数据传输
1.2、EasyExcel简介
-
Java领域解析、生成Excel比较有名的框架有Apache poi、jxl等。但他们都存在一个严重的问题就是非常的耗内存。如果你的系统并发量不大的话可能还行,但是一旦并发上来后一定会OOM或者JVM频繁的full gc。
-
EasyExcel是阿里巴巴开源的一个excel处理框架,以使用简单、节省内存著称。EasyExcel能大大减少占用内存的主要原因是在解析Excel时没有将文件数据一次性全部加载到内存中,而是从磁盘上一行行读取数据,逐个解析。
-
EasyExcel采用一行一行的解析模式,并将一行的解析结果以观察者的模式通知处理(AnalysisEventListener)
1.3、EasyExcel写
1、service_edu
模块 引入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.1</version>
</dependency>
2、创建实体类
//设置表头和添加的数据字段
@Data
@ToString
public class UserDemo {
//设置excel表头名称
@ExcelProperty("序号")
private Integer no;
//设置excel表头名称
@ExcelProperty("姓名")
private String name;
}
3、实现写操作 (第一种)
创建方法循环设置要添加到Excel的数据(写法一)推荐
public class TestEasyExcel {
public static void main(String[] args) {
String filename = "C:\\Users\\lapto\\Desktop\\UserDemo.xlsx";
EasyExcel.write(filename, UserDemo.class)
.sheet("用户列表")
.doWrite(getLists());
}
private static List<UserDemo> getLists() {
ArrayList<UserDemo> list = new ArrayList<>();
for (int i = 1; i < 4; i++) {
UserDemo demoData = new UserDemo();
demoData.setNo(i);
demoData.setName("Laptoy:" + i);
list.add(demoData);
}
return list;
}
}
4、实现写操作 (第二种)
public class TestEasyExcel {
public static void main(String[] args) {
String filename = "C:\\Users\\lapto\\Desktop\\UserDemo.xlsx";
ExcelWriter excelWriter = EasyExcel.write(filename, UserDemo.class).build();
WriteSheet build = EasyExcel.writerSheet("用户列表").build();
// 这里 需要指定写用哪个class去写
excelWriter.write(getLists(), build);
// 千万别忘记finish 会帮忙关闭流
excelWriter.finish();
}
private static List<UserDemo> getLists() {
ArrayList<UserDemo> list = new ArrayList<>();
for (int i = 1; i < 4; i++) {
UserDemo demoData = new UserDemo();
demoData.setNo(i);
demoData.setName("Laptoy:" + i);
list.add(demoData);
}
return list;
}
}
1.4、EasyExcel读
1、实体类
//设置表头和添加的数据字段
@Data
@ToString
public class UserDemo {
//设置excel表头名称
@ExcelProperty(value = "序号",index = 0)
private Integer no;
//设置excel表头名称
@ExcelProperty(value = "姓名",index = 1)
private String name;
}
2、创建读取操作的监听器
public class ExcelListener extends AnalysisEventListener<UserDemo> {
//创建list集合封装最终的数据
List<UserDemo> list = new ArrayList<>();
//一行一行去读取excel内容
@Override
public void invoke(UserDemo demoData, AnalysisContext analysisContext) {
System.out.println(demoData);
list.add(demoData);
}
//读取excel表头信息
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
System.out.println("表头信息" + headMap);
}
//读取完成后执行
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
}
}
3、调用实现最终的读取
public static void main(String[] args) {
String filename = "C:\\Users\\lapto\\Desktop\\UserDemo.xlsx";
// 指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
EasyExcel.read(filename, UserDemo.class, new ExcelListener()).sheet().doRead();
}
二、课程分类添加
2.1、后端接口
通过传入excel文件进行添加
添加时一级分类只需要添加一次,所以需要判空
1、使用代码生成器生成课程分类代码
strategy.setInclude("edu_subject");
2、控制层
@Api(tags = "课程分类管理")
@CrossOrigin //解决跨域问题
@RestController
@RequestMapping("/eduservice/subject")
public class EduSubjectController {
@Autowired
private EduSubjectService eduSubjectService;
//添加课程分类
//获取上传过来的文件,把文件内容读取出来
@PostMapping("/addSubject")
public R addSubject(MultipartFile file) {
//获取上传的excel文件 MultipartFile
eduSubjectService.saveSubject(file, eduSubjectService);
//判断返回集合是否为空
return R.ok();
}
}
2、业务层
@Service
public class EduSubjectServiceImpl extends ServiceImpl<EduSubjectMapper, EduSubject> implements EduSubjectService {
//添加课程分类
@Override
public void saveSubject(MultipartFile file, EduSubjectService eduSubjectService) {
InputStream inputStream = null;
try {
inputStream = file.getInputStream();
//调用方法进行读取
EasyExcel.read(inputStream, SubjectData.class, new SubjectExcelListener(eduSubjectService))
.sheet().doRead();
} catch (Exception e) {
e.printStackTrace();
throw new LaptoyException(20002, "请选择文件或文件格式不支持");
}
}
}
3、Listener
public class SubjectExcelListener extends AnalysisEventListener<SubjectData> {
//因为SubjectExcelListener態交给spring进行ioc管理,需要自己手动new,不能注入其他对象
public EduSubjectService eduSubjectService;
//有参,通过构造方法手动注入service进而操作数据库
public SubjectExcelListener(EduSubjectService eduSubjectService) {
this.eduSubjectService = eduSubjectService;
}
public SubjectExcelListener() {
}
//读取excel内容,一行一行读取
@Override
public void invoke(SubjectData subjectData, AnalysisContext analysisContext) {
//表示excel中没有数据,就不需要读取了
if (subjectData == null) {
throw new LaptoyException(20001, "添加失败");
}
//一行一行读取,每次读取有两个值,第一个值一级分类,第二个值二级分类
//判断是否有一级分类是否重复
EduSubject existOneSubject = this.existOneSubject(eduSubjectService, subjectData.getOneSubjectName());
if (existOneSubject == null) { //没有相同的一级分类,进行添加
existOneSubject = new EduSubject();
existOneSubject.setParentId("0"); //设置一级分类id值,0代表为一级分类
existOneSubject.setTitle(subjectData.getOneSubjectName());//设置一级分类名
eduSubjectService.save(existOneSubject);//给数据库添加一级分类
}
//判断是否有二级分类是否重复
EduSubject existTwoSubject = this.existTwoSubject(eduSubjectService, subjectData.getTwoSubjectName(), existOneSubject.getId());
if (existTwoSubject == null) {
existTwoSubject = new EduSubject();
existTwoSubject.setParentId(existOneSubject.getId());
existTwoSubject.setTitle(subjectData.getTwoSubjectName());
eduSubjectService.save(existTwoSubject);
}
}
// 判断一级分类不能重复添加
private EduSubject existOneSubject(EduSubjectService eduSubjectService, String name) {
QueryWrapper<EduSubject> wrapper = new QueryWrapper<>();
wrapper.eq("title", name).eq("parent_id", "0");
EduSubject oneSubject = eduSubjectService.getOne(wrapper);
return oneSubject;
}
// 判断二级分类不能重复添加
private EduSubject existTwoSubject(EduSubjectService eduSubjectService, String name, String parentId) {
QueryWrapper<EduSubject> wrapper = new QueryWrapper<>();
wrapper.eq("title", name).eq("parent_id", parentId);
EduSubject twoSubject = eduSubjectService.getOne(wrapper);
return twoSubject;
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
}
}
4、测试之前给实体类 edu_subject 配置填充
@TableField(fill = FieldFill.INSERT)
@ApiModelProperty(value = "创建时间")
private Date gmtCreate;
@TableField(fill = FieldFill.INSERT_UPDATE)
@ApiModelProperty(value = "更新时间")
private Date gmtModified;
5、测试
TestDemo.xlsx
数据库
2.2、前端展示
1、路由
// 课程分类模块路由
{
path: 'subject',
component: Layout,
redirect: '/edu/subject/list',
name: '课程分类管理',
meta: { title: '课程分类管理', icon: 'nested' },
children: [
{
path: 'list',
name: '课程分类列表',
component: () => import('@/views/edu/subject/list.vue'),
meta: { title: '课程分类列表', icon: 'table' }
},
{
path: 'import',
name: '课程分类导入',
component: () => import('@/views/edu/subject/import.vue'),
meta: { title: '导入课程分类', icon: 'nested' }
}
]
},
2、组件 import.vue
<template>
<div class="app-container">
<el-form label-width="120px">
<el-form-item label="Excel模板">
<el-tag>
<i class="el-icon-download" />
<a :href="'/static/test.xlsx'">点击下载模版</a>
</el-tag>
</el-form-item>
<el-form-item label="选择Excel">
<el-upload ref="upload" :auto-upload="false"
:on-success="fileUploadSuccess" :on-error="fileUploadError"
:disabled="importBtnDisabled"
:limit="1" :action="BASE_API + '/eduservice/subject/addSubject'"
name="file" accept="application/vnd.ms-excel">
<el-button slot="trigger" size="small" type="primary">选取文件</el-button>
<el-button :loading="loading"
style="margin-left: 10px" size="small" type="success"
@click="submitUpload">上传到服务器</el-button>
</el-upload>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
data() {
return {
BASE_API: process.env.BASE_API, // 接口API地址
importBtnDisabled: false, // 按钮是否禁用,
loading: false,
};
},
methods: {
//点击按钮上传文件到接口
submitUpload() {
this.importBtnDisabled = true;
this.loading = true;
this.$refs.upload.submit();
},
//上传成功后
fileUploadSuccess(resp) {
if (resp.success === true) {
this.loading = false;
this.$message({
type: "success",
message: resp.message,
});
}
},
//上传失败后
fileUploadError(resp) {
this.loading = false;
this.$message({
type: "error",
message: "导入失败",
});
},
},
}
</script>
三、课程分类列表
3.1、后端接口
1、创建VO用于封装树形结构数据
//一级分类
@Data
public class OneSubject {
private String id;
private String title;
// 二级分类
private List<TwoSubject> children = new ArrayList<>();
}
//二级分类
@Data
public class TwoSubject {
private String id;
private String title;
}
2、控制层
// 课程分类列表(树形)
@ApiOperation(value = "嵌套数据列表")
@GetMapping("/getAllSubject")
public R getAllSubject(){
//ist集合泛型是一级分类,一级分类中本身含有二级分类
List<OneSubject> list = eduSubjectService.getAllOneTwoSubject();
return R.ok().data("data",list);
}
3、业务层
// 树形显示课程分类
@Override
public List<OneSubject> getAllOneTwoSubject() {
// 查询所有一级分类 parent_id = 0
QueryWrapper<EduSubject> wrapperOne = new QueryWrapper<>();
wrapperOne.eq("parent_id", "0");
List<EduSubject> oneSubjectsList = baseMapper.selectList(wrapperOne);
// 查询所有二级分类 parent_id != 0
QueryWrapper<EduSubject> wrapperTwo = new QueryWrapper<>();
wrapperTwo.ne("parent_id", "0");
List<EduSubject> twoSubjectsList = baseMapper.selectList(wrapperTwo);
// 创建list集合,用于封装最终数据
List<OneSubject> finalSubjectList = new ArrayList<>();
//封装一级分类
for (int i = 0; i < oneSubjectsList.size(); i++) {
EduSubject oSubject = oneSubjectsList.get(i);
OneSubject oneSubject = new OneSubject();
BeanUtils.copyProperties(oSubject, oneSubject);
// 添加到list
finalSubjectList.add(oneSubject);
// 在一级分类循环遍历查询所有的二级分类
// 创建list集合封装每个一级分类的二级分类
ArrayList<TwoSubject> finalTwoSubjects = new ArrayList<>();
// 遍历二级list集合
for (int j = 0; j < twoSubjectsList.size(); j++) {
EduSubject tSubject = twoSubjectsList.get(j);
// 判断二级分类parentId和一级分类id是否一样
if (tSubject.getParentId().equals(oSubject.getId())) {
TwoSubject twoSubject = new TwoSubject();
BeanUtils.copyProperties(tSubject, twoSubject);
finalTwoSubjects.add(twoSubject);
}
}
//把一级下面所有二级分类放到oneSubject里面
oneSubject.setChildren(finalTwoSubjects);
}
return finalSubjectList;
}
4、测试
3.2、前端展示
1、创建API - src/api/teacher/subject.js
import request from '@/utils/request'
export default {
//课程分类列表
getSubjectList() {
return request({
url: "/eduservice/subject/getAllSubject",
method: 'get'
})
}
}
2、list.vue
<template>
<div class="app-container">
<el-input v-model="filterText" placeholder="输入关键字进行查询" style="margin-bottom: 30px" />
<el-tree ref="tree" :data="data" :props="defaultProps" :filter-node-method="filterNode" class="filter-tree" />
</div>
</template>
<script>
import subject from '@/api/teacher/subject.js'
export default {
data() {
return {
filterText: "",
data: [], //返回所有分类的数据
defaultProps: {
children: "children",
label: "title",
},
};
},
watch: {
filterText(val) {
this.$refs.tree.filter(val);
},
},
methods: {
getAllSubjectList() {
subject.getSubjectList()
.then(resp => {
this.data = resp.data.data
})
},
// 不区分大小写检索
filterNode(value, data) {
if (!value) return true;
return data.title.toLowerCase().indexOf(value.toLowerCase()) !== -1
},
},
created() {
this.getAllSubjectList()
},
};
</script>
3、优化导入课程分类后路由到课程分类列表 import.vue
四、添加课程信息
4.1、数据库表
edu_course
:课程表:存储课程基本信息edu_course_description
:课程简介表:存储课程简介信息edu_chapter
:课程章节表:存储课程章节信息edu_video
:课程小节表:存储章节里面小节信息
edu_teacher
:讲师表edu_subject
:分类表
edu_course 课程表
edu_course_description 课程简介表
edu_chapter 课程章节表
edu_video 课程小节表
4.2、后端接口
1、代码生成器生成代码
strategy.setInclude("edu_course", "edu_chapter", "edu_course_description", "edu_video");//根据数据库哪张表生成,有多张表就加逗号继续填写
2、创建vo用于封装表单提交数据
@ApiModel(value = "课程基本信息", description = "编辑课程基本信息的表单对象")
@Data
public class CourseInfoFormVo 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;
}
2、控制层
@Api(tags = "课程模块")
@CrossOrigin //解决跨域问题
@RestController
@RequestMapping("/eduservice/course")
public class EduCourseController {
@Autowired
private EduCourseService eduCourseService;
//添加课程基本信息方法
@PostMapping("/addCourseInfo")
public R addCourseInfo(@RequestBody CourseInfoFormVo vo){
String id = eduCourseService.saveCourseInfo(vo);
// 返回课程id给下一步操作
return R.ok().data("courseId",id);
}
}
3、课程描述的id根据数据表分析,此id与课程id为同一个,所以我们修改实体类主键生成策略为手动输入
@TableId(value = "id", type = IdType.INPUT)
private String id;
4、业务层
@Service
public class EduCourseServiceImpl extends ServiceImpl<EduCourseMapper, EduCourse> implements EduCourseService {
@Autowired
private EduCourseDescriptionService descriptionService;
@Override
public String saveCourseInfo(CourseInfoFormVo vo) {
EduCourse course = new EduCourse();
BeanUtils.copyProperties(vo, course);
boolean flag = this.save(course);
if (!flag) {
// 保存失败
throw new LaptoyException(20001, "添加课程失败");
}
// 保存成功
// 保存课程描述信息,需要将保存成功的课程id传入课程描述表
EduCourseDescription description = new EduCourseDescription();
// 保存成功自动生成了课程id,将该id赋值给课程描述表
description.setId(course.getId());
description.setDescription(vo.getDescription());
descriptionService.save(description);
return course.getId();
}
}
5、测试
{
"cover": "封面url",
"description": "描述信息",
"id": "",
"lessonNum": 0,
"price": 0,
"subjectId": "",
"subjectParentId": "",
"teacherId": "",
"title": "课程名称"
}
4.3、前端展示 初始化步骤条
1、添加路由
// 课程信息路由
{
path: '/course',
component: Layout,
redirect: '/course/list',
name: '课程管理',
meta: { title: '课程管理', icon: 'nested' },
children: [
{
path: 'list',
name: '课程列表',
component: () => import('@/views/edu/course/list.vue'),
meta: { title: '课程列表', icon: 'table' }
},
{
path: 'info',
name: '添加课程',
component: () => import('@/views/edu/course/info.vue'),
meta: { title: '添加课程', icon: 'nested' }
},
{
path: 'info/:id',
name: 'EduCourseInfoEdit',
component: () => import('@/views/edu/course/info.vue'),
meta: { title: '编辑课程基本信息', noCache: true },
hidden: true
},
{
path: 'chapter/:id',
name: 'EduCourseChapterEdit',
component: () => import('@/views/edu/course/chapter.vue'),
meta: { title: '编辑课程大纲', noCache: true },
hidden: true
},
{
path: 'publish/:id',
name: 'EduCoursePublishEdit',
component: () => import('@/views/edu/course/publish.vue'),
meta: { title: '发布课程', noCache: true },
hidden: true
}
]
},
初始化步骤组件
2、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,
};
},
methods: {
next() {
//跳转到第二步
this.$router.push('/course/chapter/1')
},
},
created() {
}
};
</script>
3、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,
};
},
methods: {
previous() {
//跳转到上一步
this.$router.push("/course/info/1");
},
next() {
//跳转到第三步
this.$router.push("/course/publish/1");
},
},
created() { },
};
</script>
4、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,
};
},
methods: {
// 跳转到上一步
previous() {
this.$router.push("/course/chapter/1");
},
publish() {
this.$router.push("/course/list");
}
},
};
</script>
4.4、填写课程基本信息
1、定义API - src/api/course.js
import request from '@/utils/request'
export default {
// 添加课程信息功能
addCourseInfo(courseInfo) {
return request({
url: "/eduservice/course/addCourseInfo",
method: 'post',
data: courseInfo,
})
}
}
2、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 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 -->
<el-form-item label="课程简介">
<el-input v-model="courseInfo.description" placeholder="" />
</el-form-item>
<!-- 课程封面 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="saveOrUpdate">保存并下一步</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import course from "@/api/teacher/course.js";
export default {
data() {
return {
saveBtnDisabled: false,
courseInfo:
{
title: "",
subjectId: "",
teacherId: "",
subjectParentId: "",
lessonNum: 0,
description: "",
cover: "",
price: 0,
},
};
},
methods: {
saveOrUpdate() {
course.addCourseInfo(this.courseInfo).then(resp => {
this.$message({
message: "添加课程信息成功",
type: "success",
})
//跳转到第二步,并带着这个课程生成的id
this.$router.push({ path: "/course/chapter/" + resp.data.courseId });
});
},
},
created() { },
};
</script>
4.5、讲师下拉列表显示
1、定义API - course.js
// 查询所有讲师
getAllTeacher() {
return request({
url: "/eduservice/teacher/findAll",
method: 'get',
})
}
2、页面
<el-form-item label="课程讲师">
<el-select v-model="courseInfo.teacherId" placeholder="请选择">
<el-option v-for="teacher in teacherLists" :key="teacher.id" :label="teacher.name" :value="teacher.id"></el-option>
</el-select>
</el-form-item>
3、逻辑
import course from "@/api/teacher/course.js";
export default {
data() {
return {
teacherLists: [], //封装所有讲师数据
};
},
methods: {
//查询所有讲师
getListTeacher() {
course.getAllTeacher().then((resp) => {
this.teacherLists = resp.data.data;
});
},
},
created() {
this.getListTeacher();
},
};
4.6、课程分类多级联动
1、API
// 查询课程分类
getSubjectList() {
return request({
url: "/eduservice/subject/getAllSubject",
method: 'get'
})
}
2、页面
<el-form-item label="所属分类">
<el-select v-model="courseInfo.subjectParentId" placeholder="一级分类" @change="subjectLevelOneChanged">
<el-option v-for="subject in subjectOneLists" :key="subject.id" :label="subject.title" :value="subject.id"></el-option>
</el-select>
<el-select v-model="courseInfo.subjectId" placeholder="二级分类">
<el-option v-for="subject in subjectTwoLists" :key="subject.id" :label="subject.title" :value="subject.id"></el-option>
</el-select>
</el-form-item>
3、逻辑
export default {
data() {
return {
subjectOneLists: [], //封装一级分类数据
subjectTwoLists: [], //封装二级分类数据
};
},
methods: {
// 查出所有分类信息
getSubjectList() {
course.getSubjectList().then((resp) => {
this.subjectOneLists = resp.data.data
})
},
// 点击一级分类查出二级分类
subjectLevelOneChanged(value) {
// value就是一级分类的id值
for (let i = 0; i < this.subjectOneLists.length; i++) {
if (this.subjectOneLists[i].id === value) {
this.subjectTwoLists = this.subjectOneLists[i].children;
}
}
}
},
created() {
this.getSubjectList();
},
};
4.7、课程封面
1、组件模板
<el-form-item label="课程封面">
<el-upload :show-file-list="false"
:on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload"
:action="BASE_API + '/eduoss/fileoss/upload'" class="avatar-uploader">
<img :src="courseInfo.cover" />
</el-upload>
</el-form-item>
2、js
import course from "@/api/teacher/course.js";
export default {
data() {
return {
BASE_API: process.env.BASE_API, // 接口API地址
courseInfo: {
cover: "https://www.baidu.com/img/flexible/logo/pc/result.png",
},
};
},
methods: {
// 图片上传
// 上传成功展示上传的图片到 <img />
handleAvatarSuccess(resp, file) {
this.courseInfo.cover = resp.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;
}
},
};
4.8、富文本编辑器 Tinymce
简介通过富文本编辑器进行编辑
Tinymce是一个传统javascript插件,默认不能用于Vue.js因此需要做一些特殊的整合步骤
1、复制脚本库
将资源里的富文本编辑器脚本库 components
statis
复制到项目的对应目录下
2、配置html变量
在 /build/webpack.dev.conf.js 中添加配置
new HtmlWebpackPlugin({
...
templateParameters: {
BASE_URL: config.dev.assetsPublicPath + config.dev.assetsSubDirectory
}
})
3、引入 js脚本 - index.html
<body>
<script src="<%=BASE_URL%>/tinymce4.7.5/tinymce.min.js"></script>
<script src="<%=BASE_URL%>/tinymce4.7.5/langs/zh_CN.js"></script>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
4、使用组件 - info.vue
<el-form-item label="课程简介">
<tinymce :height="150" v-model="courseInfo.description" />
</el-form-item>
<script>
import Tinymce from '@/components/Tinymce';
export default {
components: { Tinymce },
}
</script>
<style scoped>
.tinymce-container {
line-height: 29px;
}
</style>
最终效果
测试完毕:
课程表与课程简介表、讲师表、课程分类表 关联并保存数据成功
五、课程大纲列表
5.1、后端接口
1、创建实体类 章节和小节
章节
@Data
public class ChapterVo implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String title;
//表示小节
private List<VideoVo> children = new ArrayList<VideoVo>();
}
小节 即每个视频
@Data
public class VideoVo implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String title;
private Boolean free;
}
2、控制层
@Api(tags = "章节模块")
@CrossOrigin
@RestController
@RequestMapping("/eduservice/chapter")
public class EduChapterController {
@Autowired
private EduChapterService eduChapterService;
// 获取课程大纲列表,根据课程id进行查询
@GetMapping("/getChapterVideo/{courseId}")
public R getChapterVideo(@PathVariable String courseId){
List<ChapterVo> list = eduChapterService.getChapterVideoByCourseId(courseId);
return R.ok().data("data",list);
}
}
3、业务层
@Service
public class EduChapterServiceImpl extends ServiceImpl<EduChapterMapper, EduChapter> implements EduChapterService {
@Autowired
private EduVideoService eduVideoService;
@Override
public List<ChapterVo> getChapterVideoByCourseId(String courseId) {
// 查询章节信息
QueryWrapper<EduChapter> wrapper = new QueryWrapper<>();
wrapper.eq("course_id",courseId);
List<EduChapter> eduChapters = baseMapper.selectList(wrapper);
// 查询小节信息
QueryWrapper<EduVideo> wrapper1 = new QueryWrapper<>();
wrapper1.eq("course_id",courseId);
List<EduVideo> eduVideos = eduVideoService.list(wrapper1);
// 创建章节vo列表
ArrayList<ChapterVo> finalChapterVos = new ArrayList<>();
// 填充章节vo数据
for (int i = 0; i < eduChapters.size(); i++) {
EduChapter chapter = eduChapters.get(i);
// 创建章节vo对象
ChapterVo chapterVo = new ChapterVo();
BeanUtils.copyProperties(chapter,chapterVo);
finalChapterVos.add(chapterVo);
//填充课时vo对象
ArrayList<VideoVo> finalVideoVos = new ArrayList<>();
for (int j = 0; j < eduVideos.size(); j++) {
EduVideo video = eduVideos.get(j);
if (chapter.getId().equals(video.getChapterId())){
VideoVo videoVo = new VideoVo();
BeanUtils.copyProperties(video,videoVo);
finalVideoVos.add(videoVo);
}
}
chapterVo.setChildren(finalVideoVos);
}
return finalChapterVos;
}
}
5.2、前端实现
1、定义API - chapter.js
import request from '@/utils/request' //引入已经封装好的axios 和 拦截器
export default {
//根据课程id获取章节和小节数据列表
getChapterVideoByCourseId(courseId) {
return request({
url: `/eduservice/chapter/getChapterVideo/${courseId}`,
method: 'get',
})
},
}
2、chapter.vue 页面
<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">
<ul>
<li v-for="chapter in chapterVideoList" :key="chapter.id">
<p>
{{ chapter.title }}
<span>
<el-button type="text">添加课时</el-button>
<el-button style="" type="text">编辑</el-button>
<el-button type="text">删除</el-button>
</span>
</p>
<ul>
<li v-for="video in chapter.children" :key="video.id">
<p>
{{ video.title }}
<span class="acts">
<el-button type="text">编辑</el-button>
<el-button type="text">删除</el-button>
</span>
</p>
</li>
</ul>
</li>
</ul>
<el-form-item>
<el-button @click="dialogChapterFormVisible=true">添加章节</el-button>
<el-button @click="previous">上一步</el-button>
<el-button :disabled="saveBtnDisabled" type="primary" @click="next">下 一步</el-button>
</el-form-item>
</el-form>
</div>
<script>
import chapter from '@/api/teacher/chapter.js';
export default {
data() {
return {
saveBtnDisabled: false,
courseId: '',
chapterVideoList: [],
};
},
methods: {
//根据课程id查询对应的课程章节和小结
getChapterVideoByCourseId() {
chapter.getChapterVideoByCourseId(this.courseId)
.then(resp => {
this.chapterVideoList = resp.data.data
console.log(resp.data.data)
})
},
},
created() {
//获取路由里的id值
if (this.$route.params && this.$route.params.id) {
this.courseId = this.$route.params.id
}
//根据课程id查询对应的课程章节和小结
this.getChapterVideoByCourseId();
},
};
</script>
3、测试 - http://localhost:9528/#/course/chapter/18
六、修改课程信息
点击上一步按钮,回显当前课程id的信息,并通过 下一步按钮 进行更新课程信息
6.1、回显课程信息
1、控制层
// 根据课程id查询课程基本信息
@GetMapping("/getCourseInfoById/{courseId}")
public R getCourseInfoById(@PathVariable String courseId) {
CourseInfoFormVo courseInfoForm = eduCourseService.getCourseInfo(courseId);
return R.ok().data("data", courseInfoForm);
}
2、业务层
@Autowired
private EduCourseDescriptionService descriptionService;
@Override
public CourseInfoFormVo getCourseInfo(String courseId) {
EduCourse eduCourse = this.getById(courseId);
CourseInfoFormVo vo = new CourseInfoFormVo();
BeanUtils.copyProperties(eduCourse, vo);
EduCourseDescription description = descriptionService.getById(courseId);
vo.setDescription(description.getDescription());
return vo;
}
6.2、修改课程信息
1、控制层
// 修改课程信息
@PostMapping("/updateCourseInfo")
public R updateCourseInfo(@RequestBody CourseInfoFormVo vo) {
eduCourseService.updateCourseInfo(vo);
return R.ok();
}
2、业务层
@Override
public void updateCourseInfo(CourseInfoFormVo vo) {
// 修改课程基本信息
EduCourse eduCourse = new EduCourse();
BeanUtils.copyProperties(vo, eduCourse);
boolean flag = this.updateById(eduCourse);
// 失败
if(!flag){
throw new LaptoyException(20001,"修改课程失败");
}
// 成功
// 修改课程描述信息
EduCourseDescription description = new EduCourseDescription();
BeanUtils.copyProperties(vo,description);
descriptionService.updateById(description);
}
6.3、前端实现
1、API - course.js
//根据课程id 查询课程基本信息
getCourseInfoById(courseId) {
return request({
url: `/eduservice/course/getCourseInfoById/${courseId}`,
method: 'get',
})
},
//修改课程信息
updateCourseInfo(courseInfoForm) {
return request({
url: "/eduservice/course/updateCourseInfo",
method: 'post',
data: courseInfoForm,
})
}
2、chapter.vue
previous() {
//跳转到上一步
this.$router.push("/course/info/" + this.courseId);
},
next() {
//跳转到第三步
this.$router.push("/course/publish/" + this.courseId);
},
3、info.vue
data() {
return {
courseId: ""
}
}
methods: {
// 获取课程信息
getCourseInfo() {
course.getCourseInfoById(this.courseId).then((resp) => {
this.courseInfo = resp.data.data
})
},
},
created() {
//判断路径中是否有课程id
if (this.$route.params && this.$route.params.id) {
this.courseId = this.$route.params.id;
//根据课程id 查询课程基本信息
this.getCourseInfo()
};
...
},
4、测试
当返回上一步时二级分类回显为id
6.4、优化回显
默认二级分类的标题需要通过点击一级分类后的该方法进行展示
// 点击一级分类查出二级分类
subjectLevelOneChanged(value) {
// value就是一级分类的id值
for (let i = 0; i < this.subjectOneLists.length; i++) {
if (this.subjectOneLists[i].id === value) {
this.subjectTwoLists = this.subjectOneLists[i].children;
}
}
}
现在需要通过获取分类信息,并且将分类信息的每一个的id与当前课程id进行比较,如果相同就将二级分类进行回显标题
1、修改钩子函数
created() {
//判断路径中是否有课程id,回显操作(修改)
if (this.$route.params && this.$route.params.id) {
this.courseId = this.$route.params.id;
//根据课程id 查询课程基本信息
this.getCourseInfo()
} else {
// 路径中无课程id,新增操作
this.getSubjectList();
}
// 回显所有讲师
this.getListTeacher();
},
2、获取课程信息
// 获取课程信息
getCourseInfo() {
course.getCourseInfoById(this.courseId).then((resp) => {
this.courseInfo = resp.data.data;
//查询所有分类,包含一级和二级所有
course.getSubjectList().then((resp) => {
//获取所有一级分类
this.subjectOneLists = resp.data.data;
//把所有一级分类数组进行遍历
for (var i = 0; i < this.subjectOneLists.length; i++) {
//获取每个一级分类
var oneSubject = this.subjectOneLists[i];
//比较当前courseInfo里面的一级分类id和所有的一级分类id是否一样
if (this.courseInfo.subjectParentId == oneSubject.id) {
//获取一级分类中所有的二级分类
this.subjectTwoLists = oneSubject.children;
}
}
});
});
}
6.5、优化表单
回显数据后,再单击添加课程模块,数据还在,按理应该是清空数据
watch: {
$route(to, from) {
//路由变化方式,当路由发送变化,方法就执行
this.courseInfo = {}
},
}
6.6、修改课程信息
saveOrUpdate() {
if (this.courseInfo.id) {
this.updateCourse();
} else {
this.saveCourse();
}
},
saveCourse() {
course.addCourseInfo(this.courseInfo).then(resp => {
this.$message({
message: "添加课程信息成功",
type: "success",
})
//跳转到第二步,并带着这个课程生成的id
this.$router.push({ path: "/course/chapter/" + resp.data.courseId });
});
},
updateCourse() {
course.updateCourseInfo(this.courseInfo).then(resp => {
this.$message({
message: "修改课程信息成功",
type: "success",
})
//跳转到第二步,并带着这个课程生成的id
this.$router.push({ path: "/course/chapter/" + this.courseId });
});
},