Grain College day08 -- implementation of curriculum chapters and sections

Posted by Ratzzak on Sat, 08 Jan 2022 12:57:30 +0100


1. Add basic course information and improve
1.1 integrated text editor

Copy the contents of the two folders in the figure below from the teaching resources to the folder directory corresponding to the project (copy the component of the teaching resources and timymce under static to the component of the project. If the teaching resources are not downloaded, you can also find the corresponding files through the Vue element admin master downloaded earlier).

(1) Configure html variables

In the configuration file \ build \ webpack Dev.conf.js is configured as follows: the html variable is configured so that the project can find the path of the plug-in just copied.

new HtmlWebpackPlugin({
    ......,
    templateParameters: {
    	BASE_URL: config.dev.assetsPublicPath + config.dev.assetsSubDirectory
    }
})

(2) Import script

In index Add the following parts to the HTML file. The introduction of Chinese package will be popular, but it will not affect the operation of the framework itself.

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

(3) In our info Using plug-ins in Vue

Let's introduce this plug-in first.

//Introduce Tinymce rich text editor component
import Tinymce from '@/components/Tinymce';

export default {
    ....
  components: { Tinymce },
}

Then replace the ui component of the previous course introduction.

<!-- Course introduction-->
<el-form-item label="Course introduction">
<tinymce :height="300" v-model="courseInfo.description"/>
</el-form-item>

Finally, add a style at the end of the file to adjust the height of the button for uploading pictures. The keyword scoped means that the style is only valid on the current page.

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

The final effect is shown in the figure below.

Now add a complete course, and the test effect is as follows.

The data was successfully added, but the subject_parent_id does not appear to have a value. This is because the data passed by the back-end interface does not have this attribute in the CourseInfoForm. Add the following.

 @ApiModelProperty(value = "Primary classification ID")
 private String subjectParentId;

Note that the attribute name should be consistent with the use of the front end (refer to the figure below).

Retest to verify that the bug has been resolved.

2. Curriculum management

The course outline is also a list, which is basically similar to the function of the previous course classification list.

2.1 backend implementation

(1) Create entity class

com.wangzhou.eduservice.entity.chapter.ChapterVo

/**
 * Course chapters
 */
@Data
public class ChapterVo {
    private String id;
    private String title;
    private List<VideoVo> children = new ArrayList<>();
}

com.wangzhou.eduservice.entity.chapter.VideoVo

/**
 * Course section
 */
@Data
public class VideoVo {
    private String id;
    private String title;
}

(2)Controller

/**
 * <p>
 * Course front end controller
 * </p>
 *
 * @author wangzhou
 * @since 2021-11-12
 */
@RestController
@RequestMapping("/eduservice/edu-chapter")
public class EduChapterController {

    @Autowired
    private EduChapterService eduChapterService;

    /**
     * Obtain the chapters and sections of the course according to the course id
     * @return
     */
    @GetMapping("/getChapterVideo/{courseId}")
    public R getChapterVideo(@PathVariable String courseId) {
        List<ChapterVo> list = eduChapterService.getChapterVideo(courseId);
        return R.ok().data("list", list);
    }

}

(3)Service

/**
 * <p>
 * Course service implementation class
 * </p>
 *
 * @author wangzhou
 * @since 2021-11-12
 */
@Service
public class EduChapterServiceImpl extends ServiceImpl<EduChapterMapper, EduChapter> implements EduChapterService {
    @Autowired
    private EduVideoService eduVideoService;

    @Override
    public List<ChapterVo> getChapterVideo(String courseId) {
        // 1. Check all chapters according to the course id
        QueryWrapper<EduChapter> chapterWrapper = new QueryWrapper<>();
        chapterWrapper.eq("course_Id", courseId);
        List<EduChapter> chapterList = baseMapper.selectList(chapterWrapper);

        // 2. Check all sections according to the id course
        QueryWrapper<EduVideo> videoWrapper = new QueryWrapper<>();
        videoWrapper.eq("course_Id", courseId);
        List<EduVideo> videoList = eduVideoService.list(videoWrapper);

        // 3. Traverse all course chapters for encapsulation
        List<ChapterVo> finalChapterList = new ArrayList<>();
        for (int i = 0; i < chapterList.size(); i++) {
            EduChapter eduChapter = chapterList.get(i);
            ChapterVo chapterVo = new ChapterVo();
            BeanUtils.copyProperties(eduChapter, chapterVo);

            // 4. Traverse all course sections for encapsulation
            List<VideoVo> videoVoList = new ArrayList<>();
            for (int j = 0; j < videoList.size(); j++) {
                EduVideo eduVideo = videoList.get(j);
                if(eduVideo.getChapterId().equals(eduChapter.getId())) {
                    VideoVo videoVo = new VideoVo();
                    BeanUtils.copyProperties(eduVideo, videoVo);
                    videoVoList.add(videoVo);
                }

            }
            chapterVo.setChildren(videoVoList);
            finalChapterList.add(chapterVo);
        }
        return finalChapterList;
    }
}

