2022年最新《谷粒学院开发教程》:4 - 课程管理

资料
资料地址
后台管理系统目录前台展示系统目录
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、前端展示 初始化步骤条

element-ui step步骤条

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、课程分类多级联动

ElmentUI - 选择器

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 });
  });
},

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Laptoy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值