课程管理(二)

课程内容管理

课程内容管理指的是前后台课程详情中课程目录的内容管理,内容中包含章节和课时部分(对应了课程视频)


大概长这样,这次我们只讲拖拽功能,其他的都是经典时尚重复工作,不再赘述

后台通过 课程管理->指定课程->内容管理操作
创建组件并配置路由,同时设置跳转功能

// course/section.vue (新建)
<template>
  <div class="course-section">课程内容</div>
</template>

<script>
export default {
  name: 'CourseSection',
  // 设置路由后,通过 props 接收动态路由参数
  props: {
    courseId: {
      type: [String, Number],
      required: true
    }
  }
}
</script>

<style lang="scss" scoped></style>
// router/index.js
    ...
  {
    path: '/course/:courseId/section',
    name: 'course-section',
    component: () => import(/* webpackChunkName: 'course-section' */ '@/views/course/section.vue'),
    props: true
  }
]
...
// course/components/list.vue
...
<el-button
  @click="$router.push({
    name: 'course-section',
    params: {
      courseId: scope.row.id
    }
  })"
>内容管理</el-button>
...

展示课程内容

设置基本布局结构,底部列表使用Element的Tree组件,后续通过属性配置可以直接设置拖拽功能

  • 设置draggable实现列表拖拽
<template>
  <div class="course-section">
    <el-card>
      <div slot="header">
        课程名称
      </div>
      <el-tree
        :data="data"
        :props="defaultProps"
        draggable
      ></el-tree>
    </el-card>
  </div>
</template>

<script>
export default {
  name: 'CourseSection',
  props: {
    courseId: {
      type: [String, Number],
      required: true
    }
  },
  data () {
    return {
      data: [{
        label: '一级 1',
        children: [{
          label: '二级 1-1',
          children: [{
            label: '三级 1-1-1'
          }]
        }]
      }, {
        label: '一级 2',
        children: [{
          label: '二级 2-1',
          children: [{
            label: '三级 2-1-1'
          }]
        }, {
          label: '二级 2-2',
          children: [{
            label: '三级 2-2-1'
          }]
        }]
      }, {
        label: '一级 3',
        children: [{
          label: '二级 3-1',
          children: [{
            label: '三级 3-1-1'
          }]
        }, {
          label: '二级 3-2',
          children: [{
            label: '三级 3-2-1'
          }]
        }]
      }],
      defaultProps: {
        children: 'children',
        label: 'label'
      }
    }
  }
}
</script>

<style lang="scss" scoped></style>

请求数据创建列表内容,接口为章节内容:getSessionAndLesson接口

// services/course-section.js (新建)
import request from '@/utils/request'

// 获取章节和课时
export const getSectionAndLesson = courseId => {
  return request({
    method: 'GET',
    url: '/boss/course/section/getSectionAndLesson',
    params: {
      courseId
    }
  })
}

引入并使用

  • 响应数据的data代表章节信息,内部的LessonDTOS代表章节内的课时数据
// section.vue
...
import { getSectionAndLesson } from '@/services/course-section.js'
...
created () {
  this.loadSection()
},
methods: {
  async loadSection () {
    const { data } = await getSectionAndLesson(this.courseId)
    if (data.code === '000000') {
      console.log(data)
    }
  }
}
...

将数据绑定到视图

  • 设置sections属性保存课程内容数据
    • 修改el-tree组件中使用的数据
    • 将请求数据绑定到sections
  • 由于sections中的属性和tree需要的默认名称不同,需要修改属性名
    • children为lessonDTOS代表章节下的课时
    • 而label对于章节和课时不同,章节名为sectionName,课时名为theme
    • 通过文档得知,label可以设置为函数,内部判断数据进行处理就可以了
// section.vue
...
<!-- 3. 绑定到模板 -->
<el-tree
  :data="sections"
  ...
></el-tree>
...
<script>
...
data () {
  return {
    // 1. 声明数据
    sections: [],
    // 4. 根据响应数据调整属性
    defaultProps: {
      children: 'lessonDTOS',
      label (data) {
        return data.sectionName || data.theme
      }
    }
  }
},
...
  if (data.code === '000000') {
    // 2. 绑定数据
    this.sections = data.data
  }
...
</script>

Tree组件内容定制

