Dragou Education Management System Project Practical Combat (6)-Course Content Management

Dragou Education Management System Project Practical Combat (6)-Course Content Management

Source of notes: Lagou Education-Big Front-end Employment Training Camp

Article content: notes, insights, and experience in the learning process

Course content management

Content management components, routing processing, jump operations, dynamic path parameter transfer

//src/router/index.js-Add a new course content management route to the route { //Course content management path : '/contentManagement/:courseId' , name : 'contentManagement' , component : () => import ( /* webpackChunkName:'contentManagement' */ '@/views/course/contentManagement' ), props : true } Copy code
src/views/course/contentManagement.vue New course content management page <template> <div>Course content management</div> </template> <script> export default { name:'contentManagement', //parameters props: ['courseId'] } </script> Copy code
src/views/course/son/content.vue Add a click event to the content management button, and jump to the content management page with the course id <el-table-column label="action"> <template slot-scope="scope"> <!-- Operation button--> <el-button @click="editCoures(scope.row.id)">Edit</el-button> <el-button @click="managementCoures(scope.row.id)">Content Management</el-button> </template> </el-table-column //content management managementCoures (courseId) { this.$router.push({ name:'contentManagement', params: { courseId } }) }, Copy code

Course data display

Use tree components, use drag and drop

Use interface to get course data

The chapter name is different from the class name and needs special settings

//src/services/coures.js encapsulates the interface to get all the chapter content of the course-getSectionAndLesson //Get the content of all chapters of the course export const getSectionAndLesson = courseId => { return request({ method : 'get' , url : `/boss/course/section/getSectionAndLesson?courseId = ${courseId} ` }) } Copy code
src/views/course/contentManagement.vue tree brave tree component, use interface and remap node <template> <el-card class="box-card"> <!-- Title--> <div slot="header" class="clearfix"> <span>Edit course content-course ID: {{courseId}}</span> </div> <!-- Tree control--> <el-tree :data="treeData" :props="defaultProps" draggable></el-tree> </el-card> </template> <script> //Introduce the interface to get all chapter information import {getSectionAndLesson} from'@/services/coures' export default { name:'contentManagement', //parameters props: ['courseId'], data () { return { //Tree control data treeData: [], //Correspondence between tree control nodes defaultProps: { //The corresponding name of the child node children:'lessonDTOS', //The node name corresponds, because the chapter name and the node name are different, so you need to set two label (data) { return data.sectionName || data.theme } } } }, //life cycle hook created () { //Call the method to get all chapter data of the course this.getInfo(this.courseId) }, methods: { //Get all the content of the course async getInfo (courseId) { //call interface const {data} = await getSectionAndLesson(courseId) if (data.code === '000000') { //If the acquisition is successful, give the data to the tree structure this.treeData = data.data } } } } </script> Copy code

tree component customization

Structure setting, style beautification treatment

src/views/course/contentManagement.vue <template> <el-card class="box-card"> <!-- Title--> <div slot="header" class="clearfix"> <span>Edit course content-course ID: {{courseId}}</span> </div> <!-- Tree control--> <el-tree :data="treeData" :props="defaultProps" draggable> <!-- Custom structure--> <div class="custom-tree-node" slot-scope="{ node, data }"> <!-- Two spans for easy use of flex layout --> <span>{{ node.label }}</span> <span> <el-button type="text" size="mini" @click="() => edit(data)"> edit </el-button> <!-- Use v-if to determine whether the current node is a chapter, it is a chapter add button to add a lesson, otherwise the button is to upload video --> <el-button type="text" size="mini" v-if="node.level === 1"> Add class </el-button> <el-button type="text" size="mini" v-else> Upload video </el-button> <el-button type="text" size="mini" @click="() => now(node, data)"> status </el-button> </span> </div> </el-tree> </el-card> </template> <script> //Introduce the interface to get all chapter information import {getSectionAndLesson} from'@/services/coures' export default { name:'contentManagement', //parameters props: ['courseId'], data () { return { //Tree control data treeData: [], //Correspondence between tree control nodes defaultProps: { //The corresponding name of the child node children:'lessonDTOS', //The node name corresponds, because the chapter name and the node name are different, so you need to set two label (data) { return data.sectionName || data.theme } } } }, //life cycle hook created () { //Call the method to get all chapter data of the course this.getInfo(this.courseId) }, methods: { //Get all the content of the course async getInfo (courseId) { //call interface const {data} = await getSectionAndLesson(courseId) if (data.code === '000000') { //If the acquisition is successful, give the data to the tree structure this.treeData = data.data } } } } </script> <style lang="scss" scoped> //Tree structure style .custom-tree-node { flex: 1; display: flex; align-items: center; justify-content: space-between; font-size: 14px; padding-right: 8px; } </style> Copy code

Node drag and drop processing

To limit unreasonable dragging, judge whether it can be placed

src/views/course/contentManagement.vue Add the allow-drop attribute to the tree structure and bind the judgment method //Determine whether the method can be placed //The three parameters are the dragged element, the element to be placed, and the position allowDrop (draggingNode, dropNode, type) { //Condition 1: The position cannot be inner (internal), which means that it cannot be placed inside other nodes. //Condition 2: Two nodes have the same parent node, which means that the return type can only be dragged inside the same parent node !== 'inner' && draggingNode.parent.id === dropNode.parent.id } Copy code

Data update after dragging ca

The data update is triggered after dragging, the component provides events, and the corresponding connection is used

Determine whether it is a chapter or a lesson time. Use different interfaces

In order to avoid requesting multiple interfaces each time, use Promise.all() to process all requests

async method (await Promise.all (traversal (call interface))
Use try-cath for successful and failed operations

Add loading effect

src/services/coures.js for Ajia course update and class update interface package //Save or update chapter export const saveOrUpdateSection = data => { return request({ method : 'post' , url : '/boss/course/section/saveOrUpdateSection' , data }) } //Save or update class export const saveOrUpdate = data => { return request({ method : 'post' , url : '/boss/course/lesson/saveOrUpdate' , data }) } Copy code
//src/views/course/contentManagement.vue Use v-loading to add loading status to the tree structure, and use node-drop to add drag and drop success event function <el-tree v-loading="loading" :data="treeData" :props="defaultProps" default-expand-all draggable :allow-drop="allowDrop" @node-drop="nodeDrop"> //Drag and drop success callback function async nodeDrop (meNode, endNode, type, event) { //Change the signal value so that the tree structure is loading this.loading = true //Use try-catch to judge try { //Because there may be multiple nodes, the interface may be called multiple times, so use await Promise.all to receive all the called interfaces and send them together //Traverse all child nodes under the parent node of the node to be moved await Promise.all(endNode.parent.childNodes.map((item, index) => { //Determine whether the moving is a lesson or a chapter, the lesson has a sectionId attribute if (meNode.data.sectionId) { //If the moving is the class hour, use the class hour update interface return saveOrUpdate({ id: item.data.id, orderNum: index }) } else { //If it is not a class hour, it is a chapter, use the chapter interface return saveOrUpdateSection({ id: item.data.id, orderNum: index }) } })) //Finally a prompt pops up this.$message.success('Update successful') } catch (err) { //A prompt pops up when there is an error this.$message.error('Update failed') } //Regardless of success or failure. Must cancel the structure loading state this.loading = false }, Copy code

Upload lesson video

Click the double video button to jump to the route, create a new component, modify the route, click to jump, path parameters

//src/router/index.js Add route for uploading video { //Upload class video path : '/uploadVideo/:courseId' , name : 'uploadVideo' , component : () => import ( /* webpackChunkName:'uploadVideo' */ '@/views/course/uploadVideo' ), props : true } Copy code
src/views/course/contentManagement.vue upload video button to add a click event, jump to the upload video page with parameters <el-button type="text" size="mini" v-else @click="$router.push({ name:'uploadVideo', params: { courseId }, query: { lessonId: data.id } })"> Upload video </el-button> Copy code
src/views/course/uploadVideo.vue Create new component to write upload video <template> <el-card class="box-card"> <!-- Top title--> <div slot="header" class="clearfix"> <!-- Use the passed path parameters and ordinary parameters --> <span>Upload video/Course ID: {{courseId}}/Class ID: {{lessonId}}</span> </div> <el-form :model="form" label-width="80px"> <el-form-item label="Cover upload"> <input type="file"> </el-form-item> <el-form-item label="Video upload"> <input type="file"> </el-form-item> <el-button>Back</el-button> <el-button type="primary">Start uploading</el-button> </el-form> </el-card> </template> <script> export default { name:'uploadVideo', //courseId-path parameter props: ['courseId'], data () { return { //class id, obtained by parameter lessonId: this.$route.query.lessonId, //form form data form: {} } } } </script> Copy code
Alibaba Cloud video on demand

Alibaba Cloud-Products-Video Services-Video on Demand-Documentation and Help-On Demand Service API-Development Guide-Overview-Upload SDK-Upload SDK using js (SDK-Toolkit)-Use web SDK-You can view the example code Vue

Import the three js files in the Alibaba Cloud SDK into the HTML file in the root directory (note the name and path)

<!DOCTYPE html > < html lang = "" > < head > < meta charset = "utf-8" > < meta http-equiv = "X-UA-Compatible" content = "IE=edge" > < meta name = "viewport" content = "width=device-width, initial-scale=1.0" > < link rel = "icon" href = "<%= BASE_URL %>favicon.ico" > < title > <%= htmlWebpackPlugin.options.title %> </title > </head > < body > < noscript > < strong > We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue. </strong > </noscript > < div id = "app" > </div > <!-- built files will be auto injected --> <!-- Import the Alibaba Cloud SDK file, the absolute path used in the course, but I use the relative path here --> <!-- IE requires es6-promise --> < script src = "/aliyun/es6-promise. min.js" > </script > < script src = "/aliyun/aliyun-oss-sdk-6.13.0.min.js" > </script > < script src = "/aliyun/aliyun-upload-sdk- 1.5.2.min.js" > </script > </body > </html > Copy code
Initialize Aliyun Mountain

Document address

Method of using upload address and credentials (official recommendation)

Find the src below the Vue example-uploadauth.vue is the way to upload credentials and addresses, use vscode to view

Set the comment/* eslint-disable*/to set not to check the style of the code below the comment

Alibaba Cloud id-1618139964448548

Set the address and credentials in the upload hook

After the component is created, initialize Alibaba Cloud and directly process the code in the document

Add click upload event, click upload to get file

  • Add ref to the two buttons, use refs.name.files[0] to get file information
  • Add the two files to the Alibaba Cloud file list and start uploading (first upload the cover page and then upload the video-interface requirements)

Start uploading needs to be set in the upload hook function

src/views/course/uploadVideo.vue initialize upload function, add button click event to upload <template> <el-card class="box-card"> <!-- Top title--> <div slot="header" class="clearfix"> <!-- Use the passed path parameters and ordinary parameters --> <span>Upload video/Course ID: {{courseId}}/Class ID: {{lessonId}}</span> </div> <el-form label-width="80px"> <!-- Add cover--> <el-form-item label="Add cover page"> <input type="file" ref="img"> </el-form-item> <!-- Add video--> <el-form-item label="Add video"> <input type="file" ref="video"> </el-form-item> <el-button>Back</el-button> <!-- Start upload button to add click event--> <el-button type="primary" @click="beginUpload">Start uploading</el-button> </el-form> </el-card> </template> <script> /* eslint-disable*/ export default { name:'uploadVideo', //courseId-path parameter props: ['courseId'], data () { return { //class id, obtained by parameter lessonId: this.$route.query.lessonId, //Alibaba Cloud upload SDK uploader: null } }, //life cycle hook function created () { //Initial upload this.initAliyun() }, methods: { //Start upload button click event beginUpload () { //Add pictures and videos to the upload list, use ref to get videos and pictures this.uploader.addFile(this.$refs.img.files[0]) this.uploader.addFile(this.$refs.video.files[0]) //Start upload this.uploader.startUpload() }, initAliyun () { this.uploader = new AliyunUpload.Vod({ //Ali account ID, must have a value (the account provided by the teacher is used here) userId: '1618139964448548', //Upload to the region of VOD, the default value is'cn-shanghai',//eu-central-1, ap-southeast-1 region:'', //The default fragment size is 1 MB and cannot be less than 100 KB partSize: 1048576, //The number of parallel upload fragments, the default is 5 parallel: 5, //When the network fails, the number of re-uploads, the default is 3 retryCount: 3, //When the network fails, the re-upload interval, the default is 2 seconds retryDuration: 2, //Start upload onUploadstarted: function (uploadInfo) { console.log('Start uploading') console.log(uploadInfo) }, //File upload successfully onUploadSuccee: function (uploadInfo) { }, //File upload failed onUploadFailed: function (uploadInfo, code, message) { }, //File upload progress, unit: byte onUploadProgress: function (uploadInfo, totalSize, loadedPercent) { }, //Upload credentials or STS token timeout onUploadTokenExpired: function (uploadInfo) { }, //End of uploading all files onUploadEnd:function(uploadInfo){} }) } } } </script> Copy code
Package interface

Use Alibaba Cloud interface (4)

src/services/aliyun-upload.js package Alibaba Cloud interface that needs to be used // Import interface module import request from '@/utils/request' //Get Aliyun upload image credentials export const aliyunImagUploadAddressAdnAuth = () => { return request({ method : 'get' , url : '/boss/course/upload/aliyunImagUploadAddressAdnAuth.json' }) } //Obtain the Alibaba Cloud upload video credentials export const aliyunVideoUploadAddressAdnAuth = params => { return request({ method : 'get' , url : '/boss/course/upload/aliyunVideoUploadAddressAdnAuth.json' , params }) } // Aliyun transcoding request export const aliyunTransCode = data => { return request({ method : 'post' , url : '/boss/course/upload/aliyunTransCode.json' , data }) } //Get Aliyun transcoding progress export const aliyunTransCodePercent = lessonId => { return request({ method : 'get' , url : '/boss/course/upload/aliyunTransCodePercent.json' , params : { lessonId } }) } Copy code
Upload voucher processing

It is recommended to add window .Aliyun... to avoid problems when creating Aliyun Mountain.

Determine whether the uploaded file is a video or a picture for separate processing

Save the image upload address for video upload

Save the vouchers and addresses returned by the pictures and videos, and finally set the address to Alibaba Cloud (there is in the sample code)

Pay attention to this

src/views/course/uploadVideo.vue <template> <el-card class="box-card"> <!-- Top title--> <div slot="header" class="clearfix"> <!-- Use the passed path parameters and ordinary parameters --> <span>Upload video/Course ID: {{courseId}}/Class ID: {{lessonId}}</span> </div> <el-form label-width="80px"> <!-- Add cover--> <el-form-item label="Add cover page"> <input type="file" ref="img"> </el-form-item> <!-- Add video--> <el-form-item label="Add video"> <input type="file" ref="video"> </el-form-item> <el-button>Back</el-button> <!-- Start upload button to add click event--> <el-button type="primary" @click="beginUpload">Start uploading</el-button> </el-form> </el-card> </template> <script> /* eslint-disable*/ //Introduce four encapsulated Alibaba Cloud related interfaces import { aliyunImagUploadAddressAdnAuth, aliyunVideoUploadAddressAdnAuth, aliyunTransCode, aliyunTransCodePercent } from'@/services/aliyun-upload' export default { name:'uploadVideo', //courseId-path parameter props: ['courseId'], data () { return { //class id, obtained by parameter lessonId: this.$route.query.lessonId, //Alibaba Cloud upload SDK uploader: null, //The map's address imageURL:'' } }, //life cycle hook function created () { //Initial upload this.initAliyun() }, methods: { //Start upload button click event beginUpload () { //Add pictures and videos to the upload list, use ref to get videos and pictures const uploader = this.uploader uploader.addFile(this.$refs.img.files[0]) uploader.addFile(this.$refs.video.files[0]) //Start upload uploader.startUpload() }, initAliyun () { this.uploader = new AliyunUpload.Vod({ //Ali account ID, must have a value (the account provided by the teacher is used here) userId: '1618139964448548', //Upload to the region of VOD, the default value is'cn-shanghai',//eu-central-1, ap-southeast-1 region:'', //The default fragment size is 1 MB and cannot be less than 100 KB partSize: 1048576, //The number of parallel upload fragments, the default is 5 parallel: 5, //When the network fails, the number of re-uploads, the default is 3 retryCount: 3, //When the network fails, the re-upload interval, the default is 2 seconds retryDuration: 2, //Start upload onUploadstarted: async uploadInfo => { //Create a new variable to receive credential information let ressAdnAuth = null //Determine whether the file is a picture or a video if (uploadInfo.isImage) { //If it is a picture, call the picture interface const {data} = await aliyunImagUploadAddressAdnAuth() if (data.code === '000000') { //Save the credential information after successful acquisition ressAdnAuth = data.data //Save the picture address, you will use it later this.imageURL = ressAdnAuth.imageURL } } else { //If it is not a picture, then it is a video, call the video interface const {data} = await aliyunVideoUploadAddressAdnAuth({ //Setting parameters fileName: uploadInfo.file.name, imageUrl: this.imageURL }) if (data.code === '000000') { //If the acquisition is successful, the information is also saved ressAdnAuth = data.data } } //Set Alibaba Cloud credentials this.uploader.setUploadAuthAndAddress( uploadInfo, ressAdnAuth.uploadAuth, ressAdnAuth.uploadAddress, ressAdnAuth.imageId || ressAdnAuth.videoId) }, //File upload successfully onUploadSuccee: function (uploadInfo) { }, //File upload failed onUploadFailed: function (uploadInfo, code, message) { }, //File upload progress, unit: byte onUploadProgress: function (uploadInfo, totalSize, loadedPercent) { }, //Upload credentials or STS token timeout onUploadTokenExpired: function (uploadInfo) { }, //End of uploading all files onUploadEnd:function(uploadInfo){ console.log('All files upload completed') } }) } } } </script> Copy code
Transcoding

The key data to be sent in the transcoding request:

  • Transcoding lesson id-lessonId
  • Picture address-coverImageUrI
  • fileId-video id
  • fileName-video name

Transcode after uploading all files

When transcoding is successful, adjust the round-robin transcoding progress, use the timer to query the transcoding progress circularly, pay attention to stop the timer

Add structure to monitor progress

File upload progress can be directly used Alibaba Cloud built-in hook

Pay attention to the order

Click upload to reset data

<template> <el-card class="box-card"> <!-- Top title--> <div slot="header" class="clearfix"> <!-- Use the passed path parameters and ordinary parameters --> <span>Upload video/Course ID: {{courseId}}/Class ID: {{lessonId}}</span> </div> <el-form label-width="80px"> <!-- Add cover--> <el-form-item label="Add cover page"> <input type="file" ref="img"> </el-form-item> <!-- Add video--> <el-form-item label="Add video"> <input type="file" ref="video"> </el-form-item> <el-form-item label="Upload progress" v-if="uploading"> <el-progress :percentage="uploadProgress" :text-inside="true" :stroke-width="30" style="width: 300px"></el-progress> </el-form-item> <el-form-item label="transcoding progress" v-if="transcoding"> <el-progress :percentage="transcodingProgress" :text-inside="true" :stroke-width="30" style="width: 300px"></el-progress> </el-form-item> <el-button>Back</el-button> <!-- Start upload button to add click event--> <el-button type="primary" @click="beginUpload">Start uploading</el-button> </el-form> </el-card> </template> <script> /* eslint-disable*/ //Introduce four encapsulated Alibaba Cloud related interfaces import { aliyunImagUploadAddressAdnAuth, aliyunVideoUploadAddressAdnAuth, aliyunTransCode, aliyunTransCodePercent } from'@/services/aliyun-upload' export default { name:'uploadVideo', //courseId-path parameter props: ['courseId'], data () { return { //class id, obtained by parameter lessonId: this.$route.query.lessonId, //Alibaba Cloud upload SDK uploader: null, //The map's address imageURL:'', //video id videoId: null, //Transcoding progress transcodingProgress: 0, //Transcoding status transcoding: false, //upload progress uploadProgress: 0, //Upload status uploading: false } }, //life cycle hook function created () { //Initial upload this.initAliyun() }, methods: { //Start upload button click event beginUpload () { //Reset all status and progress this.imageURL='' this.videoId = null this.transcodingProgress = 0 this.uploadProgress = 0 //Add pictures and videos to the upload list, use ref to get videos and pictures const uploader = this.uploader uploader.addFile(this.$refs.img.files[0]) uploader.addFile(this.$refs.video.files[0]) //Start upload uploader.startUpload() }, initAliyun () { this.uploader = new AliyunUpload.Vod({ //Ali account ID, must have a value (the account provided by the teacher is used here) userId: '1618139964448548', //Upload to the region of VOD, the default value is'cn-shanghai',//eu-central-1, ap-southeast-1 region:'', //The default fragment size is 1 MB and cannot be less than 100 KB partSize: 1048576, //The number of parallel upload fragments, the default is 5 parallel: 5, //When the network fails, the number of re-uploads, the default is 3 retryCount: 3, //When the network fails, the re-upload interval, the default is 2 seconds retryDuration: 2, //Start upload onUploadstarted: async uploadInfo => { this.uploading = true //Create a new variable to receive credential information let ressAdnAuth = null //Determine whether the file is a picture or a video if (uploadInfo.isImage) { //If it is a picture, call the picture interface const {data} = await aliyunImagUploadAddressAdnAuth() if (data.code === '000000') { //Save the credential information after successful acquisition ressAdnAuth = data.data //Save the picture address, you will use it later this.imageURL = ressAdnAuth.imageURL } } else { //If it is not a picture, then it is a video, call the video interface const {data} = await aliyunVideoUploadAddressAdnAuth({ //Setting parameters fileName: uploadInfo.file.name, imageUrl: this.imageURL }) if (data.code === '000000') { //If the acquisition is successful, the information is also saved ressAdnAuth = data.data this.videoId = data.data.videoId } } //Set Alibaba Cloud credentials this.uploader.setUploadAuthAndAddress( uploadInfo, ressAdnAuth.uploadAuth, ressAdnAuth.uploadAddress, ressAdnAuth.imageId || ressAdnAuth.videoId) }, //File upload successfully onUploadSuccee: function (uploadInfo) { }, //File upload failed onUploadFailed: function (uploadInfo, code, message) { }, //File upload progress, unit: byte onUploadProgress: (uploadInfo, totalSize, loadedPercent) => { //The picture does not require progress, all judgments are not pictures to output the progress if (!uploadInfo.isImage) { this.uploadProgress = Math.floor(loadedPercent * 100) } }, //Upload credentials or STS token timeout onUploadTokenExpired: function (uploadInfo) { }, //End of uploading all files onUploadEnd: async uploadInfo => { //Request transcoding after successful upload const {data} = await aliyunTransCode({ lessonId: this.lessonId, coverImageUrl: this.imageURL, fileId: this.videoId, fileName: this.$refs.video.files[0].name }) if (data.code === '000000') { //If the transcoding application is successful, set the transcoding status this.transcoding = true //Set the timer to get the cyclic transcoding progress const timer = setInterval(async () => { //Get transcoding progress const {data} = await aliyunTransCodePercent(this.lessonId) if (data.code === '000000') { //Get successful and synchronize the transcoding progress to the data this.transcodingProgress = data.data //If the transcoding progress is judged to be 100%, delete the timer if (data.data === 100) clearInterval(timer) } }, 1000) } } }) } } } </script> Copy code

Deployment release

Packing--->