谷粒学院16万字笔记+1600张配图(八)——课程管理

项目源码与所需资料
链接:https://pan.baidu.com/s/1azwRyyFwXz5elhQL0BhkCA?pwd=8z59
提取码:8z59

文章目录

demo08-课程管理

1.课程发布流程

在这里插入图片描述

2.课程相关表

2.1课程相关表的关系

在这里插入图片描述

解释一下为什么讲师表和课程表是一对多的关系而不是多对多的关系?

  • 首先我们肯定知道一位老师对应多门课程
  • 我们再来说为什么不可能会有一门课程对应多位老师
    • 比如说:张三讲了一门java课程,;李四也讲了一门java课程。首先我们知道这两个java课程是两个老师讲的,所以这两个java课程的id一定不同,既然id不同,我们就可以认为是两门不同的课程,这也就是说,不可能会有一门课程对应多位老师
  • 所以说讲师表和课程表是一对多的关系

2.2创建数据表

我们现在知道了课程管理的实现需要六张表:edu_course、edu_course_description、edu_chapter、edu_video、edu_teacher、edu_subject。其中edu_teacher、edu_subject我们已经在前面创建过了,所以我们现在只需要创建其余四个数据表:去资料–>数据库脚本–>guli_edu.sql中找到创建这四个数据表的sql语句,复制到数据库中并执行这些语句