Tree 组件默认只有文本内容,而章节与课时除了文本之外还有具体的按钮结构,对应的功能还是各不相同的,这个时候就需要通过作用域插槽来进行内容定制,具体的方式参见以下Element文档

  • 通过作用域插槽接收的node为当前节点的Node对象,data为当前节点的数据对象
// section.vue
...
<el-tree ... >
  <!-- 设置插槽,并通过插槽接收组件暴露的数据 -->
  <div class="inner" slot-scope="{ node, data }">
    <!-- 设置内容 -->
    <span>{{ node.label }}</span>
    <!-- 设置后续按钮结构 -->
    <!-- section 结构 -->
    <span v-if="data.sectionName" class="actions">
      <el-button>编辑</el-button>
      <el-button>添加课时</el-button>
      <el-button>状态</el-button>
    </span>
    <!-- lesson 结构 -->
    <span v-else class="actions">
      <el-button>编辑</el-button>
      <el-button>上传视频</el-button>
      <el-button>状态</el-button>
    </span>
  </div>
</el-tree>
...

调整样式

<style lang="scss" scoped>
.inner {
  // 浏览器观察到父元素设置了 flex,所以当前元素 flex: 1 占满一行
  flex: 1;
  // 内部元素希望左右分开,所以给当前元素设置 flex
  display: flex;
  justify-content: space-between;
  align-items: center;
  // 其他样式美化
  padding: 10px;
  border-bottom: 1px solid #ebeef5;
}

// 当前行具有类名 .el-tree-node__content 设置了固定高度 26px, 这里要改为 auto 自适应
// 由于为 Tree 组件内的元素,需要设置样式穿透
::v-deep .el-tree-node__content {
  height: auto;
}
</style>
 

设置完毕,内部的编辑与显示隐藏是相同功能,这些功能(新增,编辑等)就不再赘述

节点拖动处理

Tree的拖拽不设条件,但是业务中,肯定是有逻辑存在的,比如说将章节拖到课时一级就是不可能存在的逻辑,应该针对这些请求设置一些规则
通过Tree组件的属性可以定制拖拽功能

  • allow-drop:通过回调返回的布尔值判断当前节点是否能被放置,三个参数
    • draggingNode:正在拖拽的节点
    • dropNode:放置的目标节点
    • type:放置在目标节点的哪个位置
      • 这个type有三种情况:prev(同级前),inner(内部),next(同级后)
// section.vue
...
<el-tree
  :data="sections"
  :props="defaultProps"
  draggable
  :allow-drop="handleAllowDrop"
>
...
handleAllowDrop (draggingNode, dropNode, type) {
  // 1. 不能有放入内部的操作,但例如将章节1拖拽到章节2的课时1之前时,type 为 prev,需要进一步处理
  // 2. 所有课时都具有 sectionId,通过下面的条件,限制章节不能移动到课时前后,也不能将章节的课时移动到其他章节
  return type !== 'inner' && draggingNode.data.sectionId === dropNode.data.sectionId
}

拖拽更新数据处理

一般来说,后端会提供接口将当前的章节最新顺序上传,但是项目中并没有提供这样的接口,提供的单个课时位置更新的接口,所以我们需要进行遍历!依次更新处理(好处就在于我们可以借此来练习批量请求的处理操作)
首先封装接口

// services/course-section.js
...
// 新增或更新章节
export const saveOrUpdateSection = data => {
  return request({
    method: 'POST',
    url: '/boss/course/section/saveOrUpdateSection',
    data
  })
}

// 新增或更新课时(因课时功能较少,此处未单独封装模块,可自行处理)
export const saveOrUpdateLesson = data => {
  return request({
    method: 'POST',
    url: '/boss/course/lesson/saveOrUpdate',
    data
  })
}

Tree组件提供了node-drop方法,处理拖动后的结果

// section.vue
...
<el-tree
    ...
  @node-drop="handleNodeDrop"