(3) Testing

Create some data in the database. The test results using swagger UI are as follows.

{
  "success": true,
  "code": 20000,
  "message": "success",
  "data": {
    "list": [
      {
        "id": "1181729226915577857",
        "title": "Chapter VII: I/O flow",
        "children": [
          {
            "id": "1189471423678939138",
            "title": "test"
          },
          {
            "id": "1189476403626409986",
            "title": "22"
          }
        ]
      },
      {
        "id": "15",
        "title": "Chapter I: Java introduction",
        "children": [
          {
            "id": "17",
            "title": "Section I: Java brief introduction"
          },
          {
            "id": "18",
            "title": "Section 2: expressions and assignment statements"
          },
          {
            "id": "19",
            "title": "Section III: String class"
          },
          {
            "id": "20",
            "title": "Section IV: procedural style"
          }
        ]
      },
      {
        "id": "32",
        "title": "Chapter 2: console input and output",
        "children": [
          {
            "id": "1182499307429339137",
            "title": "Section I"
          }
        ]
      },
      {
        "id": "44",
        "title": "Chapter 3: control flow",
        "children": [
          {
            "id": "1189434737808990210",
            "title": "test"
          }
        ]
      },
      {
        "id": "48",
        "title": "Chapter IV: definition of class",
        "children": []
      },
      {
        "id": "63",
        "title": "Chapter 5: array",
        "children": []
      },
      {
        "id": "64",
        "title": "Chapter VI: Inheritance",
        "children": []
      }
    ]
  }
}
2.2 front end implementation

We can use the previous course list method to implement the front end, that is, we can directly use the template. Let's write the bottom layer to realize the tree structure display of the course outline.

Create a new chapter js.

export default {
  // Obtain chapter and subsection information according to the course id
  getChapterVideo(courseId) {
    return request({
      url: '/eduservice/edu-chapter/getChapterVideo' + courseId,
      method: 'get'
    })
  }
}

chapter.vue.

<script>
import chapter from "@/api/edu/chapter.js";
export default {
  data() {
    return {
      saveBtnDisabled: false,
      chapterVideoList:[],
      courseId:''
    };
  },
  methods: {
    //Jump to the previous step
    previous() {
      this.$router.push({ path: "/course/info/1" });
    },
    next() {
      //Jump to step 3
      this.$router.push({ path: "/course/publish/1" });
    },
    getChapterVideo(){
      chapter.getChapterVideo(this.courseId)
        .then(resp => {
          this.chapterVideoList = resp.data.list
        })
    }
   
  },
  created() {
    if(this.$route.params && this.$route.params.id) {
      this.courseId = this.$route.params.id
    }
    this.getChapterVideo()
  },
};
</script>

Add ui components to display data.

.<template>
  <div class="app-container">
    <h2 style="text-align: center">Publish new courses</h2>
    <el-steps
      :active="2"
      process-status="wait"
      align-center
      style="margin-
bottom: 40px;"
    >
      <el-step title="Fill in the basic information of the course" />
      <el-step title="Create syllabus" />
      <el-step title="Final release" />
    </el-steps>
    
      <ul>
      <li v-for="chapter in chapterVideoList" :key="chapter.id">
        <p>
          {{ chapter.title }}
        </p>

        <ul>
          <li v-for="video in chapter.children" :key="video.id">
            {{ video.title }}
          </li>
        </ul>
      </li>
    </ul>


    <el-form label-width="120px">
      <el-form-item>
        <el-button @click="previous">Previous step</el-button>
        <el-button :disabled="saveBtnDisabled" type="primary" @click="next"
          >next step</el-button
        >
      </el-form-item>
    </el-form>
  </div>
</template>

Remember to add @ CrossOrigin annotation to the back-end interface to solve the cross domain problem. The test results are as follows.