CREATE TABLE `edu_chapter` (
  `id` char(19) NOT NULL COMMENT '章节ID',
  `course_id` char(19) NOT NULL COMMENT '课程ID',
  `title` varchar(50) NOT NULL COMMENT '章节名称',
  `sort` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '显示排序',
  `gmt_create` datetime NOT NULL COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_course_id` (`course_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT COMMENT='课程';

CREATE TABLE `edu_course` (
  `id` char(19) NOT NULL COMMENT '课程ID',
  `teacher_id` char(19) NOT NULL COMMENT '课程讲师ID',
  `subject_id` char(19) NOT NULL COMMENT '课程专业ID',
  `subject_parent_id` char(19) NOT NULL COMMENT '课程专业父级ID',
  `title` varchar(50) NOT NULL COMMENT '课程标题',
  `price` decimal(10,2) unsigned NOT NULL DEFAULT '0.00' COMMENT '课程销售价格,设置为0则可免费观看',
  `lesson_num` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '总课时',
  `cover` varchar(255) CHARACTER SET utf8 NOT NULL COMMENT '课程封面图片路径',
  `buy_count` bigint(10) unsigned NOT NULL DEFAULT '0' COMMENT '销售数量',
  `view_count` bigint(10) unsigned NOT NULL DEFAULT '0' COMMENT '浏览数量',
  `version` bigint(20) unsigned NOT NULL DEFAULT '1' COMMENT '乐观锁',
  `status` varchar(10) NOT NULL DEFAULT 'Draft' COMMENT '课程状态 Draft未发布  Normal已发布',
  `is_deleted` tinyint(3) DEFAULT NULL COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
  `gmt_create` datetime NOT NULL COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_title` (`title`),
  KEY `idx_subject_id` (`subject_id`),
  KEY `idx_teacher_id` (`teacher_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT COMMENT='课程';

CREATE TABLE `edu_course_description` (
  `id` char(19) NOT NULL COMMENT '课程ID',
  `description` text COMMENT '课程简介',
  `gmt_create` datetime NOT NULL COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程简介';

CREATE TABLE `edu_video` (
  `id` char(19) NOT NULL COMMENT '视频ID',
  `course_id` char(19) NOT NULL COMMENT '课程ID',
  `chapter_id` char(19) NOT NULL COMMENT '章节ID',
  `title` varchar(50) NOT NULL COMMENT '节点名称',
  `video_source_id` varchar(100) DEFAULT NULL COMMENT '云端视频资源',
  `video_original_name` varchar(100) DEFAULT NULL COMMENT '原始文件名称',
  `sort` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '排序字段',
  `play_count` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '播放次数',
  `is_free` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否可以试听:0收费 1免费',
  `duration` float NOT NULL DEFAULT '0' COMMENT '视频时长(秒)',
  `status` varchar(20) NOT NULL DEFAULT 'Empty' COMMENT 'Empty未上传 Transcoding转码中  Normal正常',
  `size` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '视频源文件大小(字节)',
  `version` bigint(20) unsigned NOT NULL DEFAULT '1' COMMENT '乐观锁',
  `gmt_create` datetime NOT NULL COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_course_id` (`course_id`),
  KEY `idx_chapter_id` (`chapter_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT COMMENT='课程视频';

在这里插入图片描述

3.添加课程基本信息后端

3.1生成代码

1.将代码生成器中的strategy.setInclude(“edu_subject”);改为strategy.setInclude(“edu_course”,“edu_course_description”,“edu_chapter”,“edu_video”);

在这里插入图片描述

2.执行代码生成器生成代码

3.课程简介依赖于课程基本信息,我们会在课程基本信息中修改课程简介,但是不会单独对课程简介进行修改,所以不需要课程简介的controller,将其删掉

在这里插入图片描述

4.修改控制器EduCourseController中的代码:

  • 给这个控制器加一个@CrossOrigin注解,解决跨域问题

  • 我有强迫症,我要像老师一样,将这个控制器代码中的@RequestMapping("/eduservice/edu-course")改为@RequestMapping("/eduservice/course")

在这里插入图片描述

5.给实体类EduCourse和EduCourseDescription的两个属性gmtCreate、gmtModified添加注解@TableField

在这里插入图片描述

在这里插入图片描述

3.2分析

1.前端给后端提交数据,我们怎么接收这些数据呢?还像以前一样使用实体类EduCourse封装数据吗?可是前端提交的数据中有一个数据是课程简介,EduCourse实体类并没有这个属性呀!正确的做法是:单独创建一个vo类来封装前端传过来的数据

2.我们将表单提交过来的数据添加到数据库中时需要向两张表添加数据:课程表和课程简介表

3.课程基本信息中的所属讲师和所属分类可以在前端使用下拉列表来实现,而且所属分类需要做成二级联动的效果(因为课程分类分为一级分类、二级分类)

3.3创建vo类

1.在entity–>vo包下创建vo类CourseInfoVo

@ApiModel(value = "课程基本信息", description = "编辑课程基本信息的表单对象")
@Data
public class CourseInfoVo {
    
    
    @ApiModelProperty(value = "课程ID")
    private String id;

    @ApiModelProperty(value = "课程讲师ID")
    private String teacherId;

    @ApiModelProperty(value = "课程专业ID")
    private String subjectId;

    @ApiModelProperty(value = "课程标题")
    private String title;

    @ApiModelProperty(value = "课程销售价格,设置为0则可免费观看")
    private BigDecimal price;

    @ApiModelProperty(value = "总课时")
    private Integer lessonNum;

    @ApiModelProperty(value = "课程封面图片路径")
    private String cover;

    @ApiModelProperty(value = "课程简介")
    private String description;
}

在这里插入图片描述

  • 表示价格时用Double或Float可能会有精度问题,用BigDecimal就不会有精度问题

2.我们此时创建的vo类中并没有属性subjectParentId,这也就意味着从前端传过来的数据中的subjectParentId不会被封装到实体类中,那么向edu_course表插入数据时就不会给subject_parent_id字段赋值,但是因为edu_course表的subject_parent_id字段必须非空,所以我们需要改变数据表edu_course的subject_parent_id字段,给它暂时随便设置一个默认值

在这里插入图片描述

3.4控制层

在控制器EduCourseController中编写代码:

@RestController
@RequestMapping("/eduservice/course")
@CrossOrigin
public class EduCourseController {
    
    

    @Autowired
    private EduCourseService courseService;

    //添加课程基本信息的方法
    @PostMapping("addCourseInfo")
    public R addCourseInfo(@RequestBody CourseInfoVo courseInfoVo) {
    
    
        courseService.saveCourseInfo(courseInfoVo);
        return R.ok();
    }
}

在这里插入图片描述

3.5业务层

1.在接口EduCourseService中定义抽象方法

public interface EduCourseService extends IService<EduCourse> {
    
    

    //添加课程基本信息的方法
    void saveCourseInfo(CourseInfoVo courseInfoVo);
}

在这里插入图片描述

2.在实现类EduCourseServiceImpl中实现刚刚定义的抽象方法

@Service
public class EduCourseServiceImpl extends ServiceImpl<EduCourseMapper, EduCourse> implements EduCourseService {
    
    

    //注入EduCourseDescriptionService
    @Autowired
    private EduCourseDescriptionService courseDescriptionService;

    //添加课程基本信息的方法
    @Override
    public void saveCourseInfo(CourseInfoVo courseInfoVo) {
    
    
        //1.向课程表添加课程基本信息
        //①将CourseInfoVo对象转换为EduCourse对象
        EduCourse eduCourse = new EduCourse();
        BeanUtils.copyProperties(courseInfoVo, eduCourse);
        //②执行课程模块的持久层方法向课程表插入数据
        int insert = baseMapper.insert(eduCourse);
        //③判断是否插入成功
        if (insert <= 0) {
    
    
            //添加失败
            throw new GuliException(20001, "添加课程信息失败");
        }

        //2.向课程简介表添加课程简介
        //①将CourseInfoVo对象转换为courseDescription对象
        EduCourseDescription courseDescription = new EduCourseDescription();
        BeanUtils.copyProperties(courseInfoVo, courseDescription);
        //②设置课程简介id和刚刚插入的课程id相同,这样才可以一对一
        courseDescription.setId(eduCourse.getId());
        //③执行课程简介模块的业务层方法向课程简介表插入数据
        courseDescriptionService.save(courseDescription);
    }
}

在这里插入图片描述

  • 忘了BeanUtils.copyProperties(courseInfoVo, eduCourse);baseMapper.insert(eduCourse);的可以回去看"demo07-课程分类管理"的"7.4.2业务层实现类"最下面我说的两段话

  • 为什么要注入EduCourseDescriptionService呢:因为我们后面会用到课程简介模块的业务层方法(截图中的第52行:courseDescriptionService.save(courseDescription);)

  • 那为什么可以在这里注入EduCourseDescriptionService呢:因为EduCourseServiceImpl和EduCourseDescriptionServiceImpl都交给了Spring管理,所以可以实现注入(有点听不懂的可以去看"demo07-课程分类管理"的"4.7.2遇到的问题"的第1步)

  • 截图中第50行的eduCourse.getId()可以获得刚刚插入的课程id,我们说过很多次了,这是因为执行完插入操作(截图中的第38行)后id自动回填到eduCourse对象中,忘了的再去看"demo02-MybatisPlus"的"2.9添加操作"

3.我们知道,在向课程简介表插入数据时,数据id是我们自己设置的,所以我们要修改一下EduCourseDescription实体类中id的生成策略:

IdType.ID_WORKER_STR改为IdType.INPUT

在这里插入图片描述

3.6测试

1.启动service_edu项目,访问http://localhost:8001/swagger-ui.htm

2.输入数据,然后点击"Try it out!"进行测试

{
  "cover": "string",
  "description": "string0821",
  "lessonNum": 0,
  "price": 0,
  "subjectId": "string",
  "teacherId": "string",
  "title": "string0821"
}

在这里插入图片描述

3.去数据库查看数据,可以看到测试成功

在这里插入图片描述

在这里插入图片描述

4.添加课程基本信息前端

4.1添加课程管理路由

在src–>router–>index.js中添加路由:

{
    
    
  path: '/course',
  component: Layout,
  redirect: '/course/list',
  name: '课程管理',
  meta: {
    
     title: '课程管理', icon: 'example' },
  children: [
    {
    
    
      path: 'list',
      name: '课程列表',
      component: () => import('@/views/edu/course/list'),
      meta: {
    
     title: '课程列表', icon: 'table' }
    },
    {
    
    
      path: 'info',
      name: '添加课程',
      component: () => import('@/views/edu/course/info'),
      meta: {
    
     title: '添加课程', icon: 'tree' }
    },
    {
    
    
      path: 'info/:id',
      name: 'EduCourseInfoEdit',
      component: () => import('@/views/edu/course/info'),
      meta: {
    
     title: '编辑课程基本信息', noCache: true },
      hidden: true
    },
    {
    
    
      path: 'chapter/:id',
      name: 'EduCourseChapterEdit',
      component: () => import('@/views/edu/course/chapter'),
      meta: {
    
     title: '编辑课程大纲', noCache: true },
      hidden: true
    },
    {
    
    
      path: 'publish/:id',
      name: 'EduCoursePublishEdit',
      component: () => import('@/views/edu/course/publish'),
      meta: {
    
     title: '发布课程', noCache: true },
      hidden: true
    }
  ]
},

在这里插入图片描述

4.2创建vue页面

根据上一步在路由中添加的路径,我们在src–>views–>edu目录下创建目录course,然后在course目录下:

1.创建三个vue页面:info.vue、chapter.vue、publish.vue。这三个页面的作用:我们在"1.课程发布流程"说过添加课程分三步走:编辑课程基本信息–>编辑课程大纲–>课程最终发布,我们创建的这三个vue页面就分别对应这三个步骤

2.创建list.vue页面作为课程列表页面

在这里插入图片描述

4.3给info.vue页面添加步骤条

正常情况下还是复制element-ui中合适例子的代码到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 //保存按钮是否禁用
    }
  },
  created() {
  },
  methods: {
    next() {
      //跳转到第二步
      this.$router.push({path: '/course/chapter/1'})
    }
  }
}
</script>

在这里插入图片描述

  • 点击"保存并下一步 "时调用next方法,该方法内部负责跳转到第二步
  • 截图中第33行的this.$router.push({path: '/course/chapter/1'})中的课程id我们暂时固定写为1

4.4给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
        }
    },
    created() {
    },
    methods: {
        //跳转到第一步
        previous() {
            this.$router.push({path: '/course/info/1'})
        },
        //跳转到第三步
        next() {
            this.$router.push({path: '/course/publish/1'})
        }
    }
}
</script>