>
...
<script>
import { getSectionAndLesson, saveOrUpdateSection, saveOrUpdateLesson } from '@/services/course-section.js'
...
// 设置节点拖动后的数据更新
async handleNodeDrop (draggingNode, dropNode, tyoe, event) {
  // 1. 无论是章节还是课时, dropNode 都有parent(draggingNode.parent 总为 null), 内部有childNodes
  // - dropNode.parent.childNodes 可获取拖拽项所在列表的所有数据
  // - 遍历操作
  // 4. 由于是批量请求,可以使用 Promise.all() 便于进行统一操作
  //   - 将 map 返回的,由 Axios 调用返回的 Promise 对象组成的数组,传入到 Promise.all() 中
  //   - 设置 async await 并进行 try..catch 处理
  try {
    await Promise.all(dropNode.parent.childNodes.map((item, index) => {
      // 2. 对章节与课时进行分别处理
      //   - 除了 draggingNode.data.sectionId 外,draggingNode.lessonDTOS 也可以判断
      if (draggingNode.data.lessonDTOS) {
        // 章节操作
        return saveOrUpdateSection({
          id: item.data.id,
          // 按现在的索引顺序给当前级别列表设置排序序号
          orderNum: index
        })
      } else {
        // 课时操作(同上)
        return saveOrUpdateLesson({
          id: item.data.id,
          // 按现在的索引顺序给当前级别列表设置排序序号
          orderNum: index
        })
      }
    }))
    this.$message.success('数据更新成功')
  } catch (err) {
    this.$message.success('数据更新失败', err)
  }
}
...
</script>
...

之后呢,在请求过程中添加一下loading效果,我们就可以来体会体会Promise.all的好处了

  • 示例中,遍历+请求的结构,这个时候如果我们设置await就会变成了一个个发送了
  • 通过Promise.all统一发送,统一进行结束处理,非常方便!
// course-section.vue
...
<el-tree
  v-loading="isLoading"
  ...
>
...
<script>
data () {
  return {
    ...
    isLoading: false
  }
},
...
async handleNodeDrop (draggingNode, dropNode, tyoe, event) {
  this.isLoading = true
  ...
  try {
    ...
  } catch (err) {
    ...
  }
  this.isLoading = false
}
</script>

这个地方主要是业务练习,实际开发过程中让后端处理这方面的逻辑就会快很多了

上传视频处理

通过在线示例演示之后可以发现,设置上传课时视频的组件,配置路由和设置跳转

// course/video.vue (新建)
<template>
  <div class="course-video">上传课时视频/div>
</template>

<script>
export default {
  name: 'CourseVideo'
}
</script>

<style lang="scss" scoped></style>
// router/index.js
...
    {
    path: '/course/:courseId/video',
    name: 'course-video',
    component: () => import(/* webpackChunkName: 'course-video' */ '@/views/course/video.vue'),
    props: true
  }
]
...

设置跳转时,由于模板中不用加this,可以params中的courseId: this.courseId简写成了courseId

// course/section.vue
...
<el-button
  type="success"
  @click="$router.push({
    name: 'course-video',
    params: {
      courseId
    }
  })"
>上传视频</el-button>
...

接受数据并且设置页面结构,顶部的课程相关信息展示自行完成(不再赘述)
注意:这里采用普通input标签操作,使用el-input的话DOM操作会很繁琐

// course/video.vue
<template>
  <div class="course-video">
    <el-card>
      <div slot="header">
        课程相关信息
      </div>
      <el-form>
        <el-form-item label="视频上传">
          <input type="file">
        </el-form-item>
        <el-form-item label="封面上传">
          <input type="file">
        </el-form-item>
        <el-form-item>
          <el-button type="primary">开始上传</el-button>
          <el-button @click="$router.push({
            name: 'course-section',
            params: {
              courseId
            }
          })">返回</el-button>
        </el-form-item>
      </el-form>
    </el-card>
  </div>
</template>

<script>
export default {
  name: 'CourseVideo',
  props: {
    courseId: {
      type: [String, Number],
      required: true
    }
  }
}
</script>
...

阿里云视频点播

这是一个集音视频采集,编辑,上传,自动化转码处理,媒体资源管理,高效云剪辑处理,分发加速,视频播放于一体的一站式音视频点播解决方案
上传视频功能并没有后台上传,采用的方案是使用第三方服务“阿里云视频点播”,采用第三方视频服务是一种主流方案,让公司可以更专注核心业务而不是单独维护一套视频点播系统
官方功能概述:地址
操作一共分为两步:

  • 获取上传授权的相关操作已经由后台处理了
  • 文件上传到需要查看阿里云官方文档操作

操作指引

