美文网首页
课程管理(二)

课程管理(二)

作者: amanohina | 来源:发表于2021-03-15 17:19 被阅读0次

    课程内容管理

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


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

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

    // 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静态文件服务器就可以进行本地浏览了
    至此,全部项目就完成了

    相关文章

      网友评论

          本文标题:课程管理(二)

          本文链接:https://www.haomeiwen.com/subject/mibkcltx.html