在这里插入图片描述

同样的,我们这里课程id也暂且固定写为1

4.5给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
        }
    },
    created() {
    },
    methods: {
        //跳转到第二步
        previous() {
            this.$router.push({path: '/course/chapter/1'})
        },
        //跳转到list.vue页面
        publish() {
            this.$router.push({path: '/course/list'})
        }
    }
}
</script>

在这里插入图片描述

同样的,跳转到第二步时路径中的课程id也暂且固定写为1

4.6再次编写info.vue

4.6.1给该页面添加表单组件

将info.vue中的<el-form>标签(在"4.3给info.vue页面添加步骤条"的截图中绿色方框圈起来的部分)删掉,替换为下面的代码

<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>

  <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="next">保存并下一步</el-button>
  </el-form-item>
</el-form>

在这里插入图片描述

4.6.2定义数据模型

上一步在info.vue编写的代码中用到了courseInfo对象,所以我们需要在data() {...}中定义数据模型courseInfo,这个对象的属性我们是否定义都可以(我们在"demo05-讲师管理前端"的"7.1去element-ui找示例代码"的第2步的截图中说过)。这里我是定义出来了courseInfo对象的属性

courseInfo: {
    
    
  title: '',
  subjectId: '',
  teacherId: '',
  lessonNum: 0,
  description: '',
  cover: '',
  price: 0
}