文档位置:

  • 阿里云视频点播页面->上传SDK->客户端上传->使用JavaScript上传SDK
    操作步骤:
  • SDK下载,这里的SDK可以理解为包含一系列特定功能的文件
  • 页面中找到Web端SDK下载部分,选择实例代码,下载


  • 解压


  • 找到aliyun-upload-sdk目录


  • 添加到项目根目录中的public目录下,作为静态资源使用



    最终的结构为:

.
├── aliyun-upload-sdk
│ ├── aliyun-upload-sdk-1.5.0.min.js
│ └── lib
│ ├── aliyun-oss-sdk-5.3.1.min.js
│ └── es6-promise.min.js
├── favicon.ico
└── index.html

SDK文件处理

由于这些JS文件,没有进行模块化处理,所以我们在项目中需要通过全局引入的方式使用

  • 把文件引入到public/index.html,这些public的静态资源不会被webpack处理
  • 引入时建议通过/设置路径,/在服务器中表示根目录
  • 引入顺序参考文档
// /public/index.html
<!DOCTYPE html>
<html lang="en">
  <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>
    <!-- 引入阿里云视频上传SDK -->
    <script src="/aliyun-upload-sdk/lib/es6-promise.min.js"></script>
    <script src="/aliyun-upload-sdk/lib/aliyun-oss-sdk-5.3.1.min.js"></script>
    <script src="/aliyun-upload-sdk/aliyun-upload-sdk-1.5.0.min.js"></script>
    <!-- built files will be auto injected -->
  </body>
</html>

体验官方上传示例

从官方下载下来的示例代码中找到Vue示例,路径是"aliyun-upload-sdk-1.5.0demo\vue\vue-demo\src",内部的"STSToken.vue"(STS方式)与"UploadAuth.vue"(上传地址和凭证方式)对应两种上传方式,官方推荐的是第二种
修改了路由之后我们走一波功能测试
这里的文件无需进行风格处理

  • 在JS中书写/* eslint-disable */可以局部禁用eslint检验
    • 书写位置之后就都可以不检验了
  • 模板错误可以在图示位置添加空格就可以了
    那么直接快进到测试完毕,观察一下流程
  • 得出结论,上传凭证和地址信息(授权)是在onUploadstarted中设置的
    • 阿里云提供的回调中,只有onUploadstarted书写为了不规范的camelCase,注意拼写



      最后将路径更改为我们自己的组件,设置功能启动!

初始化阿里云上传

第一步下载完毕之后,下一步我们需要初始化上传实例,设置到video.vue中
userId为后端提供的

  • AliYunUpload 可以添加window访问
  • 这里提供测试ID为'1618139964448548'
  • 处理其他的内容和格式
// course/video.vue
...
<script>
export default {
  name: 'CourseVideo',
  props: {
    courseId: {
      type: [String, Number],
      required: true
    }
  },
  data () {
    return {
      uploader: null
    }
  },
  created () {
    this.initUploader()
  },
  methods: {
    // 初始化上传对象
    initUploader () {
      // 官方示例:声明 AliyunUpload.Vod 初始化回调。
      this.uploader = new window.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('onUploadstarted', uploadInfo)
        },
        // 文件上传成功
        onUploadSucceed: function (uploadInfo) {
          console.log('onUploadSucceed', uploadInfo)
        },
        // 文件上传失败
        onUploadFailed: function (uploadInfo, code, message) {
          console.log('onUploadFailed', uploadInfo, code, message)
        },
        // 文件上传进度,单位:字节
        onUploadProgress: function (uploadInfo, totalSize, loadedPercent) {
          console.log('onUploadProgress', uploadInfo, totalSize, loadedPercent)
        },
        // 上传凭证超时
        onUploadTokenExpired: function (uploadInfo) {
          console.log('onUploadTokenExpired', uploadInfo)
        },
        // 全部文件上传结束
        onUploadEnd: function (uploadInfo) {
          console.log('onUploadEnd', uploadInfo)
        }
      })
    }
  }
}
</script>

给上传按钮添加点击事件

// video.vue
...
<el-button type="primary"
  @click="handleUpload"
>开始上传</el-button>
...
<script>
...
handleUpload () {

}
...
</script>

点击获取文件

  • 文本域添加ref
  • 通过$refs读取
// video.vue
...
<el-form-item label="视频上传">
  <input ref="video-file" type="file">
</el-form-item>
<el-form-item label="封面上传">
  <input ref="image-file" type="file">