Finally, you can add a little style.

.<template>
  <div class="app-container">
    <h2 style="text-align: center">Publish new courses</h2>
    <el-steps
      :active="2"
      process-status="wait"
      align-center
      style="margin-
bottom: 40px;"
    >
      <el-step title="Fill in the basic information of the course" />
      <el-step title="Create syllabus" />
      <el-step title="Final release" />
    </el-steps>
    
      <ul>
      <li v-for="chapter in chapterVideoList" :key="chapter.id">
        <p>
          {{ chapter.title }}
          <span>
            <el-button type="text">Add session</el-button>
            <el-button style="" type="text">edit</el-button>
            <el-button type="text">delete</el-button>
          </span>
        </p>

        <!-- video -->
        <ul class="chapterList videoList">
          <li v-for="video in chapter.children" :key="video.id">
            <p>
              {{ video.title }}
              <span class="acts">
                <el-button type="text">edit</el-button>
                <el-button type="text">delete</el-button>
              </span>
            </p>
          </li>
        </ul> 
      </li>
    </ul>


    <el-form label-width="120px">
      <el-form-item>
        <el-button @click="previous">Previous step</el-button>
        <el-button :disabled="saveBtnDisabled" type="primary" @click="next"
          >next step</el-button
        >
      </el-form-item>
    </el-form>
  </div>
</template>
3. Modify course functions

Click the previous step in the figure below to return to the course addition page and modify the course. The following is to realize this process.

The business is actually very simple: write back the data and update the written back data to the database.

3.1 backend implementation

To implement two interfaces. (1) Query the basic information of the course according to the course id, (2) modify the basic course information.

CourseController

 // Query course information
    @GetMapping("/getCourseInfo/{courseId}")
    public R getCourseInfo(@PathVariable String courseId) {
        CourseInfoForm courseInfoForm= eduCourseService.getCourseInfo(courseId);
        return R.ok().data("courseInfoForm", courseInfoForm);
    }

 // Modify course information
    @PostMapping("/updateCourseInfo")
    public R updateCourseInfo(@RequestBody CourseInfoForm courseInfoForm) {
        eduCourseService.updateCourseInfo(courseInfoForm);
        return R.ok();
    }

EduCourseServiceImpl

    @Override
    public CourseInfoForm getCourseInfo(String courseId) {
        CourseInfoForm courseInfoForm = new CourseInfoForm();
        // 1. Query Curriculum
        EduCourse eduCourse= baseMapper.selectById(courseId);
        BeanUtils.copyProperties(eduCourse, courseInfoForm);
        // 2. Query description table
        EduCourseDescription eduCourseDescription = eduCourseDescriptionService.getById(courseId);
        courseInfoForm.setDescription(eduCourseDescription.getDescription());
        return courseInfoForm;
    }

  @Override
    public void updateCourseInfo(CourseInfoForm courseInfoForm) {
        // 1. Revise the curriculum
        EduCourse eduCourse = new EduCourse();
        BeanUtils.copyProperties(courseInfoForm, eduCourse);
        int update = baseMapper.updateById(eduCourse);
        if(update <= 0) {
            throw new GuliException(20001, "Failed to modify course");
        }
        // 2. Modify description table
        EduCourseDescription eduCourseDescription = new EduCourseDescription();
        eduCourseDescription.setDescription(courseInfoForm.getDescription());
        eduCourseDescriptionService.updateById(eduCourseDescription);
    }
3.2 front end implementation

course.js

    //Query basic course information according to course id
    getCourseInfoById(courseId){
        return request({
            url:`/eduservice/edu-course/getCourseInfo/${courseId}`,
            method: 'get',
        })
    },
    //Modify course information
    updateCourseInfo(courseInfoForm){
        return request({
            url:"/eduservice/edu-course/updateCourseInfo",
            method: 'post',
            data: courseInfoForm,
        })
    }

info.vue modifies the jump url.

 
     //Jump to the previous step
    previous() {
      this.$router.push({ path: "/course/info/" + this.courseId });
    },
    next() {
      //Jump to step 3
      this.$router.push({ path: "/course/publish/"+ this.courseId });
    }
}

Finally, in info Data write back is implemented in Vue.