在这里插入图片描述

4.7在api中定义方法

在src–>api–>edu目录下创建一个course.js文件并编写方法

import request from '@/utils/request'

export default {
    
    
  //1.添加课程基本信息
  addCourseInfo(courseInfo) {
    
    
    return request({
    
    
      url: `/eduservice/course/addCourseInfo`,
      method: 'post',
      data: courseInfo
    })
  }
}

在这里插入图片描述

4.8再次编写info.vue

4.8.1引入js文件

我们要使用上一步创建的js文件中的方法,所以需要引入上一步创建的js文件

import course from '@/api/edu/course'

在这里插入图片描述

4.8.2修改函数名

将点击事件的函数改名为saveOrUpdate

原因:这个页面和src–>views–>edu–>teacher–>save.vue页面一样,既有添加功能,又有修改功能,所以这个页面的按钮也是有两个作用:添加、修改,所以本着见名知意的原则,我们将函数名改为saveOrUpdate

在这里插入图片描述

4.8.3修改函数saveOrUpdate的内容

saveOrUpdate() {
    
    
  course.addCourseInfo(this.courseInfo)
    .then(response => {
    
    
      //1.提示成功
      this.$message({
    
    
          type: 'success',
          message: '添加课程基本信息成功'
      })
      //2.跳转到第二步
      this.$router.push({
    
    path: '/course/chapter/1'})
    })
}

在这里插入图片描述

4.9测试

1.启动service_edu、nginx、前端项目,然后访问http://localhost:9528

2.在"添加课程"路由下的页面填写信息后点击"保存并下一步"

在这里插入图片描述

3.可以看到测试成功

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

但注意一点:第一张截图的路径参数固定是1,这显然是不合理的,我们在后面的"4.10让前端添加后能得到课程id"会解决这个问题

4.10让前端添加后能得到课程id

1."4.3给info.vue页面添加步骤条"的截图中的第33行:this.$router.push({path: '/course/chapter/1'}),表示添加课程基本信息成功后路由跳转时课程id固定为1,这显然是不合理的,解决这种现象的前提是前端添加后能得到课程id

2.我们想要前端添加后能得到课程id,就需要先修改后端代码,让后端返回课程id

①控制层:

courseService.saveCourseInfo(courseInfoVo);改为String id = courseService.saveCourseInfo(courseInfoVo);

return R.ok();改为return R.ok().data("courseId", id);

在这里插入图片描述

②业务层:

将接口中的saveCourseInfo方法的返回值由void改为String

在这里插入图片描述

将实现类的saveCourseInfo方法的返回值由void改为String,并且在该方法的最后加上一行代码用于返回值:return eduCourse.getId();

在这里插入图片描述