</el-form-item>
...
<script>
...
handleUpload () {
  // 获取上传的文件(视频、图片)
  const videoFile = this.$refs['video-file'].files[0]
  const imageFile = this.$refs['image-file'].files[0]
}
...
</script>

从文档中找到将文件添加到上传列表的方式,进行响应的处理

  • uploader.addFile()将文件 添加到上传列表,多次调用会按照顺序发送文件(接口要求先发图)
  • uploader.startUpload()开始上传
handleUpload () {
  // 获取上传的文件(视频、图片)
  const videoFile = this.$refs['video-file'].files[0]
  const imageFile = this.$refs['image-file'].files[0]
  // 将文件添加到上传列表
  const uploader = this.uploader
  //  - 文档示例:uploader.addFile(event.target.files[i], null, null, null, paramData)
  uploader.addFile(imageFile)
  uploader.addFile(videoFile)
  // 开始上传
  //  - 开始上传后,上面的文件回按添加的顺序依次上传
  //  - 这时会触发 onUploadStarted 事件
  uploader.startUpload()
}

触发上传后,文件并没有真正开始上传,因为还需要发送上传凭证和地址,需要使用到后端提供的接口
实际上的执行流程就是:

  • 调用uploader.startUpload()调用方法开始上传
  • 调用uploader.setUploadAuthAddress()设置上传凭证和地址(在onUploadstarted钩子中)
    • 凭证需要依赖后端获得
  • 凭证没有问题,上传才开始执行

封装上传凭证和地址接口

由于需要上传视频和上传封面,要封装俩接口

  • 获取阿里云图片上传凭证:接口
  • 获取阿里云视频上传凭证:接口
    创建文件封装阿里云相关接口(4个)
// services/aliyun-upload.js
import request from '@/utils/request'

// 获取阿里云图片上传凭证(image 少了个 e)
export const aliyunImageUploadAddressAndAuth = () => {
  return request({
    method: 'GET',
    url: '/boss/course/upload/aliyunImagUploadAddressAdnAuth.json'
  })
}

// 获取阿里云视频上传凭证(有两个请求参数)
export const aliyunVideoUploadAddressAndAuth = params => {
  return request({
    method: 'GET',
    url: '/boss/course/upload/aliyunVideoUploadAddressAdnAuth.json',
    params
  })
}

// 阿里云转码请求(transcode 是一个词,中间不用驼峰)
export const aliyunVideoTrancode = data => {
  return request({
    method: 'POST',
    url: '/boss/course/upload/aliyunTransCode.json',
    data
  })
}

// 阿里云转码进度
export const getAliyunTranscodePercent = lessonId => {
  return request({
    method: 'GET',
    url: '/boss/course/upload/aliyunTransCodePercent.json',
    params: {
      lessonId
    }
  })
}

引入到页面中

// course/video.js
...
import {
  aliyunImagUploadAddressAndAuth,
  aliyunVideoUploadAddressAndAuth,
  aliyunVideoTranscode,
  getAliyunTranscodePercent
} from '@/services/aliyun-upload'
...

上传凭证处理

由于存在图片和视频两种上传类型,所以要先在onUploadstarted中检测
操作步骤:

  • 调用接口获取凭证
  • 设置凭证
  • 成功上传
// video.vue
...
data () {
  return {
    ...
    imageURL: ''
  }
},
...
// 开始上传(uploader.startUpload() 触发后执行该回调)
//   - 将回调更改为箭头函数,以便在内部通过 this 操作 Vue 实例
onUploadstarted: async uploadInfo => {
  // 一、获取凭证
  // console.log(uploadInfo)
  // 1. 声明变量存储得到上传凭证
  let uploadAddressAndAuth = null
  // 2. 根据 isImage 检测上传文件类型
  if (uploadInfo.isImage) {
    const { data } = await getAliyunImagUploadAddressAndAuth()
    if (data.code === '000000') {
      // 3. data.data 即为凭证信息组成的对象
      uploadAddressAndAuth = data.data
      // 5. 保存图片地址,给视频接口使用
      this.imageURL = uploadAddressAndAuth.imageURL
    }
  } else {
    // 4. 观察 uploadInfo 数据,根据请求参数名设置参数
    //   - 由于视频接口要求传入封面图片地址 imageUrl,所以必须先发图再发视频(后端
    //     - 先将图片数据存储给 this,便于视频接口使用
    const { data } = await getAliyunVideoUploadAddressAndAuth({
      fileName: uploadInfo.file.name,
      imageUrl: this.imageURL
    })
    if (data.code === '000000') {
      // 6. 存储凭证
      //  - 图片与视频上传的区别在于图片存在 imageId,视频为 videoId,其他相同
      uploadAddressAndAuth = data.data
    }
  }
  // 二、设置凭证
  this.uploader.setUploadAuthAndAddress(
    uploadInfo,
    uploadAddressAndAuth.uploadAuth,
    uploadAddressAndAuth.uploadAddress,
    uploadAddressAndAuth.imageId || uploadAddressAndAuth.videoId
  )
  // 设置完毕,上传进度开始执行
},
...

