谷粒学院
富文本编辑器Tinymce
一、Tinymce可视化编辑器
参考:https://panjiachen.gitee.io/vue-element-admin/#/example/create
二、组件初始化
Tinymce是一个传统javascript插件,默认不能用于Vue.js因此需要做一些特殊的整合步骤
1、复制脚本库
将脚本库复制到项目的static目录下(在vue-element-admin-master的static路径下)
2、配置html变量
在 guli-admin/build/webpack.dev.conf.js 中添加配置
使在html页面中可是使用这里定义的BASE_URL变量
new HtmlWebpackPlugin({
......,
templateParameters: {
BASE_URL: config.dev.assetsPublicPath + config.dev.assetsSubDirectory
}
})
3、引入js脚本
在guli-admin/index.html 中引入js脚本
三、组件引入
为了让Tinymce能用于Vue.js项目,vue-element-admin-master对Tinymce进行了封装,下面我们将它引入到我们的课程信息页面
1、复制组件
src/components/Tinymce
2、引入组件
课程信息组件中引入 Tinymce
//引入Tinymce富文本编辑器组件
import Tinymce from '@/components/Tinymce';
export default {
....
components: { Tinymce },
}
3、组件模板
<!-- 课程简介-->
<el-form-item label="课程简介">
<tinymce :height="300" v-model="courseInfo.description"/>
</el-form-item>
4、组件样式
在info.vue文件的最后添加如下代码,调整上传图片按钮的高度
<style scoped>
.tinymce-container {
line-height: 29px;
}
</style>
5、显示效果
四、测试
-
图片的base64编码
Tinymce中的图片上传功能直接存储的是图片的base64编码,因此无需图片服务器
- 后台报错
- 修改数据库description字段类型
- 发现一级分类没有值
- 在后端8001,com.achang.eduservice.entity.vo.CourseInfoForm中添加一级分类属性
要求跟前端对应的数据名字一致,不然无法获取
@ApiModelProperty(value = "一级分类ID")
private String subjectParentId;
- 再次测试,成功有值,且对应id
课程大纲列表功能
一、后端实现
1、创建两个实体类
章节和小节,在章节实体类使用list表示小节
- VideoVo
@Data
public class VideoVo implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String title;
private Boolean free;
}
- ChapterVo
@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>();
}
2、Controller
- EduChapterController
@RestController
@RequestMapping("/eduservice/edu-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("allChapterVideo",list);
}
}
3、Service
- service接口
public interface EduChapterService extends IService<EduChapter> {
List<ChapterVo> getChapterVideoByCourseId(String courseId);
}
- serviceImpl
@Service
public class EduChapterServiceImpl extends ServiceImpl<EduChapterMapper, EduChapter> implements EduChapterService {
@Autowired
private EduVideoService eduVideoService;
@Override
public List<ChapterVo> getChapterVideoByCourseId(String courseId) {
//最终要的数据列表
ArrayList<ChapterVo> finalChapterVos = new ArrayList<>();
//查询章节信息
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数据
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;
}
}
4、测试
二、前端实现
1、定义api
chapter.js
import request from '@/utils/request' //引入已经封装好的axios 和 拦截器
export default{
//根据课程id获取章节和小节数据列表
getChapterVideoByCourseId(courseId){
return request({
url:`/eduservice/edu-chapter/getChapterVideo/${courseId}`,
method: 'get',
})
},
}
2、引入api脚本方法
import chapter from '@/api/teacher/chapter.js';
3、定义方法、变量、created()调用
export default {
data() {
return {
courseId:'',
chapterVideoList:[],
....
};
},
methods: {
//根据课程id查询对应的课程章节和小结
getChapterVideoByCourseId(){
chapter.getChapterVideoByCourseId(this.courseId)
.then(resp=>{
this.chapterVideoList = resp.data.allChapterVideo
})
},
...
},
created() {
//获取路由里的id值
if(this.$route.params && this.$route.params.id){
this.courseId = this.¥route.params.id
}
//根据课程id查询对应的课程章节和小结
this.getChapterVideoByCourseId();
},
};
</script>
4、组件模版
<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">
{{ video.title }}
</li>
</ul>
</li>
<li>
<!-- 视频 -->
<ul class="chanpterList videoList">
<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>
课程修改功能
一、后端接口实现
1、根据课程id查询课程基本信息
- EduCourseController
//根据课程id查询课程基本信息
@GetMapping("/getCourseInfoById/{courseId}")
public R getCourseInfoById(@PathVariable String courseId){
CourseInfoForm courseInfoForm = eduCourseService.getCourseInfo(courseId);
return R.ok().data("courseInfoForm",courseInfoForm);
}
- EduCourseService
//根据课程id查询课程基本信息
CourseInfoForm getCourseInfo(String courseId);
- EduCourseServiceImpl
//课程描述注入
@Autowired
private EduCourseDescriptionService eduCourseDescriptionService;
@Override
public CourseInfoForm getCourseInfo(String courseId) {
//查询课程表
EduCourse eduCourse = baseMapper.selectById(courseId);
CourseInfoForm courseInfoForm = new CourseInfoForm();
BeanUtils.copyProperties(eduCourse,courseInfoForm);
//查询简介表
EduCourseDescription courseDescription = eduCourseDescriptionService.getById(courseId);
courseInfoForm.setDescription(courseDescription.getDescription());
return courseInfoForm;
}
2、修改课程信息
- EduCourseController
//修改课程信息
@PostMapping("/updateCourseInfo")
public R updateCourseInfo(@RequestBody CourseInfoForm courseInfoForm){
eduCourseService.updateCourseInfo(courseInfoForm);
return R.ok();
}
- EduCourseService
//修改课程信息
void updateCourseInfo(CourseInfoForm courseInfoForm);
- EduCourseServiceImpl
@Override
public void updateCourseInfo(CourseInfoForm courseInfoForm) {
//1、修改课程表
EduCourse eduCourse = new EduCourse();
BeanUtils.copyProperties(courseInfoForm,eduCourse);
int update = baseMapper.updateById(eduCourse);
if (update <= 0){
throw new AchangException(20001,"修改课程信息失败");
}
//2、修改描述信息
EduCourseDescription eduCourseDescription = new EduCourseDescription();
eduCourseDescription.setDescription(courseInfoForm.getDescription());
eduCourseDescription.setId(courseInfoForm.getId());
eduCourseDescriptionService.updateById(eduCourseDescription);
}
二、前端实现
- 定义api方法
guli-admin\src\api\teacher\course.js
//根据课程id 查询课程基本信息
getCourseInfoById(courseId){
return request({
url:`/eduservice/edu-course/getCourseInfoById/${courseId}`,
method: 'get',
})
},
//修改课程信息
updateCourseInfo(courseInfoForm){
return request({
url:"/eduservice/edu-course/updateCourseInfo",
method: 'post',
data: courseInfoForm,
})
}
- 修改chapter页面跳转路径
guli-admin\src\views\edu\course\chapter.vue
//跳转到上一步
previous() {
this.$router.push({ path: "/course/info/"+this.courseId});
},
next() {
//跳转到第三步
this.$router.push({ path: "/course/publish/"+this.courseId});
}
- info页面 created()
created() {
//判断路径中是否有课程id
if (this.$route.params && this.$route.params.id) {
this.courseId = this.$route.params.id;
//根据课程id 查询课程基本信息
this.getCourseInfo()
},
....
}
- info页面 methods
methods: {
//获取课程信息
getCourseInfo() {
course.getCourseInfoById(this.courseId).then((resp) => {
this.courseInfo = resp.data.courseInfoForm
})
},
....
}
- info页面 data
data() {
return {
...
courseId: "",
};
}
- 测试
- 上面问题,二级分类中的数据显示出现了问题
created() {
//判断路径中是否有课程id
if (this.$route.params && this.$route.params.id) {
this.courseId = this.$route.params.id;
//根据课程id 查询课程基本信息
this.getCourseInfo();
} else {
//初始化所有讲师
this.getListTeacher();
//初始化一级分类
this.getOneSubject();
}
}
//获取课程信息
getCourseInfo() {
course.getCourseInfoById(this.courseId).then((resp) => {
this.courseInfo = resp.data.courseInfoForm;
//查询所有分类,包含一级和二级所有
subject.getSubjectList().then((resp) => {
//获取所有一级分类
this.subjectOneLists = resp.data.list;
//把所有一级分类数组进行遍历
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;
}
}
});
//初始化所有讲师
this.getListTeacher();
});
}
- 出现一个bug,就是回显数据后,再单击添加课程,数据还在,按理应该是清空数据
添加监听器,监听路由,如果路由变化,就将courseInfo的数据清空
watch: {
$route(to, from) {
//路由变化方式,当路由发送变化,方法就执行
console.log("watch $route");
this.courseInfo={}
},
}
- 实现修改功能
guli-admin\src\views\edu\course\info.vue
//添加课程
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 });
});
},
//判断是修改还是新增
saveOrUpdate() {
//判断courseInfo中是否有id值
if (this.courseInfo.id) {
//有id值,为修改
this.updateCourse();
} else {
//没id值,为添加
this.saveCourse();
}
}
课程章节添加修改删除功能
一、后端接口实现
- controller
//添加章节
@PostMapping("addChapter")
public R addChapter(@RequestBody EduChapter eduChapter){
eduChapterService.save(eduChapter);
return R.ok();
}
//根据章节id查询
@GetMapping("getChapter/{chapterId}")
public R getChapter(@PathVariable String chapterId){
EduChapter eduChapter = eduChapterService.getById(chapterId);
return R.ok().data("chapter",eduChapter);
}
//修改章节
@PostMapping("updateChapter")
public R updateChapter(@RequestBody EduChapter eduChapter){
eduChapterService.updateById(eduChapter);
return R.ok();
}
//删除章节
@DeleteMapping("deleteById/{chapterId}")
public R deleteById(@PathVariable String chapterId){
boolean flag = eduChapterService.deleteChapter(chapterId);
if (flag){
return R.ok();
}else {
return R.error();
}
}
- service
boolean deleteChapter(String chapterId);
- serviceImpl
//删除章节的方法
@Override
public boolean deleteChapter(String chapterId) {
//根据chapter章节id 查询查询小节表,如果查询有数据,则不删除
QueryWrapper<EduVideo> wrapper = new QueryWrapper<>();
wrapper.eq("chapter_id",chapterId);
int count = eduVideoService.count(wrapper);
//判断
if (count>0){
//能查询出来小节,不进行删除
throw new AchangException(20001,"还有小节数据,不能删除");
}else {
//不能查询出小节,进行删除
int delete = baseMapper.deleteById(chapterId);
return delete>0;
}
}
二、前端页面实现
- 定义api
//添加章节
addChapter(chapter) {
return request({
url: `/eduservice/edu-chapter/addChapter`,
method: `post`,
data: chapter
})
},
//根据id查询章节
updateChapterById(chapterID) {
return request({
url: `/eduservice/edu-chapter/getChapter/${chapterID}`,
method: `get`,
})
},
//修改章节
updateChapter(chapter) {
return request({
url: `/eduservice/edu-chapter/updateChapter`,
method: `post`,
data: chapter
})
},
//删除章节
deleteById(chapterID) {
return request({
url: `/eduservice/edu-chapter/deleteById/${chapterID}`,
method: `delete`,
})
}
- 引入api
import chapter from "@/api/teacher/chapter.js";
-
【添加课程】前端实现
methods: {
//添加章节
saveChapter() {
//设置课程id到chapter对象中
this.chapter.courseId = this.courseId
chapter.addChapter(this.chapter).then((resp) => {
//关闭弹框
this.dialogChapterFormVisible = false;
//提示信息
this.$message({
message: "添加章节成功",
type: "success",
});
//刷新页面
this.getChapterVideoByCourseId()
});
},
saveOrUpdate() {
this.saveChapter()
}
}
- 设置com.achang.eduservice.entity.EduChapter的创建时间和更新时间自动增添
- 测试新增章节功能是否优先
- 设置弹出表单时,清空数据
//弹出添加章节表单
openChapterDialog(){
//清空之前的数据
this.chapter={}
//显示弹框
this.dialogChapterFormVisible = true
}
- 【修改功能】实现
//修改章节
updateChapter() {
//设置课程id到chapter对象中
this.chapter.courseId = this.courseId;
chapter.updateChapter(this.chapter).then((resp) => {
//关闭弹框
this.dialogChapterFormVisible = false;
//提示信息
this.$message({
message: "修改章节成功",
type: "success",
});
//刷新页面
this.getChapterVideoByCourseId();
});
}
saveOrUpdate() {
if (this.chapter.id) {
//修改章节
this.updateChapter();
} else {
//新增章节
this.saveChapter();
}
}
- 【删除功能】实现
//删除章节
removeById(chapterId) {
this.$confirm("此操作将永久删除章节信息, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
//点击确定,执行then方法
chapter.deleteById(chapterId).then((resp) => {
//删除成功
//提示信息
this.$message({
type: "success",
message: "删除成功!",
});
//刷新页面
this.getChapterVideoByCourseId();
});
});
}
课程章节小节功能
一、后端接口实现
- com.achang.eduservice.controller.EduVideoController
@RestController
@RequestMapping("/eduservice/edu-video")
@CrossOrigin //解决跨域问题
public class EduVideoController {
@Autowired
private EduVideoService eduVideoService;
//添加小节
@PostMapping("/addVideo")
public R addVideo(@RequestBody EduVideo eduVideo){
eduVideoService.save(eduVideo);
return R.ok();
}
//删除小节
// TODO 后面这个方法需要完善,删除小节的时候,同时也要把视频删除
@DeleteMapping("/deleteVideo/{id}")
public R deleteVideo(@PathVariable String id){
eduVideoService.removeById(id);
return R.ok();
}
//修改小节
@PostMapping("/updateVideo")
public R updateVideo(@RequestBody EduVideo eduVideo){
eduVideoService.updateById(eduVideo);
return R.ok();
}
//根据小节id查询
@GetMapping("/getVideoById/{videoId}")
public R getVideoById(@PathVariable String videoId){
EduVideo eduVideo = eduVideoService.getById(videoId);
return R.ok().data("video",eduVideo);
}
}
二、前端实现
- 页面搭建
<!--添加小节表单-->
<!-- 添加和修改课时表单 -->
<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(video.id)"
>确 定</el-button
>
</div>
</el-dialog>
- api
guli-admin\src\api\teacher\video.js
import request from '@/utils/request' //引入已经封装好的axios 和 拦截器
export default {
//添加小节
addVideo(video) {
return request({
url: `/eduservice/edu-video/addVideo`,
method: `post`,
data: video
})
},
//根据id查询小节
getVideoById(videoId) {
return request({
url: `/eduservice/edu-video/getVideoById/${videoId}`,
method: `get`,
})
},
//修改小节
updateVideo(video) {
return request({
url: `/eduservice/edu-video/updateVideo`,
method: `post`,
data: video
})
},
//删除小节
deleteById(videoId) {
return request({
url: `/eduservice/edu-video/deleteVideo/${videoId}`,
method: `delete`,
})
},
}
- 引入api
import video from "@/api/teacher/video.js";
- 方法定义
//添加小节弹框的方法
openEditVideo(chapterId) {
//清空之前的数据
this.video = {};
//显示弹框
this.dialogVideoFormVisible = true;
//设置章节id
this.video.chapterId = chapterId;
},
- 添加方法
//添加小节
addVideo() {
//设置课程id
this.video.courseId = this.courseId;
video.addVideo(this.video).then((resp) => {
//关闭弹框
this.dialogVideoFormVisible = false;
//提示信息
this.$message({
message: "添加小节成功",
type: "success",
});
//刷新页面
this.getChapterVideoByCourseId();
});
},
- 给EduVideo,entity对象添加自动插入时间和修改时间
- 测试通过
- 【删除小节】功能
//删除小节
removeVideo(videoId) {
this.$confirm("此操作将永久删除小节信息, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
//点击确定,执行then方法
video.deleteById(videoId).then((resp) => {
//删除成功
//提示信息
this.$message({
type: "success",
message: "删除成功!",
});
//刷新页面
this.getChapterVideoByCourseId();
});
});
},
- 【小节修改】功能
//修改小节表单回显
getVideoById(videoId) {
//弹出小节弹窗
this.dialogVideoFormVisible = true;
video.getVideoById(videoId).then((resp) => {
this.video = resp.data.video;
});
},
- 【小节修改】功能
//小节修改
updateVideorById(videoId) {
//设置小节id到video对象中
this.video.id = videoId;
video.updateVideo(this.video).then((resp) => {
//关闭弹框
this.dialogVideoFormVisible = false;
//提示信息
this.$message({
message: "修改小节成功",
type: "success",
});
//刷新页面
this.getChapterVideoByCourseId();
});
},
- 测试
课程最终发布
一、后端实现
- com.achang.eduservice.entity.vo.CoursePublishVo
@Data
public class CoursePublishVo implements Serializable {
private static final long serialVersionUID = 1L;
private String id;//课程id
private String title; //课程名称
private String cover; //封面
private Integer lessonNum;//课时数
private String subjectLevelOne;//一级分类
private String subjectLevelTwo;//二级分类
private String teacherName;//讲师名称
private String price;//价格 ,只用于显示
}
- 数据访问层
接口:EduCourseMapper
public interface EduCourseMapper extends BaseMapper<EduCourse> {
public CoursePublishVo getPublishCourseInfo(String courseId);
}
实现:EduCourseMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.achang.eduservice.mapper.EduCourseMapper">
<select id="getPublishCourseInfo" resultType="com.achang.eduservice.entity.vo.CoursePublishVo">
SELECT
ec.id,
ec.title,
ec.cover,
ec.lesson_num AS lessonNum,
ec.price,
s1.title AS subjectLevelOne,
s2.title AS subjectLevelTwo,
t.name AS teacherName
FROM
edu_course ec
LEFT JOIN edu_teacher t ON ec.id = t.id
LEFT JOIN edu_subject s1 ON ec.subject_parent_id = s1.id
LEFT JOIN edu_subject s2 ON ec.id = s2.id
WHERE
ec.id = #{id}
</select>
</mapper>
- EduCourseMapper接口
@Component
public interface EduCourseMapper extends BaseMapper<EduCourse> {
public CoursePublishVo getPublishCourseInfo(String courseId);
}
- EduCourseService接口
根据课程id查询课程确认信息
public CoursePublishVo getPublishCourseInfo(String courseId);
- EduCourseServiceImpl
//根据课程id查询课程确认信息
@Override
public CoursePublishVo getPublishCourseInfo(String courseId) {
return eduCourseMapper.getPublishCourseInfo(courseId);
}
- EduCourseController
//根据课程id查询课程确认信息
@GetMapping("/getpublishCourseInfo/{id}")
public R getpublishCourseInfo(@PathVariable String id){
CoursePublishVo publishCourseInfo = eduCourseService.getPublishCourseInfo(id);
return R.ok().data("publishCourse",publishCourseInfo);
}
-
测试,访问:http://localhost:8801/swagger-ui.html
-
报错
- 原因、解决方式
- application.properties下指定xml文件夹
#配置mapper xml文件的路径
mybatis-plus.mapper-locations=classpath:com/achang/eduservice/mapper/xml/*.xml
- 再次测试,成功
二、前端实现
…明天继续