methods: {  
 getCourseInfo() {
    course.getCourseInfoById(this.courseId)
    .then((resp) => {
      this.courseInfoform = resp.data.courseInfoForm // Note the case here. The front-end naming is not standardized, which leads to this small problem
    })
  }

  },
  created() {
      if(this.$route.params && this.$route.params.id) {
        this.courseId = this.$route.params.id
        this.getCourseInfo()
      }
  }

Testing. It is found that according to a small bug: the secondary classification of the course displays the id.

What's going on? The default display principle of drop-down list data is: compare the id of the list storing drop-down options with the id to be displayed by default one by one, and write back the data in the matching list. The primary classification initializes subjectOneList by default in the page, but the secondary classification subjectTwoList does not. The following is a specific solution. You can better understand this process by looking at the code.

methods: { 
	getCourseInfo() {
    course.getCourseInfoById(this.courseId)
    .then((resp) => {
      // Get current course information
      this.courseInfoform = resp.data.courseInfoForm
    
      // Query all level-1 course classifications and initialize subjectOneList
     subject.getAllSubject()
     .then((response) => {
        this.subjectOneList = response.data.list
         for(var i = 0; i<this.subjectOneList.length; i++) {
          // Obtain the primary course classification id from the current course information, and compare it with all course classification IDs. If it is equal, all secondary classification IDS will be displayed
          var oneSubject = this.subjectOneList[i]
          if(oneSubject.id == this.courseInfoform.subjectParentId) {
             this.subjectTwoList = oneSubject.children
          }
      }
     })
     
    })
  }

  },
  created() { 
      // modify
      if(this.$route.params && this.$route.params.id) {
        this.courseId = this.$route.params.id
        this.getCourseInfo()
        this.getListTeacher()
      } else{
        // add to
         this.getListTeacher()
         this.getOneSubject()
      }
  }

There is also a small bug: click add course after echoing the data in the previous step, and the echoed data is still there. Let's solve it.

  watch: {
    $route(to, from) {
      //Route change mode. When the route sending changes, the method is executed
      console.log("watch $route");
      this.courseInfo={}
    }
  }

Finally, adjust saveOrUpdate.

    addCourse() {
       course.addCourseInfo(this.courseInfoform).then(resp => {
        this.$message({
          message: "Successfully added course information",
          type: "success",
        })
        //Jump to step 2 with the id generated by the course
        this.$router.push({ path: "/course/chapter/"+resp.data.courseId });
      });
    },
    updateCourse() {
       course.updateCourseInfo(this.courseInfoform).then(resp => {
        this.$message({
          message: "Course information modified successfully",
          type: "success",
        })
        //Jump to step 2 and take the id generated by this course. Note that the back end does not return id when updating, but we can get it in the following way
        this.$router.push({ path: "/course/chapter/"+ this.courseId }); 
      });
    },
    saveOrUpdate() {
      if(this.courseInfoform.id) {
            this.updateCourse()
      } else {
        this.addCourse()
      }
    },
4. Addition, deletion and modification of course chapters
4.1 adding course chapters

(1)UI

Make a small button.

  <el-button type="text" @click="editChapter(chapter.id)">edit</el-button>

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (IMG iqjmncab-1641641538981) (C: / users / 24724 / appdata / roaming / typora user images / image-20211207210533035. PNG)]

The button effect is as follows, ugly.

Then click Add to display the form for adding course information. The ui of the form is as follows.

<!-- Add and modify chapter forms -->
<el-dialog :visible.sync="dialogChapterFormVisible" title="Add chapter">
    <el-form :model="chapter" label-width="120px">
        <el-form-item label="head word">
            <el-input v-model="chapter.title"/>
        </el-form-item>
        <el-form-item label="Chapter sorting">
            <el-input-number v-model="chapter.sort" :min="0" controls-position="right"/>
        </el-form-item>
    </el-form>
    <div slot="footer" class="dialog-footer">
        <el-button @click="dialogChapterFormVisible = false">Cancel</el-button>
        <el-button type="primary" @click="saveOrUpdate">determine</el-button>
    </div>

</el-dialog>

The dialogChapterFormVisible above is used to control the display and hiding of pop ups. We set its default value to false in data. The chapter is used above, and the next initialization is also performed.

 data() { 
    return {
      saveBtnDisabled: false,
      chapterVideoList:[],
      courseId:'',
      dialogChapterFormVisible:false, // The chapter pop-up box is set to off by default
      chapter:{} // Packaging chapter
    };
  }