3.将info.vue页面的saveOrUpdate方法中的this.$router.push({path: ‘/course/chapter/1’})改为this.$router.push({path: ‘/course/chapter/’ + response.data.courseId})

在这里插入图片描述

4.再次测试:

重启后端的service_edu项目,访问http://localhost:9528,然后在"添加课程"路由下的页面填写信息后点击"保存并下一步",可以看到地址栏中的路径参数变为了添加的课程的id

在这里插入图片描述

4.11显示讲师下拉列表

4.11.1给info.vue添加下拉列表组件

1.去element-ui的组件中找到下拉列表的例子,将代码复制过来根据需求进行修改

在这里插入图片描述

2.老师已经给过我们代码了

<!-- 课程讲师 -->
<el-form-item label="课程讲师">
  <el-select
    v-model="courseInfo.teacherId"
    placeholder="请选择">
    <el-option
      v-for="teacher in teacherList"
      :key="teacher.id"
      :label="teacher.name"
      :value="teacher.id"/>
  </el-select>
</el-form-item>

在这里插入图片描述

截图中第24行的v-model="courseInfo.teacherId"和第30行的:value="teacher.id"共同作用,目的是:提交表单数据时可以将讲师id赋值给courseInfo对象的teacherId属性

3.上一张截图中第27行的teacherList就是讲师数据,所以我们我们需要在模型数据中定义teacherList

在这里插入图片描述

4.11.2在api中定义方法

1.首先我们要知道讲师的下拉列表我们肯定不会且不能分页显示,所以调用的是后端的"查询所有讲师"的方法而不是"分页查询所有讲师"的方法。注意了:我们在"demo03-后台讲师管理模块"的"10.4自定义异常处理"的第3步给"查询所有讲师"的方法内部故意添加了一个异常,现在我们将它删掉

在这里插入图片描述

2.在src–>api–>edu–>course.js中定义方法

//2.查询所有讲师
getListTeacher() {
    
    
  return request({
    
    
    url: `/eduservice/edu-teacher/findAll`,
    method: 'get'
  })
}

在这里插入图片描述

4.11.3调用api中的方法

在info.vue的methods: {...}中调用刚刚在api中定义的方法

//查询所有讲师
getListTeacher() {
    
    
  course.getListTeacher()
    .then(response => {
    
    
      this.teacherList = response.data.items
    })
},

在这里插入图片描述

4.11.4初始化所有讲师

created() {...}中调用上一步定义的getListTeacher方法以初始化所有讲师

created() {
    
    
  //初始化所有讲师
  this.getListTeacher()
},

在这里插入图片描述

4.11.5测试

1.访问http://localhost:9528/,点击"添加课程"路由,可以看到下拉列表成功显示

在这里插入图片描述

2.填写信息后点击"保存并下一步"

在这里插入图片描述

3.测试成功

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

4.12显示课程一级分类

4.12.1分析

在我们进入info.vue页面后,所有一级分类都显示,二级分类为空,当我们选择了一级分类后就会显示该一级分类下的所有二级分类

4.12.2添加一级分类的下拉列表组件

1.在info.vue中添加一级分类的下拉列表组件

<el-form-item label="课程分类">
  <!-- 一级分类 -->
  <el-select
    v-model="courseInfo.subjectParentId"
    placeholder="一级分类">
    <el-option
      v-for="subject in subjectOneList"
      :key="subject.id"
      :label="subject.title"
      :value="subject.id"/>
  </el-select>
</el-form-item>

在这里插入图片描述

2.上一张截图中的第22和第25行分别用了courseInfo.subjectParentIdsubjectOneList数据模型,我们现在就去定义这两个数据模型

在这里插入图片描述

但其实是否在courseInfo对象中定义subjectParentId 属性都是可以的:我们在"demo05-讲师管理前端"的"7.1去element-ui找示例代码"的第2步的截图中说过:因为进行了双向数据绑定(上一张截图的第22行v-model="courseInfo.subjectParentId"),所以对象中的属性是否定义都可以

3.给vo类CourseInfoVo添加属性:

添加课程基本信息时前端是通过调用api中的addCourseInfo方法(该方法在src–>api–>edu–>course.js中)实现的,我们知道,调用该方法时给方法传的参数是info.vue中的数据模型courseInfo对象,我们在上一步给该数据模型中定义一个subjectParentId属性,那既然这样的话后端用于封装前端数据的vo类CourseInfoVo中就一定要有属性subjectParentId,但我们在"3.3创建vo类"时忘了给类CourseInfoVo添加subjectParentId属性,这太致命了,我们赶紧给类CourseInfoVo添加这个属性:

