笔记来源:拉勾教育 - 大前端就业集训营
文章内容:学习过程中的笔记、感悟、和经验
课程内容管理
内容管理组件、路由处理、跳转操作,动态路径传参
//src/router/index.js - 在路由中添加新的课程内容管理路由
{
// 课程内容管理
path: '/contentManagement/:courseId',
name: 'contentManagement',
component: () => import(/* webpackChunkName:'contentManagement' */ '@/views/course/contentManagement'),
props: true
}
src/views/course/contentManagement.vue 新建课程内容管理页面
<template>
<div>课程内容管理</div>
</template>
<script>
export default {
name: 'contentManagement',
// 参数
props: ['courseId']
}
</script>
src/views/course/son/content.vue 内容管理按钮添加点击事件,带着课程id跳转到内容管理页面
<el-table-column
label="操作">
<template slot-scope="scope">
<!-- 操作按钮 -->
<el-button @click="editCoures(scope.row.id)">编辑</el-button>
<el-button @click="managementCoures(scope.row.id)">内容管理</el-button>
</template>
</el-table-column
// 内容管理
managementCoures (courseId) {
this.$router.push({
name: 'contentManagement',
params: {
courseId
}
})
},
课程数据展示
使用树形组件,使用拖拽功能
使用接口 获取课程数据
章节名和课时名字不同,需要特殊设置
// src/services/coures.js 封装获取课程全部章节内容接口 - getSectionAndLesson
// 获取课程全部章节内容
export const getSectionAndLesson = courseId => {
return request({
method: 'get',
url: `/boss/course/section/getSectionAndLesson?courseId=${courseId}`
})
}
src/views/course/contentManagement.vue 树勇树形组件、使用接口并且重新对应节点
<template>
<el-card class="box-card">
<!-- 标题 -->
<div slot="header" class="clearfix">
<span>编辑课程内容 - 课程ID:{{courseId}}</span>
</div>
<!-- 树形控件 -->
<el-tree :data="treeData" :props="defaultProps" draggable></el-tree>
</el-card>
</template>
<script>
// 引入接口 获取全部章节信息
import { getSectionAndLesson } from '@/services/coures'
export default {
name: 'contentManagement',
// 参数
props: ['courseId'],
data () {
return {
// 树形控件数据
treeData: [],
// 树形控件节点对应关系
defaultProps: {
// 子节点对应名字
children: 'lessonDTOS',
// 节点名称对应,因为章节名和节点名不同,所以要设置两个
label (data) {
return data.sectionName || data.theme
}
}
}
},
// 生命周期钩子
created () {
// 调用方法获取课程全部章节数据
this.getInfo(this.courseId)
},
methods: {
// 获取课程全部内容
async getInfo (courseId) {
// 调用接口
const { data } = await getSectionAndLesson(courseId)
if (data.code === '000000') {
// 如果获取成功,把数据交给树形结构
this.treeData = data.data
}
}
}
}
</script>
tree组件定制
结构设置,样式美化处理
src/views/course/contentManagement.vue
<template>
<el-card class="box-card">
<!-- 标题 -->
<div slot="header" class="clearfix">
<span>编辑课程内容 - 课程ID:{{courseId}}</span>
</div>
<!-- 树形控件 -->
<el-tree :data="treeData" :props="defaultProps" draggable>
<!-- 自定义结构 -->
<div class="custom-tree-node" slot-scope="{ node, data }">
<!-- 两个span 便于使用flex布局 -->
<span>{{ node.label }}</span>
<span>
<el-button
type="text"
size="mini"
@click="() => edit(data)">
编辑
</el-button>
<!-- 使用v-if判断当前节点是不是章节,是章节添加按钮添加课时,否则按钮位为传视频 -->
<el-button
type="text"
size="mini"
v-if="node.level === 1">
添加课时
</el-button>
<el-button
type="text"
size="mini"
v-else>
上传视频
</el-button>
<el-button
type="text"
size="mini"
@click="() => now(node, data)">
状态
</el-button>
</span>
</div>
</el-tree>
</el-card>
</template>
<script>
// 引入接口 获取全部章节信息
import { getSectionAndLesson } from '@/services/coures'
export default {
name: 'contentManagement',
// 参数
props: ['courseId'],
data () {
return {
// 树形控件数据
treeData: [],
// 树形控件节点对应关系
defaultProps: {
// 子节点对应名字
children: 'lessonDTOS',
// 节点名称对应,因为章节名和节点名不同,所以要设置两个
label (data) {
return data.sectionName || data.theme
}
}
}
},
// 生命周期钩子
created () {
// 调用方法获取课程全部章节数据
this.getInfo(this.courseId)
},
methods: {
// 获取课程全部内容
async getInfo (courseId) {
// 调用接口
const { data } = await getSectionAndLesson(courseId)
if (data.code === '000000') {
// 如果获取成功,把数据交给树形结构
this.treeData = data.data
}
}
}
}
</script>
<style lang="scss" scoped>
// 树形结构样式
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
</style>
节点拖拽处理
要限制不合理拖拽,判断是否能放置
src/views/course/contentManagement.vue 给树形结构添加allow-drop属性并绑定判断方法
// 判断是否可放置方法
// 三个参数分别是拖动的元素,要放置的元素,位置
allowDrop (draggingNode, dropNode, type) {
// 条件1:位置不能是inner(内部),意味着不可以放到其他节点内部
// 条件2:两个节点有相同的父节点,意味着只能在同一个父节点内部拖动
return type !== 'inner' && draggingNode.parent.id === dropNode.parent.id
}
拖拽后数据更新ca
拖拽后触发数据更新,组件提供事件,使用对应接
判断是章节还是课时。使用不同接口
为了避免每次都要请求多次接口,使用Promise.all()处理全部的请求 async 方法(await Promise.all(遍历(调用接口))使用try - cath进行成功和失败的操作
添加loading效果
src/services/coures.js 替阿甲课程更新和课时更新接口的封装
// 保存或者更新章节
export const saveOrUpdateSection = data => {
return request({
method: 'post',
url: '/boss/course/section/saveOrUpdateSection',
data
})
}
// 保存或者更新课时
export const saveOrUpdate = data => {
return request({
method: 'post',
url: '/boss/course/lesson/saveOrUpdate',
data
})
}
// src/views/course/contentManagement.vue 使用v-loading给树形结构添加加载中状态,使用node-drop添加拖拽成功事件函数
<el-tree v-loading="loading" :data="treeData" :props="defaultProps" default-expand-all draggable :allow-drop="allowDrop" @node-drop="nodeDrop">
// 拖拽成功回调函数
async nodeDrop (meNode, endNode, type, event) {
// 更改信号值使树形结构处于加载中
this.loading = true
// 使用try-catch进行判断
try {
// 因为可能会包含多个节点所以可能会多次调用接口,所以使用await Promise.all接收全部调用的接口一起发送
// 遍历要移动到的节点的父节点下的所有子节点
await Promise.all(endNode.parent.childNodes.map((item, index) => {
// 判断移动的是课时还是章节,课时有sectionId属性
if (meNode.data.sectionId) {
// 如果移动的是课时,使用课时更新接口
return saveOrUpdate({
id: item.data.id,
orderNum: index
})
} else {
// 如果不是课时,那就是章节,使用章节接口
return saveOrUpdateSection({
id: item.data.id,
orderNum: index
})
}
}))
// 最后弹出提示
this.$message.success('更新成功')
} catch (err) {
// 出现错误也弹出提示
this.$message.error('更新失败')
}
// 不管成功还是失败。都要把结构加载状态取消
this.loading = false
},
上传课时视频
点击双传视频按钮跳转路由,新建组件,修改路由,点击跳转,路径参数
//src/router/index.js 添加上传视频的路由
{
// 上传课时视频
path: '/uploadVideo/:courseId',
name: 'uploadVideo',
component: () => import(/* webpackChunkName:'uploadVideo' */ '@/views/course/uploadVideo'),
props: true
}
src/views/course/contentManagement.vue 上传视频按钮添加点击事件,带参数跳转到上传视频页面
<el-button
type="text"
size="mini"
v-else
@click="$router.push({
name: 'uploadVideo',
params: {
courseId
},
query: {
lessonId: data.id
}
})">
上传视频
</el-button>
src/views/course/uploadVideo.vue 新建组件书写上传视频
<template>
<el-card class="box-card">
<!-- 顶部标题 -->
<div slot="header" class="clearfix">
<!-- 使用传递过来的路径参数和普通参数 -->
<span>上传视频 / 课程ID:{{courseId}} / 课时ID:{{lessonId}}</span>
</div>
<el-form :model="form" label-width="80px">
<el-form-item label="封面上传">
<input type="file">
</el-form-item>
<el-form-item label="视频上传">
<input type="file">
</el-form-item>
<el-button>返回</el-button>
<el-button type="primary">开始上传</el-button>
</el-form>
</el-card>
</template>
<script>
export default {
name: 'uploadVideo',
// courseId - 路径参数
props: ['courseId'],
data () {
return {
// 课时id,通过参数获取
lessonId: this.$route.query.lessonId,
// form表单数据
form: {}
}
}
}
</script>
阿里云视频点播
阿里云 - 产品 - 视频服务 - 视频点播 - 文档与帮助 - 点播服务API - 开发指南 - 概述 - 上传SDK - 使用js上传SDK(SDK - 工具包 ) - 使用web端SDK - 可以查看实例代码Vue
将阿里云SDK中的三个js文件引入根目录下的HTML文件(注意名称和路径)
<!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 -->
<!-- 引入阿里云SDK文件,课程中使用的绝对路径,但我这里使用相对路径 -->
<!-- IE需要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>
初始化阿里云山传
使用上传地址和凭证的方式 (官方推荐)
找到Vue示例下面的src - uploadauth.vue就是使用上传凭证和地址的方式,使用vscode查看
设置注释/* eslint-disable*/就可以设置不对注释下面的代码进行风格校验
阿里云id - 1618139964448548
上传钩子中设置地址和凭证
组件创建完毕后初始化阿里云,直接文档中的代码进行处理
添加点击上传事件,点击上传获取文件
- 给两个按钮添加ref,使用refs.名字.files[0]获取文件信息
- 将两个文件添加到阿里云文件列表中然后启动上传(先传封面再传视频-接口要求)
开始上传需要在上传钩子函数中设置
src/views/course/uploadVideo.vue 初始化上传功能,添加按钮点击事件实现上传
<template>
<el-card class="box-card">
<!-- 顶部标题 -->
<div slot="header" class="clearfix">
<!-- 使用传递过来的路径参数和普通参数 -->
<span>上传视频 / 课程ID:{{courseId}} / 课时ID:{{lessonId}}</span>
</div>
<el-form label-width="80px">
<!-- 添加封面 -->
<el-form-item label="添加封面">
<input type="file" ref="img">
</el-form-item>
<!-- 添加视频 -->
<el-form-item label="添加视频">
<input type="file" ref="video">
</el-form-item>
<el-button>返回</el-button>
<!-- 开始上传按钮添加点击事件 -->
<el-button type="primary" @click="beginUpload">开始上传</el-button>
</el-form>
</el-card>
</template>
<script>
/* eslint-disable*/
export default {
name: 'uploadVideo',
// courseId - 路径参数
props: ['courseId'],
data () {
return {
// 课时id,通过参数获取
lessonId: this.$route.query.lessonId,
// 阿里云上传SDK
uploader: null
}
},
// 生命周期钩子函数
created () {
// 初始化上传
this.initAliyun()
},
methods: {
// 开始上传按钮点击事件
beginUpload () {
// 将图片和视频添加到上传列表,使用ref获取视频和图片
this.uploader.addFile(this.$refs.img.files[0])
this.uploader.addFile(this.$refs.video.files[0])
// 开始上传
this.uploader.startUpload()
},
initAliyun () {
this.uploader = new AliyunUpload.Vod({
//阿里账号ID,必须有值(这里使用老师提供的账号)
userId: '1618139964448548',
//上传到视频点播的地域,默认值为'cn-shanghai',//eu-central-1,ap-southeast-1
region: '',
//分片大小默认1 MB,不能小于100 KB
partSize: 1048576,
//并行上传分片个数,默认5
parallel: 5,
//网络原因失败时,重新上传次数,默认为3
retryCount: 3,
//网络原因失败时,重新上传间隔时间,默认为2秒
retryDuration: 2,
//开始上传
onUploadstarted: function (uploadInfo) {
console.log('开始上传啦')
console.log(uploadInfo)
},
//文件上传成功
onUploadSuccee: function (uploadInfo) {
},
//文件上传失败
onUploadFailed: function (uploadInfo, code, message) {
},
//文件上传进度,单位:字节
onUploadProgress: function (uploadInfo, totalSize, loadedPercent) {
},
//上传凭证或STS token超时
onUploadTokenExpired: function (uploadInfo) {
},
//全部文件上传结束
onUploadEnd:function(uploadInfo){}
})
}
}
}
</script>
封装接口
使用阿里云接口(4个)
src/services/aliyun-upload.js 封装需要使用的阿里云接口
// 引入接口模块
import request from '@/utils/request'
// 获取阿里云上传图片凭证
export const aliyunImagUploadAddressAdnAuth = () => {
return request({
method: 'get',
url: '/boss/course/upload/aliyunImagUploadAddressAdnAuth.json'
})
}
// 获取阿里云上传视频凭证
export const aliyunVideoUploadAddressAdnAuth = params => {
return request({
method: 'get',
url: '/boss/course/upload/aliyunVideoUploadAddressAdnAuth.json',
params
})
}
// 阿里云转码请求
export const aliyunTransCode = data => {
return request({
method: 'post',
url: '/boss/course/upload/aliyunTransCode.json',
data
})
}
// 获取阿里云转码进度
export const aliyunTransCodePercent = lessonId => {
return request({
method: 'get',
url: '/boss/course/upload/aliyunTransCodePercent.json',
params: {
lessonId
}
})
}
上传凭证处理
建议创建阿里云山传的时候加上window.Aliyun.....避免问题
判断上传的文件是视频还是图片从事分别处理
保存图片上传地址,用于视频上传
保存图片和视频返回的凭证和地址,最后把地址设置给阿里云(示例代码里面有)
注意this
src/views/course/uploadVideo.vue
<template>
<el-card class="box-card">
<!-- 顶部标题 -->
<div slot="header" class="clearfix">
<!-- 使用传递过来的路径参数和普通参数 -->
<span>上传视频 / 课程ID:{{courseId}} / 课时ID:{{lessonId}}</span>
</div>
<el-form label-width="80px">
<!-- 添加封面 -->
<el-form-item label="添加封面">
<input type="file" ref="img">
</el-form-item>
<!-- 添加视频 -->
<el-form-item label="添加视频">
<input type="file" ref="video">
</el-form-item>
<el-button>返回</el-button>
<!-- 开始上传按钮添加点击事件 -->
<el-button type="primary" @click="beginUpload">开始上传</el-button>
</el-form>
</el-card>
</template>
<script>
/* eslint-disable*/
// 引入封装的四个阿里云相关接口呀
import {
aliyunImagUploadAddressAdnAuth,
aliyunVideoUploadAddressAdnAuth,
aliyunTransCode,
aliyunTransCodePercent
} from '@/services/aliyun-upload'
export default {
name: 'uploadVideo',
// courseId - 路径参数
props: ['courseId'],
data () {
return {
// 课时id,通过参数获取
lessonId: this.$route.query.lessonId,
// 阿里云上传SDK
uploader: null,
// 图片地址
imageURL: ''
}
},
// 生命周期钩子函数
created () {
// 初始化上传
this.initAliyun()
},
methods: {
// 开始上传按钮点击事件
beginUpload () {
// 将图片和视频添加到上传列表,使用ref获取视频和图片
const uploader = this.uploader
uploader.addFile(this.$refs.img.files[0])
uploader.addFile(this.$refs.video.files[0])
// 开始上传
uploader.startUpload()
},
initAliyun () {
this.uploader = new AliyunUpload.Vod({
//阿里账号ID,必须有值(这里使用老师提供的账号)
userId: '1618139964448548',
//上传到视频点播的地域,默认值为'cn-shanghai',//eu-central-1,ap-southeast-1
region: '',
//分片大小默认1 MB,不能小于100 KB
partSize: 1048576,
//并行上传分片个数,默认5
parallel: 5,
//网络原因失败时,重新上传次数,默认为3
retryCount: 3,
//网络原因失败时,重新上传间隔时间,默认为2秒
retryDuration: 2,
//开始上传
onUploadstarted: async uploadInfo => {
// 新建一个变量用于接收凭证信息
let ressAdnAuth = null
// 判断文件是图片还是视频
if (uploadInfo.isImage) {
// 如果是图片调用图片接口
const { data } = await aliyunImagUploadAddressAdnAuth()
if (data.code === '000000') {
// 获取成功后把凭证信息存起来
ressAdnAuth = data.data
// 保存图片地址,后面要使用
this.imageURL = ressAdnAuth.imageURL
}
} else {
// 如果不是图片那就是视频,调用视频接口
const { data } = await aliyunVideoUploadAddressAdnAuth({
// 设置参数
fileName: uploadInfo.file.name,
imageUrl: this.imageURL
})
if (data.code === '000000') {
// 如果获取成功同样保存信息
ressAdnAuth = data.data
}
}
// 设置阿里云凭证
this.uploader.setUploadAuthAndAddress(
uploadInfo,
ressAdnAuth.uploadAuth,
ressAdnAuth.uploadAddress,
ressAdnAuth.imageId || ressAdnAuth.videoId)
},
//文件上传成功
onUploadSuccee: function (uploadInfo) {
},
//文件上传失败
onUploadFailed: function (uploadInfo, code, message) {
},
//文件上传进度,单位:字节
onUploadProgress: function (uploadInfo, totalSize, loadedPercent) {
},
//上传凭证或STS token超时
onUploadTokenExpired: function (uploadInfo) {
},
//全部文件上传结束
onUploadEnd:function(uploadInfo){
console.log('全部文件上传完成')
}
})
}
}
}
</script>
转码处理
转码请求重点需要发送的数据:
- 转码课时id - lessonId
- 图片地址 - coverImageUrI
- fileId - 视频id
- fileName - 视频名称
全部文件上传完毕后进行转码
转码成功调轮循转码进度,使用定时器循环查询转码进度,注意要停止定时器
添加结构监听进度
文件上传进度可以直接使用阿里云内置钩子
注意先后顺序
点击上传重置数据
<template>
<el-card class="box-card">
<!-- 顶部标题 -->
<div slot="header" class="clearfix">
<!-- 使用传递过来的路径参数和普通参数 -->
<span>上传视频 / 课程ID:{{courseId}} / 课时ID:{{lessonId}}</span>
</div>
<el-form label-width="80px">
<!-- 添加封面 -->
<el-form-item label="添加封面">
<input type="file" ref="img">
</el-form-item>
<!-- 添加视频 -->
<el-form-item label="添加视频">
<input type="file" ref="video">
</el-form-item>
<el-form-item label="上传进度" 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="转码进度" v-if="transcoding">
<el-progress :percentage="transcodingProgress" :text-inside="true" :stroke-width="30" style="width: 300px"></el-progress>
</el-form-item>
<el-button>返回</el-button>
<!-- 开始上传按钮添加点击事件 -->
<el-button type="primary" @click="beginUpload">开始上传</el-button>
</el-form>
</el-card>
</template>
<script>
/* eslint-disable*/
// 引入封装的四个阿里云相关接口呀
import {
aliyunImagUploadAddressAdnAuth,
aliyunVideoUploadAddressAdnAuth,
aliyunTransCode,
aliyunTransCodePercent
} from '@/services/aliyun-upload'
export default {
name: 'uploadVideo',
// courseId - 路径参数
props: ['courseId'],
data () {
return {
// 课时id,通过参数获取
lessonId: this.$route.query.lessonId,
// 阿里云上传SDK
uploader: null,
// 图片地址
imageURL: '',
// 视频id
videoId: null,
// 转码进度
transcodingProgress: 0,
// 转码状态
transcoding: false,
// 上传进度
uploadProgress: 0,
// 上传状态
uploading: false
}
},
// 生命周期钩子函数
created () {
// 初始化上传
this.initAliyun()
},
methods: {
// 开始上传按钮点击事件
beginUpload () {
// 重置所有状态和进度
this.imageURL= ''
this.videoId= null
this.transcodingProgress= 0
this.uploadProgress= 0
// 将图片和视频添加到上传列表,使用ref获取视频和图片
const uploader = this.uploader
uploader.addFile(this.$refs.img.files[0])
uploader.addFile(this.$refs.video.files[0])
// 开始上传
uploader.startUpload()
},
initAliyun () {
this.uploader = new AliyunUpload.Vod({
//阿里账号ID,必须有值(这里使用老师提供的账号)
userId: '1618139964448548',
//上传到视频点播的地域,默认值为'cn-shanghai',//eu-central-1,ap-southeast-1
region: '',
//分片大小默认1 MB,不能小于100 KB
partSize: 1048576,
//并行上传分片个数,默认5
parallel: 5,
//网络原因失败时,重新上传次数,默认为3
retryCount: 3,
//网络原因失败时,重新上传间隔时间,默认为2秒
retryDuration: 2,
//开始上传
onUploadstarted: async uploadInfo => {
this.uploading = true
// 新建一个变量用于接收凭证信息
let ressAdnAuth = null
// 判断文件是图片还是视频
if (uploadInfo.isImage) {
// 如果是图片调用图片接口
const { data } = await aliyunImagUploadAddressAdnAuth()
if (data.code === '000000') {
// 获取成功后把凭证信息存起来
ressAdnAuth = data.data
// 保存图片地址,后面要使用
this.imageURL = ressAdnAuth.imageURL
}
} else {
// 如果不是图片那就是视频,调用视频接口
const { data } = await aliyunVideoUploadAddressAdnAuth({
// 设置参数
fileName: uploadInfo.file.name,
imageUrl: this.imageURL
})
if (data.code === '000000') {
// 如果获取成功同样保存信息
ressAdnAuth = data.data
this.videoId = data.data.videoId
}
}
// 设置阿里云凭证
this.uploader.setUploadAuthAndAddress(
uploadInfo,
ressAdnAuth.uploadAuth,
ressAdnAuth.uploadAddress,
ressAdnAuth.imageId || ressAdnAuth.videoId)
},
//文件上传成功
onUploadSuccee: function (uploadInfo) {
},
//文件上传失败
onUploadFailed: function (uploadInfo, code, message) {
},
//文件上传进度,单位:字节
onUploadProgress: (uploadInfo, totalSize, loadedPercent) => {
// 图片不需要进度,所有判断不是图片才输出进度
if (!uploadInfo.isImage) {
this.uploadProgress = Math.floor(loadedPercent * 100)
}
},
//上传凭证或STS token超时
onUploadTokenExpired: function (uploadInfo) {
},
//全部文件上传结束
onUploadEnd: async uploadInfo => {
// 上传成功后请求转码
const { data } = await aliyunTransCode({
lessonId: this.lessonId,
coverImageUrl: this.imageURL,
fileId: this.videoId,
fileName: this.$refs.video.files[0].name
})
if (data.code === '000000') {
// 如果转码申请成功设置转码状态
this.transcoding = true
// 设置定时器获取循环转码进度
const timer = setInterval(async () => {
// 获取转码进度
const { data } = await aliyunTransCodePercent(this.lessonId)
if (data.code === '000000') {
// 获取成功把转码进度同步给数据
this.transcodingProgress = data.data
// 判断转码进度为100%则删除定时器
if (data.data === 100) clearInterval(timer)
}
}, 1000)
}
}
})
}
}
}
</script>
部署发布
打包 --->