Finally, modify the previously edited button and bind the dialogChapterFormVisible property.

  <el-button type="text" @click="dialogChapterFormVisible = true">Add chapter</el-button>

The test results are as follows.

(2) Back end

By the way, the interface for adding, deleting, modifying and querying course chapters is written at one time. EduChapterController.

 @PostMapping("/addChapter")
    public R addChapter(@RequestBody EduChapter chapter) {
        eduChapterService.save(chapter);
        return R.ok();
    }

    @GetMapping("/getChapter/{courseId}")
    public R getChapterById(@PathVariable String courseId) {
        EduChapter eduChapter= eduChapterService.getById(courseId);
        return R.ok().data("chapter", eduChapter);
    }

    @PostMapping("/updateChapter")
    public R updateChapter(@RequestBody EduChapter chapter) {
        eduChapterService.updateById(chapter);
        return R.ok();
    }

    @DeleteMapping("/deleteChapter")
    public R deleteChapter(@PathVariable String courseId) {
        Boolean flag = eduChapterService.removeById(courseId);
        if(flag) {
            return R.ok();
        } else {
            return R.error();
        }
        
    }

The above interface looks good, but there is another small problem. When deleting a chapter, we directly call the removeById method, so that the chapter is indeed deleted, but the section of the chapter is in an awkward position: it no longer belongs to any section (as shown in the figure below). That won't work.

In fact, in the actual development, we need to implement the deleted business logic in the service layer for the problem similar to deleting the primary classification in the primary and secondary classification. We usually have two kinds of implementation logic:

  • When deleting a section, the following sections will be deleted together
  • If there is a section, it is not allowed to delete the section. You must delete the section after the section is deleted

The second logic is implemented below.

Then the controller changes the name of the called service method.

Implementation method of EduChapterServiceImpl

  @Override
    public Boolean deleteChapter(String courseId) {
        QueryWrapper<EduVideo> wrapper = new QueryWrapper<>();
        wrapper.eq("course_id", courseId);
        // You only need to judge whether there is a video, but you don't really need to get a video
        int count = eduVideoService.count(wrapper);
        if(count > 1) {
            throw new GuliException(20001, "Cannot delete");
        } else {
            int delete =  baseMapper.deleteById(courseId);
            return delete > 0;
        }

    }

(3) Front end interface

chapter.js.

  // Add chapter
  addChapter(chapter) {
    return request({
      url: '/eduservice/edu-chapter/addChapter/',
      method: 'post',
      data: chapter
    })
  },
  // Query chapter by id
  getChapter(courseId) {
    return request({
      url: '/eduservice/edu-chapter/getChapter/' + courseId,
      method: 'get'
    })
  },
  // Modify chapter
  updateChapter(chapter) {
    return request({
      url: '/eduservice/edu-chapter/updateChapter/',
      method: 'post',
      data: chapter
    })
  },
  deleteChapter(courseId) {
    return request({
      url: '/eduservice/edu-chapter/deleteChapter/' + courseId,
      method: 'delete'
    })
  }

(4) Front end calling interface display data

Implement the saveOrUpdate method for submitting the form bound by the OK button in the figure below.

chapter.vue

   saveOrUpdate() {
      chapter.addChapter(this.chapter)
      .then(resp => {
        // 1. Close the pop-up box
        this.dialogChapterFormVisible = false
        // 2. Prompt success
         this.$message({
          message: "Added Course Chapter successfully",
          type: "success",
        })
        // 3. Refresh the page (just re query the data)
        this.getChapterVideo()
      })
   }

Is it feeling: simple, boring and boring. Test.

An error was reported. Obviously, it was reported by the back end. Look at the back end. Tell us that courseid is not given a default value.

Look at the database. The courseId must pass values.

The chapter in the data does not give the default value of courseId.

We actually got the courseId before.

Therefore, we can encapsulate the data into EduChapter before transmitting it to EduChapter.

  saveOrUpdate() {
      this.chapter.courseId = this.courseId,
      chapter.addChapter(this.chapter)
      .then(resp => {
        // 1. Close the pop-up box
        this.dialogChapterFormVisible = false
        // 2. Prompt success
         this.$message({
          message: "Added Course Chapter successfully",
          type: "success",
        })
        // 3. Refresh the page (just re query the data)
        this.getChapterVideo()
      })
   }
  }

Also, remember to annotate the time related attributes in the back-end EduChapter.

Another test will succeed.