在这里插入图片描述

4.12.3在api中定义方法

需要在api中定义方法来调用后端"查询课程一级分类"的接口,这一步我们在"demo07-课程分类管理"的"8.1定义api方法"已经做过了,我们已经在src–>api–>edu–>subject.js中定义了这个方法

在这里插入图片描述

4.12.4调用api中的方法

1.想要调用subject.js中的方法,就需要先引入这个文件

import subject from '@/api/edu/subject'

在这里插入图片描述

2.接下来我们定义方法来调用api中的getSubjectList方法

//查询所有的一级分类
getOneSubject() {
    
    
  subject.getSubjectList()
    .then(response => {
    
    
      this.subjectOneList = response.data.list
    })
},

在这里插入图片描述

4.12.5初始化所有一级分类

created() {...}中调用上一步定义的getOneSubject方法以初始化所有一级分类

//初始化所有一级分类
this.getOneSubject()

在这里插入图片描述

4.12.6测试

点击"添加课程"路由可以看到测试成功

在这里插入图片描述

4.13显示课程二级分类

4.13.1添加二级分类的下拉列表组件

1.在info.vue中添加二级分类的下拉列表组件

<!-- 二级分类 -->
<el-select
  v-model="courseInfo.subjectId"
  placeholder="二级分类">
  <el-option
    v-for="subject in subjectTwoList"
    :key="subject.id"
    :label="subject.title"
    :value="subject.id"/>
</el-select>

在这里插入图片描述

2.上一张截图中的第36行用了subjectTwoList数据模型,我们现在就去定义这个数据模型

在这里插入图片描述

4.13.2给一级分类下拉框绑定事件

在"4.12.1分析"说过,当我们选择了一级分类后就会显示该一级分类下的所有二级分类,那么人家怎么我们什么时候选择了一级分类呢?这就需要给一级分类的下拉框绑定一个类似onclick的事件,只是onclick事件是给按钮绑定的,给下拉列表绑定时需要用onchange事件

在这里插入图片描述

4.13.3定义change事件方法

1.从上张截图我们可以知道,触发change事件后调用的方法是subjectLevelOneChanged,那么下面我们就来编写这个方法,要求方法内部可以实现:点击某个一级分类后,能显示该一级分类下的所有二级分类

2.虚假的实现方式:

首先我们知道选择好一级分类后就会得到一级分类的id("4.12.2添加一级分类的下拉列表组件"的截图中的第22行的v-model="courseInfo.subjectParentId")。那么我们①在后端编写一个接口,该接口的作用是通过一级分类的id查询其下的所有二级分类并返回,②然后前端api中再定义一个方法用于调用后端的这个接口,③最后在subjectLevelOneChanged方法中调用api就可以得到该一级分类下的所有二级分类。但不建议这样做,因为这样做的话我们不仅要在后端再写一个接口,而且还要查询数据库

3.真正的实现方式:

①我们在subject.js中定义的api调用的是后端的getAllSubject接口。这个接口返回的数据是List<OneSubject>;而OneSubject实体类中的属性是id、title、List<TwoSubject>类型的children;并且TwoSubject实体类中的属性是id、title。所以说,我们调用subject.js中的getSubjectList方法后得到的数据中既有一级分类的id、title,也有二级分类的id、title

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

②所以我们可以在subjectLevelOneChanged方法内部遍历从后端得到数据,就可以得到二级分类。这样的话,既不用再后端编写接口,也不用查询数据库!

//点击某个一级分类后,能显示该一级分类下的所有二级分类
subjectLevelOneChanged(value) {
    
     //value是传过来的一级分类id
  //遍历所有分类,包含一级和二级
  for(var i = 0;i < this.subjectOneList.length;i++) {
    
    
    //遍历得到的一级分类
    var oneSubject = this.subjectOneList[i]
    //判断遍历得到的一级分类id和我们点击的一级分类id是否一样
    if(oneSubject.id == value)
      //获取该一级分类下所有的二级分类
      this.subjectTwoList = oneSubject.children
  }
},

在这里插入图片描述

  • 截图中第109行的this.subjectOneList.length和第113行的oneSubject.id:为什么使用subjectOneList时需要加this,而使用oneSubject时就不需要加this呢?

    • 因为subjectOneList是在数据模型中(data() {...})定义的,而oneSubject是在方法subjectLevelOneChanged内部定义的
  • 定义的subjectLevelOneChanged方法是有参数的,按理说我们调用这个方法时肯定要将一级分类的id作为参数传过去的呀,但是我们绑定事件时并没有手动给subjectLevelOneChanged方法传参(在"4.13.2给一级分类下拉框绑定事件"的截图中的第23行:@change="subjectLevelOneChanged"),这是因为框架给我们做了封装:人家调用subjectLevelOneChanged方法时可以自己把一级分类id传过来,不需要我们手动传参(在"demo05-讲师管理前端"的"6.3修改getList方法"也说过)