视频转码处理

转码请求接口之前就已经封装好了,文档中显示有多个请求参数,其实主要就需要:

  • lessonId:课时Id
    • 需要在上传视频跳转时添加相应的参数
  • coverImageUrl:封面图片地址
  • fileId:视频Id
  • fileName:视频名称
    更新跳转时的参数设置:
// section.vue
...
<el-button
  type="success"
  @click="$router.push({
    name: 'course-video',
    params: {
      courseId
    },
    query: {
      lessonId: data.id
    }
  })"
>上传视频</el-button>

当所有的文件都上传了之后才可以进行转码,故而应该在onUploadEnd回调中操作

// video.vue
...
data () {
  return {
    ...
    videoId: null
  }
},
...
onUploadstarted: async uploadInfo => {
  ...
  if (uploadInfo.isImage) {
    ...
  } else {
    ...
    if (data.code === '000000') {
      ...
      this.videoId = data.data.videoId
    }
  }
  ...
},
...
// 全部文件上传结束
onUploadEnd: async uploadInfo => {
  // 调用接口
  const { data } = await aliyunVideoTranscode({
    lessonId: this.$route.query.lessonId,
    coverImageUrl: this.imageURL,
    fileName: this.$refs['video-file'].files[0].name,
    fileId: this.videoId
  })
  console.log(data)
}

转码请求发送之后,还需要轮询一下转码的进度

// video.vue
...
onUploadEnd: async uploadInfo => {
  const { data } = await aliyunVideoTranscode({
    ...
  })
  if (data.code === '000000') {
    // 转码开始后,需要轮询转码进度
    const timer = setInterval(async () => {
      const { data } = getAliyunTranscodePercent(this.$route.query.lessonId)
      if (data === 100) {
        // 当上传进度为 100,停止定时器,并进行提示
        clearInterval(timer)
        this.$message.success('转码成功')
      }
    }, 1000)
  }
}
...

转码成功之后,前台页面只需要查看视频是否成功上传即可
将转码进度渲染到视图方便查看

// video.vue
...
data () {
  return {
    ...
    uploadPercent: 0,
    isUploadSuccess: false,
    isTranscodeSuccess: false
  }
},
...
<el-form-item>
  <p v-if="uploadPercent !== 0">视频上传中:{{ uploadPercent }}%</p>
  <p v-if="isUploadSuccess">视频转码中:{{ isTranscodeSuccess ? '完成' : '正在转码,请稍后...' }} </p>
</el-form-item>
...
// 文件上传进度,单位:字节
//   - 修改为箭头函数,内部 this 才能访问 Vue 实例
onUploadProgress: (uploadInfo, totalSize, loadedPercent) => {
  console.log('onUploadProgress', uploadInfo, totalSize, loadedPercent)
  // 只对视频上传进度进行监测即可
  if (!uploadInfo.isImage) {
    this.uploadPercent = Math.floor(loadedPercent * 100)
  }
},
...
// 全部文件上传结束
onUploadEnd: async uploadInfo => {
  this.isUploadSuccess = true
  ...
    if (data === 100) {
      this.isTranscodeSuccess = true
      ...
    }
  ...
},
...
handleUpload () {
  // 点击上传时重置状态信息
  this.isTranscodeSuccess = false
  this.isUploadSuccess = false
  this.uploadPercent = 0
  ...
}
...

大功告成啦!

最后一步,发布部署

项目打包

项目打包了之后,打包后的文件生成在dist目录中

npm run build

得到了以下提示,说明打包成功,可以看到打包的详细信息


打包之后,通过serve静态文件服务器就可以进行本地浏览了
至此,全部项目就完成了

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容