There is also a small bug. When clicking Add chapter the second time, the previously added form data is not cleared. The solution is as follows. The event @ Click is changed to the binding method, and the data is cleared in the method.

  <el-button type="text" @click="openChapterDialog">Add chapter</el-button>
 openChapterDialog() {
     this.dialogChapterFormVisible = true,
     this.chapter.title = ''
     this.chapter.sort = 0
   }
  }
4.2 modifying course chapters

Note that after adding chapters, there are editing options in the displayed course chapters. This is the ui we implemented before. Now let's implement the functions of this ui.

Bind the Edit button to the method.

Realize the modification function.

 saveChapter() {
      this.chapter.courseId = this.courseId,
      chapter.addChapter(this.chapter)
      .then(resp => {
        // 1. Close the pop-up box
        this.dialogChapterFormVisible = false
        // 2. Prompt success
         this.$message({
          message: "Added Course Chapter successfully",
          type: "success",
        })
        // 3. Refresh the page (just re query the data)
        this.getChapterVideo()
      })
   },
   openChapterDialog() {
     this.dialogChapterFormVisible = true,
     this.chapter.title = ''
     this.chapter.sort = 0
   },
   openEditChapter(chapterId) {
     this.dialogChapterFormVisible = true,
     chapter.getChapter(chapterId)
     .then(resp => {
       this.chapter = resp.data.chapter
     })
   },
   //Modify chapter
    updateChapter() {
      //Set the course id into the chapter object
      this.chapter.courseId = this.courseId;
      chapter.updateChapter(this.chapter).then((resp) => {
        //Close the bullet box
        this.dialogChapterFormVisible = false;
        //Prompt information
        this.$message({
          message: "Chapter modified successfully",
          type: "success",
        });
        //Refresh page
        this.getChapterVideo();
      });
    },
    saveOrUpdate() {
      if (this.chapter.id) {
        //Modify chapter
        this.updateChapter();
      } else {
        //New chapter
        this.saveChapter();
      }
    }