4.13.4测试

在这里插入图片描述

4.13.5完善

1.在一级分类中选择"前端开发",然后在二级分类中选择"vue"

在这里插入图片描述

2.重新选择一级分类为"后端开发",可以看到二级分类的"vue"仍存在

在这里插入图片描述

在这里插入图片描述

3.先看截图

在这里插入图片描述

从截图第33行的v-model="courseInfo.subjectId",我们可以知道:二级分类下拉列表和数据模型中courseInfo对象的subjectId属性做了双向绑定,那么重新选择一级分类后数据模型中的courseInfo.subjectId仍有数据,所以就会出现上面这种情况。现在我们在subjectLevelOneChanged方法中添加一行代码使得:重新选择一级分类后,我们要清空数据模型中的courseInfo.subjectId数据

//清空二级分类的id值
this.courseInfo.subjectId = ''

在这里插入图片描述

4.14封面上传

上传文件到阿里云oss是后端需要做的,我们已经实现了,所以我们接下来只需要编写前端代码就可以了

4.14.1添加上传组件

1.在info.vue中添加上传组件模板

<!-- 课程封面-->
<el-form-item label="课程封面">
  <el-upload
    :show-file-list="false"
    :on-success="handleAvatarSuccess"
    :before-upload="beforeAvatarUpload"
    :action="BASE_API+'/eduoss/fileoss'"
    class="avatar-uploader">
    <img :src="courseInfo.cover">
  </el-upload>
</el-form-item>

在这里插入图片描述

  • :show-file-list="false":有了这段代码就不会再显示文件上传列表

在这里插入图片描述

  • :on-success="handleAvatarSuccess":上传成功后调用的方法,方法内部一般用来得到文件的url
  • :before-upload="beforeAvatarUpload":上传之前调用的方法,方法内部一般用来规定文件类型和文件大小
  • :action="BASE_API+'/eduoss/fileoss'":后端接口地址
  • class="avatar-uploader":样式
  • 还记不记得在"demo07-课程分类管理"的"5.2创建vue页面"的第3步的①中说的:auto-upload="false"?这行代码的作用是关闭自动上传。因为我们这里的上传组件中没有添加这行代码,所以选择好照片后就会自动上传到服务器

2.上一张截图中的第72行用了BASE_API数据模型,我们现在就去定义这个数据模型

在这里插入图片描述

不知道红框添加的这行代码作用的去看"demo06-上传讲师头像"的"6.3使用组件"

4.14.2定义方法

接下来我们来编写上传之前调用的beforeAvatarUpload方法和上传成功后调用的handleAvatarSuccess方法

//上传封面成功调用的方法
handleAvatarSuccess(response) {
    
     //response就是后端返回的数据
  this.courseInfo.cover = response.data.url
},
//上传封面之前调用的方法
beforeAvatarUpload(file) {
    
    
  const isJPG = file.type === 'image/jpeg'
  const isLt2M = file.size / 1024 / 1024 < 2
  if (!isJPG) {
    
    
    this.$message.error('上传头像图片只能是 JPG 格式!')
  }
  if (!isLt2M) {
    
    
    this.$message.error('上传头像图片大小不能超过 2MB!')
  }
  return isJPG && isLt2M
},

在这里插入图片描述

4.14.3测试

1.启动service_edu、service_oss、前端项目、nginx,然后访问http://localhost:9528/

2.点击"添加课程"路由

在这里插入图片描述

可以看到"课程封面"这里是个空白,啥也没有,在"4.14.1添加上传组件"说过,我们此时添加的这个组件是自动上传的,所以这里是空白(老师是这样说的,我暂时也没理解到这里面的因果关系),为了用户体验我们可以添加一个默认封面

3.随便找一找.jpg类型并且大小不超过2MB的照片,我们将其放到static目录下

在这里插入图片描述

4.将默认封面的路径赋值给数据模型中的courseInfo对象的cover属性

在这里插入图片描述

5.保存并刷新页面,此时就可以看到默认封面了

在这里插入图片描述

6.点击这个默认封面,就会让我们选择电脑中的图片做封面

7.我们选择好照片后点击"打开",此时就会自动将这个封面上传到阿里云oss中,并显示出来

在这里插入图片描述