4.3 deleting course chapters

         //Delete chapter
    removeById(chapterId) {
      this.$confirm("This operation will permanently delete the chapter information, Continue?", "Tips", {
        confirmButtonText: "determine",
        cancelButtonText: "cancel",
        type: "warning",
      }).then(() => {
        //Click OK to execute the then method
        chapter.deleteChapter(chapterId).then((resp) => {
          //Delete succeeded
          //Prompt information
          this.$message({
            type: "success",
            message: "Delete succeeded!",
          });
          //Refresh page
          this.getChapterVideo();
        });
      });
5. Addition, deletion, modification and query of course sections

Let's implement the back-end interface first.

/**
 * <p>
 * Course video front end controller
 * </p>
 *
 * @author wangzhou
 * @since 2021-11-12
 */
@RestController
@RequestMapping("/eduservice/edu-video")
@CrossOrigin
public class EduVideoController {
    @Autowired
    private EduVideoService service;
    // Add section
    @PostMapping("/addVideo")
    public R addVideo(@RequestBody EduVideo video) {
        service.save(video);
        return R.ok();
    }
    // Delete a section Todo when deleting a section, you should also delete the corresponding video under the section
    @DeleteMapping("/deleteVideo{videoId}")
    public R deleteVideo(@PathVariable String videoId) {
        Boolean flag = service.removeById(videoId);
        if(flag) {
            return R.ok();
        } else {
            return R.error();
        }
    }

    // Find section
    @GetMapping("/getVideoById{videoId}")
    public R getVideoById(@PathVariable String videoId) {
        EduVideo video = service.getById(videoId);
        return R.ok().data("video", video);
    }

    // Modify section
    @PostMapping("/updateVideo")
    public R updateVideo(@RequestBody EduVideo video) {
        service.updateById(video);
        return R.ok();
    }

}

Friendly tips, remember to add the @ TableField annotation in EduVideo.

The next step is to implement the lower interface of the front end and create a new video js.

import request from '@/utils/request' 

export default {
  // Add section
  addVideo(video) {
    return request({
      url: '/eduservice/edu-video/addVideo/',
      method: 'post',
      data: video
    })
  },
  // Query section by id
  getVideo(videoId) {
    return request({
      url: '/eduservice/edu-video/getVideoById/' + videoId,
      method: 'get'
    })
  },
  // Modify section
  updateVideo(video) {
    return request({
      url: '/eduservice/edu-video/updateVideo/',
      method: 'post',
      data: video
    })
  },
  // Delete Measures 
  deleteVideo(videoId) {
    return request({
      url: '/eduservice/edu-video/deleteVideo/' + videoId,
      method: 'delete'
    })
  }
}

At present, it is a very simple and ordinary operation. Then, in the front end, the chapter Vue start calling the interface to realize the function.

.<template>
  <div class="app-container">
    <h2 style="text-align: center">Publish new courses</h2>
    <el-steps
      :active="2"
      process-status="wait"
      align-center
      style="margin-
bottom: 40px;"
    >
      <el-step title="Fill in the basic information of the course" />
      <el-step title="Create syllabus" />
      <el-step title="Final release" />
    </el-steps>
    
      <ul>
      <li v-for="chapter in chapterVideoList" :key="chapter.id">
        <p>
          {{ chapter.title }}
          <span>
            <el-button type="text" @click="openVideoDialog(chapter.id)">Add session</el-button>
            <el-button style="" type="text" @click="openEditChapter(chapter.id)">edit</el-button>
            <el-button type="text"  @click ="removeById(chapter.id)">delete</el-button>
          </span>
        </p>

        <!-- video -->
        <ul class="chapterList videoList">
          <li v-for="video in chapter.children" :key="video.id">
            <p>
              {{ video.title }}
              <span class="acts">
                <el-button type="text" @click="openVideoDialog(chapter.id)">edit</el-button>
                <el-button type="text" @click="removeVideo">delete</el-button>
              </span>
            </p>
          </li>
        </ul> 
      </li>
    </ul>

  <el-button type="text" @click="openChapterDialog">Add chapter</el-button>
  <!-- Add and modify chapter forms -->
    <el-dialog :visible.sync="dialogChapterFormVisible" title="Add chapter">
        <el-form :model="chapter" label-width="120px">
            <el-form-item label="head word">
                <el-input v-model="chapter.title"/>
            </el-form-item>
            <el-form-item label="Chapter sorting">
                <el-input-number v-model="chapter.sort" :min="0" controls-position="right"/>
            </el-form-item>
        </el-form>
        <div slot="footer" class="dialog-footer">
            <el-button @click="dialogChapterFormVisible = false">Cancel</el-button>
            <el-button type="primary" @click="saveOrUpdate">determine</el-button>
        </div>

    </el-dialog>

    <!--Add section form-->
    <!-- Add and modify session forms -->
    <el-dialog :visible.sync="dialogVideoFormVisible" title="Add session">
      <el-form :model="video" label-width="120px">
        <el-form-item label="Class title">
          <el-input v-model="video.title" />
        </el-form-item>
        <el-form-item label="Class ranking">
          <el-input-number
            v-model="video.sort"
            :min="0"
            controls-
            position="right"
          />
        </el-form-item>
        <el-form-item label="Is it free">
          <el-radio-group v-model="video.free">
            <el-radio :label="true">free</el-radio>
            <el-radio :label="false">default</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="Upload video">
          <!-- TODO -->
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="dialogVideoFormVisible = false">Cancel</el-button>
        <el-button
          :disabled="saveVideoBtnDisabled"
          type="primary"
          @click="saveOrUpdateVideo()"
          >determine</el-button
        >
      </div>
    </el-dialog>


    <el-form label-width="120px">
      <el-form-item>
        <el-button @click="previous">Previous step</el-button>
        <el-button :disabled="saveBtnDisabled" type="primary" @click="next"
          >next step</el-button
        >
      </el-form-item>
    </el-form>

  </div>
</template>

<script>
import chapter from "@/api/edu/chapter.js";
import video from "@/api/edu/video.js";

export default {
  data() { 
    return {
      saveBtnDisabled: false,
      chapterVideoList:[],
      courseId:'',
      dialogChapterFormVisible:false, // The chapter pop-up box is set to off by default
      dialogVideoFormVisible:false, // The section pop-up box is set to off by default
      chapter:{
        title:'',
        sort:0
      }, // Packaging chapter
      video: {
        title:'',
        sort:0,
        free:0,
        videoSourceId:''
      } //Packaging section
    };
  },
  methods: {
    // ------------------Section-------------
    openVideoDialog(id) {
        //Clear previous data
      this.video = {};
       //Set chapter id
      this.video.chapterId = id;
      //Show pop ups
      this.dialogVideoFormVisible = true;
    },
    // saveVideoBtnDisabled() {

    // },
            //Add section
    addVideo() {
      //Set course id
      this.video.courseId = this.courseId;
      video.addVideo(this.video).then((resp) => {
        //Close the bullet box
        this.dialogVideoFormVisible = false;
        //Prompt information
        this.$message({
          message: "Section added successfully",
          type: "success",
        });
        //Refresh page
        this.getChapterVideo();
      });
    },

    saveOrUpdateVideo() {
      if(this.video.id) {
        this.updateVideorById(this.video.id);
      } else {
        this.addVideo();
      }
    },

        //Modify section form echo
    getVideoById(videoId) {
      //Pop up section pop-up
      this.dialogVideoFormVisible = true;
      video.getVideoById(videoId).then((resp) => {
        this.video = resp.data.video;
      });
    },


        //Subsection modification
    updateVideorById(videoId) {
      //Set the section id to the video object
      this.video.id = videoId;
      video.updateVideo(this.video).then((resp) => {
        //Close the bullet box
        this.dialogVideoFormVisible = false;
        //Prompt information
        this.$message({
          message: "Section modified successfully",
          type: "success",
        });
        //Refresh page
        this.getChapterVideo();
      });
    },

        //Delete Measures 
    removeVideo(videoId) {
      this.$confirm("This action will permanently delete the section information, Continue?", "Tips", {
        confirmButtonText: "determine",
        cancelButtonText: "cancel",
        type: "warning",
      }).then(() => {
        //Click OK to execute the then method
        video.deleteById(videoId).then((resp) => {
          //Delete succeeded
          //Prompt information
          this.$message({
            type: "success",
            message: "Delete succeeded!",
          });
          //Refresh page
          this.getChapterVideo();
        });
      });
    },


    // ------------------Chapter-------------
    //Jump to the previous step
    previous() {
      this.$router.push({ path: "/course/info/" + this.courseId });
    },
    next() {
      //Jump to step 3
      this.$router.push({ path: "/course/publish/"+ this.courseId });
    },
    getChapterVideo(){
      chapter.getChapterVideo(this.courseId)
        .then(resp => {
          this.chapterVideoList = resp.data.list
        })
    },
   
   saveChapter() {
      this.chapter.courseId = this.courseId,
      chapter.addChapter(this.chapter)
      .then(resp => {
        // 1. Close the pop-up box
        this.dialogChapterFormVisible = false
        // 2. Prompt success
         this.$message({
          message: "Added Course Chapter successfully",
          type: "success",
        })
        // 3. Refresh the page (just re query the data)
        this.getChapterVideo()
      })
   },
   openChapterDialog() {
     this.dialogChapterFormVisible = true,
     this.chapter.title = ''
     this.chapter.sort = 0
   },
   openEditChapter(chapterId) {
     this.dialogChapterFormVisible = true,
     chapter.getChapter(chapterId)
     .then(resp => {
       this.chapter = resp.data.chapter
     })
   },
   //Modify chapter
    updateChapter() {
      //Set the course id into the chapter object
      this.chapter.courseId = this.courseId;
      chapter.updateChapter(this.chapter).then((resp) => {
        //Close the bullet box
        this.dialogChapterFormVisible = false;
        //Prompt information
        this.$message({
          message: "Chapter modified successfully",
          type: "success",
        });
        //Refresh page
        this.getChapterVideo();
      });
    },
    saveOrUpdate() {
      if (this.chapter.id) {
        //Modify chapter
        this.updateChapter();
      } else {
        //New chapter
        this.saveChapter();
      }
    },
       //Delete chapter
    removeById(chapterId) {
      this.$confirm("This operation will permanently delete the chapter information, Continue?", "Tips", {
        confirmButtonText: "determine",
        cancelButtonText: "cancel",
        type: "warning",
      }).then(() => {
        //Click OK to execute the then method
        chapter.deleteChapter(chapterId).then((resp) => {
          //Delete succeeded
          //Prompt information
          this.$message({
            type: "success",
            message: "Delete succeeded!",
          });
          //Refresh page
          this.getChapterVideo();
        });
      });
    }



  },
  
  created() {
    if(this.$route.params && this.$route.params.id) {
      this.courseId = this.$route.params.id
    }
    this.getChapterVideo()
  },
};
</script>

<style>
</style>

Topics: Javascript Front-end Vue.js