在这里插入图片描述

8.去阿里云的oss控制台可以看到我们确实上传了这个封面

在这里插入图片描述

4.14.4一键启动多个项目

我们刚刚在测试的时候需要启动service_edu、service_oss这两个Spring Boot项目,我们以前都是点击一个项目就启动一个项目,可是以后需要启动的项目多了,总不能还是点击一个项目启动一个项目吧,解决这种情况的方法是:配置一键启动多个项目

1.确保我们准备一键启动的SpringBoot项目此时都是运行状态

在这里插入图片描述

2.点击View–>Tool Windows–>Service

在这里插入图片描述

3.先点击加号(“+”)再点击"Run Configuration Type"

在这里插入图片描述

4.找到"Spring Boot"并点击

在这里插入图片描述

5.选中你想要一键启动的项目(按着Ctrl或Shift都可以进行多选),然后右键选择"Group Configurations…"

在这里插入图片描述

6.随便起个名字,点击"OK"

在这里插入图片描述

7.现在就配置完成了,我们来测试一下:

①先停掉所有项目

在这里插入图片描述

②点击"Services"

在这里插入图片描述

③在弹出的界面中右键mxy(我在第6步自己起的名字)并选中"Run",就可以一键启动多个项目了

在这里插入图片描述

4.15整合文本编辑器

课程简介部分本来是很简单的一个输入框,我们现在给它整合一个富文本编辑器(整合过程不用记,这个过程以后是交给前端干的,这个过程就相当于开发一个vue插件的过程)

4.15.1初始化组件-复制文本编辑器组件

1.将资料中的Tinymce文件夹复制到前端项目的components目录下

在这里插入图片描述

在这里插入图片描述

2.将资料中的tinymce4.7.5文件夹复制到前端项目的static目录下

在这里插入图片描述

在这里插入图片描述

4.15.2初始化组件-配置html变量

在build–>webpack.dev.conf.js中添加配置

templateParameters: {
    
    
  BASE_URL: config.dev.assetsPublicPath + config.dev.assetsSubDirectory
}

在这里插入图片描述

4.15.3初始化组件-引入js脚本

在index.html中引入js脚本

<script src=<%= BASE_URL %>/tinymce4.7.5/tinymce.min.js></script>
<script src=<%= BASE_URL %>/tinymce4.7.5/langs/zh_CN.js></script>

在这里插入图片描述

  • 这两行代码放在index.html文件的任何位置都是可以的
  • 保存后会爆红(截图中用绿色方框圈起来的部分),我们不用管这个,对我们的运行没有影响,这个是框架本身的问题

4.15.4使用组件–引入并声明组件

1.在info.vue中引入组件

import Tinymce from '@/components/Tinymce'

在这里插入图片描述

2.声明组件

components: {
    
     Tinymce }, //声明组件

在这里插入图片描述

4.15.5使用组件–添加富文本编辑器组件

1.删掉截图中红框圈起来的部分

在这里插入图片描述

2.将下面的代码复制到上一步删除的位置

<!-- 课程简介-->
<el-form-item label="课程简介">
    <tinymce :height="300" v-model="courseInfo.description"/>
</el-form-item>

在这里插入图片描述

4.15.6使用组件–组件样式

样式是锦上添花,为了让更美观,当然,不想用样式也可以

在info.vue文件的最后添加如下代码,调整上传图片按钮的高度

<style scoped>
.tinymce-container {
  line-height: 29px;
}
</style>

在这里插入图片描述

截图中第181行的scoped表示:该样式只在当前页面有效

4.15.7测试

1.因为我们修改了配置文件,所以需要重启前端项目

在这里插入图片描述

2.我们把基本信息填写一下,做一个最终测试,填写简介的时候可以点击"上传图片"来给简介插入图片

在这里插入图片描述

3.可以看到我们成功添加了这条数据

在这里插入图片描述

在这里插入图片描述

4.将edu_course_description表中刚刚插入的那条数据的description字段(存储课程简介)的值复制出来研究一下

在这里插入图片描述

img标签属性中的src="data:image/jpeg;base64,iVBORw0KG...是图片地址,我们存储讲师头像和课程封面时都是将图片存到了阿里云oss中,然后在数据库存储图片的url,但是富文本编辑是直接将图片文件存储到了数据库中,并不需要图片服务器,不过呢,是先将文图片件做base64编码,然后将base64编码存储到数据库中

5.还有一个事,edu_course_description表中的description字段是类型是text,这种类型利于存储更多的数据,但是并不是无限大,所以不能存储太大的图片,否则存不下

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/maxiangyu_/article/details/127026589